| /* |
| * 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.IFPainter; |
| 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; |
| |
| /** |
| * {@link 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()); |
| } |
| |
| } |