/* * 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.IOException; import java.util.Map; import java.util.Stack; 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.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.Font; import org.apache.fop.fonts.FontTriplet; 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.IFContext; import org.apache.fop.render.intermediate.IFException; import org.apache.fop.render.intermediate.IFState; import org.apache.fop.render.java2d.FontMetricsMapper; import org.apache.fop.render.java2d.Java2DPainter; import org.apache.fop.traits.BorderProps; import org.apache.fop.traits.RuleStyle; import org.apache.fop.util.CharUtilities; /** * {@code IFPainter} implementation that produces PCL 5. */ public class PCLPainter extends AbstractIFPainter implements PCLConstants { /** logging instance */ private static Log log = LogFactory.getLog(PCLPainter.class); private static final boolean DEBUG = false; private PCLDocumentHandler parent; /** The PCL generator */ private PCLGenerator gen; private PCLPageDefinition currentPageDefinition; private int currentPrintDirection = 0; //private GeneralPath currentPath = null; private Stack graphicContextStack = new Stack(); private GraphicContext graphicContext = new GraphicContext(); /** * 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) { this.parent = parent; this.gen = parent.getPCLGenerator(); this.state = IFState.create(); this.currentPageDefinition = pageDefinition; } /** {@inheritDoc} */ public IFContext getContext() { return this.parent.getContext(); } PCLRenderingUtil getPCLUtil() { return this.parent.getPCLUtil(); } /** @return the target resolution */ protected int getResolution() { int resolution = (int)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) 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 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); } catch (IOException ioe) { throw new IFException("I/O error in fillRect()", ioe); } } } } /** {@inheritDoc} */ public void drawBorderRect(final Rectangle rect, final BorderProps before, final BorderProps after, final BorderProps start, final BorderProps end) throws IFException { if (isSpeedOptimized()) { super.drawBorderRect(rect, before, after, start, end); return; } if (before != null || after != null || start != null || end != 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(), parent.getFontInfo(), state); try { painter.drawBorderRect(rect, before, after, start, end); } 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(), parent.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[] dx, 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 = parent.getFontInfo().getInternalFontKey(triplet); boolean pclFont = getPCLUtil().isAllTextAsBitmaps() ? false : HardcodedFonts.setFont(gen, fontKey, state.getFontSize(), text); if (pclFont) { drawTextNative(x, y, letterSpacing, wordSpacing, dx, text, triplet); } else { drawTextAsBitmap(x, y, letterSpacing, wordSpacing, dx, text, triplet); if (DEBUG) { state.setTextColor(Color.GRAY); HardcodedFonts.setFont(gen, "F1", state.getFontSize(), text); drawTextNative(x, y, letterSpacing, wordSpacing, dx, text, triplet); } } } catch (IOException ioe) { throw new IFException("I/O error in drawText()", ioe); } } private void drawTextNative(int x, int y, int letterSpacing, int wordSpacing, int[] dx, String text, FontTriplet triplet) throws IOException { Color textColor = state.getTextColor(); if (textColor != null) { gen.setTransparencyMode(true, false); gen.selectGrayscale(textColor); } gen.setTransparencyMode(true, true); setCursorPos(x, y); float fontSize = state.getFontSize() / 1000f; Font font = parent.getFontInfo().getFontInstance(triplet, state.getFontSize()); int l = text.length(); 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'); } for (int i = 0; i < l; i++) { char orgChar = text.charAt(i); char ch; float glyphAdjust = 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); glyphAdjust = -(10 * spaceDiff / fontSize); } else { ch = font.mapChar(orgChar); } } sb.append(ch); if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) { glyphAdjust += wordSpacing; } glyphAdjust += letterSpacing; if (dx != null && i < dxl - 1) { glyphAdjust += dx[i + 1]; } if (glyphAdjust != 0) { sb.append("\u001B&a+").append(gen.formatDouble2(glyphAdjust / 100.0)).append('H'); } } gen.getOutputStream().write(sb.toString().getBytes(gen.getTextEncoding())); } private static final double SAFETY_MARGIN_FACTOR = 0.05; private Rectangle getTextBoundingBox(int x, int y, int letterSpacing, int wordSpacing, int[] dx, 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 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[] dx, final String text, FontTriplet triplet) throws IFException { //Use Java2D to paint different fonts via bitmap final Font font = parent.getFontInfo().getFontInstance(triplet, state.getFontSize()); //for cursive fonts, so the text isn't clipped final FontMetricsMapper mapper = (FontMetricsMapper)parent.getFontInfo().getMetricsFor( font.getFontName()); 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, dx, 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(), parent.getFontInfo(), state); try { painter.drawText(x, y, letterSpacing, wordSpacing, dx, 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 = (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()); } }