]> source.dussan.org Git - xmlgraphics-fop.git/commitdiff
Added support for an optional two-pass production for PostScript output to minimize...
authorJeremias Maerki <jeremias@apache.org>
Fri, 2 Mar 2007 17:30:16 +0000 (17:30 +0000)
committerJeremias Maerki <jeremias@apache.org>
Fri, 2 Mar 2007 17:30:16 +0000 (17:30 +0000)
A simple test file with a few images and a few configured fonts of which most are not used shows, that with the two-pass approach the file size of the generated PostScript file decreased from 510KB to 160KB. Note that enabling this feature will increase the memory requirements in the PostScript interpreter when there are many images. Documentation follows.

git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@513857 13f79535-47bb-0310-9956-ffa450edef68

lib/xmlgraphics-commons-1.1.jar [deleted file]
lib/xmlgraphics-commons-1.2svn.jar [new file with mode: 0644]
src/foschema/fop-configuration.xsd
src/java/org/apache/fop/render/ps/PSFontUtils.java
src/java/org/apache/fop/render/ps/PSImageFormResource.java [new file with mode: 0644]
src/java/org/apache/fop/render/ps/PSImageUtils.java
src/java/org/apache/fop/render/ps/PSRenderer.java
src/java/org/apache/fop/render/ps/ResourceHandler.java [new file with mode: 0644]
status.xml

diff --git a/lib/xmlgraphics-commons-1.1.jar b/lib/xmlgraphics-commons-1.1.jar
deleted file mode 100644 (file)
index 7fd9648..0000000
Binary files a/lib/xmlgraphics-commons-1.1.jar and /dev/null differ
diff --git a/lib/xmlgraphics-commons-1.2svn.jar b/lib/xmlgraphics-commons-1.2svn.jar
new file mode 100644 (file)
index 0000000..becb45a
Binary files /dev/null and b/lib/xmlgraphics-commons-1.2svn.jar differ
index ad959f9473065071d6ff96ade09ad840bfffa7ba..c4d9ac47f07e0a25cc3dc05f5f5a706b93acdfd9 100644 (file)
             <xsd:documentation>Configuration elements used by the PostScript renderer,
           MIME type application/postscript</xsd:documentation>
           </xsd:annotation>
-          <xsd:element name="auto-rotate-landscape">
+          <xsd:element name="auto-rotate-landscape" type="xsd:boolean" default="false" minOccurs="0">
             <xsd:annotation>
-              <xsd:documentation>auto-rotate-landscape is used by the PostScript renderer,
-            MIME type application/postscript.</xsd:documentation>
+              <xsd:documentation>When set to "true" a landscape page is automatically 
+                rotated and specified as a landscape page in PostScript.</xsd:documentation>
+            </xsd:annotation>
+          </xsd:element>
+          <xsd:element name="language-level" default="3" minOccurs="0">
+            <xsd:annotation>
+              <xsd:documentation>Specifies the PostScript language level to use when 
+                generating PostScript code.
+                language-level is used by the PostScript renderer,
+                MIME type application/postscript.</xsd:documentation>
             </xsd:annotation>
             <xsd:simpleType>
-              <xsd:restriction base="xsd:string">
-                <xsd:enumeration value="false"/>
-                <xsd:enumeration value="true"/>
+              <xsd:restriction base="xsd:positiveInteger">
+                <xsd:enumeration value="2"/>
+                <xsd:enumeration value="3"/>
               </xsd:restriction>
             </xsd:simpleType>
           </xsd:element>
+          <xsd:element name="optimize-resources" type="xsd:boolean" default="false" minOccurs="0">
+            <xsd:annotation>
+              <xsd:documentation>When set to "true" PostScript resources are optimized by making a 
+                second pass over the PostScript file (rewriting it). Optimized means that no duplicate 
+                images are written to the stream and only used fonts are added to the PostScript file.</xsd:documentation>
+            </xsd:annotation>
+          </xsd:element>
         </xsd:sequence>
         <xsd:sequence>
           <xsd:annotation>
     <xsd:sequence>
       <xsd:element name="font-triplet" type="fontTripletType" maxOccurs="unbounded"/>
     </xsd:sequence>
-    <xsd:attribute name="metrics-url" type="xsd:anyURI" use="required"/>
+    <xsd:attribute name="metrics-url" type="xsd:anyURI" use="optional"/>
     <xsd:attribute name="embed-url" type="xsd:anyURI" use="optional"/>
     <xsd:attribute name="kerning" use="optional" default="no">
       <xsd:simpleType>
index 5ece1fae908b598098c136d71e49335b816bda6f..c568fe8269eb03229109a76899e50ada7df36e94 100644 (file)
@@ -29,6 +29,8 @@ import java.util.Map;
 import javax.xml.transform.Source;
 import javax.xml.transform.stream.StreamSource;
 
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
 import org.apache.fop.fonts.CustomFont;
 import org.apache.fop.fonts.Font;
 import org.apache.fop.fonts.FontInfo;
@@ -38,12 +40,16 @@ import org.apache.fop.fonts.Typeface;
 import org.apache.xmlgraphics.ps.DSCConstants;
 import org.apache.xmlgraphics.ps.PSGenerator;
 import org.apache.xmlgraphics.ps.PSResource;
