blob: f071c6250ccdd25692f82590168185d1a59ec9a9 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* $Id$ */
package org.apache.fop.pdf;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.io.output.CountingOutputStream;
public class PDFLinearization {
private PDFDocument doc;
private Map<PDFPage, Set<PDFObject>> pageObjsMap = new HashMap<PDFPage, Set<PDFObject>>();
private PDFDictionary linearDict;
private HintTable hintTable;
public PDFLinearization(PDFDocument doc) {
this.doc = doc;
}
static class HintTable extends PDFStream {
private List<PDFPage> pages;
int pageStartPos;
List<Integer> sharedLengths = new ArrayList<Integer>();
List<Integer> pageLengths = new ArrayList<Integer>();
List<Integer> contentStreamLengths = new ArrayList<Integer>();
List<Integer> objCount = new ArrayList<Integer>();
Map<String, int[]> hintGroups = new HashMap<String, int[]>();
public HintTable(PDFDocument doc) {
super(false);
doc.assignObjectNumber(this);
doc.addObject(this);
pages = doc.pageObjs;
for (int i = 0; i < pages.size(); i++) {
pageLengths.add(0);
contentStreamLengths.add(0);
objCount.add(0);
}
hintGroups.put("/C", new int[4]);
hintGroups.put("/L", new int[4]);
hintGroups.put("/I", new int[4]);
hintGroups.put("/E", new int[4]);
hintGroups.put("/O", new int[4]);
hintGroups.put("/V", new int[4]);
}
@Override
public PDFFilterList getFilterList() {
return new PDFFilterList(getDocument().isEncryptionActive());
}
@Override
protected void outputRawStreamData(OutputStream os) throws IOException {
CountingOutputStream bos = new CountingOutputStream(os);
//start header
writeULong(1, bos); //1
writeULong(pageStartPos, bos); //2
writeCard16(32, bos); //3
writeULong(0, bos); //4
writeCard16(32, bos); //5
writeULong(0, bos); //6
writeCard16(0, bos); //7
writeULong(0, bos); //8
writeCard16(32, bos); //9
writeCard16(0, bos); //10
writeCard16(0, bos); //11
writeCard16(0, bos); //12
writeCard16(4, bos); //13
//end header
for (PDFPage page : pages) {
writeULong(objCount.get(page.pageIndex) - 1, bos);
}
for (PDFPage page : pages) {
writeULong(pageLengths.get(page.pageIndex), bos);
}
for (PDFPage page : pages) {
writeULong(contentStreamLengths.get(page.pageIndex), bos);
}
writeSharedTable(bos);
for (Map.Entry<String, int[]> group : hintGroups.entrySet()) {
put(group.getKey(), bos.getCount());
for (int i : group.getValue()) {
writeULong(i, bos);
}
if (group.getKey().equals("/C")) {
writeULong(0, bos);
writeCard16(0, bos);
}
}
}
private void writeSharedTable(CountingOutputStream bos) throws IOException {
put("/S", bos.getCount());
//Shared object hint table, header section
writeULong(0, bos); //1
writeULong(0, bos); //2
writeULong(sharedLengths.size(), bos); //3
writeULong(sharedLengths.size(), bos); //4
writeCard16(0, bos); //5
writeULong(0, bos); //6
writeCard16(32, bos); //7
for (int i : sharedLengths) {
writeULong(i, bos);
}
writeULong(0, bos);
}
private void writeCard16(int s, OutputStream bos) throws IOException {
byte b1 = (byte)((s >> 8) & 0xff);
byte b2 = (byte)(s & 0xff);
bos.write(b1);
bos.write(b2);
}
private void writeULong(int s, OutputStream bos) throws IOException {
byte b1 = (byte)((s >> 24) & 0xff);
byte b2 = (byte)((s >> 16) & 0xff);
byte b3 = (byte)((s >> 8) & 0xff);
byte b4 = (byte)(s & 0xff);
bos.write(b1);
bos.write(b2);
bos.write(b3);
bos.write(b4);
}
}
static class LinearPDFDictionary extends PDFDictionary {
private int lastsize = -1;
public LinearPDFDictionary(PDFDocument doc) {
put("Linearized", 1);
put("/L", 0);
PDFArray larray = new PDFArray();
larray.add(0);
larray.add(0);
put("/H", larray);
doc.assignObjectNumber(this);
getObjectNumber().getNumber();
put("/O", getObjectNumber().getNumber() + 3);
put("/E", 0);
put("/N", doc.pageObjs.size());
put("/T", 0);
}
public int output(OutputStream stream) throws IOException {
int size = super.output(stream);
int padding = lastsize - size + 32;
if (lastsize == -1) {
padding = 32;
lastsize = size;
}
writePadding(padding, stream);
return size + padding;
}
}
private Set<PDFObject> assignNumbers() throws IOException {
Set<PDFObject> page1Children = getPage1Children();
if (!doc.pageObjs.isEmpty()) {
for (int i = 1; i < doc.pageObjs.size(); i++) {
PDFPage page = doc.pageObjs.get(i);
Set<PDFObject> children = pageObjsMap.get(page);
for (PDFObject c : children) {
if (!page1Children.contains(c) && c.hasObjectNumber()) {
c.getObjectNumber().getNumber();
}
}
}
for (PDFObject o : doc.objects) {
if (o instanceof PDFDests || o instanceof PDFOutline) {
for (PDFObject c : getChildren(o)) {
c.getObjectNumber().getNumber();
}
}
if (o instanceof PDFInfo || o instanceof PDFPageLabels) {
o.getObjectNumber().getNumber();
}
}
for (PDFObject o : doc.objects) {
if (!page1Children.contains(o)) {
o.getObjectNumber().getNumber();
}
}
}
linearDict = new LinearPDFDictionary(doc);
for (PDFObject o : page1Children) {
o.getObjectNumber().getNumber();
}
sort(doc.objects);
return page1Children;
}
private void sort(List<PDFObject> objects) {
Collections.sort(objects, new Comparator<PDFObject>() {
public int compare(PDFObject o1, PDFObject o2) {
return Integer.compare(o1.getObjectNumber().getNumber(), o2.getObjectNumber().getNumber());
}
});
}
private Set<PDFObject> getChildren(PDFObject o) {
Set<PDFObject> children = new LinkedHashSet<PDFObject>();
children.add(o);
o.getChildren(children);
return children;
}
public void outputPages(OutputStream stream) throws IOException {
Collections.sort(doc.pageObjs, new Comparator<PDFPage>() {
public int compare(PDFPage o1, PDFPage o2) {
return Integer.compare(o1.pageIndex, o2.pageIndex);
}
});
doc.objects.addAll(doc.trailerObjects);
doc.trailerObjects = null;
if (doc.getStructureTreeElements() != null) {
doc.objects.addAll(doc.getStructureTreeElements());
doc.structureTreeElements = null;
}
for (int i = 0; i < doc.objects.size() * 2; i++) {
doc.indirectObjectOffsets.add(0L);
}
Set<PDFObject> page1Children = assignNumbers();
doc.streamIndirectObject(linearDict, new ByteArrayOutputStream());
for (PDFObject o : page1Children) {
doc.objects.remove(o);
}
int sizeOfRest = doc.objects.size();
ByteArrayOutputStream fakeHeaderTrailerStream = new ByteArrayOutputStream();
long topTrailer = doc.position;
doc.writeTrailer(fakeHeaderTrailerStream, sizeOfRest, page1Children.size() + 1,
page1Children.size() + sizeOfRest + 1, Long.MAX_VALUE, 0);
doc.position += fakeHeaderTrailerStream.size();
ByteArrayOutputStream pageStream = new ByteArrayOutputStream();
writeObjects(page1Children, pageStream, sizeOfRest + 1);
long trailerOffset = doc.position;
ByteArrayOutputStream footerTrailerStream = new ByteArrayOutputStream();
doc.writeTrailer(footerTrailerStream, 0, sizeOfRest, sizeOfRest, 0, topTrailer);
doc.position += footerTrailerStream.size();
linearDict.put("/L", doc.position);
PDFDocument.outputIndirectObject(linearDict, stream);
CountingOutputStream realTrailer = new CountingOutputStream(stream);
doc.writeTrailer(realTrailer, sizeOfRest, page1Children.size() + 1,
page1Children.size() + sizeOfRest + 1, trailerOffset, 0);
writePadding(fakeHeaderTrailerStream.size() - realTrailer.getCount(), stream);
for (PDFObject o : page1Children) {
PDFDocument.outputIndirectObject(o, stream);
if (o instanceof HintTable) {
break;
}
}
stream.write(pageStream.toByteArray());
stream.write(footerTrailerStream.toByteArray());
}
private Set<PDFObject> getPage1Children() throws IOException {
Set<PDFObject> page1Children = new LinkedHashSet<PDFObject>();
if (!doc.pageObjs.isEmpty()) {
PDFPage page1 = doc.pageObjs.get(0);
page1Children.add(doc.getRoot());
hintTable = new HintTable(doc);
page1Children.add(hintTable);
page1Children.add(page1);
page1.getChildren(page1Children);
doc.objects.remove(doc.getPages());
doc.objects.add(0, doc.getPages());
pageObjsMap.put(page1, page1Children);
for (int i = 1; i < doc.pageObjs.size(); i++) {
PDFPage page = doc.pageObjs.get(i);
pageObjsMap.put(page, getChildren(page));
}
}
return page1Children;
}
private static void writePadding(int padding, OutputStream stream) throws IOException {
for (int i = 0; i < padding; i++) {
stream.write(" ".getBytes("UTF-8"));
}
}
private void writeObjects(Set<PDFObject> children1, OutputStream pageStream, int sizeOfRest) throws IOException {
writePage1(children1, pageStream);
linearDict.put("/E", doc.position);
for (PDFPage page : doc.pageObjs) {
if (page.pageIndex != 0) {
writePage(page, pageStream);
}
}
while (!doc.objects.isEmpty()) {
PDFObject o = doc.objects.remove(0);
if (o instanceof PDFOutline) {
writeObjectGroup("/O", getChildren(o), pageStream);
} else if (o instanceof PDFDests) {
writeObjectGroup("/E", getChildren(o), pageStream);
} else if (o instanceof PDFInfo) {
writeObjectGroup("/I", getChildren(o), pageStream);
} else if (o instanceof PDFPageLabels) {
writeObjectGroup("/L", getChildren(o), pageStream);
} else if (o instanceof PDFStructTreeRoot) {
writeObjectGroup("/C", getChildren(o), pageStream);
} else {
doc.streamIndirectObject(o, pageStream);
}
}
linearDict.put("/T", doc.position + 8 + String.valueOf(sizeOfRest).length());
}
private void writeObjectGroup(String name, Set<PDFObject> objects, OutputStream pageStream)
throws IOException {
List<PDFObject> children = new ArrayList<PDFObject>(objects);
sort(children);
int[] values = hintTable.hintGroups.get(name);
values[0] = children.iterator().next().getObjectNumber().getNumber();
values[1] = (int) doc.position;
values[2] = children.size();
for (PDFObject o : children) {
values[3] += doc.streamIndirectObject(o, pageStream);
doc.objects.remove(o);
}
}
private void writePage1(Set<PDFObject> children1, OutputStream pageStream) throws IOException {
hintTable.pageStartPos = (int) doc.position;
OutputStream stream = new ByteArrayOutputStream();
Set<PDFObject> sharedChildren = getSharedObjects();
int page1Len = 0;
int objCount = 0;
int sharedCount = 0;
for (PDFObject o : children1) {
if (o instanceof HintTable) {
PDFArray a = (PDFArray) linearDict.get("/H");
a.set(0, doc.position);
doc.streamIndirectObject(o, stream);
a.set(1, doc.position - (Double)a.get(0));
stream = pageStream;
} else {
int len = doc.streamIndirectObject(o, stream);
if (o instanceof PDFStream && hintTable.contentStreamLengths.get(0) == 0) {
hintTable.contentStreamLengths.set(0, len);
}
if (!(o instanceof PDFRoot)) {
page1Len += len;
objCount++;
}
if (sharedChildren.contains(o)) {
hintTable.sharedLengths.set(sharedCount, len);
sharedCount++;
}
}
}
hintTable.pageLengths.set(0, page1Len);
hintTable.objCount.set(0, objCount);
}
private Set<PDFObject> getSharedObjects() {
Set<PDFObject> pageSharedChildren = getChildren(doc.pageObjs.get(0));
for (int i = 0; i < pageSharedChildren.size(); i++) {
hintTable.sharedLengths.add(0);
}
return pageSharedChildren;
}
private void writePage(PDFPage page, OutputStream pageStream) throws IOException {
Set<PDFObject> children = pageObjsMap.get(page);
int pageLen = 0;
int objCount = 0;
for (PDFObject c : children) {
if (doc.objects.contains(c)) {
int len = doc.streamIndirectObject(c, pageStream);
if (c instanceof PDFStream) {
hintTable.contentStreamLengths.set(page.pageIndex, len);
}
pageLen += len;
doc.objects.remove(c);
objCount++;
}
}
hintTable.pageLengths.set(page.pageIndex, pageLen);
hintTable.objCount.set(page.pageIndex, objCount);
}
}