Skip to content

Commit

Permalink
FOP-3215: Add support for PDF object streams
Browse files Browse the repository at this point in the history
  • Loading branch information
simonsteiner1984 committed Oct 22, 2024
1 parent 9ac71d8 commit b624743
Show file tree
Hide file tree
Showing 12 changed files with 150 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -298,4 +298,8 @@ public void registerChildren() {
getDocument().registerObject(refLength);
}
}

public boolean supportsObjectStream() {
return false;
}
}
71 changes: 53 additions & 18 deletions fop-core/src/main/java/org/apache/fop/pdf/PDFDocument.java
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ public class PDFDocument {

private FileIDGenerator fileIDGenerator;

private ObjectStreamManager objectStreamManager;

private boolean accessibilityEnabled;

private boolean mergeFontsEnabled;
Expand All @@ -185,6 +187,8 @@ public class PDFDocument {

protected boolean outputStarted;

private boolean objectStreamsEnabled;

/**
* Creates an empty PDF document.
*
Expand Down Expand Up @@ -1027,15 +1031,36 @@ public void output(OutputStream stream) throws IOException {
//Write out objects until the list is empty. This approach (used with a
//LinkedList) allows for output() methods to create and register objects
//on the fly even during serialization.
while (this.objects.size() > 0) {
PDFObject object = this.objects.remove(0);

if (objectStreamsEnabled) {
List<PDFObject> indirectObjects = new ArrayList<>();
while (objects.size() > 0) {
PDFObject object = objects.remove(0);
if (object.supportsObjectStream()) {
addToObjectStream(object);
} else {
indirectObjects.add(object);
}
}
objects.addAll(indirectObjects);
}

while (objects.size() > 0) {
PDFObject object = objects.remove(0);
streamIndirectObject(object, stream);
}
}

private void addToObjectStream(CompressedObject object) {
if (objectStreamManager == null) {
objectStreamManager = new ObjectStreamManager(this);
}
objectStreamManager.add(object);
}

protected void writeTrailer(OutputStream stream, int first, int last, int size, long mainOffset, long startxref)
throws IOException {
TrailerOutputHelper trailerOutputHelper = mayCompressStructureTreeElements()
TrailerOutputHelper trailerOutputHelper = useObjectStreams()
? new CompressedTrailerOutputHelper()
: new UncompressedTrailerOutputHelper();
if (structureTreeElements != null) {
Expand Down Expand Up @@ -1148,7 +1173,7 @@ private void createDestinations() {
}

private void outputTrailerObjectsAndXref(OutputStream stream) throws IOException {
TrailerOutputHelper trailerOutputHelper = mayCompressStructureTreeElements()
TrailerOutputHelper trailerOutputHelper = useObjectStreams()
? new CompressedTrailerOutputHelper()
: new UncompressedTrailerOutputHelper();
if (structureTreeElements != null) {
Expand All @@ -1170,10 +1195,15 @@ private void outputTrailerObjectsAndXref(OutputStream stream) throws IOException
stream.write(encode(trailer));
}

private boolean mayCompressStructureTreeElements() {
return accessibilityEnabled
&& versionController.getPDFVersion().compareTo(Version.V1_5) >= 0
&& !isLinearizationEnabled();
private boolean useObjectStreams() {
if (objectStreamsEnabled && linearizationEnabled) {
throw new UnsupportedOperationException("Linearization and use-object-streams can't be both enabled");
}
if (objectStreamsEnabled && isEncryptionActive()) {
throw new UnsupportedOperationException("Encryption and use-object-streams can't be both enabled");
}
return objectStreamsEnabled || (accessibilityEnabled
&& versionController.getPDFVersion().compareTo(Version.V1_5) >= 0 && !isLinearizationEnabled());
}

private TrailerDictionary createTrailerDictionary(boolean addRoot) {
Expand Down Expand Up @@ -1236,25 +1266,22 @@ public long outputCrossReferenceObject(OutputStream stream,
}

private class CompressedTrailerOutputHelper implements TrailerOutputHelper {

private ObjectStreamManager structureTreeObjectStreams;

public void outputStructureTreeElements(OutputStream stream)
throws IOException {
public void outputStructureTreeElements(OutputStream stream) {
assert structureTreeElements.size() > 0;
structureTreeObjectStreams = new ObjectStreamManager(PDFDocument.this);
if (objectStreamManager == null) {
objectStreamManager = new ObjectStreamManager(PDFDocument.this);
}
for (PDFStructElem structElem : structureTreeElements) {
structureTreeObjectStreams.add(structElem);
objectStreamManager.add(structElem);
}
}

public long outputCrossReferenceObject(OutputStream stream,
TrailerDictionary trailerDictionary, int first, int last, int size) throws IOException {
// Outputting the object streams should not have created new indirect objects
assert objects.isEmpty();
new CrossReferenceStream(PDFDocument.this, ++objectcount, trailerDictionary, position,
indirectObjectOffsets,
structureTreeObjectStreams.getCompressedObjectReferences())
new CrossReferenceStream(PDFDocument.this, trailerDictionary, position,
indirectObjectOffsets, objectStreamManager.getCompressedObjectReferences())
.output(stream);
return position;
}
Expand Down Expand Up @@ -1290,4 +1317,12 @@ public boolean isFormXObjectEnabled() {
public void setFormXObjectEnabled(boolean b) {
formXObjectEnabled = b;
}

public void setObjectStreamsEnabled(boolean b) {
objectStreamsEnabled = b;
}

public int getObjectCount() {
return objectcount;
}
}
3 changes: 3 additions & 0 deletions fop-core/src/main/java/org/apache/fop/pdf/PDFNumber.java
Original file line number Diff line number Diff line change
Expand Up @@ -120,5 +120,8 @@ protected String toPDFString() {
return sb.toString();
}

public boolean supportsObjectStream() {
return false;
}
}

6 changes: 5 additions & 1 deletion fop-core/src/main/java/org/apache/fop/pdf/PDFObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
* Object has a number and a generation (although the generation will always
* be 0 in new documents).
*/
public abstract class PDFObject implements PDFWritable {
public abstract class PDFObject implements PDFWritable, CompressedObject {

/** logger for all PDFObjects (and descendants) */
protected static final Log log = LogFactory.getLog(PDFObject.class.getName());
Expand Down Expand Up @@ -358,4 +358,8 @@ protected boolean contentEquals(PDFObject o) {

public void getChildren(Set<PDFObject> children) {
}

public boolean supportsObjectStream() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public int output(OutputStream stream) throws IOException {
startOfDocMDP = countingOutputStream.getByteCount();
return super.output(stream);
}
throw new IOException("Disable pdf linearization");
throw new IOException("Disable pdf linearization and use-object-streams");
}
}

Expand Down
3 changes: 1 addition & 2 deletions fop-core/src/main/java/org/apache/fop/pdf/PDFStructElem.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@
/**
* Class representing a PDF Structure Element.
*/
public class PDFStructElem extends StructureHierarchyMember
implements StructureTreeElement, CompressedObject, Serializable {
public class PDFStructElem extends StructureHierarchyMember implements StructureTreeElement, Serializable {
private static final List<StructureType> BLSE = Arrays.asList(StandardStructureTypes.Table.TABLE,
StandardStructureTypes.List.L, StandardStructureTypes.Paragraphlike.P);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ public class CrossReferenceStream extends CrossReferenceObject {

private final List<ObjectReference> objectReferences;

public CrossReferenceStream(PDFDocument document,
public CrossReferenceStream(PDFDocument document, TrailerDictionary trailerDictionary, long startxref,
List<Long> uncompressedObjectReferences, List<CompressedObjectReference> compressedObjectReferences) {
this(document, document.getObjectCount() + 1, trailerDictionary, startxref,
uncompressedObjectReferences, compressedObjectReferences);
}

protected CrossReferenceStream(PDFDocument document,
int objectNumber,
TrailerDictionary trailerDictionary,
long startxref,
Expand All @@ -56,7 +62,7 @@ public CrossReferenceStream(PDFDocument document,
super(trailerDictionary, startxref);
this.document = document;
this.objectNumber = objectNumber;
this.objectReferences = new ArrayList<ObjectReference>(uncompressedObjectReferences.size());
this.objectReferences = new ArrayList<>(uncompressedObjectReferences.size());
for (Long offset : uncompressedObjectReferences) {
objectReferences.add(offset == null ? null : new UncompressedObjectReference(offset));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
import static org.apache.fop.render.pdf.PDFRendererOption.LINEARIZATION;
import static org.apache.fop.render.pdf.PDFRendererOption.MERGE_FONTS;
import static org.apache.fop.render.pdf.PDFRendererOption.MERGE_FORM_FIELDS;
import static org.apache.fop.render.pdf.PDFRendererOption.OBJECT_STREAMS;
import static org.apache.fop.render.pdf.PDFRendererOption.OUTPUT_PROFILE;
import static org.apache.fop.render.pdf.PDFRendererOption.PDF_A_MODE;
import static org.apache.fop.render.pdf.PDFRendererOption.PDF_UA_MODE;
Expand Down Expand Up @@ -155,6 +156,7 @@ private void configure(Configuration cfg, FOUserAgent userAgent, boolean strict)
parseAndPut(MERGE_FORM_FIELDS, cfg);
parseAndPut(LINEARIZATION, cfg);
parseAndPut(FORM_XOBJECT, cfg);
parseAndPut(OBJECT_STREAMS, cfg);
parseAndPut(VERSION, cfg);
configureSignParams(cfg);
} catch (ConfigurationException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ Boolean deserialize(String value) {
return Boolean.valueOf(value);
}
},
OBJECT_STREAMS("use-object-streams", false) {
@Override
Boolean deserialize(String value) {
return Boolean.valueOf(value);
}
},
/** Rendering Options key for the ICC profile for the output intent. */
OUTPUT_PROFILE("output-profile") {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import static org.apache.fop.render.pdf.PDFRendererOption.LINEARIZATION;
import static org.apache.fop.render.pdf.PDFRendererOption.MERGE_FONTS;
import static org.apache.fop.render.pdf.PDFRendererOption.MERGE_FORM_FIELDS;
import static org.apache.fop.render.pdf.PDFRendererOption.OBJECT_STREAMS;
import static org.apache.fop.render.pdf.PDFRendererOption.OUTPUT_PROFILE;
import static org.apache.fop.render.pdf.PDFRendererOption.PDF_A_MODE;
import static org.apache.fop.render.pdf.PDFRendererOption.PDF_UA_MODE;
Expand Down Expand Up @@ -157,4 +158,8 @@ public Boolean getLinearizationEnabled() {
public Boolean getFormXObjectEnabled() {
return (Boolean)properties.get(FORM_XOBJECT);
}

public Boolean getObjectStreamsEnabled() {
return (Boolean)properties.get(OBJECT_STREAMS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,7 @@ public PDFDocument setupPDFDocument(OutputStream out) throws IOException {
pdfDoc.setMergeFormFieldsEnabled(rendererConfig.getMergeFormFieldsEnabled());
pdfDoc.setLinearizationEnabled(rendererConfig.getLinearizationEnabled());
pdfDoc.setFormXObjectEnabled(rendererConfig.getFormXObjectEnabled());
pdfDoc.setObjectStreamsEnabled(rendererConfig.getObjectStreamsEnabled());

return this.pdfDoc;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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.awt.geom.Rectangle2D;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

import org.junit.Assert;
import org.junit.Test;

import org.apache.fop.render.pdf.PDFContentGenerator;

public class PDFObjectStreamTestCase {
@Test
public void testObjectStreamsEnabled() throws IOException {
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
PDFDocument doc = new PDFDocument("");
Map<String, List<String>> filterMap = new HashMap<>();
List<String> filterList = new ArrayList<>();
filterList.add("null");
filterMap.put("default", filterList);
doc.setFilterMap(filterMap);
doc.setObjectStreamsEnabled(true);
PDFResources resources = new PDFResources(doc);
doc.addObject(resources);
PDFResourceContext context = new PDFResourceContext(resources);
ByteArrayOutputStream out = new ByteArrayOutputStream();
PDFContentGenerator gen = new PDFContentGenerator(doc, out, context);
Rectangle2D.Float f = new Rectangle2D.Float();
PDFPage page = new PDFPage(resources, 0, f, f, f, f);
doc.registerObject(page);
doc.addImage(context, new BitmapImage("", 1, 1, new byte[0], null));
gen.flushPDFDoc();
doc.outputTrailer(out);
Assert.assertTrue(out.toString().contains("/Subtype /Image"));
Assert.assertTrue(out.toString().contains("<<\n /Type /ObjStm\n /N 3\n /First 15\n /Length 260\n>>\n"
+ "stream\n8 0\n9 52\n4 121\n<<\n/Producer"));
}
}

0 comments on commit b624743

Please sign in to comment.