]> source.dussan.org Git - xmlgraphics-fop.git/commitdiff
FOP-3215: Add support for PDF object streams
authorSimon Steiner <ssteiner@apache.org>
Tue, 22 Oct 2024 08:47:25 +0000 (09:47 +0100)
committerSimon Steiner <ssteiner@apache.org>
Tue, 22 Oct 2024 08:47:25 +0000 (09:47 +0100)
12 files changed:
fop-core/src/main/java/org/apache/fop/pdf/AbstractPDFStream.java
fop-core/src/main/java/org/apache/fop/pdf/PDFDocument.java
fop-core/src/main/java/org/apache/fop/pdf/PDFNumber.java
fop-core/src/main/java/org/apache/fop/pdf/PDFObject.java
fop-core/src/main/java/org/apache/fop/pdf/PDFSignature.java
fop-core/src/main/java/org/apache/fop/pdf/PDFStructElem.java
fop-core/src/main/java/org/apache/fop/pdf/xref/CrossReferenceStream.java
fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererConfig.java
fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOption.java
fop-core/src/main/java/org/apache/fop/render/pdf/PDFRendererOptionsConfig.java
fop-core/src/main/java/org/apache/fop/render/pdf/PDFRenderingUtil.java
fop-core/src/test/java/org/apache/fop/pdf/PDFObjectStreamTestCase.java [new file with mode: 0644]

index 8dba4981d39d949c4059794c98cfedf7c498b48b..c70b812c3ce0a560799031cbf13b4cbcf313656f 100644 (file)
@@ -298,4 +298,8 @@ public abstract class AbstractPDFStream extends PDFObject {
             getDocument().registerObject(refLength);
         }
     }
+
+    public boolean supportsObjectStream() {
+        return false;
+    }
 }
index 34421858ae20cf79d6124cc9a8492173cbd3a0fa..12f14354ed9e58893e399ce4c571406467a73ebe 100644 (file)
@@ -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<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) {
@@ -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;
+    }
 }
index 26489c6ec527b38fc393b263abfd571c72256e17..5b362f1bed756b64863727d7b2b891ca165dcb18 100644 (file)
@@ -120,5 +120,8 @@ public class PDFNumber extends PDFObject {
         return sb.toString();
     }
 
+    public boolean supportsObjectStream() {
+        return false;
+    }
 }
 
index 12402d72c6eeb53e3a5caf43b9c4f166f4d46bee..e1e42906236d30de9d411706a7b9ebc185c107a8 100644 (file)
@@ -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<PDFObject> children) {
     }
+
+    public boolean supportsObjectStream() {
+        return true;
+    }
 }
index 5bdf5ebdff0ff720ecbda2c19df4778b7f868b21..1ff785553f0f041683a25479b1f3ff27169870f1 100644 (file)
@@ -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");
         }
     }
 
index 4f4845cfcc06f8b2811ac2c55f4123901d9c31c5..cc299fa1d1e08c6ec746fe150b02409073c8b12d 100644 (file)
@@ -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<StructureType> BLSE = Arrays.asList(StandardStructureTypes.Table.TABLE,
             StandardStructureTypes.List.L, StandardStructureTypes.Paragraphlike.P);
 
index 9dbd317a7fa45e52619344a7aa982879d190004a..c7c3765d5f3a3c57ceeb950ef32f8d494e1ef922 100644 (file)
@@ -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,
@@ -56,7 +62,7 @@ public class CrossReferenceStream extends CrossReferenceObject {
         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));
         }
index 075e6037e7078f4eb34276f4435b35771d2923bc..358cbab217e6e4f1633f560987ba9b12c19d0b10 100644 (file)
@@ -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) {
index f39d4e08bce02986bbb8f9e8fc9cc5e325cd4e12..20b97772a10cdeaaff0bc3181048c89c826d7839 100644 (file)
@@ -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
index 250857709a98a664ac26b8d5f1a8323436f80b15..4ebd2da489bb4731e12fa154b6322e715a6dba12 100644 (file)
@@ -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);
+    }
 }
index 8a6ebe5be702bd9f6e17d7ac85d0122331dfd070..5d565cb47974665c7334e3ca78773d7528090275 100644 (file)
@@ -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 (file)
index 0000000..702c320
--- /dev/null
@@ -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"));
+    }
+}