From bc969fe4aa27bfb7d044e2ae4588f74754d45f6d Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Mon, 17 Feb 2020 22:29:29 +0000 Subject: [PATCH] PPTX2PNG - fix SVG gradients git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1874150 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/poi/sl/draw/PathGradientPaint.java | 38 ++-- .../poi/xslf/draw/SVGPOIGraphics2D.java | 72 +++++++ .../poi/xslf/draw/SVGRenderExtension.java | 185 ++++++++++++++++++ .../apache/poi/xslf/util/OutputFormat.java | 109 +++++++++++ .../org/apache/poi/xslf/util/PPTX2PNG.java | 81 +------- 5 files changed, 390 insertions(+), 95 deletions(-) create mode 100644 src/ooxml/java/org/apache/poi/xslf/draw/SVGPOIGraphics2D.java create mode 100644 src/ooxml/java/org/apache/poi/xslf/draw/SVGRenderExtension.java create mode 100644 src/ooxml/java/org/apache/poi/xslf/util/OutputFormat.java diff --git a/src/java/org/apache/poi/sl/draw/PathGradientPaint.java b/src/java/org/apache/poi/sl/draw/PathGradientPaint.java index a26abadd2e..42d81956ea 100644 --- a/src/java/org/apache/poi/sl/draw/PathGradientPaint.java +++ b/src/java/org/apache/poi/sl/draw/PathGradientPaint.java @@ -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; } } } diff --git a/src/ooxml/java/org/apache/poi/xslf/draw/SVGPOIGraphics2D.java b/src/ooxml/java/org/apache/poi/xslf/draw/SVGPOIGraphics2D.java new file mode 100644 index 0000000000..a057d6db7d --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/draw/SVGPOIGraphics2D.java @@ -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); + } +} diff --git a/src/ooxml/java/org/apache/poi/xslf/draw/SVGRenderExtension.java b/src/ooxml/java/org/apache/poi/xslf/draw/SVGRenderExtension.java new file mode 100644 index 0000000000..35536714ed --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/draw/SVGRenderExtension.java @@ -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 + *

+ * Taken (with permission) from https://gist.github.com/msteiger/4509119, + * including the fixes that are discussed in the comments + * + * @author Martin Steiger + * + * @see Gradient paints not working in Apache Batik's svggen + * @see BATIK-1032 + */ +@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())); + } +} diff --git a/src/ooxml/java/org/apache/poi/xslf/util/OutputFormat.java b/src/ooxml/java/org/apache/poi/xslf/util/OutputFormat.java new file mode 100644 index 0000000000..9f0a6f7742 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xslf/util/OutputFormat.java @@ -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(); + } + } + } +} diff --git a/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java b/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java index bb01debd23..1e4c92a5b1 100644 --- a/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java +++ b/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java @@ -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(); - } - } - } } -- 2.39.5