FOP-2882: Allow PDFFormXObject to improve performance

git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop-pdf-images/trunk@1866665 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/lib/fop.jar b/lib/fop.jar
index 3242ade..844011d 100644
--- a/lib/fop.jar
+++ b/lib/fop.jar
Binary files differ
diff --git a/src/java/org/apache/fop/render/pdf/pdfbox/AbstractPDFBoxHandler.java b/src/java/org/apache/fop/render/pdf/pdfbox/AbstractPDFBoxHandler.java
index 3612bef..758f1b6 100644
--- a/src/java/org/apache/fop/render/pdf/pdfbox/AbstractPDFBoxHandler.java
+++ b/src/java/org/apache/fop/render/pdf/pdfbox/AbstractPDFBoxHandler.java
@@ -49,7 +49,7 @@
  */
 public abstract class AbstractPDFBoxHandler {
 
-    protected String createStreamForPDF(ImagePDF image, PDFPage targetPage, FOUserAgent userAgent,
+    protected Object createStreamForPDF(ImagePDF image, PDFPage targetPage, FOUserAgent userAgent,
                                         AffineTransform at, FontInfo fontinfo, Rectangle pos,
                                         Map<Integer, PDFArray> pageNumbers,
                                         PDFLogicalStructureHandler handler,
@@ -108,8 +108,7 @@
         if (handler != null) {
             adapter.setCurrentMCID(handler.getPageParentTree().length());
         }
-        String stream = adapter.createStreamFromPDFBoxPage(pddoc, page, originalImageUri,
-                 at, fontinfo, pos);
+        Object stream = adapter.createStreamFromPDFBoxPage(pddoc, page, originalImageUri, at, fontinfo, pos);
         if (userAgent.isAccessibilityEnabled() && curentSessionElem != null) {
             TaggedPDFConductor conductor = new TaggedPDFConductor(curentSessionElem, handler, page, adapter);
             conductor.handleLogicalStructure(pddoc);
diff --git a/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapter.java b/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapter.java
index 55cf5af..0841c6d 100644
--- a/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapter.java
+++ b/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxAdapter.java
@@ -21,6 +21,7 @@
 
 import java.awt.Rectangle;
 import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -69,6 +70,7 @@
 import org.apache.fop.pdf.PDFArray;
 import org.apache.fop.pdf.PDFDictionary;
 import org.apache.fop.pdf.PDFDocument;
+import org.apache.fop.pdf.PDFFormXObject;
 import org.apache.fop.pdf.PDFName;
 import org.apache.fop.pdf.PDFNumber;
 import org.apache.fop.pdf.PDFObject;
@@ -341,8 +343,8 @@
      * @return the stream
      * @throws IOException if an I/O error occurs
      */
-    public String createStreamFromPDFBoxPage(PDDocument sourceDoc, PDPage page, String key,
-                                             AffineTransform atdoc, FontInfo fontinfo, Rectangle pos)
+    public Object createStreamFromPDFBoxPage(PDDocument sourceDoc, PDPage page, String key,
+                                                     AffineTransform atdoc, FontInfo fontinfo, Rectangle pos)
         throws IOException {
         handleAnnotations(sourceDoc, page, atdoc);
         if (pageNumbers.containsKey(targetPage.getPageIndex())) {
@@ -359,24 +361,18 @@
             fontsBackup = new COSDictionary(fonts);
             MergeFontsPDFWriter m = new MergeFontsPDFWriter(fonts, fontinfo, uniqueName, parentFonts, currentMCID);
             newStream = m.writeText(pdStream);
-//            if (newStream != null) {
-//                for (Object f : fonts.keySet().toArray()) {
-//                    COSDictionary fontdata = (COSDictionary)fonts.getDictionaryObject((COSName)f);
-//                    if (getUniqueFontName(fontdata) != null) {
-//                        fonts.removeItem((COSName)f);
-//                    }
-//                }
-//            }
         }
-        if (newStream == null) {
-            newStream = (String) clonedVersion.get(key);
+        if (!pdfDoc.isFormXObjectEnabled()) {
             if (newStream == null) {
-                PDFWriter writer = new PDFWriter(uniqueName, currentMCID);
-                newStream = writer.writeText(pdStream);
-                clonedVersion.put(key, newStream);
+                newStream = (String) clonedVersion.get(key);
+                if (newStream == null) {
+                    PDFWriter writer = new PDFWriter(uniqueName, currentMCID);
+                    newStream = writer.writeText(pdStream);
+                    clonedVersion.put(key, newStream);
+                }
             }
+            pdStream = new PDStream(sourceDoc, new ByteArrayInputStream(newStream.getBytes("ISO-8859-1")));
         }
-        pdStream = new PDStream(sourceDoc, new ByteArrayInputStream(newStream.getBytes("ISO-8859-1")));
         mergeXObj(sourcePageResources, fontinfo, uniqueName);
         PDFDictionary pageResources = (PDFDictionary)cloneForNewDocument(sourcePageResources);
 
@@ -419,6 +415,11 @@
         if (pageStream == null) {
             pageStream = new PDFStream();
         }
+
+        if (pdfDoc.isFormXObjectEnabled()) {
+            return getFormXObject(pageResources, pageStream, key, page);
+        }
+
         if (originalPageContents != null) {
             transferDict(originalPageContents, pageStream, filter);
         }
@@ -464,6 +465,57 @@
         return pdStream;
     }
 
+    private PDFFormXObject getFormXObject(PDFDictionary pageResources, PDFStream pageStream, String key, PDPage page)
+        throws IOException {
+        if (pdfDoc.isMergeFontsEnabled()) {
+            throw new RuntimeException("merge-fonts and form-xobject can't both be enabled");
+        }
+        if (!pageResources.hasObjectNumber()) {
+            pdfDoc.registerObject(pageResources);
+        }
+        PDFFormXObject form = pdfDoc.addFormXObject(null, pageStream, pageResources.makeReference(), key);
+        final Set<String> page2Form = new HashSet<String>(Arrays.asList("Group", "LastModified", "Metadata"));
+        transferDict(page.getCOSObject(), pageStream, page2Form, true);
+
+        AffineTransform at = form.getMatrix();
+        PDRectangle mediaBox = page.getMediaBox();
+        PDRectangle cropBox = page.getCropBox();
+        PDRectangle viewBox = cropBox != null ? cropBox : mediaBox;
+
+        //Handle the /Rotation entry on the page dict
+        int rotation = PDFUtil.getNormalizedRotation(page);
+
+        //Transform to FOP's user space
+        at.scale(1 / viewBox.getWidth(), 1 / viewBox.getHeight());
+        at.translate(mediaBox.getLowerLeftX() - viewBox.getLowerLeftX(),
+                mediaBox.getLowerLeftY() - viewBox.getLowerLeftY());
+        switch (rotation) {
+            case 90:
+                at.scale(viewBox.getWidth() / viewBox.getHeight(), viewBox.getHeight() / viewBox.getWidth());
+                at.translate(0, viewBox.getWidth());
+                at.rotate(-Math.PI / 2.0);
+                break;
+            case 180:
+                at.translate(viewBox.getWidth(), viewBox.getHeight());
+                at.rotate(-Math.PI);
+                break;
+            case 270:
+                at.scale(viewBox.getWidth() / viewBox.getHeight(), viewBox.getHeight() / viewBox.getWidth());
+                at.translate(viewBox.getHeight(), 0);
+                at.rotate(-Math.PI * 1.5);
+                break;
+            default:
+                //no additional transformations necessary
+                break;
+        }
+        form.setMatrix(at);
+
+        form.setBBox(new Rectangle2D.Float(
+                viewBox.getLowerLeftX(), viewBox.getLowerLeftY(),
+                viewBox.getUpperRightX(), viewBox.getUpperRightY()));
+        return form;
+    }
+
     private COSDictionary getResources(PDPage page) {
         PDResources res = page.getResources();
         if (res == null) {
diff --git a/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxImageHandler.java b/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxImageHandler.java
index 3e20aa7..88631f7 100644
--- a/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxImageHandler.java
+++ b/src/java/org/apache/fop/render/pdf/pdfbox/PDFBoxImageHandler.java
@@ -30,6 +30,7 @@
 import org.apache.xmlgraphics.image.loader.ImageFlavor;
 
 import org.apache.fop.fonts.FontInfo;
+import org.apache.fop.pdf.PDFXObject;
 import org.apache.fop.render.ImageHandler;
 import org.apache.fop.render.RenderingContext;
 import org.apache.fop.render.pdf.PDFContentGenerator;
@@ -58,6 +59,7 @@
         try {
             float x = (float)pos.getX() / 1000f;
             float y = (float)pos.getY() / 1000f;
+            float w = (float)pos.getWidth() / 1000f;
             float h = (float)pos.getHeight() / 1000f;
 
             AffineTransform pageAdjust = new AffineTransform();
@@ -68,17 +70,21 @@
                     (float)(generator.getState().getTransform().getTranslateY() - h - y));
             }
             FontInfo fontinfo = (FontInfo)context.getHint("fontinfo");
-            String stream = createStreamForPDF(pdfImage, pdfContext.getPage(), pdfContext.getUserAgent(),
+            Object stream = createStreamForPDF(pdfImage, pdfContext.getPage(), pdfContext.getUserAgent(),
                     pageAdjust, fontinfo, pos, pdfContext.getPageNumbers(),
                     pdfContext.getPdfLogicalStructureHandler(), pdfContext.getCurrentSessionStructElem());
 
             if (stream == null) {
                 return;
             }
-            if (pageAdjust.getScaleX() != 0) {
-                pageAdjust.translate(x * (1 / pageAdjust.getScaleX()), -y * (1 / -pageAdjust.getScaleY()));
+            if (stream instanceof String) {
+                if (pageAdjust.getScaleX() != 0) {
+                    pageAdjust.translate(x * (1 / pageAdjust.getScaleX()), -y * (1 / -pageAdjust.getScaleY()));
+                }
+                generator.placeImage(pageAdjust, (String) stream);
+            } else {
+                generator.placeImage(x, y, w, h, (PDFXObject) stream);
             }
-            generator.placeImage(pageAdjust, stream);
             pdfImage.close();
         } catch (Throwable t) {
             throw new RuntimeException(
diff --git a/test/java/org/apache/fop/render/pdf/PDFBoxAdapterTestCase.java b/test/java/org/apache/fop/render/pdf/PDFBoxAdapterTestCase.java
index cb78dd2..5ffd10b 100644
--- a/test/java/org/apache/fop/render/pdf/PDFBoxAdapterTestCase.java
+++ b/test/java/org/apache/fop/render/pdf/PDFBoxAdapterTestCase.java
@@ -74,6 +74,7 @@
 import org.apache.fop.pdf.PDFDictionary;
 import org.apache.fop.pdf.PDFDocument;
 import org.apache.fop.pdf.PDFFilterList;
+import org.apache.fop.pdf.PDFFormXObject;
 import org.apache.fop.pdf.PDFGState;
 import org.apache.fop.pdf.PDFPage;
 import org.apache.fop.pdf.PDFResources;
@@ -125,10 +126,11 @@
         return new PDFPage(new PDFResources(doc), 0, r, r, r, r);
     }
 
-    protected static PDFBoxAdapter getPDFBoxAdapter(boolean mergeFonts) {
+    protected static PDFBoxAdapter getPDFBoxAdapter(boolean mergeFonts, boolean formXObject) {
         PDFDocument doc = new PDFDocument("");
         PDFPage pdfpage = getPDFPage(doc);
         doc.setMergeFontsEnabled(mergeFonts);
+        doc.setFormXObjectEnabled(formXObject);
         pdfpage.setDocument(doc);
         pdfpage.setObjectNumber(1);
         return new PDFBoxAdapter(pdfpage, new HashMap(), new HashMap<Integer, PDFArray>());
@@ -221,24 +223,21 @@
         PDDocument doc = PDDocument.load(new File(pdf));
         PDPage page = doc.getPage(0);
         AffineTransform at = new AffineTransform();
-        String c = getPDFBoxAdapter(true).createStreamFromPDFBoxPage(doc, page, pdf, at, fi, new Rectangle());
-//        PDResources sourcePageResources = page.findResources();
-//        COSDictionary fonts = (COSDictionary)sourcePageResources.getCOSDictionary().getDictionaryObject(COSName.FONT);
-//        PDFBoxAdapter.PDFWriter w = adapter. new MergeFontsPDFWriter(fonts, fi, "", new ArrayList<COSName>());
-//        String c = w.writeText(page.getContents());
+        String c = (String) getPDFBoxAdapter(true, false)
+                .createStreamFromPDFBoxPage(doc, page, pdf, at, fi, new Rectangle());
         doc.close();
         return c;
     }
 
     @Test
     public void testTaggedPDFWriter() throws IOException {
-        PDFBoxAdapter adapter = getPDFBoxAdapter(false);
+        PDFBoxAdapter adapter = getPDFBoxAdapter(false, false);
         adapter.setCurrentMCID(5);
         PDDocument doc = PDDocument.load(new File(HELLOTagged));
         PDPage page = doc.getPage(0);
         AffineTransform at = new AffineTransform();
         Rectangle r = new Rectangle(0, 1650, 842000, 595000);
-        String stream = adapter.createStreamFromPDFBoxPage(doc, page, "key", at, null, r);
+        String stream = (String) adapter.createStreamFromPDFBoxPage(doc, page, "key", at, null, r);
         Assert.assertTrue(stream, stream.contains("/P <</MCID 5 >>BDC"));
         doc.close();
     }
@@ -265,7 +264,7 @@
 
     @Test
     public void testAnnot2() throws Exception {
-        PDFBoxAdapter adapter = getPDFBoxAdapter(false);
+        PDFBoxAdapter adapter = getPDFBoxAdapter(false, false);
         PDDocument doc = PDDocument.load(new File(ANNOT));
         PDPage page = doc.getPage(0);
         COSArray annots = (COSArray) page.getCOSObject().getDictionaryObject(COSName.ANNOTS);
@@ -290,7 +289,7 @@
         PDPage page = doc.getPage(0);
         AffineTransform at = new AffineTransform();
         Rectangle r = new Rectangle(0, 1650, 842000, 595000);
-        String stream = adapter.createStreamFromPDFBoxPage(doc, page, "key", at, null, r);
+        String stream = (String) adapter.createStreamFromPDFBoxPage(doc, page, "key", at, null, r);
         Assert.assertTrue(stream.contains("/Link <</MCID 5 >>BDC"));
         Assert.assertEquals(pageNumbers.size(), 4);
         PDFAnnotList annots = (PDFAnnotList) pdfpage.get("Annots");
@@ -518,7 +517,7 @@
                     pdfpage, objectCachePerFile, new HashMap<Integer, PDFArray>(), pdfCache);
             PDDocument doc = PDDocument.load(new File(pdf));
             PDPage page = doc.getPage(0);
-            String stream = adapter.createStreamFromPDFBoxPage(
+            String stream = (String) adapter.createStreamFromPDFBoxPage(
                     doc, page, pdf, new AffineTransform(), null, new Rectangle());
             doc.close();
             return stream;
@@ -566,7 +565,7 @@
         PDPage page = doc.getPage(0);
         page.setResources(null);
         AffineTransform at = new AffineTransform();
-        getPDFBoxAdapter(false).createStreamFromPDFBoxPage(doc, page, CFF1, at, new FontInfo(), new Rectangle());
+        getPDFBoxAdapter(false, false).createStreamFromPDFBoxPage(doc, page, CFF1, at, new FontInfo(), new Rectangle());
         doc.close();
     }
 
@@ -586,4 +585,33 @@
         c.setPageNumbers(new HashMap<Integer, PDFArray>());
         new PDFBoxImageHandler().handleImage(c, img, new Rectangle());
     }
+
+    @Test
+    public void testMergeFontsAndFormXObject() throws IOException {
+        String msg = "";
+        PDDocument doc = PDDocument.load(new File(IMAGE));
+        PDPage page = doc.getPage(0);
+        AffineTransform at = new AffineTransform();
+        try {
+            getPDFBoxAdapter(true, true)
+                    .createStreamFromPDFBoxPage(doc, page, IMAGE, at, new FontInfo(), new Rectangle());
+        } catch (RuntimeException e) {
+            msg = e.getMessage();
+        }
+        doc.close();
+        Assert.assertEquals(msg, "merge-fonts and form-xobject can't both be enabled");
+    }
+
+    @Test
+    public void testFormXObject() throws IOException {
+        PDDocument doc = PDDocument.load(new File(IMAGE));
+        PDPage page = doc.getPage(0);
+        AffineTransform at = new AffineTransform();
+        PDFFormXObject formXObject = (PDFFormXObject) getPDFBoxAdapter(false, true)
+                .createStreamFromPDFBoxPage(doc, page, IMAGE, at, new FontInfo(), new Rectangle());
+        doc.close();
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        formXObject.output(bos);
+        Assert.assertTrue(bos.toString("UTF-8").contains("/Type /XObject"));
+    }
 }
diff --git a/test/java/org/apache/fop/render/pdf/PDFRotateTestCase.java b/test/java/org/apache/fop/render/pdf/PDFRotateTestCase.java
index 08561bf..fc69e8a 100644
--- a/test/java/org/apache/fop/render/pdf/PDFRotateTestCase.java
+++ b/test/java/org/apache/fop/render/pdf/PDFRotateTestCase.java
@@ -75,13 +75,13 @@
     }
 
     private AffineTransform getTransform(int angle) throws IOException {
-        PDFBoxAdapter adapter = PDFBoxAdapterTestCase.getPDFBoxAdapter(false);
+        PDFBoxAdapter adapter = PDFBoxAdapterTestCase.getPDFBoxAdapter(false, false);
         PDDocument doc = PDDocument.load(new File(PDFBoxAdapterTestCase.ROTATE));
         PDPage page = doc.getPage(0);
         page.setRotation(angle);
         AffineTransform at = new AffineTransform();
         Rectangle r = new Rectangle(0, 1650, 842000, 595000);
-        String stream = adapter.createStreamFromPDFBoxPage(doc, page, "key", at, null, r);
+        String stream = (String) adapter.createStreamFromPDFBoxPage(doc, page, "key", at, null, r);
         Assert.assertTrue(stream.contains("/GS0106079 gs"));
         Assert.assertTrue(stream.contains("/TT0106079 1 Tf"));
         doc.close();