From: Simon Steiner Date: Tue, 22 Oct 2024 08:47:25 +0000 (+0100) Subject: FOP-3215: Add support for PDF object streams X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=b6247436442d04237338fd90a93efcc79c102bf8;p=xmlgraphics-fop.git FOP-3215: Add support for PDF object streams --- diff --git a/fop-core/src/main/java/org/apache/fop/pdf/AbstractPDFStream.java b/fop-core/src/main/java/org/apache/fop/pdf/AbstractPDFStream.java index 8dba4981d..c70b812c3 100644 --- a/fop-core/src/main/java/org/apache/fop/pdf/AbstractPDFStream.java +++ b/fop-core/src/main/java/org/apache/fop/pdf/AbstractPDFStream.java @@ -298,4 +298,8 @@ public abstract class AbstractPDFStream extends PDFObject { getDocument().registerObject(refLength); } } + + public boolean supportsObjectStream() { + return false; + } } diff --git a/fop-core/src/main/java/org/apache/fop/pdf/PDFDocument.java b/fop-core/src/main/java/org/apache/fop/pdf/PDFDocument.java index 34421858a..12f14354e 100644 --- a/fop-core/src/main/java/org/apache/fop/pdf/PDFDocument.java +++ b/fop-core/src/main/java/org/apache/fop/pdf/PDFDocument.java @@ -173,6 +173,8 @@ public class PDFDocument { private FileIDGenerator fileIDGenerator; + private ObjectStreamManager objectStreamManager; + private boolean accessibilityEnabled; private boolean mergeFontsEnabled; @@ -185,6 +187,8 @@ public class PDFDocument { protected boolean outputStarted; + private boolean objectStreamsEnabled; + /** * Creates an empty PDF document. * @@ -1027,15 +1031,36 @@ public class PDFDocument { //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 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) { @@ -1148,7 +1173,7 @@ public class PDFDocument { } private void outputTrailerObjectsAndXref(OutputStream stream) throws IOException { - TrailerOutputHelper trailerOutputHelper = mayCompressStructureTreeElements() + TrailerOutputHelper trailerOutputHelper = useObjectStreams() ? new CompressedTrailerOutputHelper() : new UncompressedTrailerOutputHelper(); if (structureTreeElements != null) { @@ -1170,10 +1195,15 @@ public class PDFDocument { 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) { @@ -1236,15 +1266,13 @@ public class PDFDocument { } 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); } } @@ -1252,9 +1280,8 @@ public class PDFDocument { 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; } @@ -1290,4 +1317,12 @@ public class PDFDocument { public void setFormXObjectEnabled(boolean b) { formXObjectEnabled = b; } + + public void setObjectStreamsEnabled(boolean b) { + objectStreamsEnabled = b; + } + + public int getObjectCount() { + return objectcount; + } } diff --git a/fop-core/src/main/java/org/apache/fop/pdf/PDFNumber.java b/fop-core/src/main/java/org/apache/fop/pdf/PDFNumber.java index 26489c6ec..5b362f1be 100644 --- a/fop-core/src/main/java/org/apache/fop/pdf/PDFNumber.java +++ b/fop-core/src/main/java/org/apache/fop/pdf/PDFNumber.java @@ -120,5 +120,8 @@ public class PDFNumber extends PDFObject { return sb.toString(); } + public boolean supportsObjectStream() { + return false; + } } diff --git a/fop-core/src/main/java/org/apache/fop/pdf/PDFObject.java b/fop-core/src/main/java/org/apache/fop/pdf/PDFObject.java index 12402d72c..e1e429062 100644 --- a/fop-core/src/main/java/org/apache/fop/pdf/PDFObject.java +++ b/fop-core/src/main/java/org/apache/fop/pdf/PDFObject.java @@ -34,7 +34,7 @@ import org.apache.commons.logging.LogFactory; * 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()); @@ -358,4 +358,8 @@ public abstract class PDFObject implements PDFWritable { public void getChildren(Set children) { } + + public boolean supportsObjectStream() { + return true; + } } diff --git a/fop-core/src/main/java/org/apache/fop/pdf/PDFSignature.java b/fop-core/src/main/java/org/apache/fop/pdf/PDFSignature.java index 5bdf5ebdf..1ff785553 100644 --- a/fop-core/src/main/java/org/apache/fop/pdf/PDFSignature.java +++ b/fop-core/src/main/java/org/apache/fop/pdf/PDFSignature.java @@ -120,7 +120,7 @@ public class PDFSignature { startOfDocMDP = countingOutputStream.getByteCount(); return super.output(stream); } - throw new IOException("Disable pdf linearization"); + throw new IOException("Disable pdf linearization and use-object-streams"); } } diff --git a/fop-core/src/main/java/org/apache/fop/pdf/PDFStructElem.java b/fop-core/src/main/java/org/apache/fop/pdf/PDFStructElem.java index 4f4845cfc..cc299fa1d 100644 --- a/fop-core/src/main/java/org/apache/fop/pdf/PDFStructElem.java +++ b/fop-core/src/main/java/org/apache/fop/pdf/PDFStructElem.java @@ -35,8 +35,7 @@ import org.apache.fop.util.LanguageTags; /** * 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 BLSE = Arrays.asList(StandardStructureTypes.Table.TABLE, StandardStructureTypes.List.L, StandardStructureTypes.Paragraphlike.P); diff --git a/fop-core/src/main/java/org/apache/fop/pdf/xref/CrossReferenceStream.java b/fop-core/src/main/java/org/apache/fop/pdf/xref/CrossReferenceStream.java index 9dbd317a7..c7c3765d5 100644 --- a/fop-core/src/main/java/org/apache/fop/pdf/xref/CrossReferenceStream.java +++ b/fop-core/src/main/java/org/apache/fop/pdf/xref/CrossReferenceStream.java @@ -47,7 +47,13 @@ public class CrossReferenceStream extends CrossReferenceObject { private final List objectReferences; - public CrossReferenceStream(PDFDocument document, + public CrossReferenceStream(PDFDocument document, TrailerDictionary trailerDictionary, long startxref, + List uncompressedObjectReferences, List compressedObjectReferences) { + this(document, document.getObjectCount() + 1, trailerDictionary, startxref, + uncompressedObjectReferences, compressedObjectReferences); + } + + protected CrossReferenceStream(PDFDocument document, int objectNumber, TrailerDictionary trailerDictionary, long startxref, @@ -56,7 +62,7 @@ public class CrossReferenceStream extends CrossReferenceObject { super(trailerDictionary, startxref); this.document = document; this.objectNumber = objectNumber; - this.objectReferences = new ArrayList(uncompressedObjectReferences.size()); + this.objectReferences = new ArrayList<>(uncompressedObjectReferences.size()); for (Long offset : uncompressedObjectReferences) { objectReferences.add(offset == null ? null : new UncompressedObjectReference(offset)); } diff --git a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererConfig.java b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererConfig.java index 075e6037e..358cbab21 100644 --- a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererConfig.java +++ b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererConfig.java @@ -62,6 +62,7 @@ import static org.apache.fop.render.pdf.PDFRendererOption.FORM_XOBJECT; 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; @@ -155,6 +156,7 @@ public final class PDFRendererConfig implements RendererConfig { parseAndPut(MERGE_FORM_FIELDS, cfg); parseAndPut(LINEARIZATION, cfg); parseAndPut(FORM_XOBJECT, cfg); + parseAndPut(OBJECT_STREAMS, cfg); parseAndPut(VERSION, cfg); configureSignParams(cfg); } catch (ConfigurationException e) { diff --git a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOption.java b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOption.java index f39d4e08b..20b97772a 100644 --- a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOption.java +++ b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOption.java @@ -105,6 +105,12 @@ public enum PDFRendererOption implements RendererConfigOption { 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 diff --git a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOptionsConfig.java b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOptionsConfig.java index 250857709..4ebd2da48 100644 --- a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOptionsConfig.java +++ b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOptionsConfig.java @@ -38,6 +38,7 @@ import static org.apache.fop.render.pdf.PDFRendererOption.FORM_XOBJECT; 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; @@ -157,4 +158,8 @@ public final class PDFRendererOptionsConfig { public Boolean getFormXObjectEnabled() { return (Boolean)properties.get(FORM_XOBJECT); } + + public Boolean getObjectStreamsEnabled() { + return (Boolean)properties.get(OBJECT_STREAMS); + } } diff --git a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRenderingUtil.java b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRenderingUtil.java index 8a6ebe5be..5d565cb47 100644 --- a/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRenderingUtil.java +++ b/fop-core/src/main/java/org/apache/fop/render/pdf/PDFRenderingUtil.java @@ -635,6 +635,7 @@ class PDFRenderingUtil { pdfDoc.setMergeFormFieldsEnabled(rendererConfig.getMergeFormFieldsEnabled()); pdfDoc.setLinearizationEnabled(rendererConfig.getLinearizationEnabled()); pdfDoc.setFormXObjectEnabled(rendererConfig.getFormXObjectEnabled()); + pdfDoc.setObjectStreamsEnabled(rendererConfig.getObjectStreamsEnabled()); return this.pdfDoc; } diff --git a/fop-core/src/test/java/org/apache/fop/pdf/PDFObjectStreamTestCase.java b/fop-core/src/test/java/org/apache/fop/pdf/PDFObjectStreamTestCase.java new file mode 100644 index 000000000..702c320a8 --- /dev/null +++ b/fop-core/src/test/java/org/apache/fop/pdf/PDFObjectStreamTestCase.java @@ -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> filterMap = new HashMap<>(); + List 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")); + } +}