/* * 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.pcl; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Stack; import org.w3c.dom.Document; 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.ImageSize; import org.apache.xmlgraphics.image.loader.impl.ImageGraphics2D; import org.apache.xmlgraphics.java2d.GraphicContext; import org.apache.xmlgraphics.java2d.Graphics2DImagePainter; import org.apache.fop.fonts.CIDFontType; import org.apache.fop.fonts.Font; import org.apache.fop.fonts.FontTriplet; import org.apache.fop.fonts.FontType; import org.apache.fop.fonts.LazyFont; import org.apache.fop.fonts.MultiByteFont; import org.apache.fop.fonts.Typeface; import org.apache.fop.render.ImageHandlerUtil; import org.apache.fop.render.RenderingContext; import org.apache.fop.render.intermediate.AbstractIFPainter; import org.apache.fop.render.intermediate.IFException; import org.apache.fop.render.intermediate.IFState; import org.apache.fop.render.intermediate.IFUtil; import org.apache.fop.render.java2d.CustomFontMetricsMapper; import org.apache.fop.render.java2d.FontMetricsMapper; import org.apache.fop.render.java2d.Java2DPainter; import org.apache.fop.render.pcl.fonts.PCLCharacterWriter; import org.apache.fop.render.pcl.fonts.PCLSoftFont; import org.apache.fop.render.pcl.fonts.PCLSoftFontManager; import org.apache.fop.render.pcl.fonts.PCLSoftFontManager.PCLTextSegment; import org.apache.fop.render.pcl.fonts.truetype.PCLTTFCharacterWriter; import org.apache.fop.traits.BorderProps; import org.apache.fop.traits.RuleStyle; import org.apache.fop.util.CharUtilities; /** * {@link org.apache.fop.render.intermediate.IFPainter} implementation that produces PCL 5. */ public class PCLPainter extends AbstractIFPainter<PCLDocumentHandler> implements PCLConstants { private static final boolean DEBUG = false; /** The PCL generator */ private PCLGenerator gen; private PCLPageDefinition currentPageDefinition; private int currentPrintDirection; //private GeneralPath currentPath = null; private Stack<GraphicContext> graphicContextStack = new Stack<GraphicContext>(); private GraphicContext graphicContext = new GraphicContext(); private PCLSoftFontManager sfManager; /** * Main constructor. * @param parent the parent document handler * @param pageDefinition the page definition describing the page to be rendered */ public PCLPainter(PCLDocumentHandler parent, PCLPageDefinition pageDefinition) { super(parent); this.gen = parent.getPCLGenerator(); this.state = IFState.create(); this.currentPageDefinition = pageDefinition; } PCLRenderingUtil getPCLUtil() { return getDocumentHandler().getPCLUtil(); } /** @return the target resolution */ protected int getResolution() { int resolution = Math.round(getUserAgent().getTargetResolution()); if (resolution <= 300) { return 300; } else { return 600; } } private boolean isSpeedOptimized() { return getPCLUtil().getRenderingMode() == PCLRenderingMode.SPEED; } //---------------------------------------------------------------------------------------------- /** {@inheritDoc} */ public void startViewport(AffineTransform transform, Dimension size, Rectangle clipRect) throws IFException { saveGraphicsState(); try { concatenateTransformationMatrix(transform); /* PCL cannot clip! if (clipRect != null) { clipRect(clipRect); }*/ } catch (IOException ioe) { throw new IFException("I/O error in startViewport()", ioe); } } /** {@inheritDoc} */ public void endViewport() throws IFException { restoreGraphicsState(); } /** {@inheritDoc} */ public void startGroup(AffineTransform transform, String layer) throws IFException { saveGraphicsState(); try { concatenateTransformationMatrix(transform); } catch (IOException ioe) { throw new IFException("I/O error in startGroup()", ioe); } } /** {@inheritDoc} */ public void endGroup() throws IFException { restoreGraphicsState(); } /** {@inheritDoc} */ public void drawImage(String uri, Rectangle rect) throws IFException { drawImageUsingURI(uri, rect); } /** {@inheritDoc} */ protected RenderingContext createRenderingContext() { PCLRenderingContext pdfContext = new PCLRenderingContext( getUserAgent(), this.gen, getPCLUtil()) { public Point2D transformedPoint(int x, int y) { return PCLPainter.this.transformedPoint(x, y); } public GraphicContext getGraphicContext() { return PCLPainter.this.graphicContext; } }; return pdfContext; } /** {@inheritDoc} */ public void drawImage(Document doc, Rectangle rect) throws IFException { drawImageUsingDocument(doc, rect); } /** {@inheritDoc} */ public void clipRect(Rectangle rect) throws IFException { //PCL cannot clip (only HP GL/2 can) //If you need clipping support, switch to RenderingMode.BITMAP. } /** {@inheritDoc} */ public void clipBackground(Rectangle rect, BorderProps bpsBefore, BorderProps bpsAfter, BorderProps bpsStart, BorderProps bpsEnd) throws IFException { //PCL cannot clip (only HP GL/2 can) //If you need clipping support, switch to RenderingMode.BITMAP. } /** {@inheritDoc} */ public void fillRect(Rectangle rect, Paint fill) throws IFException { if (fill == null) { return; } if (rect.width != 0 && rect.height != 0) { Color fillColor = null; if (fill != null) { if (fill instanceof Color) { fillColor = (Color)fill; } else { throw new UnsupportedOperationException("Non-Color paints NYI"); } try { setCursorPos(rect.x, rect.y); gen.fillRect(rect.width, rect.height, fillColor, getPCLUtil().isColorEnabled()); } catch (IOException ioe) { throw new IFException("I/O error in fillRect()", ioe); } } } } /** {@inheritDoc} */ public void drawBorderRect(final Rectangle rect, final BorderProps top, final BorderProps bottom, final BorderProps left, final BorderProps right) throws IFException { if (isSpeedOptimized()) { super.drawBorderRect(rect, top, bottom, left, right, null); return; } if (top != null || bottom != null || left != null || right != null) { final Rectangle boundingBox = rect; final Dimension dim = boundingBox.getSize(); Graphics2DImagePainter painter = new Graphics2DImagePainter() { public void paint(Graphics2D g2d, Rectangle2D area) { g2d.translate(-rect.x, -rect.y); Java2DPainter painter = new Java2DPainter(g2d, getContext(), getFontInfo(), state); try { painter.drawBorderRect(rect, top, bottom, left, right); } catch (IFException e) { //This should never happen with the Java2DPainter throw new RuntimeException("Unexpected error while painting borders", e); } } public Dimension getImageSize() { return dim.getSize(); } }; paintMarksAsBitmap(painter, boundingBox); } } /** {@inheritDoc} */ public void drawLine(final Point start, final Point end, final int width, final Color color, final RuleStyle style) throws IFException { if (isSpeedOptimized()) { super.drawLine(start, end, width, color, style); return; } final Rectangle boundingBox = getLineBoundingBox(start, end, width); final Dimension dim = boundingBox.getSize(); Graphics2DImagePainter painter = new Graphics2DImagePainter() { public void paint(Graphics2D g2d, Rectangle2D area) { g2d.translate(-boundingBox.x, -boundingBox.y); Java2DPainter painter = new Java2DPainter(g2d, getContext(), getFontInfo(), state); try { painter.drawLine(start, end, width, color, style); } catch (IFException e) { //This should never happen with the Java2DPainter throw new RuntimeException("Unexpected error while painting a line", e); } } public Dimension getImageSize() { return dim.getSize(); } }; paintMarksAsBitmap(painter, boundingBox); } private void paintMarksAsBitmap(Graphics2DImagePainter painter, Rectangle boundingBox) throws IFException { ImageInfo info = new ImageInfo(null, null); ImageSize size = new ImageSize(); size.setSizeInMillipoints(boundingBox.width, boundingBox.height); info.setSize(size); ImageGraphics2D img = new ImageGraphics2D(info, painter); Map hints = new java.util.HashMap(); if (isSpeedOptimized()) { //Gray text may not be painted in this case! We don't get dithering in Sun JREs. //But this approach is about twice as fast as the grayscale image. hints.put(ImageProcessingHints.BITMAP_TYPE_INTENT, ImageProcessingHints.BITMAP_TYPE_INTENT_MONO); } else { hints.put(ImageProcessingHints.BITMAP_TYPE_INTENT, ImageProcessingHints.BITMAP_TYPE_INTENT_GRAY); } hints.put(ImageHandlerUtil.CONVERSION_MODE, ImageHandlerUtil.CONVERSION_MODE_BITMAP); PCLRenderingContext context = (PCLRenderingContext)createRenderingContext(); context.setSourceTransparencyEnabled(true); try { drawImage(img, boundingBox, context, true, hints); } catch (IOException ioe) { throw new IFException( "I/O error while painting marks using a bitmap", ioe); } catch (ImageException ie) { throw new IFException( "Error while painting marks using a bitmap", ie); } } /** {@inheritDoc} */ public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[][] dp, String text) throws IFException { try { FontTriplet triplet = new FontTriplet( state.getFontFamily(), state.getFontStyle(), state.getFontWeight()); //TODO Ignored: state.getFontVariant() //TODO Opportunity for font caching if font state is more heavily used String fontKey = getFontKey(triplet); Typeface tf = getTypeface(fontKey); boolean drawAsBitmaps = getPCLUtil().isAllTextAsBitmaps(); boolean pclFont = HardcodedFonts.setFont(gen, fontKey, state.getFontSize(), text); if (pclFont) { drawTextNative(x, y, letterSpacing, wordSpacing, dp, text, triplet); } else { // TrueType conversion to a soft font (PCL 5 Technical Reference - Chapter 11) if (!drawAsBitmaps && isTrueType(tf)) { if (sfManager == null) { sfManager = new PCLSoftFontManager(gen.fontReaderMap); } if (getPCLUtil().isOptimizeResources() || sfManager.getSoftFont(tf, text) == null) { for (char c : text.toCharArray()) { tf.mapChar(c); } ByteArrayOutputStream baos = sfManager.makeSoftFont(tf, text); if (baos != null) { if (getPCLUtil().isOptimizeResources()) { gen.addFont(sfManager, tf); } else { gen.writeBytes(baos.toByteArray()); } } } String formattedSize = gen.formatDouble2(state.getFontSize() / 1000.0); gen.writeCommand(String.format("(s%sV", formattedSize)); List<PCLTextSegment> textSegments = sfManager.getTextSegments(text, tf); if (textSegments.isEmpty()) { textSegments.add(new PCLTextSegment(sfManager.getSoftFontID(tf), text)); } boolean first = true; for (PCLTextSegment textSegment : textSegments) { gen.writeCommand(String.format("(%dX", textSegment.getFontID())); PCLSoftFont softFont = sfManager.getSoftFontFromID(textSegment.getFontID()); PCLCharacterWriter charWriter = new PCLTTFCharacterWriter(softFont); gen.writeBytes(sfManager.assignFontID(textSegment.getFontID())); gen.writeBytes(charWriter.writeCharacterDefinitions(textSegment.getText())); if (first) { drawTextUsingSoftFont(x, y, letterSpacing, wordSpacing, dp, textSegment.getText(), triplet, softFont); first = false; } else { drawTextUsingSoftFont(-1, -1, letterSpacing, wordSpacing, dp, textSegment.getText(), triplet, softFont); } } } else { drawTextAsBitmap(x, y, letterSpacing, wordSpacing, dp, text, triplet); if (DEBUG) { state.setTextColor(Color.GRAY); HardcodedFonts.setFont(gen, "F1", state.getFontSize(), text); drawTextNative(x, y, letterSpacing, wordSpacing, dp, text, triplet); } } } } catch (IOException ioe) { throw new IFException("I/O error in drawText()", ioe); } } private boolean isTrueType(Typeface tf) { if (tf.getFontType().equals(FontType.TRUETYPE)) { return true; } else if (tf instanceof CustomFontMetricsMapper) { Typeface realFont = ((CustomFontMetricsMapper) tf).getRealFont(); if (realFont instanceof MultiByteFont) { return ((MultiByteFont) realFont).getCIDType().equals(CIDFontType.CIDTYPE2); } } return false; } 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; } private void drawTextNative(int x, int y, int letterSpacing, int wordSpacing, int[][] dp, String text, FontTriplet triplet) throws IOException { Color textColor = state.getTextColor(); if (textColor != null) { gen.setTransparencyMode(true, false); if (getDocumentHandler().getPCLUtil().isColorEnabled()) { gen.selectColor(textColor); } else { gen.selectGrayscale(textColor); } } gen.setTransparencyMode(true, true); setCursorPos(x, y); float fontSize = state.getFontSize() / 1000f; Font font = getFontInfo().getFontInstance(triplet, state.getFontSize()); int l = text.length(); StringBuffer sb = new StringBuffer(Math.max(16, l)); if (dp != null && dp[0] != null && dp[0][0] != 0) { if (dp[0][0] > 0) { sb.append("\u001B&a+").append(gen.formatDouble2(dp[0][0] / 100.0)).append('H'); } else { sb.append("\u001B&a-").append(gen.formatDouble2(-dp[0][0] / 100.0)).append('H'); } } if (dp != null && dp[0] != null && dp[0][1] != 0) { if (dp[0][1] > 0) { sb.append("\u001B&a-").append(gen.formatDouble2(dp[0][1] / 100.0)).append('V'); } else { sb.append("\u001B&a+").append(gen.formatDouble2(-dp[0][1] / 100.0)).append('V'); } } for (int i = 0; i < l; i++) { char orgChar = text.charAt(i); char ch; float xGlyphAdjust = 0; float yGlyphAdjust = 0; if (font.hasChar(orgChar)) { ch = font.mapChar(orgChar); } else { if (CharUtilities.isFixedWidthSpace(orgChar)) { //Fixed width space are rendered as spaces so copy/paste works in a reader ch = font.mapChar(CharUtilities.SPACE); int spaceDiff = font.getCharWidth(ch) - font.getCharWidth(orgChar); xGlyphAdjust = -(10 * spaceDiff / fontSize); } else { ch = font.mapChar(orgChar); } } sb.append(ch); if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) { xGlyphAdjust += wordSpacing; } xGlyphAdjust += letterSpacing; if (dp != null && i < dp.length && dp[i] != null) { 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) { xGlyphAdjust += dp[i + 1][0]; yGlyphAdjust += dp[i + 1][1]; } if (xGlyphAdjust != 0) { if (xGlyphAdjust > 0) { sb.append("\u001B&a+").append(gen.formatDouble2(xGlyphAdjust / 100.0)).append('H'); } else { sb.append("\u001B&a-").append(gen.formatDouble2(-xGlyphAdjust / 100.0)).append('H'); } } if (yGlyphAdjust != 0) { if (yGlyphAdjust > 0) { sb.append("\u001B&a-").append(gen.formatDouble2(yGlyphAdjust / 100.0)).append('V'); } else { sb.append("\u001B&a+").append(gen.formatDouble2(-yGlyphAdjust / 100.0)).append('V'); } } } gen.getOutputStream().write(sb.toString().getBytes(gen.getTextEncoding())); } private void drawTextUsingSoftFont(int x, int y, int letterSpacing, int wordSpacing, int[][] dp, String text, FontTriplet triplet, PCLSoftFont softFont) throws IOException { Color textColor = state.getTextColor(); if (textColor != null) { gen.setTransparencyMode(true, false); if (getDocumentHandler().getPCLUtil().isColorEnabled()) { gen.selectColor(textColor); } else { gen.selectGrayscale(textColor); } } if (x != -1 && y != -1) { setCursorPos(x, y); } float fontSize = state.getFontSize() / 1000f; Font font = getFontInfo().getFontInstance(triplet, state.getFontSize()); int l = text.length(); int[] dx = IFUtil.convertDPToDX(dp); int dxl = (dx != null ? dx.length : 0); StringBuffer sb = new StringBuffer(Math.max(16, l)); if (dx != null && dxl > 0 && dx[0] != 0) { sb.append("\u001B&a+").append(gen.formatDouble2(dx[0] / 100.0)).append('H'); } String current = ""; for (int i = 0; i < l; i++) { char orgChar = text.charAt(i); float glyphAdjust = 0; if (!font.hasChar(orgChar)) { if (CharUtilities.isFixedWidthSpace(orgChar)) { //Fixed width space are rendered as spaces so copy/paste works in a reader char ch = font.mapChar(CharUtilities.SPACE); int spaceDiff = font.getCharWidth(ch) - font.getCharWidth(orgChar); glyphAdjust = -(10 * spaceDiff / fontSize); } } if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) { glyphAdjust += wordSpacing; } current += orgChar; glyphAdjust += letterSpacing; if (dx != null && i < dxl - 1) { glyphAdjust += dx[i + 1]; } if (glyphAdjust != 0) { gen.getOutputStream().write(sb.toString().getBytes(gen.getTextEncoding())); for (int j = 0; j < current.length(); j++) { gen.getOutputStream().write(softFont.getCharCode(current.charAt(j))); } sb = new StringBuffer(); String command = (glyphAdjust > 0) ? "\u001B&a+" : "\u001B&a"; sb.append(command).append(gen.formatDouble2(glyphAdjust / 100.0)).append('H'); current = ""; } } if (!current.equals("")) { gen.getOutputStream().write(sb.toString().getBytes(gen.getTextEncoding())); for (int i = 0; i < current.length(); i++) { gen.getOutputStream().write(softFont.getCharCode(current.charAt(i))); } } } private static final double SAFETY_MARGIN_FACTOR = 0.05; private Rectangle getTextBoundingBox(int x, int y, int letterSpacing, int wordSpacing, int[][] dp, String text, Font font, FontMetricsMapper metrics) { int maxAscent = metrics.getMaxAscent(font.getFontSize()) / 1000; int descent = metrics.getDescender(font.getFontSize()) / 1000; // is negative int safetyMargin = (int) (SAFETY_MARGIN_FACTOR * font.getFontSize()); Rectangle boundingRect = new Rectangle(x, y - maxAscent - safetyMargin, 0, maxAscent - descent + 2 * safetyMargin); int l = text.length(); int[] dx = IFUtil.convertDPToDX(dp); int dxl = (dx != null ? dx.length : 0); if (dx != null && dxl > 0 && dx[0] != 0) { boundingRect.setLocation(boundingRect.x - (int) Math.ceil(dx[0] / 10f), boundingRect.y); } float width = 0.0f; for (int i = 0; i < l; i++) { char orgChar = text.charAt(i); float glyphAdjust = 0; int cw = font.getCharWidth(orgChar); if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) { glyphAdjust += wordSpacing; } glyphAdjust += letterSpacing; if (dx != null && i < dxl - 1) { glyphAdjust += dx[i + 1]; } width += cw + glyphAdjust; } int extraWidth = font.getFontSize() / 3; boundingRect.setSize((int) Math.ceil(width) + extraWidth, boundingRect.height); return boundingRect; } private void drawTextAsBitmap(final int x, final int y, final int letterSpacing, final int wordSpacing, final int[][] dp, final String text, FontTriplet triplet) throws IFException { // Use Java2D to paint different fonts via bitmap final Font font = getFontInfo().getFontInstance(triplet, state.getFontSize()); // for cursive fonts, so the text isn't clipped FontMetricsMapper mapper; try { mapper = (FontMetricsMapper) getFontInfo().getMetricsFor(font.getFontName()); } catch (Exception t) { throw new RuntimeException(t); } final int maxAscent = mapper.getMaxAscent(font.getFontSize()) / 1000; final int ascent = mapper.getAscender(font.getFontSize()) / 1000; final int descent = mapper.getDescender(font.getFontSize()) / 1000; int safetyMargin = (int) (SAFETY_MARGIN_FACTOR * font.getFontSize()); final int baselineOffset = maxAscent + safetyMargin; final Rectangle boundingBox = getTextBoundingBox(x, y, letterSpacing, wordSpacing, dp, text, font, mapper); final Dimension dim = boundingBox.getSize(); Graphics2DImagePainter painter = new Graphics2DImagePainter() { public void paint(Graphics2D g2d, Rectangle2D area) { if (DEBUG) { g2d.setBackground(Color.LIGHT_GRAY); g2d.clearRect(0, 0, (int) area.getWidth(), (int) area.getHeight()); } g2d.translate(-x, -y + baselineOffset); if (DEBUG) { Rectangle rect = new Rectangle(x, y - maxAscent, 3000, maxAscent); g2d.draw(rect); rect = new Rectangle(x, y - ascent, 2000, ascent); g2d.draw(rect); rect = new Rectangle(x, y, 1000, -descent); g2d.draw(rect); } Java2DPainter painter = new Java2DPainter(g2d, getContext(), getFontInfo(), state); try { painter.drawText(x, y, letterSpacing, wordSpacing, dp, text); } catch (IFException e) { // This should never happen with the Java2DPainter throw new RuntimeException("Unexpected error while painting text", e); } } public Dimension getImageSize() { return dim.getSize(); } }; paintMarksAsBitmap(painter, boundingBox); } /** Saves the current graphics state on the stack. */ private void saveGraphicsState() { graphicContextStack.push(graphicContext); graphicContext = (GraphicContext)graphicContext.clone(); } /** Restores the last graphics state from the stack. */ private void restoreGraphicsState() { graphicContext = graphicContextStack.pop(); } private void concatenateTransformationMatrix(AffineTransform transform) throws IOException { if (!transform.isIdentity()) { graphicContext.transform(transform); changePrintDirection(); } } private Point2D transformedPoint(int x, int y) { return PCLRenderingUtil.transformedPoint(x, y, graphicContext.getTransform(), currentPageDefinition, currentPrintDirection); } private void changePrintDirection() throws IOException { AffineTransform at = graphicContext.getTransform(); int newDir; newDir = PCLRenderingUtil.determinePrintDirection(at); if (newDir != this.currentPrintDirection) { this.currentPrintDirection = newDir; gen.changePrintDirection(this.currentPrintDirection); } } /** * Sets the current cursor position. The coordinates are transformed to the absolute position * on the logical PCL page and then passed on to the PCLGenerator. * @param x the x coordinate (in millipoints) * @param y the y coordinate (in millipoints) */ void setCursorPos(int x, int y) throws IOException { Point2D transPoint = transformedPoint(x, y); gen.setCursorPos(transPoint.getX(), transPoint.getY()); } }