Java tutorial
/* * 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: PSGenerator.java 1598621 2014-05-30 14:55:00Z ssteiner $ */ package org.apache.xmlgraphics.ps; import java.awt.Color; import java.awt.color.ColorSpace; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.text.DateFormat; import java.util.Date; import java.util.Stack; import javax.xml.transform.Source; import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.xmlgraphics.java2d.color.ColorUtil; import org.apache.xmlgraphics.java2d.color.ColorWithAlternatives; import org.apache.xmlgraphics.ps.dsc.ResourceTracker; import org.apache.xmlgraphics.util.DoubleFormatUtil; /** * This class is used to output PostScript code to an OutputStream. This class assumes that * the {@link PSProcSets#STD_PROCSET} has been added to the PostScript file. * * @version $Id: PSGenerator.java 1598621 2014-05-30 14:55:00Z ssteiner $ */ public class PSGenerator implements PSCommandMap { /** * Default postscript language level */ public static final int DEFAULT_LANGUAGE_LEVEL = 3; /** * Indicator for the PostScript interpreter that the value is provided * later in the document (mostly in the %%Trailer section). * @deprecated Please use DSCConstants.ATEND. This constant was in the wrong place. */ @Deprecated public static final Object ATEND = DSCConstants.ATEND; /** Line feed used by PostScript */ public static final char LF = '\n'; private static final String IDENTITY_H = "Identity-H"; private Log log = LogFactory.getLog(getClass()); private OutputStream out; private int psLevel = DEFAULT_LANGUAGE_LEVEL; private boolean commentsEnabled = true; private boolean compactMode = true; private PSCommandMap commandMap = PSProcSets.STD_COMMAND_MAP; private Stack<PSState> graphicsStateStack = new Stack<PSState>(); private PSState currentState; private StringBuffer doubleBuffer = new StringBuffer(16); private StringBuffer tempBuffer = new StringBuffer(256); private boolean identityHEmbedded; private PSResource procsetCIDInitResource; private PSResource identityHCMapResource; /** * Creates a new instance. * @param out the OutputStream to write the generated PostScript code to */ public PSGenerator(OutputStream out) { this.out = out; resetGraphicsState(); } /** * Indicates whether this instance is in compact mode. See {@link #setCompactMode(boolean)} * for details. * @return true if compact mode is enabled (the default) */ public boolean isCompactMode() { return this.compactMode; } /** * Controls whether this instance shall produce compact PostScript (omitting comments and * using short macros). Enabling this mode requires that the standard procset * ({@link PSProcSets#STD_PROCSET}) is included in the PostScript file. Setting this to * false produces more verbose PostScript suitable for debugging. * @param value true to enable compact mode, false for verbose mode */ public void setCompactMode(boolean value) { this.compactMode = value; } /** * Indicates whether this instance allows to write comments. See * {@link #setCommentsEnabled(boolean)} for details. * @return true if comments are enabled (the default) */ public boolean isCommentsEnabled() { return this.commentsEnabled; } /** * Controls whether this instance allows to write comments using the {@link #commentln(String)} * method. * @param value true to enable comments, false to disable them */ public void setCommentsEnabled(boolean value) { this.commentsEnabled = value; } private void resetGraphicsState() { if (!this.graphicsStateStack.isEmpty()) { throw new IllegalStateException("Graphics state stack should be empty at this point"); } this.currentState = new PSState(); } /** * Returns the OutputStream the PSGenerator writes to. * @return the OutputStream */ public OutputStream getOutputStream() { return this.out; } /** * Returns the selected PostScript level. * @return the PostScript level */ public int getPSLevel() { return this.psLevel; } /** * Sets the PostScript level that is used to generate the current document. * @param level the PostScript level (currently 1, 2 and 3 are known) */ public void setPSLevel(int level) { this.psLevel = level; } /** * Attempts to resolve the given URI. PSGenerator should be subclasses to provide more * sophisticated URI resolution. * @param uri URI to access * @return A {@link javax.xml.transform.Source} object, or null if the URI * cannot be resolved. */ public Source resolveURI(String uri) { return new javax.xml.transform.stream.StreamSource(uri); } /** * Writes a newline character to the OutputStream. * * @throws IOException In case of an I/O problem */ public final void newLine() throws IOException { out.write(LF); } /** * Formats a double value for PostScript output. * * @param value value to format * @return the formatted value */ public String formatDouble(double value) { doubleBuffer.setLength(0); DoubleFormatUtil.formatDouble(value, 3, 3, doubleBuffer); return doubleBuffer.toString(); } /** * Formats a double value for PostScript output (higher resolution). * * @param value value to format * @return the formatted value */ public String formatDouble5(double value) { doubleBuffer.setLength(0); DoubleFormatUtil.formatDouble(value, 5, 5, doubleBuffer); return doubleBuffer.toString(); } /** * Writes a PostScript command to the stream. * * @param cmd The PostScript code to be written. * @exception IOException In case of an I/O problem */ public void write(String cmd) throws IOException { /* TODO Check disabled until clarification. if (cmd.length() > 255) { throw new RuntimeException("PostScript command exceeded limit of 255 characters"); } */ out.write(cmd.getBytes("US-ASCII")); } /** * Writes the given number to the stream in decimal format. * * @param n a number * @throws IOException in case of an I/O problem */ public void write(int n) throws IOException { write(Integer.toString(n)); } /** * Writes a PostScript command to the stream and ends the line. * * @param cmd The PostScript code to be written. * @exception IOException In case of an I/O problem */ public void writeln(String cmd) throws IOException { write(cmd); newLine(); } /** * Writes a comment to the stream and ends the line. Output of comments can * be disabled to reduce the size of the generated file. * * @param comment comment to write * @exception IOException In case of an I/O problem */ public void commentln(String comment) throws IOException { if (isCommentsEnabled()) { writeln(comment); } } /** {@inheritDoc} */ public String mapCommand(String command) { if (isCompactMode()) { return this.commandMap.mapCommand(command); } else { return command; } } /** * Writes encoded data to the PostScript stream. * * @param cmd The encoded PostScript code to be written. * @exception IOException In case of an I/O problem */ public void writeByteArr(byte[] cmd) throws IOException { out.write(cmd); newLine(); } /** * Flushes the OutputStream. * * @exception IOException In case of an I/O problem */ public void flush() throws IOException { out.flush(); } /** * Escapes a character conforming to the rules established in the PostScript * Language Reference (Search for "Literal Text Strings"). * @param c character to escape * @param target target StringBuffer to write the escaped character to */ public static final void escapeChar(char c, StringBuffer target) { switch (c) { case '\n': target.append("\\n"); break; case '\r': target.append("\\r"); break; case '\t': target.append("\\t"); break; case '\b': target.append("\\b"); break; case '\f': target.append("\\f"); break; case '\\': target.append("\\\\"); break; case '(': target.append("\\("); break; case ')': target.append("\\)"); break; default: if (c > 255) { //Ignoring non Latin-1 characters target.append('?'); } else if (c < 32 || c > 127) { target.append('\\'); target.append((char) ('0' + (c >> 6))); target.append((char) ('0' + ((c >> 3) % 8))); target.append((char) ('0' + (c % 8))); //Integer.toOctalString(i) } else { target.append(c); } } } /** * Converts text by applying escaping rules established in the DSC specs. * @param text Text to convert * @return String The resulting String */ public static final String convertStringToDSC(String text) { return convertStringToDSC(text, false); } /** * Converts a <real> value for use in DSC comments. * @param value the value to convert * @return String The resulting String */ public static final String convertRealToDSC(float value) { return Float.toString(value); } /** * Converts text by applying escaping rules established in the DSC specs. * @param text Text to convert * @param forceParentheses Force the use of parentheses * @return String The resulting String */ public static final String convertStringToDSC(String text, boolean forceParentheses) { if ((text == null) || (text.length() == 0)) { return "()"; } else { int initialSize = text.length(); initialSize += initialSize / 2; StringBuffer sb = new StringBuffer(initialSize); if ((text.indexOf(' ') >= 0) || forceParentheses) { sb.append('('); for (int i = 0; i < text.length(); i++) { final char c = text.charAt(i); escapeChar(c, sb); } sb.append(')'); return sb.toString(); } else { for (int i = 0; i < text.length(); i++) { final char c = text.charAt(i); escapeChar(c, sb); } return sb.toString(); } } } /** * Writes a DSC comment to the output stream. * @param name Name of the DSC comment * @exception IOException In case of an I/O problem * @see org.apache.xmlgraphics.ps.DSCConstants */ public void writeDSCComment(String name) throws IOException { writeln("%%" + name); } /** * Writes a DSC comment to the output stream. The parameter to the DSC * comment can be any object. The object is converted to a String as * necessary. * @param name Name of the DSC comment * @param param Single parameter to the DSC comment * @exception IOException In case of an I/O problem * @see org.apache.xmlgraphics.ps.DSCConstants */ public void writeDSCComment(String name, Object param) throws IOException { writeDSCComment(name, new Object[] { param }); } /** * Writes a DSC comment to the output stream. The parameters to the DSC * comment can be any object. The objects are converted to Strings as * necessary. Please see the source code to find out what parameters are * currently supported. * @param name Name of the DSC comment * @param params Array of parameters to the DSC comment * @exception IOException In case of an I/O problem * @see org.apache.xmlgraphics.ps.DSCConstants */ public void writeDSCComment(String name, Object[] params) throws IOException { tempBuffer.setLength(0); tempBuffer.append("%%"); tempBuffer.append(name); if ((params != null) && (params.length > 0)) { tempBuffer.append(": "); for (int i = 0; i < params.length; i++) { if (i > 0) { tempBuffer.append(" "); } if (params[i] instanceof String) { tempBuffer.append(convertStringToDSC((String) params[i])); } else if (params[i] == DSCConstants.ATEND) { tempBuffer.append(DSCConstants.ATEND); } else if (params[i] instanceof Double) { tempBuffer.append(formatDouble(((Double) params[i]).doubleValue())); } else if (params[i] instanceof Number) { tempBuffer.append(params[i].toString()); } else if (params[i] instanceof Date) { DateFormat datef = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); tempBuffer.append(convertStringToDSC(datef.format((Date) params[i]))); } else if (params[i] instanceof PSResource) { tempBuffer.append(((PSResource) params[i]).getResourceSpecification()); } else { throw new IllegalArgumentException( "Unsupported parameter type: " + params[i].getClass().getName()); } } } writeln(tempBuffer.toString()); } /** * Saves the graphics state of the rendering engine. * @exception IOException In case of an I/O problem */ public void saveGraphicsState() throws IOException { writeln(mapCommand("gsave")); PSState state = new PSState(this.currentState, false); this.graphicsStateStack.push(this.currentState); this.currentState = state; } /** * Restores the last graphics state of the rendering engine. * @return true if the state was restored, false if there's a stack underflow. * @exception IOException In case of an I/O problem */ public boolean restoreGraphicsState() throws IOException { if (this.graphicsStateStack.size() > 0) { writeln(mapCommand("grestore")); this.currentState = this.graphicsStateStack.pop(); return true; } else { return false; } } /** * Returns the current graphics state. * @return the current graphics state */ public PSState getCurrentState() { return this.currentState; } /** * Issues the "showpage" command and resets the painting state accordingly. * @exception IOException In case of an I/O problem */ public void showPage() throws IOException { writeln("showpage"); resetGraphicsState(); } /** * Concats the transformation matrix. * @param a A part * @param b B part * @param c C part * @param d D part * @param e E part * @param f F part * @exception IOException In case of an I/O problem */ public void concatMatrix(double a, double b, double c, double d, double e, double f) throws IOException { AffineTransform at = new AffineTransform(a, b, c, d, e, f); concatMatrix(at); } /** * Concats the transformations matrix. * @param matrix Matrix to use * @exception IOException In case of an I/O problem */ public void concatMatrix(double[] matrix) throws IOException { concatMatrix(matrix[0], matrix[1], matrix[2], matrix[3], matrix[4], matrix[5]); } /** * Formats a transformation matrix. * @param at the AffineTransform with the matrix * @return the formatted transformation matrix (example: "[1 0 0 1 0 0]") */ public String formatMatrix(AffineTransform at) { double[] matrix = new double[6]; at.getMatrix(matrix); return "[" + formatDouble5(matrix[0]) + " " + formatDouble5(matrix[1]) + " " + formatDouble5(matrix[2]) + " " + formatDouble5(matrix[3]) + " " + formatDouble5(matrix[4]) + " " + formatDouble5(matrix[5]) + "]"; } /** * Concats the transformations matric. * @param at the AffineTransform whose matrix to use * @exception IOException In case of an I/O problem */ public void concatMatrix(AffineTransform at) throws IOException { getCurrentState().concatMatrix(at); writeln(formatMatrix(at) + " " + mapCommand("concat")); } /** * Formats a Rectangle2D to an array. * @param rect the rectangle * @return the formatted array */ public String formatRectangleToArray(Rectangle2D rect) { return "[" + formatDouble(rect.getX()) + " " + formatDouble(rect.getY()) + " " + formatDouble(rect.getWidth()) + " " + formatDouble(rect.getHeight()) + "]"; } /** * Adds a rectangle to the current path. * @param x upper left corner * @param y upper left corner * @param w width * @param h height * @exception IOException In case of an I/O problem */ public void defineRect(double x, double y, double w, double h) throws IOException { writeln(formatDouble(x) + " " + formatDouble(y) + " " + formatDouble(w) + " " + formatDouble(h) + " re"); } /** * Establishes the specified line cap style. * @param linecap the line cap style (0, 1 or 2) as defined by the setlinecap command. * @exception IOException In case of an I/O problem */ public void useLineCap(int linecap) throws IOException { if (getCurrentState().useLineCap(linecap)) { writeln(linecap + " " + mapCommand("setlinecap")); } } /** * Establishes the specified line join style. * @param linejoin the line join style (0, 1 or 2) as defined by the setlinejoin command. * @exception IOException In case of an I/O problem */ public void useLineJoin(int linejoin) throws IOException { if (getCurrentState().useLineJoin(linejoin)) { writeln(linejoin + " " + mapCommand("setlinejoin")); } } /** * Establishes the specified miter limit. * @param miterlimit the miter limit as defined by the setmiterlimit command. * @exception IOException In case of an I/O problem */ public void useMiterLimit(float miterlimit) throws IOException { if (getCurrentState().useMiterLimit(miterlimit)) { writeln(miterlimit + " " + mapCommand("setmiterlimit")); } } /** * Establishes the specified line width. * @param width the line width as defined by the setlinewidth command. * @exception IOException In case of an I/O problem */ public void useLineWidth(double width) throws IOException { if (getCurrentState().useLineWidth(width)) { writeln(formatDouble(width) + " " + mapCommand("setlinewidth")); } } /** * Establishes the specified dash pattern. * @param pattern the dash pattern as defined by the setdash command. * @exception IOException In case of an I/O problem */ public void useDash(String pattern) throws IOException { if (pattern == null) { pattern = PSState.DEFAULT_DASH; } if (getCurrentState().useDash(pattern)) { writeln(pattern + " " + mapCommand("setdash")); } } /** * Establishes the specified color (RGB). * @param col the color as defined by the setrgbcolor command. * @exception IOException In case of an I/O problem * @deprecated use useColor method instead */ @Deprecated public void useRGBColor(Color col) throws IOException { useColor(col); } /** * Establishes the specified color. * @param col the color. * @exception IOException In case of an I/O problem */ public void useColor(Color col) throws IOException { if (getCurrentState().useColor(col)) { writeln(convertColorToPS(col)); } } private String convertColorToPS(Color color) { StringBuffer codeBuffer = new StringBuffer(); //Important: Right now, CMYK colors are treated as device colors (DeviceCMYK) irrespective //of any associated color profile. All other colors are converted to sRGB (if necessary) //and the resulting RGB components are treated as DeviceRGB colors. //If all three RGB components are the same, DeviceGray is used. boolean established = false; if (color instanceof ColorWithAlternatives) { ColorWithAlternatives colExt = (ColorWithAlternatives) color; //Alternative colors have priority Color[] alt = colExt.getAlternativeColors(); for (int i = 0, c = alt.length; i < c; i++) { Color col = alt[i]; established = establishColorFromColor(codeBuffer, col); if (established) { break; } } if (log.isDebugEnabled() && alt.length > 0) { log.debug("None of the alternative colors are supported. Using fallback: " + color); } } //Fallback if (!established) { established = establishColorFromColor(codeBuffer, color); } if (!established) { establishFallbackRGB(codeBuffer, color); } return codeBuffer.toString(); } private boolean establishColorFromColor(StringBuffer codeBuffer, Color color) { //Important: see above note about color handling! float[] comps = color.getColorComponents(null); if (color.getColorSpace().getType() == ColorSpace.TYPE_CMYK) { // colorspace is CMYK writeSetColor(codeBuffer, comps, "setcmykcolor"); return true; } return false; } private void writeSetColor(StringBuffer codeBuffer, float[] comps, String command) { for (int i = 0, c = comps.length; i < c; i++) { if (i > 0) { codeBuffer.append(" "); } codeBuffer.append(formatDouble(comps[i])); } codeBuffer.append(" ").append(mapCommand(command)); } private void establishFallbackRGB(StringBuffer codeBuffer, Color color) { float[] comps; if (color.getColorSpace().isCS_sRGB()) { comps = color.getColorComponents(null); } else { if (log.isDebugEnabled()) { log.debug("Converting color to sRGB as a fallback: " + color); } ColorSpace sRGB = ColorSpace.getInstance(ColorSpace.CS_sRGB); comps = color.getColorComponents(sRGB, null); } assert comps.length == 3; boolean gray = ColorUtil.isGray(color); if (gray) { comps = new float[] { comps[0] }; } writeSetColor(codeBuffer, comps, gray ? "setgray" : "setrgbcolor"); } /** * Establishes the specified font and size. * @param name name of the font for the "F" command (see FOP Std Proc Set) * @param size size of the font * @exception IOException In case of an I/O problem */ public void useFont(String name, float size) throws IOException { if (getCurrentState().useFont(name, size)) { writeln(name + " " + formatDouble(size) + " F"); } } private ResourceTracker resTracker = new ResourceTracker(); /** * Resturns the ResourceTracker instance associated with this PSGenerator. * @return the ResourceTracker instance or null if none is assigned */ public ResourceTracker getResourceTracker() { return this.resTracker; } /** * Sets the ResourceTracker instance to be associated with this PSGenerator. * @param resTracker the ResourceTracker instance */ public void setResourceTracker(ResourceTracker resTracker) { this.resTracker = resTracker; } /** * Notifies the generator that a new page has been started and that the page resource * set can be cleared. * @deprecated Use the notifyStartNewPage() on ResourceTracker instead. */ @Deprecated public void notifyStartNewPage() { getResourceTracker().notifyStartNewPage(); } /** * Notifies the generator about the usage of a resource on the current page. * @param res the resource being used * @param needed true if this is a needed resource, false for a supplied resource * @deprecated Use the notifyResourceUsageOnPage() on ResourceTracker instead */ @Deprecated public void notifyResourceUsage(PSResource res, boolean needed) { getResourceTracker().notifyResourceUsageOnPage(res); } /** * Writes a DSC comment for the accumulated used resources, either at page level or * at document level. * @param pageLevel true if the DSC comment for the page level should be generated, * false for the document level (in the trailer) * @exception IOException In case of an I/O problem * @deprecated Use the writeResources() on ResourceTracker instead. */ @Deprecated public void writeResources(boolean pageLevel) throws IOException { getResourceTracker().writeResources(pageLevel, this); } /** * Indicates whether a particular resource is supplied, rather than needed. * @param res the resource * @return true if the resource is registered as being supplied. * @deprecated Use the isResourceSupplied() on ResourceTracker instead. */ @Deprecated public boolean isResourceSupplied(PSResource res) { return getResourceTracker().isResourceSupplied(res); } /** * Embeds the Identity-H CMap file into the output stream, if that has not * already been done. * * @return true if embedding has actually been performed, false otherwise * (which means that a call to this method had already been made earlier) * @throws IOException in case of an I/O problem */ public boolean embedIdentityH() throws IOException { if (identityHEmbedded) { return false; } else { resTracker.registerNeededResource(getProcsetCIDInitResource()); writeDSCComment(DSCConstants.BEGIN_DOCUMENT, IDENTITY_H); InputStream cmap = PSGenerator.class.getResourceAsStream(IDENTITY_H); try { IOUtils.copyLarge(cmap, out); } finally { IOUtils.closeQuietly(cmap); } writeDSCComment(DSCConstants.END_DOCUMENT); resTracker.registerSuppliedResource(getIdentityHCMapResource()); identityHEmbedded = true; return true; } } /** * Returns the PSResource instance corresponding to the Identity-H CMap * resource. * * @return the Identity-H CMap resource. */ public PSResource getIdentityHCMapResource() { if (identityHCMapResource == null) { identityHCMapResource = new PSResource(PSResource.TYPE_CMAP, IDENTITY_H); } return identityHCMapResource; } /** * Returns the PSResource instance corresponding to the CIDInit ProcSet * resource. * * @return the ProcSet CIDInit resource */ public PSResource getProcsetCIDInitResource() { if (procsetCIDInitResource == null) { procsetCIDInitResource = new PSResource(PSResource.TYPE_PROCSET, "CIDInit"); } return procsetCIDInitResource; } /** * Adds a PostScript DSC comment to the output stream requiring the * inclusion of the CIDInit ProcSet resource. * * @throws IOException in case of an I/O problem */ public void includeProcsetCIDInitResource() throws IOException { writeDSCComment(DSCConstants.INCLUDE_RESOURCE, getProcsetCIDInitResource()); } }