+import org.apache.xmlgraphics.ps.dsc.ResourceTracker;
 
 /**
  * Utility code for font handling in PostScript.
  */
 public class PSFontUtils extends org.apache.xmlgraphics.ps.PSFontUtils {
 
+    /** logging instance */
+    protected static Log log = LogFactory.getLog(PSFontUtils.class);
+    
     /**
      * Generates the PostScript code for the font dictionary.
      * @param gen PostScript generator to use for output
@@ -53,59 +59,41 @@ public class PSFontUtils extends org.apache.xmlgraphics.ps.PSFontUtils {
      */
     public static Map writeFontDict(PSGenerator gen, FontInfo fontInfo) 
                 throws IOException {
+        return writeFontDict(gen, fontInfo, fontInfo.getFonts());
+    }
+    
+    /**
+     * Generates the PostScript code for the font dictionary.
+     * @param gen PostScript generator to use for output
+     * @param fontInfo available fonts
+     * @param fonts the set of fonts to work with
+     * @return a Map of PSResource instances representing all defined fonts (key: font key)
+     * @throws IOException in case of an I/O problem
+     */
+    public static Map writeFontDict(PSGenerator gen, FontInfo fontInfo, Map fonts) 
+                throws IOException {
         gen.commentln("%FOPBeginFontDict");
-        gen.writeln("/FOPFonts 100 dict dup begin");
 
-        // write("/gfF1{/Helvetica findfont} bd");
-        // write("/gfF3{/Helvetica-Bold findfont} bd");
-        Map fonts = fontInfo.getFonts();
         Map fontResources = new java.util.HashMap();
         Iterator iter = fonts.keySet().iterator();
         while (iter.hasNext()) {
             String key = (String)iter.next();
-            Typeface tf = (Typeface)fonts.get(key);
-            if (tf instanceof LazyFont) {
-                tf = ((LazyFont)tf).getRealFont();
-            }
-            if (tf == null) {
-                //This is to avoid an NPE if a malconfigured font is in the configuration but not
-                //used in the document. If it were used, we wouldn't get this far.
-                String fallbackKey = fontInfo.getInternalFontKey(Font.DEFAULT_FONT); 
-                tf = (Typeface)fonts.get(fallbackKey);
-            }
+            Typeface tf = getTypeFace(fontInfo, fonts, key);
             PSResource fontRes = new PSResource("font", tf.getFontName());
             fontResources.put(key, fontRes);
-            boolean embeddedFont = false;
-            if (FontType.TYPE1 == tf.getFontType()) {
-                if (tf instanceof CustomFont) {
-                    CustomFont cf = (CustomFont)tf;
-                    InputStream in = getInputStreamOnFont(gen, cf);
-                    if (in != null) {
-                        gen.writeDSCComment(DSCConstants.BEGIN_RESOURCE, 
-                                fontRes);
-                        embedType1Font(gen, in);
-                        gen.writeDSCComment(DSCConstants.END_RESOURCE);
-                        gen.notifyResourceUsage(fontRes, false);
-                        embeddedFont = true;
-                    }
-                }
-            }
-            if (!embeddedFont) {
-                gen.writeDSCComment(DSCConstants.INCLUDE_RESOURCE, fontRes);
-                //Resource usage shall be handled by renderer
-                //gen.notifyResourceUsage(fontRes, true);
-            }
-            gen.commentln("%FOPBeginFontKey: " + key);
-            gen.writeln("/" + key + " /" + tf.getFontName() + " def");
-            gen.commentln("%FOPEndFontKey");
+            embedFont(gen, tf, fontRes);
         }
-        gen.writeln("end def");
         gen.commentln("%FOPEndFontDict");
+        reencodeFonts(gen, fonts);
+        return fontResources;
+    }
+
+    private static void reencodeFonts(PSGenerator gen, Map fonts) throws IOException {
         gen.commentln("%FOPBeginFontReencode");
         defineWinAnsiEncoding(gen);
         
         //Rewrite font encodings
-        iter = fonts.keySet().iterator();
+        Iterator iter = fonts.keySet().iterator();
         while (iter.hasNext()) {
             String key = (String)iter.next();
             Typeface fm = (Typeface)fonts.get(key);
@@ -115,25 +103,72 @@ public class PSFontUtils extends org.apache.xmlgraphics.ps.PSFontUtils {
                 //ignore (ZapfDingbats and Symbol run through here
                 //TODO: ZapfDingbats and Symbol should get getEncoding() fixed!
             } else if ("WinAnsiEncoding".equals(fm.getEncoding())) {
-                gen.writeln("/" + fm.getFontName() + " findfont");
-                gen.writeln("dup length dict begin");
-                gen.writeln("  {1 index /FID ne {def} {pop pop} ifelse} forall");
-                gen.writeln("  /Encoding " + fm.getEncoding() + " def");
-                gen.writeln("  currentdict");
-                gen.writeln("end");
-                gen.writeln("/" + fm.getFontName() + " exch definefont pop");
+                redefineFontEncoding(gen, fm.getFontName(), fm.getEncoding());
             } else {
                 gen.commentln("%WARNING: Only WinAnsiEncoding is supported. Font '" 
                     + fm.getFontName() + "' asks for: " + fm.getEncoding());
             }
         }
         gen.commentln("%FOPEndFontReencode");
-        return fontResources;
     }
 
+    private static Typeface getTypeFace(FontInfo fontInfo, Map fonts, String key) {
+        Typeface tf = (Typeface)fonts.get(key);
+        if (tf instanceof LazyFont) {
+            tf = ((LazyFont)tf).getRealFont();
+        }
+        if (tf == null) {
+            //This is to avoid an NPE if a malconfigured font is in the configuration but not
+            //used in the document. If it were used, we wouldn't get this far.
+            String fallbackKey = fontInfo.getInternalFontKey(Font.DEFAULT_FONT); 
+            tf = (Typeface)fonts.get(fallbackKey);
+        }
+        return tf;
+    }
+
+    /**
+     * Embeds a font in the PostScript file.
+     * @param gen the PostScript generator
+     * @param tf the font
+     * @param fontRes the PSResource associated with the font
+     * @throws IOException In case of an I/O error
+     */
+    public static void embedFont(PSGenerator gen, Typeface tf, PSResource fontRes) 
+                throws IOException {
+        boolean embeddedFont = false;
+        if (FontType.TYPE1 == tf.getFontType()) {
+            if (tf instanceof CustomFont) {
+                CustomFont cf = (CustomFont)tf;
+                if (isEmbeddable(cf)) {
+                    InputStream in = getInputStreamOnFont(gen, cf);
+                    if (in != null) {
+                        gen.writeDSCComment(DSCConstants.BEGIN_RESOURCE, 
+                                fontRes);
+                        embedType1Font(gen, in);
+                        gen.writeDSCComment(DSCConstants.END_RESOURCE);
+                        gen.getResourceTracker().registerSuppliedResource(fontRes);
+                        embeddedFont = true;
+                    } else {
+                        gen.commentln("%WARNING: Could not embed font: " + cf.getFontName());
+                        log.warn("Font " + cf.getFontName() + " is marked as supplied in the"
+                                + " PostScript file but could not be embedded!");
+                    }
+                }
+            }
+        }
+        if (!embeddedFont) {
+            gen.writeDSCComment(DSCConstants.INCLUDE_RESOURCE, fontRes);
+        }
+    }
+
+    private static boolean isEmbeddable(CustomFont font) {
+        return font.isEmbeddable() 
+                && (font.getEmbedFileName() != null || font.getEmbedResourceName() != null);
+    }
+    
     private static InputStream getInputStreamOnFont(PSGenerator gen, CustomFont font) 
                 throws IOException {
-        if (font.isEmbeddable()) {
+        if (isEmbeddable(font)) {
             Source source = font.getEmbedFileSource();
             if (source == null && font.getEmbedResourceName() != null) {
                 source = new StreamSource(PSFontUtils.class
@@ -168,4 +203,33 @@ public class PSFontUtils extends org.apache.xmlgraphics.ps.PSFontUtils {
         }
     }
 
+    /**
+     * Determines the set of fonts that will be supplied with the PS file and registers them
+     * with the resource tracker. All the fonts that are being processed are returned as a Map.
+     * @param resTracker the resource tracker
+     * @param fontInfo available fonts
+     * @param fonts the set of fonts to work with
+     * @return a Map of PSResource instances representing all defined fonts (key: font key)
+     */
+    public static Map determineSuppliedFonts(ResourceTracker resTracker, 
+            FontInfo fontInfo, Map fonts) {
+        Map fontResources = new java.util.HashMap();
+        Iterator iter = fonts.keySet().iterator();
+        while (iter.hasNext()) {
+            String key = (String)iter.next();
+            Typeface tf = getTypeFace(fontInfo, fonts, key);
+            PSResource fontRes = new PSResource("font", tf.getFontName());
+            fontResources.put(key, fontRes);
+            if (FontType.TYPE1 == tf.getFontType()) {
+                if (tf instanceof CustomFont) {
+                    CustomFont cf = (CustomFont)tf;
+                    if (isEmbeddable(cf)) {
+                        resTracker.registerSuppliedResource(fontRes);
+                    }
+                }
+            }
+        }
+        return fontResources;
+    }
+
 }
diff --git a/src/java/org/apache/fop/render/ps/PSImageFormResource.java b/src/java/org/apache/fop/render/ps/PSImageFormResource.java
new file mode 100644 (file)
index 0000000..b00e220
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * 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.render.ps;
+
+import org.apache.xmlgraphics.ps.PSResource;
+
+/**
+ * PostScript Resource class representing a FOP form. This is used by PSRenderer to keep track
+ * of images.
+ */
+public class PSImageFormResource extends PSResource {
+
+    private String uri;
+    
+    /**
+     * Create a new Form Resource.
+     * @param id An ID for the form
+     * @param uri the URI to the image
+     */
+    public PSImageFormResource(int id, String uri) {
+        this("FOPForm:" + Integer.toString(id), uri);
+    }
+    
+    /**
+    /**
+     * Create a new Form Resource.
+     * @param name the name of the resource
+     * @param uri the URI to the image
+     */
+    public PSImageFormResource(String name, String uri) {
+        super(PSResource.TYPE_FORM, name);
+        this.uri = uri;
+    }
+    
+    /**
+     * Returns the image URI.
+     * @return the image URI
+     */
+    public String getImageURI() {
+        return this.uri;
+    }
+    
+}
index a5b6675e30f2b1b544c82a2a5b9ad4e2a1b7dd2b..0ab3290778b8cb28d2e3702f48329db2592b0d23 100644 (file)
@@ -29,6 +29,7 @@ import org.apache.fop.image.EPSImage;
 import org.apache.fop.image.FopImage;
 import org.apache.fop.image.JpegImage;
 import org.apache.xmlgraphics.ps.PSGenerator;
+import org.apache.xmlgraphics.ps.PSResource;
 
 /**
  * Utility code for rendering images in PostScript. 
@@ -51,15 +52,69 @@ public class PSImageUtils extends org.apache.xmlgraphics.ps.PSImageUtils {
     public static void renderBitmapImage(FopImage img, 
                 float x, float y, float w, float h, PSGenerator gen)
                     throws IOException {
-        if (img instanceof JpegImage) {
+        boolean isJPEG = (img instanceof JpegImage && (gen.getPSLevel() >= 3));
+        byte[] imgmap = convertImageToRawBitmapArray(img, isJPEG);
+        if (imgmap == null) {
+            gen.commentln("%Image data is not available: " + img);
+            return; //Image cannot be converted 
+        }
+        
+        String imgDescription = img.getMimeType() + " " + img.getOriginalURI();
+        Dimension imgDim = new Dimension(img.getWidth(), img.getHeight());
+        Rectangle2D targetRect = new Rectangle2D.Double(x, y, w, h);
+        writeImage(imgmap, imgDim, imgDescription, targetRect, isJPEG, 
+                img.getColorSpace(), gen);
+    }
+
+    /**
+     * Renders a bitmap image (as form) to PostScript.
+     * @param img image to render
+     * @param form the form resource
+     * @param x x position
+     * @param y y position
+     * @param w width
+     * @param h height
+     * @param gen PS generator
+     * @throws IOException In case of an I/O problem while rendering the image
+     */
+    public static void renderForm(FopImage img, PSResource form, 
+                float x, float y, float w, float h, PSGenerator gen)
+                    throws IOException {
+        Rectangle2D targetRect = new Rectangle2D.Double(x, y, w, h);
+        paintForm(form, targetRect, gen);
+    }
+    
+    /**
+     * Generates a form resource for a FopImage in PostScript.
+     * @param img image to render
+     * @param form the form resource
+     * @param gen PS generator
+     * @throws IOException In case of an I/O problem while rendering the image
+     */
+    public static void generateFormResourceForImage(FopImage img, PSResource form,
+                PSGenerator gen) throws IOException {
+        boolean isJPEG = (img instanceof JpegImage && (gen.getPSLevel() >= 3));
+        byte[] imgmap = convertImageToRawBitmapArray(img, isJPEG);
+        if (imgmap == null) {
+            gen.commentln("%Image data is not available: " + img);
+            return; //Image cannot be converted 
+        }
+        
+        String imgDescription = img.getMimeType() + " " + img.getOriginalURI();
+        Dimension imgDim = new Dimension(img.getWidth(), img.getHeight());
+        writeReusableImage(imgmap, imgDim, form.getName(), imgDescription, isJPEG, 
+                img.getColorSpace(), gen);
+    }
+
+    private static byte[] convertImageToRawBitmapArray(FopImage img, boolean allowUndecodedJPEG)
+                throws IOException {
+        if (img instanceof JpegImage && allowUndecodedJPEG) {
             if (!img.load(FopImage.ORIGINAL_DATA)) {
-                gen.commentln("%JPEG image could not be processed: " + img);
-                return;
+                return null;
             }
         } else {
             if (!img.load(FopImage.BITMAP)) {
-                gen.commentln("%Bitmap image could not be processed: " + img);
-                return;
+                return null;
             }
         }
         byte[] imgmap;
@@ -68,15 +123,18 @@ public class PSImageUtils extends org.apache.xmlgraphics.ps.PSImageUtils {
         } else {
             imgmap = img.getRessourceBytes();
         }
-        
-        String imgName = img.getMimeType() + " " + img.getOriginalURI();
-        Dimension imgDim = new Dimension(img.getWidth(), img.getHeight());
-        Rectangle2D targetRect = new Rectangle2D.Double(x, y, w, h);
-        boolean isJPEG = (img instanceof JpegImage);
-        writeImage(imgmap, imgDim, imgName, targetRect, isJPEG, 
-                img.getColorSpace(), gen);
+        return imgmap;
     }
 
+    /**
+     * Renders an EPS image to PostScript.
+     * @param img EPS image to render
+     * @param x x position
+     * @param y y position
+     * @param w width
+     * @param h height
+     * @param gen PS generator
+     */
     public static void renderEPS(EPSImage img, 
             float x, float y, float w, float h,
             PSGenerator gen) {
index 6553f06b69c653406cbb3f4878553dc85583e8c2..24595368fe9ecf4329ac29c67b33a31fdfc5bc7e 100644 (file)
@@ -23,7 +23,9 @@ package org.apache.fop.render.ps;
 import java.awt.Color;
 import java.awt.geom.Rectangle2D;
 import java.awt.image.RenderedImage;
+import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.LineNumberReader;
 import java.io.OutputStream;
 import java.util.Iterator;
@@ -35,6 +37,9 @@ import javax.xml.transform.Source;
 // FOP
 import org.apache.avalon.framework.configuration.Configuration;
 import org.apache.avalon.framework.configuration.ConfigurationException;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
 import org.apache.fop.apps.FOPException;
 import org.apache.fop.area.Area;
 import org.apache.fop.area.BlockViewport;
@@ -57,6 +62,7 @@ import org.apache.fop.fo.Constants;
 import org.apache.fop.fo.extensions.ExtensionAttachment;
 import org.apache.fop.fonts.Font;
 import org.apache.fop.fonts.FontSetup;
+import org.apache.fop.fonts.LazyFont;
 import org.apache.fop.fonts.Typeface;
 import org.apache.fop.image.EPSImage;
 import org.apache.fop.image.FopImage;
@@ -66,7 +72,6 @@ import org.apache.fop.render.Graphics2DAdapter;
 import org.apache.fop.render.AbstractPathOrientedRenderer;
 import org.apache.fop.render.ImageAdapter;
 import org.apache.fop.render.RendererContext;
-import org.apache.fop.render.pdf.PDFRendererContextConstants;
 import org.apache.fop.render.ps.extensions.PSSetupCode;
 import org.apache.fop.util.CharUtilities;
 
@@ -75,6 +80,8 @@ import org.apache.xmlgraphics.ps.PSGenerator;
 import org.apache.xmlgraphics.ps.PSProcSets;
 import org.apache.xmlgraphics.ps.PSResource;
 import org.apache.xmlgraphics.ps.PSState;
+import org.apache.xmlgraphics.ps.dsc.DSCException;
+import org.apache.xmlgraphics.ps.dsc.ResourceTracker;
 
 import org.w3c.dom.Document;
 
@@ -99,17 +106,32 @@ import org.w3c.dom.Document;
  */
 public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAdapter {
 
+    /** logging instance */
+    private static Log log = LogFactory.getLog(PSRenderer.class);
+
     /** The MIME type for PostScript */
     public static final String MIME_TYPE = "application/postscript";
 
+    private static final String AUTO_ROTATE_LANDSCAPE = "auto-rotate-landscape";
+    private static final String OPTIMIZE_RESOURCES = "optimize-resources";
+    private static final String LANGUAGE_LEVEL = "language-level";
+
     /** The application producing the PostScript */
     private int currentPageNumber = 0;
 
     private boolean enableComments = true;
     private boolean autoRotateLandscape = false;
+    private int languageLevel = PSGenerator.DEFAULT_LANGUAGE_LEVEL;
 
+    /** the OutputStream the PS file is written to */
+    private OutputStream outputStream;
+    /** the temporary file in case of two-pass processing */
+    private File tempFile;
+    
     /** The PostScript generator used to output the PostScript */
     protected PSGenerator gen;
+    /** Determines whether the PS file is generated in two passes to minimize file size */
+    private boolean twoPassGeneration = false;
     private boolean ioTrouble = false;
 
     private boolean inTextMode = false;
@@ -120,13 +142,17 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda
 
     /** This is a map of PSResource instances of all fonts defined (key: font key) */
     private Map fontResources;
+    /** This is a map of PSResource instances of all forms (key: uri) */
+    private Map formResources;
     
     /**
      * @see org.apache.avalon.framework.configuration.Configurable#configure(Configuration)
      */
     public void configure(Configuration cfg) throws ConfigurationException {
         super.configure(cfg);
-        this.autoRotateLandscape = cfg.getChild("auto-rotate-landscape").getValueAsBoolean(false);
+        this.autoRotateLandscape = cfg.getChild(AUTO_ROTATE_LANDSCAPE).getValueAsBoolean(false);
+        this.languageLevel = cfg.getChild(LANGUAGE_LEVEL).getValueAsInteger(this.languageLevel);
+        this.twoPassGeneration = cfg.getChild(OPTIMIZE_RESOURCES).getValueAsBoolean(false);
 
         //Font configuration
         List cfgFonts = FontSetup.buildFontListFromConfiguration(cfg, this);
@@ -137,6 +163,46 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda
         }
     }
 
+    /**
+     * @see org.apache.fop.render.Renderer#setUserAgent(FOUserAgent)
+     */
+    public void setUserAgent(FOUserAgent agent) {
+        super.setUserAgent(agent);
+        Object obj;
+        obj = agent.getRendererOptions().get(AUTO_ROTATE_LANDSCAPE);
+        if (obj != null) {
+            this.autoRotateLandscape = booleanValueOf(obj);
+        }
+        obj = agent.getRendererOptions().get(LANGUAGE_LEVEL);
+        if (obj != null) {
+            this.languageLevel = intValueOf(obj);
+        }
+        obj = agent.getRendererOptions().get(OPTIMIZE_RESOURCES);
+        if (obj != null) {
+            this.twoPassGeneration = booleanValueOf(obj);
+        }
+    }
+
+    private boolean booleanValueOf(Object obj) {
+        if (obj instanceof Boolean) {
+            return ((Boolean)obj).booleanValue();
+        } else if (obj instanceof String) {
+            return Boolean.valueOf((String)obj).booleanValue();
+        } else {
+            throw new IllegalArgumentException("Boolean or \"true\" or \"false\" expected.");
+        }
+    }
+    
+    private int intValueOf(Object obj) {
+        if (obj instanceof Integer) {
+            return ((Integer)obj).intValue();
+        } else if (obj instanceof String) {
+            return Integer.parseInt((String)obj);
+        } else {
+            throw new IllegalArgumentException("Integer or String with a number expected.");
+        }
+    }
+    
     /**
      * Sets the landscape mode for this renderer.
      * @param value false will normally generate a "pseudo-portrait" page, true will rotate
@@ -151,13 +217,6 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda
         return this.autoRotateLandscape;
     }
 
-    /**
-     * @see org.apache.fop.render.Renderer#setUserAgent(FOUserAgent)
-     */
-    public void setUserAgent(FOUserAgent agent) {
-        super.setUserAgent(agent);
-    }
-
     /** @see org.apache.fop.render.Renderer#getGraphics2DAdapter() */
     public Graphics2DAdapter getGraphics2DAdapter() {
         return new PSGraphics2DAdapter(this);
@@ -282,11 +341,11 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda
     }
 
     /** @see org.apache.fop.render.AbstractPathOrientedRenderer */
-    protected void drawImage(String url, Rectangle2D pos, Map foreignAttributes) {
+    protected void drawImage(String uri, Rectangle2D pos, Map foreignAttributes) {
         endTextObject();
-        url = ImageFactory.getURL(url);
+        uri = ImageFactory.getURL(uri);
         ImageFactory fact = userAgent.getFactory().getImageFactory();
-        FopImage fopimage = fact.getImage(url, userAgent);
+        FopImage fopimage = fact.getImage(uri, userAgent);
         if (fopimage == null) {
             return;
         }
@@ -320,13 +379,34 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda
             } else if (fopimage instanceof EPSImage) {
                 PSImageUtils.renderEPS((EPSImage)fopimage, x, y, w, h, gen);
             } else {
-                PSImageUtils.renderBitmapImage(fopimage, x, y, w, h, gen);
+                if (isImageInlined(uri, fopimage)) {
+                    PSImageUtils.renderBitmapImage(fopimage, x, y, w, h, gen);
+                } else {
+                    PSResource form = getFormForImage(uri, fopimage);
+                    PSImageUtils.renderForm(fopimage, form, x, y, w, h, gen);
+                }
             }
         } catch (IOException ioe) {
             handleIOTrouble(ioe);
         }
     }
 
+    protected PSResource getFormForImage(String uri, FopImage fopimage) {
+        if (this.formResources == null) {
+            this.formResources = new java.util.HashMap();
+        }
+        PSResource form = (PSResource)this.formResources.get(uri);
+        if (form == null) {
+            form = new PSImageFormResource(this.formResources.size() + 1, uri);
+            this.formResources.put(uri, form);
+        }
+        return form;
+    }
+    
+    protected boolean isImageInlined(String uri, FopImage image) {
+        return !this.twoPassGeneration;
+    }
+    
     /** @see org.apache.fop.render.ImageAdapter */
     public void paintImage(RenderedImage image, RendererContext context, 
             int x, int y, int width, int height) throws IOException {
@@ -406,14 +486,48 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda
         }
     }
 
+    private String getPostScriptNameForFontKey(String key) {
+        Map fonts = fontInfo.getFonts();
+        Typeface tf = (Typeface)fonts.get(key);
+        if (tf instanceof LazyFont) {
+            tf = ((LazyFont)tf).getRealFont();
+        }
+        if (tf == null) {
+            throw new IllegalStateException("Font not available: " + key);
+        }
+        return tf.getFontName();
+    }
+    
+    /**
+     * Returns the PSResource for the given font key.
+     * @param key the font key ("F*")
+     * @return the matching PSResource
+     */
+    protected PSResource getPSResourceForFontKey(String key) {
+        PSResource res = null;
+        if (this.fontResources != null) {
+            res = (PSResource)this.fontResources.get(key);
+        } else {
+            this.fontResources = new java.util.HashMap(); 
+        }
+        if (res == null) {
+            res = new PSResource(PSResource.TYPE_FONT, getPostScriptNameForFontKey(key));
+            this.fontResources.put(key, res);
+        }
+        return res;
+    }
+    
     /**
      * Changes the currently used font.
-     * @param name name of the font
+     * @param key key of the font ("F*")
      * @param size font size
      */
-    public void useFont(String name, int size) {
+    protected void useFont(String key, int size) {
         try {
-            gen.useFont(name, size / 1000f);
+            PSResource res = getPSResourceForFontKey(key);
+            //gen.useFont(key, size / 1000f);
+            gen.useFont("/" + res.getName(), size / 1000f);
+            gen.getResourceTracker().notifyResourceUsageOnPage(res);
         } catch (IOException ioe) {
             handleIOTrouble(ioe);
         }
@@ -598,15 +712,26 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda
      */
     public void startRenderer(OutputStream outputStream)
                 throws IOException {
-        log.debug("rendering areas to PostScript");
-
+        log.debug("Rendering areas to PostScript...");
+
+        this.outputStream = outputStream;
+        OutputStream out; 
+        if (twoPassGeneration) {
+            this.tempFile = File.createTempFile("fop", null);
+            out = new java.io.FileOutputStream(this.tempFile);
+            out = new java.io.BufferedOutputStream(out);
+        } else {
+            out = this.outputStream;
+        }
+        
         //Setup for PostScript generation
-        this.gen = new PSGenerator(outputStream) {
+        this.gen = new PSGenerator(out) {
             /** Need to subclass PSGenerator to have better URI resolution */
             public Source resolveURI(String uri) {
                 return userAgent.resolveURI(uri);
             }
         };
+        this.gen.setPSLevel(this.languageLevel);
         this.currentPageNumber = 0;
 
         //PostScript Header
@@ -614,9 +739,9 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda
         gen.writeDSCComment(DSCConstants.CREATOR, new String[] {userAgent.getProducer()});
         gen.writeDSCComment(DSCConstants.CREATION_DATE, new Object[] {new java.util.Date()});
         gen.writeDSCComment(DSCConstants.LANGUAGE_LEVEL, new Integer(gen.getPSLevel()));
-        gen.writeDSCComment(DSCConstants.PAGES, new Object[] {PSGenerator.ATEND});
+        gen.writeDSCComment(DSCConstants.PAGES, new Object[] {DSCConstants.ATEND});
         gen.writeDSCComment(DSCConstants.DOCUMENT_SUPPLIED_RESOURCES, 
-                new Object[] {PSGenerator.ATEND});
+                new Object[] {DSCConstants.ATEND});
         gen.writeDSCComment(DSCConstants.END_COMMENTS);
 
         //Defaults
@@ -631,29 +756,65 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda
      */
     public void stopRenderer() throws IOException {
         //Notify resource usage for font which are not supplied
+        /* done in useFont now
         Map fonts = fontInfo.getUsedFonts();
         Iterator e = fonts.keySet().iterator();
         while (e.hasNext()) {
             String key = (String)e.next();
-            //Typeface font = (Typeface)fonts.get(key);
             PSResource res = (PSResource)this.fontResources.get(key);
-            boolean supplied = gen.isResourceSupplied(res);
-            if (!supplied) {
-                gen.notifyResourceUsage(res, true);
-            }
-        }
+            gen.notifyResourceUsage(res);
+        }*/
         
         //Write trailer
         gen.writeDSCComment(DSCConstants.TRAILER);
         gen.writeDSCComment(DSCConstants.PAGES, new Integer(this.currentPageNumber));
-        gen.writeResources(false);
+        gen.getResourceTracker().writeResources(false, gen);
         gen.writeDSCComment(DSCConstants.EOF);
         gen.flush();
+        log.debug("Rendering to PostScript complete.");
+        if (twoPassGeneration) {
+            IOUtils.closeQuietly(gen.getOutputStream());
+            rewritePostScriptFile();
+        }
+    }
+    
+    /**
+     * Used for two-pass production. This will rewrite the PostScript file from the temporary
+     * file while adding all needed resources.
+     * @throws IOException In case of an I/O error.
+     */
+    private void rewritePostScriptFile() throws IOException {
+        log.debug("Processing PostScript resources...");
+        long startTime = System.currentTimeMillis();
+        ResourceTracker resTracker = gen.getResourceTracker();
+        InputStream in = new java.io.FileInputStream(this.tempFile);
+        in = new java.io.BufferedInputStream(in);
+        try {
+            try {
+                ResourceHandler.process(this.userAgent, in, this.outputStream, 
+                        this.fontInfo, resTracker, this.formResources, this.currentPageNumber);
+                this.outputStream.flush();
+            } catch (DSCException e) {
+                throw new RuntimeException(e.getMessage());
+            }
+        } finally {
+            IOUtils.closeQuietly(in);
+            if (!this.tempFile.delete()) {
+                this.tempFile.deleteOnExit();
+                log.warn("Could not delete temporary file: " + this.tempFile);
+            }
+        }
+        if (log.isDebugEnabled()) {
+            long duration = System.currentTimeMillis() - startTime;
+            log.debug("Resource Processing complete in " + duration + " ms.");
+        }
     }
 
     /** @see org.apache.fop.render.Renderer */
     public void processOffDocumentItem(OffDocumentItem oDI) {
-        log.debug("Handling OffDocumentItem: " + oDI.getName());
+        if (log.isDebugEnabled()) {
+            log.debug("Handling OffDocumentItem: " + oDI.getName());
+        }
         if (oDI instanceof OffDocumentExtensionAttachment) {
             ExtensionAttachment attachment = ((OffDocumentExtensionAttachment)oDI).getAttachment();
             if (PSSetupCode.CATEGORY.equals(attachment.getCategory())) {
@@ -675,15 +836,18 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda
             try {
                 //Prolog
                 gen.writeDSCComment(DSCConstants.BEGIN_PROLOG);
-                PSProcSets.writeFOPStdProcSet(gen);
-                PSProcSets.writeFOPEPSProcSet(gen);
+                PSProcSets.writeStdProcSet(gen);
+                PSProcSets.writeEPSProcSet(gen);
                 gen.writeDSCComment(DSCConstants.END_PROLOG);
 
                 //Setup
                 gen.writeDSCComment(DSCConstants.BEGIN_SETUP);
                 writeSetupCodeList(setupCodeList, "SetupCode");
-                this.fontResources = PSFontUtils.writeFontDict(gen, fontInfo);
-                gen.writeln("FOPFonts begin");
+                if (!twoPassGeneration) {
+                    this.fontResources = PSFontUtils.writeFontDict(gen, fontInfo);
+                } else {
+                    gen.commentln("%FOPFontSetup");
+                }
                 gen.writeDSCComment(DSCConstants.END_SETUP);
             } catch (IOException ioe) {
                 handleIOTrouble(ioe);
@@ -729,8 +893,8 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda
         log.debug("renderPage(): " + page);
 
         this.currentPageNumber++;
-        gen.notifyStartNewPage();
-        gen.notifyResourceUsage(PSProcSets.STD_PROCSET, false);
+        gen.getResourceTracker().notifyStartNewPage();
+        gen.getResourceTracker().notifyResourceUsageOnPage(PSProcSets.STD_PROCSET);
         gen.writeDSCComment(DSCConstants.PAGE, new Object[]
                 {page.getPageNumberString(),
                  new Integer(this.currentPageNumber)});
@@ -769,7 +933,7 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda
             }
         }
         gen.writeDSCComment(DSCConstants.PAGE_RESOURCES, 
-                new Object[] {PSGenerator.ATEND});
+                new Object[] {DSCConstants.ATEND});
         gen.commentln("%FOPSimplePageMaster: " + page.getSimplePageMasterName());
         gen.writeDSCComment(DSCConstants.BEGIN_PAGE_SETUP);
         
@@ -807,8 +971,7 @@ public class PSRenderer extends AbstractPathOrientedRenderer implements ImageAda
 
         writeln("showpage");
         gen.writeDSCComment(DSCConstants.PAGE_TRAILER);
-        gen.writeResources(true);
-        gen.writeDSCComment(DSCConstants.END_PAGE);
+        gen.getResourceTracker().writeResources(true, gen);
     }
 
     /** @see org.apache.fop.render.AbstractRenderer */
diff --git a/src/java/org/apache/fop/render/ps/ResourceHandler.java b/src/java/org/apache/fop/render/ps/ResourceHandler.java
new file mode 100644 (file)
index 0000000..7d68092
--- /dev/null
@@ -0,0 +1,203 @@
+/*
+ * 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.render.ps;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.fop.apps.FOUserAgent;
+import org.apache.fop.fonts.FontInfo;
+import org.apache.fop.image.FopImage;
+import org.apache.fop.image.ImageFactory;
+import org.apache.xmlgraphics.ps.DSCConstants;
+import org.apache.xmlgraphics.ps.PSGenerator;
+import org.apache.xmlgraphics.ps.dsc.DSCException;
+import org.apache.xmlgraphics.ps.dsc.DSCFilter;
+import org.apache.xmlgraphics.ps.dsc.DSCParser;
+import org.apache.xmlgraphics.ps.dsc.DSCParserConstants;
+import org.apache.xmlgraphics.ps.dsc.DefaultNestedDocumentHandler;
+import org.apache.xmlgraphics.ps.dsc.ResourceTracker;
+import org.apache.xmlgraphics.ps.dsc.events.DSCComment;
+import org.apache.xmlgraphics.ps.dsc.events.DSCCommentDocumentNeededResources;
+import org.apache.xmlgraphics.ps.dsc.events.DSCCommentDocumentSuppliedResources;
+import org.apache.xmlgraphics.ps.dsc.events.DSCCommentLanguageLevel;
+import org.apache.xmlgraphics.ps.dsc.events.DSCCommentPage;
+import org.apache.xmlgraphics.ps.dsc.events.DSCCommentPages;
+import org.apache.xmlgraphics.ps.dsc.events.DSCEvent;
+import org.apache.xmlgraphics.ps.dsc.events.DSCHeaderComment;
+import org.apache.xmlgraphics.ps.dsc.events.PostScriptComment;
+import org.apache.xmlgraphics.ps.dsc.tools.DSCTools;
+
+/**
+ * This class is used when two-pass production is used to generate the PostScript file (setting
+ * "optimize-resources"). It uses the DSC parser from XML Graphics Commons to go over the
+ * temporary file generated by the PSRenderer and adds all used fonts and images as resources
+ * to the PostScript file.
+ */
+public class ResourceHandler implements DSCParserConstants {
+
+    /**
+     * Rewrites the temporary PostScript file generated by PSRenderer adding all needed resources
+     * (fonts and images).
+     * @param userAgent the FO user agent
+     * @param in the InputStream for the temporary PostScript file
+     * @param out the OutputStream to write the finished file to
+     * @param fontInfo the font information
+     * @param resTracker the resource tracker to use
+     * @param formResources Contains all forms used by this document (maintained by PSRenderer)
+     * @param pageCount the number of pages (given here because PSRenderer writes an "(atend)")
+     * @throws DSCException If there's an error in the DSC structure of the PS file
+     * @throws IOException In case of an I/O error
+     */
+    public static void process(FOUserAgent userAgent, InputStream in, OutputStream out, 
+            FontInfo fontInfo, ResourceTracker resTracker, Map formResources, int pageCount)
+                    throws DSCException, IOException {
+        DSCParser parser = new DSCParser(in);
+        PSGenerator gen = new PSGenerator(out);
+        parser.setNestedDocumentHandler(new DefaultNestedDocumentHandler(gen));
+        
+        //Skip DSC header
+        DSCHeaderComment header = DSCTools.checkAndSkipDSC30Header(parser);
+        header.generate(gen);
+        
+        parser.setFilter(new DSCFilter() {
+            private final Set filtered = new java.util.HashSet();
+            {
+                //We rewrite those as part of the processing
+                filtered.add(DSCConstants.PAGES);
+                filtered.add(DSCConstants.DOCUMENT_NEEDED_RESOURCES);
+                filtered.add(DSCConstants.DOCUMENT_SUPPLIED_RESOURCES);
+            }
+            public boolean accept(DSCEvent event) {
+                if (event.isDSCComment()) {
+                    //Filter %%Pages which we add manually from a parameter
+                    return !(filtered.contains(event.asDSCComment().getName()));
+                } else {
+                    return true;
+                }
+            }
+        });
+
+        //Get PostScript language level (may be missing)
+        while (true) {
+            DSCEvent event = parser.nextEvent();
+            if (event == null) {
+                reportInvalidDSC();
+            }
+            if (DSCTools.headerCommentsEndHere(event)) {
+                //Set number of pages
+                DSCCommentPages pages = new DSCCommentPages(pageCount);
+                pages.generate(gen);
+
+                PSFontUtils.determineSuppliedFonts(resTracker, fontInfo, fontInfo.getUsedFonts());
+                registerSuppliedForms(resTracker, formResources);
+                
+                //Supplied Resources
+                DSCCommentDocumentSuppliedResources supplied 
+                    = new DSCCommentDocumentSuppliedResources(
+                            resTracker.getDocumentSuppliedResources());
+                supplied.generate(gen);
+                
+                //Needed Resources
+                DSCCommentDocumentNeededResources needed 
+                    = new DSCCommentDocumentNeededResources(
+                            resTracker.getDocumentNeededResources());
+                needed.generate(gen);
+
+                //Write original comment that ends the header comments
+                event.generate(gen);
+                break;
+            }
+            if (event.isDSCComment()) {
+                DSCComment comment = event.asDSCComment();
+                if (DSCConstants.LANGUAGE_LEVEL.equals(comment.getName())) {
+                    DSCCommentLanguageLevel level = (DSCCommentLanguageLevel)comment;
+                    gen.setPSLevel(level.getLanguageLevel());
+                }
+            }
+            event.generate(gen);
+        }
+        
+        //Skip to the FOPFontSetup
+        PostScriptComment fontSetupPlaceholder = parser.nextPSComment("FOPFontSetup", gen);
+        if (fontSetupPlaceholder == null) {
+            throw new DSCException("Didn't find %FOPFontSetup comment in stream");
+        }
+        PSFontUtils.writeFontDict(gen, fontInfo, fontInfo.getUsedFonts());
+        generateForms(resTracker, userAgent, formResources, gen);
+
+        //Skip the prolog and to the first page
+        DSCComment pageOrTrailer = parser.nextDSCComment(DSCConstants.PAGE, gen);
+        if (pageOrTrailer == null) {
+            throw new DSCException("Page expected, but none found");
+        }
+        
+        //Process individual pages (and skip as necessary)
+        while (true) {
+            DSCCommentPage page = (DSCCommentPage)pageOrTrailer;
+            page.generate(gen);
+            pageOrTrailer = DSCTools.nextPageOrTrailer(parser, gen);
+            if (pageOrTrailer == null) {
+                reportInvalidDSC();
+            } else if (!DSCConstants.PAGE.equals(pageOrTrailer.getName())) {
+                pageOrTrailer.generate(gen);
+                break;
+            }
+        }
+        
+        //Write the rest
+        while (parser.hasNext()) {
+            DSCEvent event = parser.nextEvent();
+            event.generate(gen);
+        }
+    }
+
+    private static void reportInvalidDSC() throws DSCException {
+        throw new DSCException("File is not DSC-compliant: Unexpected end of file");
+    }
+
+    private static void registerSuppliedForms(ResourceTracker resTracker, Map formResources)
+            throws IOException {
+        Iterator iter = formResources.values().iterator();
+        while (iter.hasNext()) {
+            PSImageFormResource form = (PSImageFormResource)iter.next();
+            resTracker.registerSuppliedResource(form);
+        }
+    }
+
+    private static void generateForms(ResourceTracker resTracker, FOUserAgent userAgent, 
+            Map formResources, PSGenerator gen) throws IOException {
+        Iterator iter = formResources.values().iterator();
+        while (iter.hasNext()) {
+            PSImageFormResource form = (PSImageFormResource)iter.next();
+            ImageFactory fact = userAgent.getFactory().getImageFactory();
+            FopImage image = fact.getImage(form.getImageURI(), userAgent);
+            if (image == null) {
+                throw new NullPointerException("Image not found: " + form.getImageURI());
+            }
+            PSImageUtils.generateFormResourceForImage(image, form, gen);
+        }
+    }
+
+}
index fdeb3c62fdf7a0e1c60bd89daa182b9894c7d769..9e40ad4fd2c0143f56fc9b20eba31036188bbe74 100644 (file)
 
   <changes>
     <release version="FOP Trunk">
+      <action context="Code" dev="JM" type="add">
+        Add support for a two-pass production for PostScript output to minimize file size. This
+        adds images only once and adds only the fonts that are really used.
+      </action>
       <action context="Code" dev="AD" type="fix" fixes-bug="41652">
         If a line contained nothing but a linefeed, this didn't produce empty lines.
         Replaced the auxiliary zero-width box with a glue the width of a line,