PPTX2PNG - fix SVG gradients

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1874150 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Andreas Beeker 2020-02-17 22:29:29 +00:00
parent 221f840adb
commit bc969fe4aa
5 changed files with 390 additions and 95 deletions

View File

@ -42,7 +42,7 @@ import java.awt.image.WritableRaster;
import org.apache.poi.util.Internal;
@Internal
class PathGradientPaint implements Paint {
public class PathGradientPaint implements Paint {
// http://asserttrue.blogspot.de/2010/01/how-to-iimplement-custom-paint-in-50.html
private final Color[] colors;
@ -51,11 +51,11 @@ class PathGradientPaint implements Paint {
private final int joinStyle;
private final int transparency;
PathGradientPaint(float[] fractions, Color[] colors) {
this(fractions,colors,BasicStroke.CAP_ROUND,BasicStroke.JOIN_ROUND);
}
private PathGradientPaint(float[] fractions, Color[] colors, int capStyle, int joinStyle) {
this.colors = colors.clone();
this.fractions = fractions.clone();
@ -71,27 +71,28 @@ class PathGradientPaint implements Paint {
}
this.transparency = opaque ? OPAQUE : TRANSLUCENT;
}
public PaintContext createContext(ColorModel cm,
@Override
public PathGradientContext createContext(ColorModel cm,
Rectangle deviceBounds,
Rectangle2D userBounds,
AffineTransform transform,
RenderingHints hints) {
return new PathGradientContext(cm, deviceBounds, userBounds, transform, hints);
}
public int getTransparency() {
return transparency;
}
class PathGradientContext implements PaintContext {
public class PathGradientContext implements PaintContext {
final Rectangle deviceBounds;
final Rectangle2D userBounds;
protected final AffineTransform xform;
final RenderingHints hints;
/**
* for POI: the shape will be only known when the subclasses determines the concrete implementation
* for POI: the shape will be only known when the subclasses determines the concrete implementation
* in the draw/-content method, so we need to postpone the setting/creation as long as possible
**/
protected final Shape shape;
@ -121,7 +122,7 @@ class PathGradientPaint implements Paint {
Point2D start = new Point2D.Double(0, 0);
Point2D end = new Point2D.Double(gradientSteps, 0);
LinearGradientPaint gradientPaint = new LinearGradientPaint(start, end, fractions, colors, CycleMethod.NO_CYCLE, ColorSpaceType.SRGB, new AffineTransform());
Rectangle bounds = new Rectangle(0, 0, gradientSteps, 1);
pCtx = gradientPaint.createContext(cm, bounds, bounds, new AffineTransform(), hints);
}
@ -134,7 +135,7 @@ class PathGradientPaint implements Paint {
public Raster getRaster(int xOffset, int yOffset, int w, int h) {
ColorModel cm = getColorModel();
if (raster == null) createRaster();
raster = createRaster();
// TODO: eventually use caching here
WritableRaster childRaster = cm.createCompatibleWritableRaster(w, h);
@ -143,7 +144,7 @@ class PathGradientPaint implements Paint {
// usually doesn't happen ...
return childRaster;
}
Rectangle2D destRect = new Rectangle2D.Double();
Rectangle2D.intersect(childRect, deviceBounds, destRect);
int dx = (int)(destRect.getX()-deviceBounds.getX());
@ -154,7 +155,7 @@ class PathGradientPaint implements Paint {
dx = (int)(destRect.getX()-childRect.getX());
dy = (int)(destRect.getY()-childRect.getY());
childRaster.setDataElements(dx, dy, dw, dh, data);
return childRaster;
}
@ -174,10 +175,12 @@ class PathGradientPaint implements Paint {
}
return upper;
}
void createRaster() {
public WritableRaster createRaster() {
if (raster != null) {
return raster;
}
ColorModel cm = getColorModel();
raster = cm.createCompatibleWritableRaster((int)deviceBounds.getWidth(), (int)deviceBounds.getHeight());
BufferedImage img = new BufferedImage(cm, raster, false, null);
@ -203,8 +206,9 @@ class PathGradientPaint implements Paint {
}
graphics.draw(shape);
}
graphics.dispose();
return raster;
}
}
}

View File

@ -0,0 +1,72 @@
/*
* ====================================================================
* 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.
* ====================================================================
*/
package org.apache.poi.xslf.draw;
import java.awt.RenderingHints;
import java.util.Map;
import org.apache.batik.svggen.SVGGeneratorContext;
import org.apache.batik.svggen.SVGGeneratorContext.GraphicContextDefaults;
import org.apache.batik.svggen.SVGGraphics2D;
import org.apache.poi.util.Internal;
import org.w3c.dom.Document;
/**
* Wrapper class to pass changes to rendering hints down to the svg graphics context defaults
* which can be read by the extension handlers
*/
@Internal
public class SVGPOIGraphics2D extends SVGGraphics2D {
private final RenderingHints hints;
public SVGPOIGraphics2D(Document document) {
super(getCtx(document), false);
hints = getGeneratorContext().getGraphicContextDefaults().getRenderingHints();
}
private static SVGGeneratorContext getCtx(Document document) {
SVGGeneratorContext context = SVGGeneratorContext.createDefault(document);
context.setExtensionHandler(new SVGRenderExtension());
GraphicContextDefaults defs = new GraphicContextDefaults();
defs.setRenderingHints(new RenderingHints(null));
context.setGraphicContextDefaults(defs);
return context;
}
@Override
public void setRenderingHint(RenderingHints.Key hintKey, Object hintValue) {
hints.put(hintKey, hintValue);
super.setRenderingHint(hintKey, hintValue);
}
@Override
public void setRenderingHints(Map hints) {
this.hints.clear();
this.hints.putAll(hints);
super.setRenderingHints(hints);
}
@Override
public void addRenderingHints(Map hints) {
this.hints.putAll(hints);
super.addRenderingHints(hints);
}
}

View File

@ -0,0 +1,185 @@
/* ====================================================================
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.
==================================================================== */
package org.apache.poi.xslf.draw;
import static java.awt.MultipleGradientPaint.ColorSpaceType.LINEAR_RGB;
import static org.apache.batik.util.SVGConstants.*;
import java.awt.Color;
import java.awt.LinearGradientPaint;
import java.awt.MultipleGradientPaint;
import java.awt.Paint;
import java.awt.RadialGradientPaint;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.TexturePaint;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.WritableRaster;
import org.apache.batik.svggen.DefaultExtensionHandler;
import org.apache.batik.svggen.SVGColor;
import org.apache.batik.svggen.SVGGeneratorContext;
import org.apache.batik.svggen.SVGPaintDescriptor;
import org.apache.batik.svggen.SVGTexturePaint;
import org.apache.poi.sl.draw.Drawable;
import org.apache.poi.sl.draw.PathGradientPaint;
import org.apache.poi.sl.draw.PathGradientPaint.PathGradientContext;
import org.apache.poi.util.Internal;
import org.w3c.dom.Element;
/**
* Extension of Batik's DefaultExtensionHandler which handles different kinds of Paint objects
* <p>
* Taken (with permission) from https://gist.github.com/msteiger/4509119,
* including the fixes that are discussed in the comments
*
* @author Martin Steiger
*
* @see <a href="https://stackoverflow.com/questions/14258206/">Gradient paints not working in Apache Batik's svggen</a>
* @see <a href="https://issues.apache.org/jira/browse/BATIK-1032">BATIK-1032</a>
*/
@Internal
public class SVGRenderExtension extends DefaultExtensionHandler {
@Override
public SVGPaintDescriptor handlePaint(Paint paint, SVGGeneratorContext generatorContext) {
if (paint instanceof LinearGradientPaint) {
return getLgpDescriptor((LinearGradientPaint)paint, generatorContext);
}
if (paint instanceof RadialGradientPaint) {
return getRgpDescriptor((RadialGradientPaint)paint, generatorContext);
}
if (paint instanceof PathGradientPaint) {
return getPathDescriptor((PathGradientPaint)paint, generatorContext);
}
return super.handlePaint(paint, generatorContext);
}
private SVGPaintDescriptor getPathDescriptor(PathGradientPaint gradient, SVGGeneratorContext genCtx) {
RenderingHints hints = genCtx.getGraphicContextDefaults().getRenderingHints();
Shape shape = (Shape)hints.get(Drawable.GRADIENT_SHAPE);
if (shape == null) {
return null;
}
PathGradientContext context = gradient.createContext(ColorModel.getRGBdefault(), shape.getBounds(), shape.getBounds2D(), new AffineTransform(), hints);
WritableRaster raster = context.createRaster();
BufferedImage img = new BufferedImage(context.getColorModel(), raster, false, null);
SVGTexturePaint texturePaint = new SVGTexturePaint(genCtx);
TexturePaint tp = new TexturePaint(img, shape.getBounds2D());
return texturePaint.toSVG(tp);
}
private SVGPaintDescriptor getRgpDescriptor(RadialGradientPaint gradient, SVGGeneratorContext genCtx) {
Element gradElem = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_RADIAL_GRADIENT_TAG);
// Create and set unique XML id
String id = genCtx.getIDGenerator().generateID("gradient");
gradElem.setAttribute(SVG_ID_ATTRIBUTE, id);
// Set x,y pairs
setPoint(gradElem, gradient.getCenterPoint(), "cx", "cy");
setPoint(gradElem, gradient.getFocusPoint(), "fx", "fy");
gradElem.setAttribute("r", String.valueOf(gradient.getRadius()));
addMgpAttributes(gradElem, genCtx, gradient);
return new SVGPaintDescriptor("url(#" + id + ")", SVG_OPAQUE_VALUE, gradElem);
}
private SVGPaintDescriptor getLgpDescriptor(LinearGradientPaint gradient, SVGGeneratorContext genCtx) {
Element gradElem = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_LINEAR_GRADIENT_TAG);
// Create and set unique XML id
String id = genCtx.getIDGenerator().generateID("gradient");
gradElem.setAttribute(SVG_ID_ATTRIBUTE, id);
// Set x,y pairs
setPoint(gradElem, gradient.getStartPoint(), "x1", "y1");
setPoint(gradElem, gradient.getEndPoint(), "x2", "y2");
addMgpAttributes(gradElem, genCtx, gradient);
return new SVGPaintDescriptor("url(#" + id + ")", SVG_OPAQUE_VALUE, gradElem);
}
private void addMgpAttributes(Element gradElem, SVGGeneratorContext genCtx, MultipleGradientPaint gradient) {
gradElem.setAttribute(SVG_GRADIENT_UNITS_ATTRIBUTE, SVG_USER_SPACE_ON_USE_VALUE);
// Set cycle method
final String cycleVal;
switch (gradient.getCycleMethod()) {
case REFLECT:
cycleVal = SVG_REFLECT_VALUE;
break;
case REPEAT:
cycleVal = SVG_REPEAT_VALUE;
break;
case NO_CYCLE:
default:
cycleVal = SVG_PAD_VALUE;
break;
}
gradElem.setAttribute(SVG_SPREAD_METHOD_ATTRIBUTE, cycleVal);
// Set color space
final String colorSpace = (gradient.getColorSpace() == LINEAR_RGB) ? SVG_LINEAR_RGB_VALUE : SVG_SRGB_VALUE;
gradElem.setAttribute(SVG_COLOR_INTERPOLATION_ATTRIBUTE, colorSpace);
// Set transform matrix if not identity
AffineTransform tf = gradient.getTransform();
if (!tf.isIdentity()) {
String matrix = "matrix(" + tf.getScaleX() + " " + tf.getShearY()
+ " " + tf.getShearX() + " " + tf.getScaleY() + " "
+ tf.getTranslateX() + " " + tf.getTranslateY() + ")";
gradElem.setAttribute(SVG_GRADIENT_TRANSFORM_ATTRIBUTE, matrix);
}
// Convert gradient stops
Color[] colors = gradient.getColors();
float[] fracs = gradient.getFractions();
for (int i = 0; i < colors.length; i++) {
Element stop = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_STOP_TAG);
SVGPaintDescriptor pd = SVGColor.toSVG(colors[i], genCtx);
stop.setAttribute(SVG_OFFSET_ATTRIBUTE, (int) (fracs[i] * 100.0f) + "%");
stop.setAttribute(SVG_STOP_COLOR_ATTRIBUTE, pd.getPaintValue());
if (colors[i].getAlpha() != 255) {
stop.setAttribute(SVG_STOP_OPACITY_ATTRIBUTE, pd.getOpacityValue());
}
gradElem.appendChild(stop);
}
}
private static void setPoint(Element gradElem, Point2D point, String x, String y) {
gradElem.setAttribute(x, Double.toString(point.getX()));
gradElem.setAttribute(y, Double.toString(point.getY()));
}
}

View File

@ -0,0 +1,109 @@
/*
* ====================================================================
* 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.
* ====================================================================
*/
package org.apache.poi.xslf.util;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import javax.imageio.ImageIO;
import org.apache.batik.dom.GenericDOMImplementation;
import org.apache.batik.svggen.SVGGraphics2D;
import org.apache.poi.sl.draw.Drawable;
import org.apache.poi.util.Internal;
import org.apache.poi.xslf.draw.SVGPOIGraphics2D;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
/**
* Output formats for PPTX2PNG
*/
@Internal
interface OutputFormat extends Closeable {
Graphics2D getGraphics2D(double width, double height);
void writeOut(MFProxy proxy, File outFile) throws IOException;
class SVGFormat implements OutputFormat {
static final String svgNS = "http://www.w3.org/2000/svg";
private SVGGraphics2D svgGenerator;
@Override
public Graphics2D getGraphics2D(double width, double height) {
// Get a DOMImplementation.
DOMImplementation domImpl = GenericDOMImplementation.getDOMImplementation();
// Create an instance of org.w3c.dom.Document.
Document document = domImpl.createDocument(svgNS, "svg", null);
svgGenerator = new SVGPOIGraphics2D(document);
svgGenerator.setSVGCanvasSize(new Dimension((int)width, (int)height));
return svgGenerator;
}
@Override
public void writeOut(MFProxy proxy, File outFile) throws IOException {
svgGenerator.stream(outFile.getCanonicalPath(), true);
}
@Override
public void close() throws IOException {
svgGenerator.dispose();
}
}
class BitmapFormat implements OutputFormat {
private final String format;
private BufferedImage img;
private Graphics2D graphics;
BitmapFormat(String format) {
this.format = format;
}
@Override
public Graphics2D getGraphics2D(double width, double height) {
img = new BufferedImage((int)width, (int)height, BufferedImage.TYPE_INT_ARGB);
graphics = img.createGraphics();
graphics.setRenderingHint(Drawable.BUFFERED_IMAGE, new WeakReference<>(img));
return graphics;
}
@Override
public void writeOut(MFProxy proxy, File outFile) throws IOException {
if (!"null".equals(format)) {
ImageIO.write(img, format, outFile);
}
}
@Override
public void close() throws IOException {
if (graphics != null) {
graphics.dispose();
img.flush();
}
}
}
}

View File

@ -20,33 +20,24 @@
package org.apache.poi.xslf.util;
import java.awt.AlphaComposite;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.geom.Dimension2D;
import java.awt.image.BufferedImage;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.Locale;
import java.util.Set;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import org.apache.batik.dom.GenericDOMImplementation;
import org.apache.batik.svggen.SVGGraphics2D;
import org.apache.poi.common.usermodel.GenericRecord;
import org.apache.poi.poifs.filesystem.FileMagic;
import org.apache.poi.sl.draw.Drawable;
import org.apache.poi.sl.draw.EmbeddedExtractor.EmbeddedPart;
import org.apache.poi.util.Dimension2DDouble;
import org.apache.poi.util.GenericRecordJsonWriter;
import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.apache.poi.xslf.util.OutputFormat.BitmapFormat;
import org.apache.poi.xslf.util.OutputFormat.SVGFormat;
/**
* An utility to convert slides of a .pptx slide show to a PNG image
@ -266,7 +257,7 @@ public final class PPTX2PNG {
// draw stuff
proxy.draw(graphics);
outputFormat.writeOut(proxy, slideNo);
outputFormat.writeOut(proxy, new File(outdir, calcOutFile(proxy, slideNo)));
}
}
} catch (NoScratchpadException e) {
@ -395,70 +386,4 @@ public final class PPTX2PNG {
super(cause);
}
}
private interface OutputFormat extends Closeable {
Graphics2D getGraphics2D(double width, double height);
void writeOut(MFProxy proxy, int slideNo) throws IOException;
}
private class SVGFormat implements OutputFormat {
static final String svgNS = "http://www.w3.org/2000/svg";
private SVGGraphics2D svgGenerator;
@Override
public Graphics2D getGraphics2D(double width, double height) {
// Get a DOMImplementation.
DOMImplementation domImpl = GenericDOMImplementation.getDOMImplementation();
// Create an instance of org.w3c.dom.Document.
Document document = domImpl.createDocument(svgNS, "svg", null);
svgGenerator = new SVGGraphics2D(document);
svgGenerator.setSVGCanvasSize(new Dimension((int)width, (int)height));
return svgGenerator;
}
@Override
public void writeOut(MFProxy proxy, int slideNo) throws IOException {
File outfile = new File(outdir, calcOutFile(proxy, slideNo));
svgGenerator.stream(outfile.getCanonicalPath(), true);
}
@Override
public void close() throws IOException {
svgGenerator.dispose();
}
}
private class BitmapFormat implements OutputFormat {
private final String format;
private BufferedImage img;
private Graphics2D graphics;
BitmapFormat(String format) {
this.format = format;
}
@Override
public Graphics2D getGraphics2D(double width, double height) {
img = new BufferedImage((int)width, (int)height, BufferedImage.TYPE_INT_ARGB);
graphics = img.createGraphics();
graphics.setRenderingHint(Drawable.BUFFERED_IMAGE, new WeakReference<>(img));
return graphics;
}
@Override
public void writeOut(MFProxy proxy, int slideNo) throws IOException {
if (!"null".equals(format)) {
ImageIO.write(img, format, new File(outdir, calcOutFile(proxy, slideNo)));
}
}
@Override
public void close() throws IOException {
if (graphics != null) {
graphics.dispose();
img.flush();
}
}
}
}