<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>
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;
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
*/
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);
//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
}
}
+ /**
+ * 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;
+ }
+
}
--- /dev/null
+/*
+ * 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;
+ }
+
+}
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.
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;
} 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) {
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;
// 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;
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;
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;
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;
*/
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;
/** 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);
}
}
+ /**
+ * @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
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);
}
/** @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;
}
} 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 {
}
}
+ 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);
}
*/
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
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
*/
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())) {
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);
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)});
}
}
gen.writeDSCComment(DSCConstants.PAGE_RESOURCES,
- new Object[] {PSGenerator.ATEND});
+ new Object[] {DSCConstants.ATEND});
gen.commentln("%FOPSimplePageMaster: " + page.getSimplePageMasterName());
gen.writeDSCComment(DSCConstants.BEGIN_PAGE_SETUP);
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 */
--- /dev/null
+/*
+ * 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);
+ }
+ }
+
+}
<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,