blob: 9c0a9028674a7a33b11438aaa59fda08eb2aa3c0 [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.render.ps;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import javax.imageio.ImageIO;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.GVTBuilder;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.xmlgraphics.image.loader.Image;
import org.apache.xmlgraphics.image.loader.ImageException;
import org.apache.xmlgraphics.image.loader.ImageFlavor;
import org.apache.xmlgraphics.image.loader.impl.ImageGraphics2D;
import org.apache.xmlgraphics.image.loader.impl.ImageXMLDOM;
import org.apache.xmlgraphics.ps.ImageEncoder;
import org.apache.xmlgraphics.ps.ImageEncodingHelper;
import org.apache.xmlgraphics.ps.PSGenerator;
import org.apache.fop.image.loader.batik.BatikImageFlavors;
import org.apache.fop.image.loader.batik.BatikUtil;
import org.apache.fop.image.loader.batik.ImageConverterSVG2G2D;
import org.apache.fop.render.ImageHandler;
import org.apache.fop.render.RenderingContext;
import org.apache.fop.render.ps.svg.PSSVGGraphics2D;
import org.apache.fop.svg.SVGEventProducer;
import org.apache.fop.svg.SVGUserAgent;
import org.apache.fop.svg.font.FOPFontFamilyResolverImpl;
/**
* Image handler implementation which handles SVG images for PostScript output.
*/
public class PSImageHandlerSVG implements ImageHandler {
private static final Color FALLBACK_COLOR = new Color(255, 33, 117);
private HashMap<String, String> gradientsFound = new HashMap<String, String>();
private static final ImageFlavor[] FLAVORS = new ImageFlavor[] {
BatikImageFlavors.SVG_DOM
};
/** {@inheritDoc} */
public void handleImage(RenderingContext context, Image image, Rectangle pos)
throws IOException {
PSRenderingContext psContext = (PSRenderingContext)context;
PSGenerator gen = psContext.getGenerator();
ImageXMLDOM imageSVG = (ImageXMLDOM)image;
if (shouldRaster(imageSVG)) {
InputStream is = renderSVGToInputStream(imageSVG, pos);
float x = (float) pos.getX() / 1000f;
float y = (float) pos.getY() / 1000f;
float w = (float) pos.getWidth() / 1000f;
float h = (float) pos.getHeight() / 1000f;
Rectangle2D targetRect = new Rectangle2D.Double(x, y, w, h);
MaskedImage mi = convertToRGB(ImageIO.read(is));
BufferedImage ri = mi.getImage();
ImageEncoder encoder = ImageEncodingHelper.createRenderedImageEncoder(ri);
Dimension imgDim = new Dimension(ri.getWidth(), ri.getHeight());
String imgDescription = ri.getClass().getName();
ImageEncodingHelper helper = new ImageEncodingHelper(ri);
ColorModel cm = helper.getEncodedColorModel();
PSImageUtils.writeImage(encoder, imgDim, imgDescription, targetRect, cm, gen, ri, mi.getMaskColor());
} else {
//Controls whether text painted by Batik is generated using text or path operations
boolean strokeText = shouldStrokeText(imageSVG.getDocument().getChildNodes());
//TODO Configure text stroking
SVGUserAgent ua = new SVGUserAgent(context.getUserAgent(),
new FOPFontFamilyResolverImpl(psContext.getFontInfo()), new AffineTransform());
PSSVGGraphics2D graphics = new PSSVGGraphics2D(strokeText, gen);
graphics.setGraphicContext(new org.apache.xmlgraphics.java2d.GraphicContext());
BridgeContext ctx = new PSBridgeContext(ua,
(strokeText ? null : psContext.getFontInfo()),
context.getUserAgent().getImageManager(),
context.getUserAgent().getImageSessionContext());
//Cloning SVG DOM as Batik attaches non-thread-safe facilities (like the CSS engine)
//to it.
Document clonedDoc = BatikUtil.cloneSVGDocument(imageSVG.getDocument());
GraphicsNode root;
try {
GVTBuilder builder = new GVTBuilder();
root = builder.build(ctx, clonedDoc);
} catch (Exception e) {
SVGEventProducer eventProducer = SVGEventProducer.Provider.get(
context.getUserAgent().getEventBroadcaster());
eventProducer.svgNotBuilt(this, e, image.getInfo().getOriginalURI());
return;
}
// get the 'width' and 'height' attributes of the SVG document
float w = (float)ctx.getDocumentSize().getWidth() * 1000f;
float h = (float)ctx.getDocumentSize().getHeight() * 1000f;
float sx = pos.width / w;
float sy = pos.height / h;
gen.commentln("%FOPBeginSVG");
gen.saveGraphicsState();
final boolean clip = false;
if (clip) {
/*
* Clip to the svg area.
* Note: To have the svg overlay (under) a text area then use
* an fo:block-container
*/
gen.writeln("newpath");
gen.defineRect(pos.getMinX() / 1000f, pos.getMinY() / 1000f,
pos.width / 1000f, pos.height / 1000f);
gen.writeln("clip");
}
// transform so that the coordinates (0,0) is from the top left
// and positive is down and to the right. (0,0) is where the
// viewBox puts it.
gen.concatMatrix(sx, 0, 0, sy, pos.getMinX() / 1000f, pos.getMinY() / 1000f);
AffineTransform transform = new AffineTransform();
// scale to viewbox
transform.translate(pos.getMinX(), pos.getMinY());
gen.getCurrentState().concatMatrix(transform);
try {
root.paint(graphics);
} catch (Exception e) {
SVGEventProducer eventProducer = SVGEventProducer.Provider.get(
context.getUserAgent().getEventBroadcaster());
eventProducer.svgRenderingError(this, e, image.getInfo().getOriginalURI());
}
gen.restoreGraphicsState();
gen.commentln("%FOPEndSVG");
}
}
private InputStream renderSVGToInputStream(ImageXMLDOM imageSVG, Rectangle destinationRect)
throws IOException {
Rectangle rectangle;
int width;
int height;
Float widthSVG = getDimension(imageSVG.getDocument(), "width");
Float heightSVG = getDimension(imageSVG.getDocument(), "height");
if (widthSVG != null && heightSVG != null) {
width = (int) (widthSVG * 8);
height = (int) (heightSVG * 8);
rectangle = new Rectangle(0, 0, width, height);
} else {
int scale = 10;
width = destinationRect.width / scale;
height = destinationRect.height / scale;
rectangle = new Rectangle(0, 0, destinationRect.width / 100, destinationRect.height / 100);
destinationRect.width *= scale;
destinationRect.height *= scale;
}
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics2D = image.createGraphics();
try {
ImageGraphics2D img = (ImageGraphics2D) new ImageConverterSVG2G2D().convert(imageSVG, new HashMap());
img.getGraphics2DImagePainter().paint(graphics2D, rectangle);
} catch (ImageException e) {
throw new IOException(e);
}
ByteArrayOutputStream os = new ByteArrayOutputStream();
ImageIO.write(image, "png", os);
return new ByteArrayInputStream(os.toByteArray());
}
private MaskedImage convertToRGB(BufferedImage alphaImage) {
int[] red = new int[256];
int[] green = new int[256];
int[] blue = new int[256];
BufferedImage rgbImage = new BufferedImage(alphaImage.getWidth(),
alphaImage.getHeight(), BufferedImage.TYPE_INT_RGB);
//Count occurances of each colour in image
for (int cx = 0; cx < alphaImage.getWidth(); cx++) {
for (int cy = 0; cy < alphaImage.getHeight(); cy++) {
int pixelValue = alphaImage.getRGB(cx, cy);
Color pixelColor = new Color(pixelValue);
red[pixelColor.getRed()]++;
green[pixelColor.getGreen()]++;
blue[pixelColor.getBlue()]++;
}
}
//Find colour not in image
Color alphaSwap = null;
for (int i = 0; i < 256; i++) {
if (red[i] == 0) {
alphaSwap = new Color(i, 0, 0);
break;
} else if (green[i] == 0) {
alphaSwap = new Color(0, i, 0);
break;
} else if (blue[i] == 0) {
alphaSwap = new Color(0, 0, i);
break;
}
}
//Check if all variations are used in all three colours
if (alphaSwap == null) {
//Fallback colour is no unique colour channel can be found
alphaSwap = FALLBACK_COLOR;
}
//Replace alpha channel with the new mask colour
for (int cx = 0; cx < alphaImage.getWidth(); cx++) {
for (int cy = 0; cy < alphaImage.getHeight(); cy++) {
int pixelValue = alphaImage.getRGB(cx, cy);
if (pixelValue == 0) {
rgbImage.setRGB(cx, cy, alphaSwap.getRGB());
} else {
rgbImage.setRGB(cx, cy, alphaImage.getRGB(cx, cy));
}
}
}
return new MaskedImage(rgbImage, alphaSwap);
}
private static class MaskedImage {
private Color maskColor = new Color(0, 0, 0);
private BufferedImage image;
public MaskedImage(BufferedImage image, Color maskColor) {
this.image = image;
this.maskColor = maskColor;
}
public Color getMaskColor() {
return maskColor;
}
public BufferedImage getImage() {
return image;
}
}
private Float getDimension(Document document, String dimension) {
if (document.getFirstChild().getAttributes().getNamedItem(dimension) != null) {
String width = document.getFirstChild().getAttributes().getNamedItem(dimension).getNodeValue();
width = width.replaceAll("[^\\d.]", "");
return Float.parseFloat(width);
}
return null;
}
private boolean shouldRaster(ImageXMLDOM image) {
//A list of objects on which to check opacity
try {
List<String> gradMatches = new ArrayList<String>();
gradMatches.add("radialGradient");
gradMatches.add("linearGradient");
return recurseSVGElements(image.getDocument().getChildNodes(), gradMatches, false);
} finally {
gradientsFound.clear();
}
}
private boolean recurseSVGElements(NodeList childNodes, List<String> gradMatches, boolean isMatched) {
boolean opacityFound = false;
for (int i = 0; i < childNodes.getLength(); i++) {
Node curNode = childNodes.item(i);
if (isMatched && curNode.getLocalName() != null && curNode.getLocalName().equals("stop")) {
if (curNode.getAttributes().getNamedItem("style") != null) {
String[] stylePairs = curNode.getAttributes().getNamedItem("style").getNodeValue()
.split(";");
for (String stylePair : stylePairs) {
String[] style = stylePair.split(":");
if (style[0].equalsIgnoreCase("stop-opacity")) {
if (Double.parseDouble(style[1]) < 1) {
return true;
}
}
}
}
if (curNode.getAttributes().getNamedItem("stop-opacity") != null) {
String opacityValue = curNode.getAttributes().getNamedItem("stop-opacity").getNodeValue();
if (Double.parseDouble(opacityValue) < 1) {
return true;
}
}
}
String nodeName = curNode.getLocalName();
boolean inMatch = false;
if (!isMatched) {
inMatch = nodeName != null && gradMatches.contains(nodeName);
if (inMatch) {
gradientsFound.put(curNode.getAttributes().getNamedItem("id").getNodeValue(), nodeName);
}
} else {
inMatch = true;
}
opacityFound = recurseSVGElements(curNode.getChildNodes(), gradMatches, inMatch);
if (opacityFound) {
return true;
}
}
return opacityFound;
}
public static boolean shouldStrokeText(NodeList childNodes) {
for (int i = 0; i < childNodes.getLength(); i++) {
Node curNode = childNodes.item(i);
if (shouldStrokeText(curNode.getChildNodes())) {
return true;
}
if ("text".equals(curNode.getLocalName())) {
return curNode.getAttributes().getNamedItem("filter") != null;
}
}
return false;
}
/** {@inheritDoc} */
public int getPriority() {
return 400;
}
/** {@inheritDoc} */
public Class getSupportedImageClass() {
return ImageXMLDOM.class;
}
/** {@inheritDoc} */
public ImageFlavor[] getSupportedImageFlavors() {
return FLAVORS;
}
/** {@inheritDoc} */
public boolean isCompatible(RenderingContext targetContext, Image image) {
if (targetContext instanceof PSRenderingContext) {
PSRenderingContext psContext = (PSRenderingContext)targetContext;
return !psContext.isCreateForms()
&& (image == null || (image instanceof ImageXMLDOM
&& image.getFlavor().isCompatible(BatikImageFlavors.SVG_DOM)));
}
return false;
}
}