/* * 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.afp; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.GraphicsConfiguration; import java.awt.Image; import java.awt.Shape; import java.awt.Stroke; import java.awt.geom.AffineTransform; import java.awt.geom.Ellipse2D; import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.awt.geom.PathIterator; import java.awt.geom.Rectangle2D; import java.awt.image.ImageObserver; import java.awt.image.RenderedImage; import java.awt.image.renderable.RenderableImage; import java.io.IOException; import org.apache.batik.ext.awt.geom.ExtendedGeneralPath; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.fop.render.afp.goca.GraphicsSetLineType; import org.apache.fop.render.afp.modca.GraphicsObject; import org.apache.xmlgraphics.java2d.AbstractGraphics2D; import org.apache.xmlgraphics.java2d.GraphicContext; import org.apache.xmlgraphics.java2d.StrokingTextHandler; import org.apache.xmlgraphics.java2d.TextHandler; /** * 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 { private static final Log log = LogFactory.getLog(AFPGraphics2D.class); private GraphicsObject graphicsObj = null; /** Fallback text handler */ protected TextHandler fallbackTextHandler = new StrokingTextHandler(this); /** Custom text handler */ protected TextHandler customTextHandler = null; /** AFP info */ private AFPInfo afpInfo = null; /** Current AFP state */ private AFPState afpState = null; // /** The SVG document URI */ // private String documentURI = null; /** * @param textAsShapes * if true, all text is turned into shapes in the convertion. No * text is output. * */ public AFPGraphics2D(boolean textAsShapes) { super(textAsShapes); } /** * Creates a new AFPGraphics2D from an existing instance. * * @param g * the AFPGraphics2D whose properties should be copied */ public AFPGraphics2D(AFPGraphics2D g) { super(g); this.graphicsObj = g.graphicsObj; this.fallbackTextHandler = g.fallbackTextHandler; this.customTextHandler = g.customTextHandler; this.afpInfo = g.afpInfo; this.afpState = g.afpState; } /** * Sets the AFPInfo * * @param info * the AFP Info to use */ public void setAFPInfo(AFPInfo info) { this.afpInfo = info; this.afpState = info.getState(); } /** * Gets the AFPInfo * * @return the AFPInfo */ public AFPInfo getAFPInfo() { return this.afpInfo; } /** * Sets the GraphicContext * * @param gc * GraphicContext to use */ public void setGraphicContext(GraphicContext gc) { this.gc = gc; } /** * 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; float lineWidth = basicStroke.getLineWidth(); if (afpState.setLineWidth(lineWidth)) { getGraphicsObject().setLineWidth(Math.round(lineWidth * 2)); } // note: this is an approximation at best! float[] dashArray = basicStroke.getDashArray(); if (afpState.setDashArray(dashArray)) { byte type = GraphicsSetLineType.DEFAULT; // normally SOLID if (dashArray != null) { type = GraphicsSetLineType.DOTTED; // default to DOTTED // 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; } } } getGraphicsObject().setLineType(type); } } else { log.warn("Unsupported Stroke: " + stroke.getClass().getName()); } } /** * 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) { getGraphicsObject(); if (!fill) { graphicsObj.newSegment(); } Color col = getColor(); if (afpState.setColor(col)) { graphicsObj.setColor(col); } applyStroke(getStroke()); if (fill) { graphicsObj.beginArea(); } AffineTransform trans = super.getTransform(); PathIterator iter = shape.getPathIterator(trans); double[] vals = new double[6]; int[] coords = null; if (shape instanceof GeneralPath || shape instanceof ExtendedGeneralPath) { // graphics segment opening coordinates (x,y) // current position coordinates (x,y) for (int[] openingCoords = new int[2], currCoords = new int[2]; !iter.isDone(); iter.next()) { // round the coordinate values and combine with current position // coordinates int type = iter.currentSegment(vals); if (type == PathIterator.SEG_MOVETO) { log.debug("SEG_MOVETO"); openingCoords[0] = currCoords[0] = (int)Math.round(vals[0]); openingCoords[1] = currCoords[1] = (int)Math.round(vals[1]); } else { int numCoords; if (type == PathIterator.SEG_LINETO) { log.debug("SEG_LINETO"); numCoords = 2; } else if (type == PathIterator.SEG_QUADTO) { log.debug("SEG_QUADTO"); numCoords = 4; } else if (type == PathIterator.SEG_CUBICTO) { log.debug("SEG_CUBICTO"); numCoords = 6; } else { // close of the graphics segment if (type == PathIterator.SEG_CLOSE) { log.debug("SEG_CLOSE"); coords = new int[] { coords[coords.length - 2], coords[coords.length - 1], openingCoords[0], openingCoords[1] }; graphicsObj.addLine(coords); } else { log.debug("Unrecognised path iterator type: " + type); } continue; } // combine current position coordinates with new graphics // segment coordinates coords = new int[numCoords + 2]; coords[0] = currCoords[0]; coords[1] = currCoords[1]; for (int i = 0; i < numCoords; i++) { coords[i + 2] = (int) Math.round(vals[i]); } if (type == PathIterator.SEG_LINETO) { graphicsObj.addLine(coords); } else if (type == PathIterator.SEG_QUADTO || type == PathIterator.SEG_CUBICTO) { graphicsObj.addFillet(coords); } // update current position coordinates currCoords[0] = coords[coords.length - 2]; currCoords[1] = coords[coords.length - 1]; } } } else if (shape instanceof Line2D) { iter.currentSegment(vals); coords = new int[4]; coords[0] = (int) Math.round(vals[0]); coords[1] = (int) Math.round(vals[1]); iter.next(); iter.currentSegment(vals); coords[2] = (int) Math.round(vals[0]); coords[3] = (int) Math.round(vals[1]); graphicsObj.addLine(coords); } else if (shape instanceof Rectangle2D) { iter.currentSegment(vals); coords = new int[4]; coords[2] = (int) Math.round(vals[0]); coords[3] = (int) Math.round(vals[1]); iter.next(); iter.next(); iter.currentSegment(vals); coords[0] = (int) Math.round(vals[0]); coords[1] = (int) Math.round(vals[1]); graphicsObj.addBox(coords); } else if (shape instanceof Ellipse2D) { Ellipse2D elip = (Ellipse2D) shape; final double factor = afpInfo.getResolution() / 100f; graphicsObj.setArcParams( (int)Math.round(elip.getWidth() * factor), (int)Math.round(elip.getHeight() * factor), 0, 0 ); trans.transform( new double[] {elip.getCenterX(), elip.getCenterY()}, 0, vals, 0, 1); final int mh = 1; final int mhr = 0; graphicsObj.addFullArc( (int)Math.round(vals[0]), (int)Math.round(vals[1]), mh, mhr ); } else { log.error("Unrecognised shape: " + shape); } if (fill) { graphicsObj.endArea(); } } /** * {@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 s, float x, float y) { try { if (customTextHandler != null && !textAsShapes) { customTextHandler.drawString(s, x, y); } else { fallbackTextHandler.drawString(s, x, y); } } catch (IOException ioe) { handleIOException(ioe); } } /** * {@inheritDoc} */ public GraphicsConfiguration getDeviceConfiguration() { return new AFPGraphicsConfiguration(); } /** * {@inheritDoc} */ public void copyArea(int x, int y, int width, int height, int dx, int dy) { log.debug("copyArea() NYI: "); } /** * {@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); } /** * {@inheritDoc} */ public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) { //TODO: this might be achieved by creating a new IOCA image (ImageObject) // and placing it in an Overlay - but then stacking order would not be preserved. log.debug("drawImage(): NYI img=" + img + ", x=" + x + ", y=" + y + ", width=" + width + ", height=" + height + ", obs=" + observer); return false; } /** * {@inheritDoc} */ public void drawRenderableImage(RenderableImage img, AffineTransform xform) { log.debug("drawRenderableImage() NYI: img=" + img + ", xform=" + xform); } /** * {@inheritDoc} */ public void drawRenderedImage(RenderedImage img, AffineTransform xform) { log.debug("drawRenderedImage() 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); } /** * 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; } /** * @return the GOCA graphics object */ protected GraphicsObject getGraphicsObject() { return this.graphicsObj; } /** * Sets the graphics object * @param obj the graphics object */ protected void setGraphicsObject(GraphicsObject obj) { this.graphicsObj = obj; } // /** // * Sets the SVG document URI // * @param documentURI the SVG document URI // */ // public void setDocumentURI(String documentURI) { // this.documentURI = documentURI; // } }