/* * 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.afp; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.GraphicsConfiguration; import java.awt.Image; import java.awt.Paint; import java.awt.Rectangle; import java.awt.Shape; import java.awt.Stroke; import java.awt.TexturePaint; import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import java.awt.geom.PathIterator; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.ImageObserver; import java.awt.image.RenderedImage; import java.awt.image.renderable.RenderableImage; import java.io.IOException; import org.apache.commons.io.output.ByteArrayOutputStream; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.xmlgraphics.image.loader.ImageInfo; import org.apache.xmlgraphics.image.loader.ImageSize; import org.apache.xmlgraphics.image.loader.impl.ImageRendered; import org.apache.xmlgraphics.java2d.AbstractGraphics2D; import org.apache.xmlgraphics.java2d.GraphicContext; import org.apache.xmlgraphics.java2d.StrokingTextHandler; import org.apache.xmlgraphics.java2d.TextHandler; import org.apache.xmlgraphics.ps.ImageEncodingHelper; import org.apache.xmlgraphics.util.MimeConstants; import org.apache.xmlgraphics.util.UnitConv; import org.apache.fop.afp.goca.GraphicsSetLineType; import org.apache.fop.afp.modca.GraphicsObject; import org.apache.fop.afp.svg.AFPGraphicsConfiguration; import org.apache.fop.fonts.FontInfo; import org.apache.fop.svg.NativeImageHandler; /** * This is a concrete implementation of AbstractGraphics2D (and * therefore of Graphics2D) which is able to generate GOCA byte * codes. * * @see org.apache.xmlgraphics.java2d.AbstractGraphics2D */ public class AFPGraphics2D extends AbstractGraphics2D implements NativeImageHandler { private static final Log log = LogFactory.getLog(AFPGraphics2D.class); private static final int X = 0; private static final int Y = 1; private static final int X1 = 0; private static final int Y1 = 1; private static final int X2 = 2; private static final int Y2 = 3; private static final int X3 = 4; private static final int Y3 = 5; /** graphics object */ private GraphicsObject graphicsObj = null; /** Fallback text handler */ protected TextHandler fallbackTextHandler = new StrokingTextHandler(); /** Custom text handler */ protected TextHandler customTextHandler = null; /** AFP resource manager */ private AFPResourceManager resourceManager = null; /** AFP resource info */ private AFPResourceInfo resourceInfo = null; /** Current AFP state */ private AFPPaintingState paintingState = null; /** AFP graphics configuration */ private final AFPGraphicsConfiguration graphicsConfig = new AFPGraphicsConfiguration(); /** The AFP FontInfo */ private FontInfo fontInfo; /** * Main constructor * * @param textAsShapes * if true, all text is turned into shapes in the convertion. No * text is output. * @param paintingState painting state * @param resourceManager resource manager * @param resourceInfo resource info * @param fontInfo font info */ public AFPGraphics2D(boolean textAsShapes, AFPPaintingState paintingState, AFPResourceManager resourceManager, AFPResourceInfo resourceInfo, FontInfo fontInfo) { super(textAsShapes); setPaintingState(paintingState); setResourceManager(resourceManager); setResourceInfo(resourceInfo); setFontInfo(fontInfo); } /** * Copy Constructor * * @param g2d * a AFPGraphics2D whose properties should be copied */ public AFPGraphics2D(AFPGraphics2D g2d) { super(g2d); this.paintingState = g2d.paintingState; this.resourceManager = g2d.resourceManager; this.resourceInfo = g2d.resourceInfo; this.fontInfo = g2d.fontInfo; this.graphicsObj = g2d.graphicsObj; this.fallbackTextHandler = g2d.fallbackTextHandler; this.customTextHandler = g2d.customTextHandler; } /** * Sets the AFP resource manager * * @param resourceManager the AFP resource manager */ private void setResourceManager(AFPResourceManager resourceManager) { this.resourceManager = resourceManager; } /** * Sets the AFP resource info * * @param resourceInfo the AFP resource info */ private void setResourceInfo(AFPResourceInfo resourceInfo) { this.resourceInfo = resourceInfo; } /** * Returns the GOCA graphics object * * @return the GOCA graphics object */ public GraphicsObject getGraphicsObject() { return this.graphicsObj; } /** * Sets the GOCA graphics object * * @param obj the GOCA graphics object */ public void setGraphicsObject(GraphicsObject obj) { this.graphicsObj = obj; } /** * Sets the AFP painting state * * @param paintingState the AFP painting state */ private void setPaintingState(AFPPaintingState paintingState) { this.paintingState = paintingState; } /** * Returns the AFP painting state * * @return the AFP painting state */ public AFPPaintingState getPaintingState() { return this.paintingState; } /** * Sets the FontInfo * * @param fontInfo the FontInfo */ private void setFontInfo(FontInfo fontInfo) { this.fontInfo = fontInfo; } /** * Returns the FontInfo * * @return the FontInfo */ public FontInfo getFontInfo() { return this.fontInfo; } /** * Sets the GraphicContext * * @param gc * GraphicContext to use */ public void setGraphicContext(GraphicContext gc) { this.gc = gc; } private int getResolution() { return this.paintingState.getResolution(); } /** * Converts a length value to an absolute value. * Please note that this only uses the "ScaleY" factor, so this will result * in a bad value should "ScaleX" and "ScaleY" be different. * @param length the length * @return the absolute length */ public double convertToAbsoluteLength(double length) { AffineTransform current = getTransform(); double mult = getResolution() / (double)UnitConv.IN2PT; double factor = -current.getScaleY() / mult; return length * factor; } /** IBM's AFP Workbench paints lines that are wider than expected. We correct manually. */ private static final double GUESSED_WIDTH_CORRECTION = 1.7; private static final double SPEC_NORMAL_LINE_WIDTH = UnitConv.in2pt(0.01); //"approx" 0.01 inch private static final double NORMAL_LINE_WIDTH = SPEC_NORMAL_LINE_WIDTH * GUESSED_WIDTH_CORRECTION; /** * Apply the stroke to the AFP graphics object. * This takes the java stroke and outputs the appropriate settings * to the AFP graphics object so that the stroke attributes are handled. * * @param stroke the java stroke */ protected void applyStroke(Stroke stroke) { if (stroke instanceof BasicStroke) { BasicStroke basicStroke = (BasicStroke) stroke; // set line width float lineWidth = basicStroke.getLineWidth(); if (false) { //Old approach. Retained until verified problems with 1440 resolution graphicsObj.setLineWidth(Math.round(lineWidth / 2)); } else { double absoluteLineWidth = lineWidth * Math.abs(getTransform().getScaleY()); double multiplier = absoluteLineWidth / NORMAL_LINE_WIDTH; graphicsObj.setLineWidth((int)Math.round(multiplier)); //TODO Use GSFLW instead of GSLW for higher accuracy? } //No line join, miter limit and end cap support in GOCA. :-( // set line type/style (note: this is an approximation at best!) float[] dashArray = basicStroke.getDashArray(); if (paintingState.setDashArray(dashArray)) { byte type = GraphicsSetLineType.DEFAULT; // normally SOLID if (dashArray != null) { type = GraphicsSetLineType.DOTTED; // default to plain DOTTED if dashed line // float offset = basicStroke.getDashPhase(); if (dashArray.length == 2) { if (dashArray[0] < dashArray[1]) { type = GraphicsSetLineType.SHORT_DASHED; } else if (dashArray[0] > dashArray[1]) { type = GraphicsSetLineType.LONG_DASHED; } } else if (dashArray.length == 4) { if (dashArray[0] > dashArray[1] && dashArray[2] < dashArray[3]) { type = GraphicsSetLineType.DASH_DOT; } else if (dashArray[0] < dashArray[1] && dashArray[2] < dashArray[3]) { type = GraphicsSetLineType.DOUBLE_DOTTED; } } else if (dashArray.length == 6) { if (dashArray[0] > dashArray[1] && dashArray[2] < dashArray[3] && dashArray[4] < dashArray[5]) { type = GraphicsSetLineType.DASH_DOUBLE_DOTTED; } } } graphicsObj.setLineType(type); } } else { log.warn("Unsupported Stroke: " + stroke.getClass().getName()); } } /** * Apply the java paint to the AFP. * This takes the java paint sets up the appropriate AFP commands * for the drawing with that paint. * Currently this supports the gradients and patterns from batik. * * @param paint the paint to convert to AFP * @param fill true if the paint should be set for filling * @return true if the paint is handled natively, false if the paint should be rasterized */ private boolean applyPaint(Paint paint, boolean fill) { if (paint instanceof Color) { return true; } log.debug("NYI: applyPaint() " + paint + " fill=" + fill); if (paint instanceof TexturePaint) { // TexturePaint texturePaint = (TexturePaint)paint; // BufferedImage bufferedImage = texturePaint.getImage(); // AffineTransform at = paintingState.getTransform(); // int x = (int)Math.round(at.getTranslateX()); // int y = (int)Math.round(at.getTranslateY()); // drawImage(bufferedImage, x, y, null); } return false; } /** * Handle the Batik drawing event * * @param shape * the shape to draw * @param fill * true if the shape is to be drawn filled */ private void doDrawing(Shape shape, boolean fill) { if (!fill) { graphicsObj.newSegment(); } graphicsObj.setColor(gc.getColor()); applyPaint(gc.getPaint(), fill); if (fill) { graphicsObj.beginArea(); } else { applyStroke(gc.getStroke()); } AffineTransform trans = gc.getTransform(); PathIterator iter = shape.getPathIterator(trans); if (shape instanceof Line2D) { double[] dstPts = new double[6]; iter.currentSegment(dstPts); int[] coords = new int[4]; coords[X1] = (int) Math.round(dstPts[X]); coords[Y1] = (int) Math.round(dstPts[Y]); iter.next(); iter.currentSegment(dstPts); coords[X2] = (int) Math.round(dstPts[X]); coords[Y2] = (int) Math.round(dstPts[Y]); graphicsObj.addLine(coords); } else if (shape instanceof Rectangle2D) { double[] dstPts = new double[6]; iter.currentSegment(dstPts); int[] coords = new int[4]; coords[X2] = (int) Math.round(dstPts[X]); coords[Y2] = (int) Math.round(dstPts[Y]); iter.next(); iter.next(); iter.currentSegment(dstPts); coords[X1] = (int) Math.round(dstPts[X]); coords[Y1] = (int) Math.round(dstPts[Y]); graphicsObj.addBox(coords); } else if (shape instanceof Ellipse2D) { double[] dstPts = new double[6]; Ellipse2D elip = (Ellipse2D) shape; double scale = trans.getScaleX(); double radiusWidth = elip.getWidth() / 2; double radiusHeight = elip.getHeight() / 2; graphicsObj.setArcParams( (int)Math.round(radiusWidth * scale), (int)Math.round(radiusHeight * scale), 0, 0 ); double[] srcPts = new double[] {elip.getCenterX(), elip.getCenterY()}; trans.transform(srcPts, 0, dstPts, 0, 1); final int mh = 1; final int mhr = 0; graphicsObj.addFullArc( (int)Math.round(dstPts[X]), (int)Math.round(dstPts[Y]), mh, mhr ); } else { processPathIterator(iter); } if (fill) { graphicsObj.endArea(); } } /** * Processes a path iterator generating the necessary painting operations. * * @param iter PathIterator to process */ private void processPathIterator(PathIterator iter) { double[] dstPts = new double[6]; for (int[] openingCoords = new int[2]; !iter.isDone(); iter.next()) { switch (iter.currentSegment(dstPts)) { case PathIterator.SEG_LINETO: graphicsObj.addLine(new int[] { (int)Math.round(dstPts[X]), (int)Math.round(dstPts[Y]) }, true); break; case PathIterator.SEG_QUADTO: graphicsObj.addFillet(new int[] { (int)Math.round(dstPts[X1]), (int)Math.round(dstPts[Y1]), (int)Math.round(dstPts[X2]), (int)Math.round(dstPts[Y2]) }, true); break; case PathIterator.SEG_CUBICTO: graphicsObj.addFillet(new int[] { (int)Math.round(dstPts[X1]), (int)Math.round(dstPts[Y1]), (int)Math.round(dstPts[X2]), (int)Math.round(dstPts[Y2]), (int)Math.round(dstPts[X3]), (int)Math.round(dstPts[Y3]) }, true); break; case PathIterator.SEG_MOVETO: openingCoords = new int[] { (int)Math.round(dstPts[X]), (int)Math.round(dstPts[Y]) }; graphicsObj.setCurrentPosition(openingCoords); break; case PathIterator.SEG_CLOSE: graphicsObj.addLine(openingCoords, true); break; default: log.debug("Unrecognised path iterator type"); break; } } } /** {@inheritDoc} */ public void draw(Shape shape) { log.debug("draw() shape=" + shape); doDrawing(shape, false); } /** {@inheritDoc} */ public void fill(Shape shape) { log.debug("fill() shape=" + shape); doDrawing(shape, true); } /** * Central handler for IOExceptions for this class. * * @param ioe * IOException to handle */ public void handleIOException(IOException ioe) { // TODO Surely, there's a better way to do this. log.error(ioe.getMessage()); ioe.printStackTrace(); } /** {@inheritDoc} */ public void drawString(String str, float x, float y) { try { if (customTextHandler != null && !textAsShapes) { customTextHandler.drawString(this, str, x, y); } else { fallbackTextHandler.drawString(this, str, x, y); } } catch (IOException ioe) { handleIOException(ioe); } } /** {@inheritDoc} */ public GraphicsConfiguration getDeviceConfiguration() { return graphicsConfig; } /** {@inheritDoc} */ public Graphics create() { return new AFPGraphics2D(this); } /** {@inheritDoc} */ public void dispose() { this.graphicsObj = null; } /** {@inheritDoc} */ public boolean drawImage(Image img, int x, int y, ImageObserver observer) { return drawImage(img, x, y, img.getWidth(observer), img.getHeight(observer), observer); } private BufferedImage buildBufferedImage(Dimension size) { return new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB); } private AFPImageObjectInfo createImageObjectInfo( RenderedImage img, int x, int y, int width, int height) throws IOException { ImageInfo imageInfo = new ImageInfo(null, "image/unknown"); ImageSize size = new ImageSize(img.getWidth(), img.getHeight(), 72); imageInfo.setSize(size); ImageRendered imageRendered = new ImageRendered(imageInfo, img, null); RenderedImage renderedImage = imageRendered.getRenderedImage(); // create image object info AFPImageObjectInfo imageObjectInfo = new AFPImageObjectInfo(); imageObjectInfo.setMimeType(MimeConstants.MIME_AFP_IOCA_FS45); int bitsPerPixel = paintingState.getBitsPerPixel(); imageObjectInfo.setBitsPerPixel(bitsPerPixel); imageObjectInfo.setResourceInfo(resourceInfo); int dataHeight = renderedImage.getHeight(); imageObjectInfo.setDataHeight(dataHeight); int dataWidth = renderedImage.getWidth(); imageObjectInfo.setDataWidth(dataWidth); boolean colorImages = paintingState.isColorImages(); imageObjectInfo.setColor(colorImages); ByteArrayOutputStream boas = new ByteArrayOutputStream(); ImageEncodingHelper.encodeRenderedImageAsRGB(renderedImage, boas); byte[] imageData = boas.toByteArray(); // convert to grayscale if (!colorImages) { boas.reset(); imageObjectInfo.setBitsPerPixel(bitsPerPixel); ImageEncodingHelper.encodeRGBAsGrayScale( imageData, dataWidth, dataHeight, bitsPerPixel, boas); imageData = boas.toByteArray(); } imageObjectInfo.setData(imageData); if (imageInfo != null) { imageObjectInfo.setUri(imageInfo.getOriginalURI()); } // create object area info AFPObjectAreaInfo objectAreaInfo = new AFPObjectAreaInfo(); objectAreaInfo.setX(x); objectAreaInfo.setY(y); objectAreaInfo.setWidth(width); objectAreaInfo.setHeight(height); int resolution = paintingState.getResolution(); objectAreaInfo.setWidthRes(resolution); objectAreaInfo.setHeightRes(resolution); imageObjectInfo.setObjectAreaInfo(objectAreaInfo); return imageObjectInfo; } /** * Draws an AWT image into a BufferedImage using an AWT Graphics2D implementation * * @param img the AWT image * @param bufferedImage the AWT buffered image * @param width the image width * @param height the image height * @param observer the image observer * @return true if the image was drawn */ private boolean drawBufferedImage(Image img, BufferedImage bufferedImage, int width, int height, ImageObserver observer) { java.awt.Graphics2D g2d = bufferedImage.createGraphics(); try { g2d.setComposite(AlphaComposite.SrcOver); Color color = new Color(1, 1, 1, 0); g2d.setBackground(color); g2d.setPaint(color); g2d.fillRect(0, 0, width, height); int imageWidth = bufferedImage.getWidth(); int imageHeight = bufferedImage.getHeight(); Rectangle clipRect = new Rectangle(0, 0, imageWidth, imageHeight); g2d.clip(clipRect); g2d.setComposite(gc.getComposite()); return g2d.drawImage(img, 0, 0, imageWidth, imageHeight, observer); } finally { g2d.dispose(); //drawn so dispose immediately to free system resource } } /** {@inheritDoc} */ public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) { // draw with AWT Graphics2D Dimension imageSize = new Dimension(width, height); BufferedImage bufferedImage = buildBufferedImage(imageSize); boolean drawn = drawBufferedImage(img, bufferedImage, width, height, observer); if (drawn) { AffineTransform at = gc.getTransform(); float[] srcPts = new float[] {x, y}; float[] dstPts = new float[srcPts.length]; at.transform(srcPts, 0, dstPts, 0, 1); x = Math.round(dstPts[X]); y = Math.round(dstPts[Y]); try { // get image object info AFPImageObjectInfo imageObjectInfo = createImageObjectInfo(bufferedImage, x, y, width, height); // create image resource resourceManager.createObject(imageObjectInfo); return true; } catch (IOException ioe) { handleIOException(ioe); } } return false; } /** {@inheritDoc} */ public void drawRenderedImage(RenderedImage img, AffineTransform xform) { int imgWidth = img.getWidth(); int imgHeight = img.getHeight(); AffineTransform at = paintingState.getData().getTransform(); AffineTransform gat = gc.getTransform(); int graphicsObjectHeight = graphicsObj.getObjectEnvironmentGroup().getObjectAreaDescriptor().getHeight(); int x = (int)Math.round(at.getTranslateX() + gat.getTranslateX()); int y = (int)Math.round(at.getTranslateY() - (gat.getTranslateY() - graphicsObjectHeight)); int width = (int)Math.round(imgWidth * gat.getScaleX()); int height = (int)Math.round(imgHeight * -gat.getScaleY()); try { // get image object info AFPImageObjectInfo imageObjectInfo = createImageObjectInfo(img, x, y, width, height); // create image resource resourceManager.createObject(imageObjectInfo); } catch (IOException ioe) { handleIOException(ioe); } } /** * Sets a custom TextHandler implementation that is responsible for painting * text. The default TextHandler paints all text as shapes. A custom * implementation can implement text painting using text painting operators. * * @param handler * the custom TextHandler implementation */ public void setCustomTextHandler(TextHandler handler) { this.customTextHandler = handler; } /** {@inheritDoc} */ public void drawRenderableImage(RenderableImage img, AffineTransform xform) { log.debug("drawRenderableImage() NYI: img=" + img + ", xform=" + xform); } /** {@inheritDoc} */ public FontMetrics getFontMetrics(Font f) { log.debug("getFontMetrics() NYI: f=" + f); return null; } /** {@inheritDoc} */ public void setXORMode(Color col) { log.debug("setXORMode() NYI: col=" + col); } /** {@inheritDoc} */ public void addNativeImage(org.apache.xmlgraphics.image.loader.Image image, float x, float y, float width, float height) { log.debug("NYI: addNativeImage() " + "image=" + image + ",x=" + x + ",y=" + y + ",width=" + width + ",height=" + height); } /** {@inheritDoc} */ public void copyArea(int x, int y, int width, int height, int dx, int dy) { log.debug("copyArea() NYI: "); } }