| /* |
| * 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.awt.Color; |
| import java.awt.Dimension; |
| import java.awt.Paint; |
| import java.awt.Point; |
| import java.awt.Rectangle; |
| import java.awt.geom.AffineTransform; |
| import java.io.IOException; |
| import java.util.Map; |
| |
| import org.w3c.dom.Document; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| |
| import org.apache.xmlgraphics.image.loader.ImageException; |
| import org.apache.xmlgraphics.image.loader.ImageInfo; |
| import org.apache.xmlgraphics.image.loader.ImageProcessingHints; |
| import org.apache.xmlgraphics.image.loader.ImageSessionContext; |
| import org.apache.xmlgraphics.ps.PSGenerator; |
| import org.apache.xmlgraphics.ps.PSResource; |
| |
| import org.apache.fop.fonts.Font; |
| import org.apache.fop.fonts.FontTriplet; |
| import org.apache.fop.fonts.LazyFont; |
| import org.apache.fop.fonts.MultiByteFont; |
| import org.apache.fop.fonts.SingleByteFont; |
| import org.apache.fop.fonts.Typeface; |
| import org.apache.fop.render.RenderingContext; |
| import org.apache.fop.render.intermediate.AbstractIFPainter; |
| import org.apache.fop.render.intermediate.BorderPainter; |
| import org.apache.fop.render.intermediate.GraphicsPainter; |
| import org.apache.fop.render.intermediate.IFException; |
| import org.apache.fop.render.intermediate.IFState; |
| import org.apache.fop.traits.BorderProps; |
| import org.apache.fop.traits.RuleStyle; |
| import org.apache.fop.util.CharUtilities; |
| import org.apache.fop.util.HexEncoder; |
| |
| /** |
| * IFPainter implementation that produces PostScript. |
| */ |
| public class PSPainter extends AbstractIFPainter<PSDocumentHandler> { |
| |
| /** logging instance */ |
| private static Log log = LogFactory.getLog(PSPainter.class); |
| |
| private final GraphicsPainter graphicsPainter; |
| |
| private BorderPainter borderPainter; |
| |
| private boolean inTextMode; |
| |
| /** |
| * Default constructor. |
| * @param documentHandler the parent document handler |
| */ |
| public PSPainter(PSDocumentHandler documentHandler) { |
| this(documentHandler, IFState.create()); |
| } |
| |
| protected PSPainter(PSDocumentHandler documentHandler, IFState state) { |
| super(documentHandler); |
| this.graphicsPainter = new PSGraphicsPainter(getGenerator()); |
| this.borderPainter = new BorderPainter(graphicsPainter); |
| this.state = state; |
| } |
| |
| private PSGenerator getGenerator() { |
| return getDocumentHandler().getGenerator(); |
| } |
| |
| /** {@inheritDoc} */ |
| public void startViewport(AffineTransform transform, Dimension size, Rectangle clipRect) |
| throws IFException { |
| try { |
| PSGenerator generator = getGenerator(); |
| saveGraphicsState(); |
| generator.concatMatrix(toPoints(transform)); |
| } catch (IOException ioe) { |
| throw new IFException("I/O error in startViewport()", ioe); |
| } |
| if (clipRect != null) { |
| clipRect(clipRect); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public void endViewport() throws IFException { |
| try { |
| restoreGraphicsState(); |
| } catch (IOException ioe) { |
| throw new IFException("I/O error in endViewport()", ioe); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public void startGroup(AffineTransform transform, String layer) throws IFException { |
| try { |
| PSGenerator generator = getGenerator(); |
| saveGraphicsState(); |
| generator.concatMatrix(toPoints(transform)); |
| } catch (IOException ioe) { |
| throw new IFException("I/O error in startGroup()", ioe); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public void endGroup() throws IFException { |
| try { |
| restoreGraphicsState(); |
| } catch (IOException ioe) { |
| throw new IFException("I/O error in endGroup()", ioe); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| protected Map createDefaultImageProcessingHints(ImageSessionContext sessionContext) { |
| Map hints = super.createDefaultImageProcessingHints(sessionContext); |
| |
| //PostScript doesn't support alpha channels |
| hints.put(ImageProcessingHints.TRANSPARENCY_INTENT, |
| ImageProcessingHints.TRANSPARENCY_INTENT_IGNORE); |
| //TODO We might want to support image masks in the future. |
| return hints; |
| } |
| |
| /** {@inheritDoc} */ |
| protected RenderingContext createRenderingContext() { |
| PSRenderingContext psContext = new PSRenderingContext( |
| getUserAgent(), getGenerator(), getFontInfo()); |
| return psContext; |
| } |
| |
| /** {@inheritDoc} */ |
| protected void drawImageUsingImageHandler(ImageInfo info, Rectangle rect) |
| throws ImageException, IOException { |
| if (!getDocumentHandler().getPSUtil().isOptimizeResources() |
| || PSImageUtils.isImageInlined(info, |
| (PSRenderingContext)createRenderingContext())) { |
| super.drawImageUsingImageHandler(info, rect); |
| } else { |
| if (log.isDebugEnabled()) { |
| log.debug("Image " + info + " is embedded as a form later"); |
| } |
| //Don't load image at this time, just put a form placeholder in the stream |
| PSResource form = getDocumentHandler().getFormForImage(info.getOriginalURI()); |
| PSImageUtils.drawForm(form, info, rect, getGenerator()); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public void drawImage(String uri, Rectangle rect) throws IFException { |
| try { |
| endTextObject(); |
| } catch (IOException ioe) { |
| throw new IFException("I/O error in drawImage()", ioe); |
| } |
| drawImageUsingURI(uri, rect); |
| } |
| |
| /** {@inheritDoc} */ |
| public void drawImage(Document doc, Rectangle rect) throws IFException { |
| try { |
| endTextObject(); |
| } catch (IOException ioe) { |
| throw new IFException("I/O error in drawImage()", ioe); |
| } |
| drawImageUsingDocument(doc, rect); |
| } |
| |
| /** {@inheritDoc} */ |
| public void clipRect(Rectangle rect) throws IFException { |
| try { |
| PSGenerator generator = getGenerator(); |
| endTextObject(); |
| generator.defineRect(rect.x / 1000.0, rect.y / 1000.0, |
| rect.width / 1000.0, rect.height / 1000.0); |
| generator.writeln(generator.mapCommand("clip") + " " + generator.mapCommand("newpath")); |
| } catch (IOException ioe) { |
| throw new IFException("I/O error in clipRect()", ioe); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public void clipBackground(Rectangle rect, |
| BorderProps bpsBefore, BorderProps bpsAfter, |
| BorderProps bpsStart, BorderProps bpsEnd) throws IFException { |
| |
| try { |
| borderPainter.clipBackground(rect, |
| bpsBefore, bpsAfter, bpsStart, bpsEnd); |
| } catch (IOException ioe) { |
| throw new IFException("I/O error while clipping background", ioe); |
| } |
| |
| } |
| |
| /** {@inheritDoc} */ |
| public void fillRect(Rectangle rect, Paint fill) throws IFException { |
| if (fill == null) { |
| return; |
| } |
| if (rect.width != 0 && rect.height != 0) { |
| try { |
| endTextObject(); |
| PSGenerator generator = getGenerator(); |
| if (fill != null) { |
| if (fill instanceof Color) { |
| generator.useColor((Color)fill); |
| } else { |
| throw new UnsupportedOperationException("Non-Color paints NYI"); |
| } |
| } |
| generator.defineRect(rect.x / 1000.0, rect.y / 1000.0, |
| rect.width / 1000.0, rect.height / 1000.0); |
| generator.writeln(generator.mapCommand("fill")); |
| } catch (IOException ioe) { |
| throw new IFException("I/O error in fillRect()", ioe); |
| } |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public void drawBorderRect(Rectangle rect, BorderProps top, BorderProps bottom, |
| BorderProps left, BorderProps right, Color innerBackgroundColor) throws IFException { |
| if (top != null || bottom != null || left != null || right != null) { |
| try { |
| endTextObject(); |
| if (getDocumentHandler().getPSUtil().getRenderingMode() == PSRenderingMode.SIZE |
| && hasOnlySolidBorders(top, bottom, left, right)) { |
| super.drawBorderRect(rect, top, bottom, left, right, innerBackgroundColor); |
| } else { |
| this.borderPainter.drawBorders(rect, top, bottom, left, right, innerBackgroundColor); |
| } |
| } catch (IOException ioe) { |
| throw new IFException("I/O error in drawBorderRect()", ioe); |
| } |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public void drawLine(Point start, Point end, int width, Color color, RuleStyle style) |
| throws IFException { |
| try { |
| endTextObject(); |
| this.graphicsPainter.drawLine(start, end, width, color, style); |
| } catch (IOException ioe) { |
| throw new IFException("I/O error in drawLine()", ioe); |
| } |
| } |
| |
| private Typeface getTypeface(String fontName) { |
| if (fontName == null) { |
| throw new NullPointerException("fontName must not be null"); |
| } |
| Typeface tf = getFontInfo().getFonts().get(fontName); |
| if (tf instanceof LazyFont) { |
| tf = ((LazyFont)tf).getRealFont(); |
| } |
| return tf; |
| } |
| |
| /** |
| * Saves the graphics state of the rendering engine. |
| * @throws IOException if an I/O error occurs |
| */ |
| protected void saveGraphicsState() throws IOException { |
| endTextObject(); |
| getGenerator().saveGraphicsState(); |
| } |
| |
| /** |
| * Restores the last graphics state of the rendering engine. |
| * @throws IOException if an I/O error occurs |
| */ |
| protected void restoreGraphicsState() throws IOException { |
| endTextObject(); |
| getGenerator().restoreGraphicsState(); |
| } |
| |
| /** |
| * Indicates the beginning of a text object. |
| * @throws IOException if an I/O error occurs |
| */ |
| protected void beginTextObject() throws IOException { |
| if (!inTextMode) { |
| PSGenerator generator = getGenerator(); |
| generator.saveGraphicsState(); |
| generator.writeln("BT"); |
| inTextMode = true; |
| } |
| } |
| |
| /** |
| * Indicates the end of a text object. |
| * @throws IOException if an I/O error occurs |
| */ |
| protected void endTextObject() throws IOException { |
| if (inTextMode) { |
| inTextMode = false; |
| PSGenerator generator = getGenerator(); |
| generator.writeln("ET"); |
| generator.restoreGraphicsState(); |
| } |
| } |
| |
| private String formatMptAsPt(PSGenerator gen, int value) { |
| return gen.formatDouble(value / 1000.0); |
| } |
| |
| /* Disabled: performance experiment (incomplete) |
| |
| private static final String ZEROS = "0.00"; |
| |
| private String formatMptAsPt1(int value) { |
| String s = Integer.toString(value); |
| int len = s.length(); |
| StringBuffer sb = new StringBuffer(); |
| if (len < 4) { |
| sb.append(ZEROS.substring(0, 5 - len)); |
| sb.append(s); |
| } else { |
| int dec = len - 3; |
| sb.append(s.substring(0, dec)); |
| sb.append('.'); |
| sb.append(s.substring(dec)); |
| } |
| return sb.toString(); |
| }*/ |
| |
| /** {@inheritDoc} */ |
| public void drawText(int x, int y, int letterSpacing, int wordSpacing, |
| int[][] dp, String text) throws IFException { |
| try { |
| //Do not draw text if font-size is 0 as it creates an invalid PostScript file |
| if (state.getFontSize() == 0) { |
| return; |
| } |
| PSGenerator generator = getGenerator(); |
| generator.useColor(state.getTextColor()); |
| FontTriplet triplet = new FontTriplet(state.getFontFamily(), state.getFontStyle(), state.getFontWeight()); |
| String fontKey = getFontKey(triplet); |
| Typeface typeface = getTypeface(fontKey); |
| if (typeface instanceof MultiByteFont && ((MultiByteFont) typeface).hasSVG()) { |
| boolean success = drawSVGText((MultiByteFont) typeface, triplet, x, y, text, state); |
| if (success) { |
| return; |
| } |
| } |
| beginTextObject(); |
| //TODO Ignored: state.getFontVariant() |
| //TODO Opportunity for font caching if font state is more heavily used |
| int sizeMillipoints = state.getFontSize(); |
| |
| // This assumes that *all* CIDFonts use a /ToUnicode mapping |
| SingleByteFont singleByteFont = null; |
| if (typeface instanceof SingleByteFont) { |
| singleByteFont = (SingleByteFont)typeface; |
| } |
| Font font = getFontInfo().getFontInstance(triplet, sizeMillipoints); |
| |
| PSFontResource res = getDocumentHandler().getPSResourceForFontKey(fontKey); |
| boolean isOpenTypeFont = typeface instanceof MultiByteFont && ((MultiByteFont)typeface).isOTFFile(); |
| useFont(fontKey, sizeMillipoints, isOpenTypeFont); |
| |
| if (dp != null && dp[0] != null) { |
| x += dp[0][0]; |
| y -= dp[0][1]; |
| } |
| generator.writeln("1 0 0 -1 " + formatMptAsPt(generator, x) |
| + " " + formatMptAsPt(generator, y) + " Tm"); |
| |
| int textLen = text.length(); |
| int start = 0; |
| if (singleByteFont != null) { |
| //Analyze string and split up in order to paint in different sub-fonts/encodings |
| int currentEncoding = -1; |
| for (int i = 0; i < textLen; i++) { |
| char c = text.charAt(i); |
| char mapped = typeface.mapChar(c); |
| int encoding = mapped / 256; |
| if (currentEncoding != encoding) { |
| if (i > 0) { |
| writeText(text, start, i - start, |
| letterSpacing, wordSpacing, dp, font, typeface, false); |
| } |
| if (encoding == 0) { |
| useFont(fontKey, sizeMillipoints, false); |
| } else { |
| useFont(fontKey + "_" + Integer.toString(encoding), sizeMillipoints, false); |
| } |
| currentEncoding = encoding; |
| start = i; |
| } |
| } |
| } else { |
| if (typeface instanceof MultiByteFont && ((MultiByteFont)typeface).isOTFFile()) { |
| //Analyze string and split up in order to paint in different sub-fonts/encodings |
| int curEncoding = 0; |
| for (int i = start; i < textLen; i++) { |
| char orgChar = text.charAt(i); |
| |
| MultiByteFont mbFont = (MultiByteFont)typeface; |
| mbFont.mapChar(orgChar); |
| int origGlyphIdx = mbFont.findGlyphIndex(orgChar); |
| int newGlyphIdx = mbFont.getUsedGlyphs().get(origGlyphIdx); |
| int encoding = newGlyphIdx / 256; |
| if (encoding != curEncoding) { |
| if (i != 0) { |
| writeText(text, start, i - start, letterSpacing, wordSpacing, dp, font, typeface, |
| true); |
| start = i; |
| } |
| generator.useFont("/" + res.getName() + "." + encoding, sizeMillipoints / 1000f); |
| curEncoding = encoding; |
| } |
| } |
| } else { |
| useFont(fontKey, sizeMillipoints, false); |
| } |
| } |
| writeText(text, start, textLen - start, letterSpacing, wordSpacing, dp, font, typeface, |
| typeface instanceof MultiByteFont); |
| } catch (IOException ioe) { |
| throw new IFException("I/O error in drawText()", ioe); |
| } |
| } |
| |
| private void writeText(String text, int start, int len, |
| int letterSpacing, int wordSpacing, int[][] dp, |
| Font font, Typeface tf, boolean multiByte) throws IOException { |
| PSGenerator generator = getGenerator(); |
| int end = start + len; |
| int initialSize = len; |
| initialSize += initialSize / 2; |
| |
| boolean hasLetterSpacing = (letterSpacing != 0); |
| boolean needTJ = false; |
| |
| int lineStart = 0; |
| StringBuffer accText = new StringBuffer(initialSize); |
| StringBuffer sb = new StringBuffer(initialSize); |
| boolean isOTF = multiByte && ((MultiByteFont)tf).isOTFFile(); |
| for (int i = start; i < end; i++) { |
| int orgChar = text.charAt(i); |
| int ch; |
| int cw; |
| int xGlyphAdjust = 0; |
| int yGlyphAdjust = 0; |
| |
| if (CharUtilities.isFixedWidthSpace(orgChar)) { |
| //Fixed width space are rendered as spaces so copy/paste works in a reader |
| ch = font.mapChar(CharUtilities.SPACE); |
| cw = font.getCharWidth(orgChar); |
| xGlyphAdjust = font.getCharWidth(ch) - cw; |
| } else { |
| if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) { |
| xGlyphAdjust -= wordSpacing; |
| } |
| |
| // surrogate pairs have to be merged in a single code point |
| if (CharUtilities.containsSurrogatePairAt(text, i)) { |
| orgChar = Character.toCodePoint((char) orgChar, text.charAt(++i)); |
| } |
| |
| ch = font.mapCodePoint(orgChar); |
| } |
| |
| if (dp != null && i < dp.length && dp[i] != null) { |
| // get x advancement adjust |
| xGlyphAdjust -= dp[i][2] - dp[i][0]; |
| yGlyphAdjust += dp[i][3] - dp[i][1]; |
| } |
| if (dp != null && i < dp.length - 1 && dp[i + 1] != null) { |
| // get x placement adjust for next glyph |
| xGlyphAdjust -= dp[i + 1][0]; |
| yGlyphAdjust += dp[i + 1][1]; |
| } |
| if (!multiByte || isOTF) { |
| char codepoint = (char)(ch % 256); |
| if (isOTF) { |
| accText.append(HexEncoder.encode(codepoint, 2)); |
| } else { |
| PSGenerator.escapeChar(codepoint, accText); //add character to accumulated text |
| } |
| } else { |
| accText.append(HexEncoder.encode(ch)); |
| } |
| if (xGlyphAdjust != 0 || yGlyphAdjust != 0) { |
| needTJ = true; |
| if (sb.length() == 0) { |
| sb.append('['); //Need to start TJ |
| } |
| if (accText.length() > 0) { |
| if ((sb.length() - lineStart + accText.length()) > 200) { |
| sb.append(PSGenerator.LF); |
| lineStart = sb.length(); |
| } |
| lineStart = writePostScriptString(sb, accText, multiByte, lineStart); |
| sb.append(' '); |
| accText.setLength(0); //reset accumulated text |
| } |
| if (yGlyphAdjust == 0) { |
| sb.append(Integer.toString(xGlyphAdjust)).append(' '); |
| } else { |
| sb.append('['); |
| sb.append(Integer.toString(yGlyphAdjust)).append(' '); |
| sb.append(Integer.toString(xGlyphAdjust)).append(']').append(' '); |
| } |
| } |
| } |
| if (needTJ) { |
| if (accText.length() > 0) { |
| if ((sb.length() - lineStart + accText.length()) > 200) { |
| sb.append(PSGenerator.LF); |
| } |
| writePostScriptString(sb, accText, multiByte); |
| } |
| if (hasLetterSpacing) { |
| sb.append("] " + formatMptAsPt(generator, letterSpacing) + " ATJ"); |
| } else { |
| sb.append("] TJ"); |
| } |
| } else { |
| writePostScriptString(sb, accText, multiByte); |
| if (hasLetterSpacing) { |
| StringBuffer spb = new StringBuffer(); |
| spb.append(formatMptAsPt(generator, letterSpacing)) |
| .append(" 0 "); |
| sb.insert(0, spb.toString()); |
| sb.append(" " + generator.mapCommand("ashow")); |
| } else { |
| sb.append(" " + generator.mapCommand("show")); |
| } |
| } |
| generator.writeln(sb.toString()); |
| } |
| |
| private void writePostScriptString(StringBuffer buffer, StringBuffer string, |
| boolean multiByte) { |
| writePostScriptString(buffer, string, multiByte, 0); |
| } |
| |
| private int writePostScriptString(StringBuffer buffer, StringBuffer string, boolean multiByte, |
| int lineStart) { |
| buffer.append(multiByte ? '<' : '('); |
| int l = string.length(); |
| int index = 0; |
| int maxCol = 200; |
| buffer.append(string.substring(index, Math.min(index + maxCol, l))); |
| index += maxCol; |
| while (index < l) { |
| if (!multiByte) { |
| buffer.append('\\'); |
| } |
| buffer.append(PSGenerator.LF); |
| lineStart = buffer.length(); |
| buffer.append(string.substring(index, Math.min(index + maxCol, l))); |
| index += maxCol; |
| } |
| buffer.append(multiByte ? '>' : ')'); |
| return lineStart; |
| } |
| |
| private void useFont(String key, int size, boolean otf) throws IOException { |
| PSFontResource res = getDocumentHandler().getPSResourceForFontKey(key); |
| PSGenerator generator = getGenerator(); |
| String name = "/" + res.getName(); |
| if (otf) { |
| name += ".0"; |
| } |
| generator.useFont(name, size / 1000f); |
| res.notifyResourceUsageOnPage(generator.getResourceTracker()); |
| } |
| } |