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 8dba498..c70b812 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 @@
             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 3442185..12f1435 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 @@
 
     private FileIDGenerator fileIDGenerator;
 
+    private ObjectStreamManager objectStreamManager;
+
     private boolean accessibilityEnabled;
 
     private boolean mergeFontsEnabled;
@@ -185,6 +187,8 @@
 
     protected boolean outputStarted;
 
+    private boolean objectStreamsEnabled;
+
     /**
      * Creates an empty PDF document.
      *
@@ -1027,15 +1031,36 @@
         //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 @@
     }
 
     private void outputTrailerObjectsAndXref(OutputStream stream) throws IOException {
-        TrailerOutputHelper trailerOutputHelper = mayCompressStructureTreeElements()
+        TrailerOutputHelper trailerOutputHelper = useObjectStreams()
                 ? new CompressedTrailerOutputHelper()
                 : new UncompressedTrailerOutputHelper();
         if (structureTreeElements != null) {
@@ -1170,10 +1195,15 @@
         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 @@
     }
 
     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 @@
                 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 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 26489c6..5b362f1 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 @@
         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 12402d7..e1e4290 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 @@
  * 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 void getChildren(Set<PDFObject> 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 5bdf5eb..1ff7855 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 @@
                 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 4f4845c..cc299fa 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 @@
 /**
  * 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);
 
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 9dbd317..c7c3765 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 @@
 
     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 @@
         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));
         }
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 075e603..358cbab 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.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 @@
                 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 f39d4e0..20b9777 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 @@
             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 2508577..4ebd2da 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.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 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 8a6ebe5..5d565cb 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 @@
         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 0000000..702c320
--- /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<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"));
+    }
+}