/* * 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.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 java.util.ArrayList; import java.util.List; 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.GraphicsConfigurationWithTransparency; import org.apache.xmlgraphics.java2d.StrokingTextHandler; import org.apache.xmlgraphics.java2d.TextHandler; 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.modca.ResourceObject; import org.apache.fop.afp.util.CubicBezierApproximator; import org.apache.fop.fonts.FontInfo; import org.apache.fop.render.afp.AFPImageHandlerRenderedImage; import org.apache.fop.render.afp.AFPRenderingContext; import org.apache.fop.svg.NativeImageHandler; /** * This is a concrete implementation of {@link AbstractGraphics2D} (and * therefore of {@link java.awt.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; /** Fallback text handler */ protected TextHandler fallbackTextHandler = new StrokingTextHandler(); /** Custom text handler */ protected TextHandler customTextHandler; /** AFP resource manager */ private AFPResourceManager resourceManager; /** AFP resource info */ private AFPResourceInfo resourceInfo; /** Current AFP state */ private AFPPaintingState paintingState; /** AFP graphics configuration */ private final GraphicsConfigurationWithTransparency graphicsConfig = new GraphicsConfigurationWithTransparency(); /** 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; } /** * Returns the AFP resource manager associated with this {@link java.awt.Graphics2D} instance. * @return the resource manager */ public AFPResourceManager getResourceManager() { return this.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; } /** * 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 and correct it; NOTE: apparently we need to correct the width so that the // output looks OK since the default with depends on the output device float lineWidth = basicStroke.getLineWidth(); float correction = paintingState.getLineWidthCorrection(); graphicsObj.setLineWidth(lineWidth * correction); //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]; double[] currentPosition = new double[2]; List fillets = new ArrayList(); for (int[] openingCoords = new int[2]; !iter.isDone(); iter.next()) { switch (iter.currentSegment(dstPts)) { case PathIterator.SEG_LINETO: flush(fillets); graphicsObj.addLine(new int[] { (int)Math.round(dstPts[X]), (int)Math.round(dstPts[Y]) }, true); currentPosition = new double[]{dstPts[X], dstPts[Y]}; break; case PathIterator.SEG_QUADTO: flush(fillets); 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); currentPosition = new double[]{dstPts[X2], dstPts[Y2]}; break; case PathIterator.SEG_CUBICTO: double[] cubicCoords = new double[] {currentPosition[0], currentPosition[1], dstPts[X1], dstPts[Y1], dstPts[X2], dstPts[Y2], dstPts[X3], dstPts[Y3]}; double[][] quadParts = CubicBezierApproximator.fixedMidPointApproximation( cubicCoords); if (quadParts.length >= 4) { for (double[] quadPts : quadParts) { if (quadPts != null && quadPts.length == 4) { fillets.add((int) Math.round(quadPts[X1])); fillets.add((int) Math.round(quadPts[Y1])); fillets.add((int) Math.round(quadPts[X2])); fillets.add((int) Math.round(quadPts[Y2])); currentPosition = new double[]{quadPts[X2], quadPts[Y2]}; } } } break; case PathIterator.SEG_MOVETO: flush(fillets); openingCoords = new int[] { (int)Math.round(dstPts[X]), (int)Math.round(dstPts[Y]) }; currentPosition = new double[]{dstPts[X], dstPts[Y]}; graphicsObj.setCurrentPosition(openingCoords); break; case PathIterator.SEG_CLOSE: flush(fillets); graphicsObj.addLine(openingCoords, true); currentPosition = new double[]{openingCoords[0], openingCoords[1]}; break; default: LOG.debug("Unrecognised path iterator type"); break; } } flush(fillets); } private void flush(List fillets) { List intList = listToIntLists(fillets); for (int[] ints : intList) { graphicsObj.addFillet(ints, true); } } private List listToIntLists(List input) { List out = new ArrayList(); while (!input.isEmpty()) { int[] data = new int[Math.min(100, input.size())]; for (int i = 0; i < data.length; i++) { data[i] = input.remove(0); } out.add(data); } return out; } /** {@inheritDoc} */ @Override public void draw(Shape shape) { LOG.debug("draw() shape=" + shape); doDrawing(shape, false); } /** {@inheritDoc} */ @Override 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} */ @Override 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} */ @Override public GraphicsConfiguration getDeviceConfiguration() { return graphicsConfig; } /** {@inheritDoc} */ @Override public Graphics create() { return new AFPGraphics2D(this); } /** {@inheritDoc} */ @Override public void dispose() { this.graphicsObj = null; } /** {@inheritDoc} */ @Override 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); } /** * 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} */ @Override 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) { drawRenderedImage(bufferedImage, new AffineTransform()); } return false; } /** {@inheritDoc} */ @Override public void drawRenderedImage(RenderedImage img, AffineTransform xform) { int imgWidth = img.getWidth(); int imgHeight = img.getHeight(); AffineTransform gat = gc.getTransform(); int graphicsObjectHeight = graphicsObj.getObjectEnvironmentGroup().getObjectAreaDescriptor().getHeight(); double toMillipointFactor = UnitConv.IN2PT * 1000 / (double)paintingState.getResolution(); double x = gat.getTranslateX(); double y = -(gat.getTranslateY() - graphicsObjectHeight); x = toMillipointFactor * x; y = toMillipointFactor * y; double w = toMillipointFactor * imgWidth * gat.getScaleX(); double h = toMillipointFactor * imgHeight * -gat.getScaleY(); AFPImageHandlerRenderedImage handler = new AFPImageHandlerRenderedImage(); String uri = null; if (resourceManager.getResourceLevelDefaults() .getDefaultResourceLevel(ResourceObject.TYPE_GRAPHIC).isPrintFile()) { uri = resourceInfo.getUri(); } ImageInfo imageInfo = new ImageInfo(uri, null); imageInfo.setSize(new ImageSize( img.getWidth(), img.getHeight(), paintingState.getResolution())); imageInfo.getSize().calcSizeFromPixels(); ImageRendered red = new ImageRendered(imageInfo, img, null); Rectangle targetPos = new Rectangle( (int)Math.round(x), (int)Math.round(y), (int)Math.round(w), (int)Math.round(h)); AFPRenderingContext context = new AFPRenderingContext(null, resourceManager, paintingState, fontInfo, null); resourceManager.includeCached = false; try { handler.handleImage(context, red, targetPos); } catch (IOException ioe) { handleIOException(ioe); } resourceManager.includeCached = true; } /** * 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} */ @Override public void drawRenderableImage(RenderableImage img, AffineTransform xform) { LOG.debug("drawRenderableImage() NYI: img=" + img + ", xform=" + xform); } /** {@inheritDoc} */ @Override public FontMetrics getFontMetrics(Font f) { LOG.debug("getFontMetrics() NYI: f=" + f); return null; } /** {@inheritDoc} */ @Override 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} */ @Override public void copyArea(int x, int y, int width, int height, int dx, int dy) { LOG.debug("copyArea() NYI: "); } public void clearRect(int x, int y, int width, int height) { } }