/*
 * 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: AFPGraphics2D.java 1825861 2018-03-05 09:39:38Z ssteiner $ */

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 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 AbstractGraphics2D (and therefore of Graphics2D) which is able to generate GOCA byte codes.
See Also:
/** * 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
/** graphics object */
private GraphicsObject graphicsObj;
Fallback text handler
/** Fallback text handler */
protected TextHandler fallbackTextHandler = new StrokingTextHandler();
Custom text handler
/** Custom text handler */
protected TextHandler customTextHandler;
AFP resource manager
/** AFP resource manager */
private AFPResourceManager resourceManager;
AFP resource info
/** AFP resource info */
private AFPResourceInfo resourceInfo;
Current AFP state
/** Current AFP state */
private AFPPaintingState paintingState;
AFP graphics configuration
/** AFP graphics configuration */
private final GraphicsConfigurationWithTransparency graphicsConfig = new GraphicsConfigurationWithTransparency();
The AFP FontInfo
/** The AFP FontInfo */
private FontInfo fontInfo;
Main constructor
Params:
  • textAsShapes – if true, all text is turned into shapes in the convertion. No text is output.
  • paintingState – painting state
  • resourceManager – resource manager
  • resourceInfo – resource info
  • fontInfo – font info
/** * 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
Params:
  • g2d – a AFPGraphics2D whose properties should be copied
/** * 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
Params:
  • resourceManager – the AFP resource manager
/** * 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 Graphics2D instance.
Returns:the resource manager
/** * 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
Params:
  • resourceInfo – the AFP resource info
/** * Sets the AFP resource info * * @param resourceInfo the AFP resource info */
private void setResourceInfo(AFPResourceInfo resourceInfo) { this.resourceInfo = resourceInfo; }
Returns the GOCA graphics object
Returns:the GOCA graphics object
/** * Returns the GOCA graphics object * * @return the GOCA graphics object */
public GraphicsObject getGraphicsObject() { return this.graphicsObj; }
Sets the GOCA graphics object
Params:
  • obj – the GOCA graphics object
/** * Sets the GOCA graphics object * * @param obj the GOCA graphics object */
public void setGraphicsObject(GraphicsObject obj) { this.graphicsObj = obj; }
Sets the AFP painting state
Params:
  • paintingState – the AFP painting state
/** * Sets the AFP painting state * * @param paintingState the AFP painting state */
private void setPaintingState(AFPPaintingState paintingState) { this.paintingState = paintingState; }
Returns the AFP painting state
Returns:the AFP painting state
/** * Returns the AFP painting state * * @return the AFP painting state */
public AFPPaintingState getPaintingState() { return this.paintingState; }
Sets the FontInfo
Params:
  • fontInfo – the FontInfo
/** * Sets the FontInfo * * @param fontInfo the FontInfo */
private void setFontInfo(FontInfo fontInfo) { this.fontInfo = fontInfo; }
Returns the FontInfo
Returns:the FontInfo
/** * Returns the FontInfo * * @return the FontInfo */
public FontInfo getFontInfo() { return this.fontInfo; }
Sets the GraphicContext
Params:
  • gc – GraphicContext to use
/** * 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.
Params:
  • length – the length
Returns:the absolute length
/** * 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.
Params:
  • stroke – the java stroke
/** * 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.
Params:
  • paint – the paint to convert to AFP
  • fill – true if the paint should be set for filling
Returns:true if the paint is handled natively, false if the paint should be rasterized
/** * 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
Params:
  • shape – the shape to draw
  • fill – true if the shape is to be drawn filled
/** * 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.
Params:
  • iter – PathIterator to process
/** * 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]; 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); currentPosition = new double[]{dstPts[X], dstPts[Y]}; 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); 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) { graphicsObj.addFillet(new int[]{ (int) Math.round(quadPts[X1]), (int) Math.round(quadPts[Y1]), (int) Math.round(quadPts[X2]), (int) Math.round(quadPts[Y2]) }, true); currentPosition = new double[]{quadPts[X2], quadPts[Y2]}; } } } break; case PathIterator.SEG_MOVETO: 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: graphicsObj.addLine(openingCoords, true); currentPosition = new double[]{openingCoords[0], openingCoords[1]}; break; default: LOG.debug("Unrecognised path iterator type"); break; } } }
{@inheritDoc}
/** {@inheritDoc} */
@Override public void draw(Shape shape) { LOG.debug("draw() shape=" + shape); doDrawing(shape, false); }
{@inheritDoc}
/** {@inheritDoc} */
@Override public void fill(Shape shape) { LOG.debug("fill() shape=" + shape); doDrawing(shape, true); }
Central handler for IOExceptions for this class.
Params:
  • ioe – IOException to handle
/** * 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}
/** {@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}
/** {@inheritDoc} */
@Override public GraphicsConfiguration getDeviceConfiguration() { return graphicsConfig; }
{@inheritDoc}
/** {@inheritDoc} */
@Override public Graphics create() { return new AFPGraphics2D(this); }
{@inheritDoc}
/** {@inheritDoc} */
@Override public void dispose() { this.graphicsObj = null; }
{@inheritDoc}
/** {@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
Params:
  • img – the AWT image
  • bufferedImage – the AWT buffered image
  • width – the image width
  • height – the image height
  • observer – the image observer
Returns:true if the image was drawn
/** * 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}
/** {@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}
/** {@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.
Params:
  • handler – the custom TextHandler implementation
/** * 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}
/** {@inheritDoc} */
@Override public void drawRenderableImage(RenderableImage img, AffineTransform xform) { LOG.debug("drawRenderableImage() NYI: img=" + img + ", xform=" + xform); }
{@inheritDoc}
/** {@inheritDoc} */
@Override public FontMetrics getFontMetrics(Font f) { LOG.debug("getFontMetrics() NYI: f=" + f); return null; }
{@inheritDoc}
/** {@inheritDoc} */
@Override public void setXORMode(Color col) { LOG.debug("setXORMode() NYI: col=" + col); }
{@inheritDoc}
/** {@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}
/** {@inheritDoc} */
@Override public void copyArea(int x, int y, int width, int height, int dx, int dy) { LOG.debug("copyArea() NYI: "); } }