blob: 88a15ad12db46735b39458553abd1654011c5a78 [file] [log] [blame]
/*
* 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<Integer> fillets = new ArrayList<Integer>();
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<Integer> fillets) {
List<int[]> intList = listToIntLists(fillets);
for (int[] ints : intList) {
graphicsObj.addFillet(ints, true);
}
}
private List<int[]> listToIntLists(List<Integer> input) {
List<int[]> out = new ArrayList<int[]>();
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) {
}
}