]> source.dussan.org Git - poi.git/commitdiff
#64844 - Incorrect sizes of images in SVG
authorAndreas Beeker <kiwiwings@apache.org>
Sat, 8 May 2021 21:56:06 +0000 (21:56 +0000)
committerAndreas Beeker <kiwiwings@apache.org>
Sat, 8 May 2021 21:56:06 +0000 (21:56 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1889686 13f79535-47bb-0310-9956-ffa450edef68

12 files changed:
poi-ooxml/src/main/java/org/apache/poi/xslf/draw/SVGPOIGraphics2D.java
poi-ooxml/src/main/java/org/apache/poi/xslf/draw/SVGRenderExtension.java
poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFShape.java
poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFTexturePaint.java
poi-ooxml/src/main/java/org/apache/poi/xslf/util/SVGFormat.java
poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFFill.java
poi/src/main/java/org/apache/poi/sl/draw/BitmapImageRenderer.java
poi/src/main/java/org/apache/poi/sl/draw/DrawPaint.java
poi/src/main/java/org/apache/poi/sl/draw/DrawTexturePaint.java
poi/src/main/java/org/apache/poi/sl/draw/Drawable.java
poi/src/main/java/org/apache/poi/sl/draw/ImageRenderer.java
poi/src/main/java/org/apache/poi/sl/usermodel/PaintStyle.java

index b2b40f9163933dc4a86de91cb1b8ab7a97ac33c8..09dfa114926a992295ff999d4df42f20742a7046 100644 (file)
@@ -40,6 +40,7 @@ public class SVGPOIGraphics2D extends SVGGraphics2D {
     public SVGPOIGraphics2D(Document document, boolean textAsShapes) {
         super(getCtx(document), textAsShapes);
         hints = getGeneratorContext().getGraphicContextDefaults().getRenderingHints();
+        ((SVGRenderExtension)getGeneratorContext().getExtensionHandler()).setSvgGraphics2D(this);
     }
 
     private static SVGGeneratorContext getCtx(Document document) {
index 1af8c2d45e619d80e8e49bab0a74e16e287e2f65..e6e72058b86ace0d28674037d4d8a3f4ee470887 100644 (file)
 package org.apache.poi.xslf.draw;
 
 import static java.awt.MultipleGradientPaint.ColorSpaceType.LINEAR_RGB;
+import static org.apache.batik.svggen.SVGSyntax.ID_PREFIX_IMAGE;
+import static org.apache.batik.svggen.SVGSyntax.ID_PREFIX_PATTERN;
+import static org.apache.batik.svggen.SVGSyntax.SIGN_POUND;
+import static org.apache.batik.svggen.SVGSyntax.URL_PREFIX;
+import static org.apache.batik.svggen.SVGSyntax.URL_SUFFIX;
 import static org.apache.batik.util.SVGConstants.*;
+import static org.apache.poi.sl.usermodel.PictureData.PictureType.GIF;
+import static org.apache.poi.sl.usermodel.PictureData.PictureType.JPEG;
+import static org.apache.poi.sl.usermodel.PictureData.PictureType.PNG;
 
 import java.awt.Color;
 import java.awt.LinearGradientPaint;
@@ -29,20 +37,41 @@ import java.awt.RenderingHints;
 import java.awt.Shape;
 import java.awt.TexturePaint;
 import java.awt.geom.AffineTransform;
+import java.awt.geom.Dimension2D;
 import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
 import java.awt.image.BufferedImage;
 import java.awt.image.ColorModel;
 import java.awt.image.WritableRaster;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.zip.CRC32;
+
+import javax.imageio.ImageIO;
 
 import org.apache.batik.svggen.DefaultExtensionHandler;
 import org.apache.batik.svggen.SVGColor;
 import org.apache.batik.svggen.SVGGeneratorContext;
+import org.apache.batik.svggen.SVGGraphics2D;
 import org.apache.batik.svggen.SVGPaintDescriptor;
 import org.apache.batik.svggen.SVGTexturePaint;
+import org.apache.poi.sl.draw.BitmapImageRenderer;
+import org.apache.poi.sl.draw.DrawTexturePaint;
 import org.apache.poi.sl.draw.Drawable;
+import org.apache.poi.sl.draw.ImageRenderer;
 import org.apache.poi.sl.draw.PathGradientPaint;
 import org.apache.poi.sl.draw.PathGradientPaint.PathGradientContext;
+import org.apache.poi.sl.usermodel.Insets2D;
+import org.apache.poi.sl.usermodel.PaintStyle;
+import org.apache.poi.sl.usermodel.SimpleShape;
+import org.apache.poi.util.Dimension2DDouble;
 import org.apache.poi.util.Internal;
+import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 
 
@@ -57,6 +86,21 @@ import org.w3c.dom.Element;
  */
 @Internal
 public class SVGRenderExtension extends DefaultExtensionHandler {
+    private static final int LINE_LENGTH = 65;
+    private static final String XLINK_NS = "http://www.w3.org/1999/xlink";
+    private final Map<Long, String> imageMap = new HashMap<>();
+    private WeakReference<SVGGraphics2D> svgGraphics2D = null;
+
+
+    public SVGGraphics2D getSvgGraphics2D() {
+        return (svgGraphics2D != null) ? svgGraphics2D.get() : null;
+    }
+
+    public void setSvgGraphics2D(SVGGraphics2D svgGraphics2D) {
+        this.svgGraphics2D = new WeakReference<>(svgGraphics2D);
+    }
+
+
     @Override
     public SVGPaintDescriptor handlePaint(Paint paint, SVGGeneratorContext generatorContext) {
         if (paint instanceof LinearGradientPaint) {
@@ -71,6 +115,10 @@ public class SVGRenderExtension extends DefaultExtensionHandler {
             return getPathDescriptor((PathGradientPaint)paint, generatorContext);
         }
 
+        if (paint instanceof DrawTexturePaint) {
+            return getDtpDescriptor((DrawTexturePaint)paint, generatorContext);
+        }
+
         return super.handlePaint(paint, generatorContext);
     }
 
@@ -158,8 +206,8 @@ public class SVGRenderExtension extends DefaultExtensionHandler {
         }
 
         // Convert gradient stops
-        Color[] colors = gradient.getColors();
-        float[] fracs = gradient.getFractions();
+        final Color[] colors = gradient.getColors();
+        final float[] fracs = gradient.getFractions();
 
         for (int i = 0; i < colors.length; i++) {
             Element stop = genCtx.getDOMFactory().createElementNS(SVG_NAMESPACE_URI, SVG_STOP_TAG);
@@ -180,4 +228,156 @@ public class SVGRenderExtension extends DefaultExtensionHandler {
         gradElem.setAttribute(x, Double.toString(point.getX()));
         gradElem.setAttribute(y, Double.toString(point.getY()));
     }
+
+    private SVGPaintDescriptor getDtpDescriptor(DrawTexturePaint tdp, SVGGeneratorContext genCtx) {
+        String imgID = getImageID(tdp, genCtx);
+        Document domFactory = genCtx.getDOMFactory();
+
+        Element patternDef = domFactory.createElementNS(SVG_NAMESPACE_URI, SVG_PATTERN_TAG);
+        String patID = genCtx.getIDGenerator().generateID(ID_PREFIX_PATTERN);
+
+        PaintStyle.TexturePaint fill = tdp.getFill();
+
+        Insets2D stretch = fill.getStretch();
+        if (stretch == null) {
+            stretch = new Insets2D(0,0,0,0);
+        }
+
+        Rectangle2D anchorRect = tdp.getAnchorRect();
+        String x = genCtx.doubleString(-stretch.left/100_000 * anchorRect.getWidth());
+        String y = genCtx.doubleString(-stretch.top/100_000 * anchorRect.getHeight());
+        String w = genCtx.doubleString((100_000+stretch.left+stretch.right)/100_000 * anchorRect.getWidth());
+        String h = genCtx.doubleString((100_000+stretch.top+stretch.bottom)/100_000 * anchorRect.getHeight());
+
+        Dimension2D scale = fill.getScale();
+        if (scale == null) {
+            scale = new Dimension2DDouble(1,1);
+        }
+        Point2D offset = fill.getOffset();
+        if (offset == null) {
+            offset = new Point2D.Double(0,0);
+        }
+
+        PaintStyle.FlipMode flipMode = fill.getFlipMode();
+        if (flipMode == null) {
+            flipMode = PaintStyle.FlipMode.NONE;
+        }
+
+        setAttribute(genCtx, patternDef,
+            null, SVG_PATTERN_UNITS_ATTRIBUTE, SVG_OBJECT_BOUNDING_BOX_VALUE,
+            null, SVG_ID_ATTRIBUTE, patID,
+            null, SVG_X_ATTRIBUTE, offset.getX(),
+            null, SVG_Y_ATTRIBUTE, offset.getY(),
+            null, SVG_WIDTH_ATTRIBUTE, genCtx.doubleString(scale.getWidth()*100)+"%",
+            null, SVG_HEIGHT_ATTRIBUTE, genCtx.doubleString(scale.getHeight()*100)+"%",
+            null, SVG_PRESERVE_ASPECT_RATIO_ATTRIBUTE, SVG_NONE_VALUE,
+            null, SVG_VIEW_BOX_ATTRIBUTE, x+" "+ y+" "+ w+" "+h
+        );
+
+        org.apache.poi.sl.usermodel.Shape slShape = fill.getShape();
+
+        // TODO: the rotation handling is incomplete and the scale handling is missing
+        //  see DrawTexturePaint on how to do it for AWT
+        if (!fill.isRotatedWithShape() && slShape instanceof SimpleShape) {
+            double rot = ((SimpleShape)slShape).getRotation();
+            if (rot != 0) {
+                setAttribute(genCtx, patternDef,
+                    null, SVG_PATTERN_TRANSFORM_ATTRIBUTE, "rotate(" + genCtx.doubleString(-rot) + ")");
+            }
+        }
+
+        Element useImageEl = domFactory.createElementNS(SVG_NAMESPACE_URI, SVG_USE_TAG);
+        useImageEl.setAttributeNS(null, "href", "#"+imgID);
+        patternDef.appendChild(useImageEl);
+
+        String patternAttrBuf = URL_PREFIX + SIGN_POUND + patID + URL_SUFFIX;
+        return new SVGPaintDescriptor(patternAttrBuf, SVG_OPAQUE_VALUE, patternDef);
+    }
+
+    private String getImageID(DrawTexturePaint tdp, SVGGeneratorContext genCtx) {
+        final ImageRenderer imgRdr = tdp.getImageRenderer();
+
+        byte[] imgData = null;
+        String contentType = null;
+        if (imgRdr instanceof BitmapImageRenderer) {
+            BitmapImageRenderer bir = (BitmapImageRenderer)imgRdr;
+            String ct = bir.getCachedContentType();
+            if (PNG.contentType.equals(ct) ||
+                JPEG.contentType.equals(ct) ||
+                GIF.contentType.equals(ct)) {
+                contentType = ct;
+                imgData = bir.getCachedImage();
+            }
+        }
+        if (imgData == null) {
+            BufferedImage bi = imgRdr.getImage();
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            try {
+                ImageIO.write(bi, "PNG", bos);
+            } catch (IOException e) {
+                return null;
+            }
+            imgData = bos.toByteArray();
+            contentType = PNG.contentType;
+        }
+
+        CRC32 crc = new CRC32();
+        crc.update(imgData);
+        Long imageCrc = crc.getValue();
+
+        String imgID = imageMap.get(imageCrc);
+        if (imgID != null) {
+            return imgID;
+        }
+
+        Document domFactory = genCtx.getDOMFactory();
+        Rectangle2D anchorRect = tdp.getAnchorRect();
+
+        imgID = genCtx.getIDGenerator().generateID(ID_PREFIX_IMAGE);
+        imageMap.put(imageCrc, imgID);
+
+        // length of a base64 string
+        int sbLen = ((4 * imgData.length / 3) + 3) & ~3;
+        // add line breaks every 65 chars and a few more padding chars
+        sbLen += sbLen / LINE_LENGTH + 30;
+        StringBuilder sb = new StringBuilder(sbLen);
+        sb.append("data:");
+        sb.append(contentType);
+        sb.append(";base64,\n");
+        sb.append(Base64.getMimeEncoder(LINE_LENGTH, "\n".getBytes(StandardCharsets.US_ASCII)).encodeToString(imgData));
+
+        Element imageEl = domFactory.createElementNS(SVG_NAMESPACE_URI, SVG_IMAGE_TAG);
+        setAttribute(genCtx, imageEl,
+            null, SVG_ID_ATTRIBUTE, imgID,
+            null, SVG_PRESERVE_ASPECT_RATIO_ATTRIBUTE, SVG_NONE_VALUE,
+            null, SVG_X_ATTRIBUTE, anchorRect.getX(),
+            null, SVG_Y_ATTRIBUTE, anchorRect.getY(),
+            null, SVG_WIDTH_ATTRIBUTE, anchorRect.getWidth(),
+            null, SVG_HEIGHT_ATTRIBUTE, anchorRect.getHeight(),
+            XLINK_NS, "xlink:href", sb.toString()
+        );
+
+        getSvgGraphics2D().getDOMTreeManager().addOtherDef(imageEl);
+
+        return imgID;
+    }
+
+    private static void setAttribute(SVGGeneratorContext genCtx, Element el, Object... params) {
+        for (int i=0; i<params.length; i+=3) {
+            String ns = (String)params[i];
+            String name = (String)params[i+1];
+            Object oval = params[i+2];
+            String val;
+            if (oval instanceof String) {
+                val = (String)oval;
+            } else if (oval instanceof Number) {
+                val = genCtx.doubleString(((Number) oval).doubleValue());
+            } else if (oval == null) {
+                val = "";
+            } else {
+                val = oval.toString();
+            }
+            el.setAttributeNS(ns, name, val);
+        }
+    }
 }
index 5a267595e2deec4c36882cacd007df86e6895cb5..ba546e8f16ad7458598cd130f81c33e8d9e7740c 100644 (file)
@@ -425,7 +425,7 @@ public abstract class XSLFShape implements Shape<XSLFShape,XSLFTextParagraph> {
 
     @SuppressWarnings("WeakerAccess")
     protected PaintStyle selectPaint(final CTBlipFillProperties blipFill, final PackagePart parentPart, CTSchemeColor phClr, final XSLFTheme theme) {
-        return new XSLFTexturePaint(blipFill, parentPart, phClr, theme, _sheet);
+        return new XSLFTexturePaint(this, blipFill, parentPart, phClr, theme, _sheet);
     }
 
     @SuppressWarnings("WeakerAccess")
index 655be92305f7c43f04255d2440923bf52bbc50ba..985109dfb4eb95884bfb90849d21adf6755fc7a9 100644 (file)
@@ -32,6 +32,7 @@ import org.apache.poi.openxml4j.opc.PackageRelationship;
 import org.apache.poi.sl.usermodel.ColorStyle;
 import org.apache.poi.sl.usermodel.Insets2D;
 import org.apache.poi.sl.usermodel.PaintStyle;
+import org.apache.poi.sl.usermodel.Shape;
 import org.apache.poi.util.Dimension2DDouble;
 import org.apache.poi.util.Internal;
 import org.apache.poi.util.Units;
@@ -46,6 +47,7 @@ import org.openxmlformats.schemas.drawingml.x2006.main.STTileFlipMode;
 
 @Internal
 public class XSLFTexturePaint implements PaintStyle.TexturePaint {
+    private final XSLFShape shape;
     private final CTBlipFillProperties blipFill;
     private final PackagePart parentPart;
     private final CTBlip blip;
@@ -53,7 +55,8 @@ public class XSLFTexturePaint implements PaintStyle.TexturePaint {
     private final XSLFTheme theme;
     private final XSLFSheet sheet;
 
-    public XSLFTexturePaint(final CTBlipFillProperties blipFill, final PackagePart parentPart, CTSchemeColor phClr, final XSLFTheme theme, final XSLFSheet sheet) {
+    public XSLFTexturePaint(final XSLFShape shape, final CTBlipFillProperties blipFill, final PackagePart parentPart, CTSchemeColor phClr, final XSLFTheme theme, final XSLFSheet sheet) {
+        this.shape = shape;
         this.blipFill = blipFill;
         this.parentPart = parentPart;
         blip = blipFill.getBlip();
@@ -100,7 +103,7 @@ public class XSLFTexturePaint implements PaintStyle.TexturePaint {
 
     @Override
     public boolean isRotatedWithShape() {
-        return blipFill.isSetRotWithShape() && blipFill.getRotWithShape();
+        return !blipFill.isSetRotWithShape() || blipFill.getRotWithShape();
     }
 
     @Override
@@ -165,6 +168,10 @@ public class XSLFTexturePaint implements PaintStyle.TexturePaint {
         return colors;
     }
 
+    @Override
+    public Shape getShape() {
+        return shape;
+    }
 
     private static Insets2D getRectVal(CTRelativeRect rect) {
         return rect == null ? null : new Insets2D(
index 1725aa4f108f5d7fb0fa034b927aa8271ab38165..3122855bdedf1a77828d395885b89dac9dac59fd 100644 (file)
@@ -29,6 +29,7 @@ import java.nio.charset.StandardCharsets;
 
 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;
@@ -53,6 +54,7 @@ public class SVGFormat implements OutputFormat {
         Document document = domImpl.createDocument(svgNS, "svg", null);
         svgGenerator = new SVGPOIGraphics2D(document, textAsShapes);
         svgGenerator.setSVGCanvasSize(new Dimension((int)width, (int)height));
+        svgGenerator.setRenderingHint(Drawable.CACHE_IMAGE_SOURCE, true);
         return svgGenerator;
     }
 
index bb2c52b21425c126ab292643aadf3b58f7329f02..50ddf9cdf21d97c2399e73ae2f08b511ecf59cb2 100644 (file)
@@ -44,6 +44,7 @@ import org.apache.poi.sl.usermodel.PaintStyle.GradientPaint;
 import org.apache.poi.sl.usermodel.PaintStyle.GradientPaint.GradientType;
 import org.apache.poi.sl.usermodel.PaintStyle.TexturePaint;
 import org.apache.poi.sl.usermodel.PictureData;
+import org.apache.poi.sl.usermodel.Shape;
 import org.apache.poi.util.BitField;
 import org.apache.poi.util.BitFieldFactory;
 import org.apache.poi.util.LittleEndian;
@@ -137,28 +138,28 @@ public final class HSLFFill {
      * The default value for this property is 0x0.
      */
     private static final BitField FILL_USE_HIT_TEST_FILL = BitFieldFactory.getInstance(0x00080000);
-    
+
     /**
      * A bit that specifies whether the fillShape bit is set.
      * A value of 0x0 specifies that the fillShape MUST be ignored.
      * The default value for this property is 0x0.
      */
     private static final BitField FILL_USE_FILL_SHAPE = BitFieldFactory.getInstance(0x00040000);
-    
+
     /**
      * A bit that specifies whether the fillUseRect bit is set.
      * A value of 0x0 specifies that the fillUseRect MUST be ignored.
      * The default value for this property is 0x0.
      */
     private static final BitField FILL_USE_FILL_USE_RECT = BitFieldFactory.getInstance(0x00020000);
-    
+
     /**
      * A bit that specifies whether the fNoFillHitTest bit is set.
      * A value of 0x0 specifies that the fNoFillHitTest MUST be ignored.
      * The default value for this property is 0x0.
      */
     private static final BitField FILL_USE_NO_FILL_HIT_TEST = BitFieldFactory.getInstance(0x00010000);
-    
+
     /**
      * A bit that specifies how to recolor a picture fill. If this bit is set to 0x1, the pictureFillCrMod
      * property of the picture fill is used for recoloring. If this bit is set to 0x0, the fillCrMod property,
@@ -167,14 +168,14 @@ public final class HSLFFill {
      * The default value for this property is 0x0.
      */
     private static final BitField FILL_RECOLOR_FILL_AS_PICTURE = BitFieldFactory.getInstance(0x00000040);
-    
+
     /**
      * A bit that specifies whether the fill is rotated with the shape.
      * If UseUseShapeAnchor equals 0x0, this value MUST be ignored.
      * The default value for this property is 0x0.
      */
     private static final BitField FILL_USE_SHAPE_ANCHOR = BitFieldFactory.getInstance(0x00000020);
-    
+
     /**
      * A bit that specifies whether the fill is rendered if the shape is a 2-D shape.
      * If this bit is set to 0x1, the fill of this shape is rendered based on the properties of the Fill Style
@@ -182,14 +183,14 @@ public final class HSLFFill {
      * If UseFilled is 0x0, this value MUST be ignored. The default value for this property is 0x1.
      */
     private static final BitField FILL_FILLED = BitFieldFactory.getInstance(0x00000010);
-    
+
     /**
      * A bit that specifies whether this fill will be hit tested.
      * If UsefHitTestFill equals 0x0, this value MUST be ignored.
      * The default value for this property is 0x1.
      */
     private static final BitField FILL_HIT_TEST_FILL = BitFieldFactory.getInstance(0x00000008);
-    
+
     /**
      * A bit that specifies how the fill is aligned. If this bit is set to 0x1, the fill is
      * aligned relative to the shape so that it moves with the shape. If this bit is set to 0x0,
@@ -197,7 +198,7 @@ public final class HSLFFill {
      * The default value for this property is 0x1.
      */
     private static final BitField FILL_FILL_SHAPE = BitFieldFactory.getInstance(0x00000004);
-    
+
     /**
      * A bit that specifies whether to use the rectangle specified by the fillRectLeft, fillRectRight,
      * fillRectTop, and fillRectBottom properties, rather than the bounding rectangle of the shape,
@@ -205,7 +206,7 @@ public final class HSLFFill {
      * The default value for this property is 0x0.
      */
     private static final BitField FILL_FILL_USE_RECT = BitFieldFactory.getInstance(0x00000002);
-    
+
     /**
      * A bit that specifies whether this shape will be hit tested as though it were filled.
      * If UsefNoFillHitTest equals 0x0, this value MUST be ignored.
@@ -270,7 +271,7 @@ public final class HSLFFill {
                 return null;
         }
     }
-    
+
     private boolean isRotatedWithShape() {
         // NOFILLHITTEST can be in the normal escher opt record but also in the tertiary record
         // the extended bit fields seem to be in the second
@@ -351,11 +352,11 @@ public final class HSLFFill {
             public ColorStyle[] getGradientColors() {
                 return colors.stream().map(this::wrapColor).toArray(ColorStyle[]::new);
             }
-            
+
             private ColorStyle wrapColor(Color col) {
                 return (col == null) ? null : DrawPaint.createSolidPaint(col).getSolidColor();
             }
-            
+
             @Override
             public float[] getGradientFractions() {
                 float[] frc = new float[fractions.size()];
@@ -364,19 +365,19 @@ public final class HSLFFill {
                 }
                 return frc;
             }
-            
+
             @Override
             public boolean isRotatedWithShape() {
                 return HSLFFill.this.isRotatedWithShape();
             }
-            
+
             @Override
             public GradientType getGradientType() {
                 return gradientType;
             }
         };
     }
-    
+
     private TexturePaint getTexturePaint() {
         final HSLFPictureData pd = getPictureData();
         if (pd == null) {
@@ -403,6 +404,11 @@ public final class HSLFFill {
             public boolean isRotatedWithShape() {
                 return HSLFFill.this.isRotatedWithShape();
             }
+
+            @Override
+            public Shape getShape() {
+                return shape;
+            }
         };
     }
 
@@ -503,7 +509,7 @@ public final class HSLFFill {
                 HSLFShape.setEscherProperty(opt, EscherPropertyTypes.FILL__FILLOPACITY, alphaFP);
             }
         }
-        
+
         EscherSimpleProperty p = HSLFShape.getEscherProperty(opt, EscherPropertyTypes.FILL__NOFILLHITTEST);
         int propVal = (p == null) ? 0 : p.getPropertyValue();
         propVal = FILL_FILLED.setBoolean(propVal, color != null);
index 96e63a64488fc89ae592cf5733adde86501f931d..aa2b13e64cbb4c2a08786800be2a04ced14f145b 100644 (file)
@@ -52,6 +52,9 @@ public class BitmapImageRenderer implements ImageRenderer {
     private static final Logger LOG = LogManager.getLogger(BitmapImageRenderer.class);
 
     protected BufferedImage img;
+    private boolean doCache;
+    private byte[] cachedImage;
+    private String cachedContentType;
 
     @Override
     public boolean canRender(String contentType) {
@@ -68,11 +71,26 @@ public class BitmapImageRenderer implements ImageRenderer {
 
     @Override
     public void loadImage(InputStream data, String contentType) throws IOException {
-        img = readImage(data, contentType);
+        InputStream in = data;
+        if (doCache) {
+            ByteArrayOutputStream bos = new ByteArrayOutputStream();
+            IOUtils.copy(data, bos);
+            cachedImage = bos.toByteArray();
+            cachedContentType = contentType;
+            in = new ByteArrayInputStream(cachedImage);
+        }
+        img = readImage(in, contentType);
     }
 
     @Override
     public void loadImage(byte[] data, String contentType) throws IOException {
+        if (data == null) {
+            return;
+        }
+        if (doCache) {
+            cachedImage = data.clone();
+            cachedContentType = contentType;
+        }
         img = readImage(new ByteArrayInputStream(data), contentType);
     }
 
@@ -331,4 +349,23 @@ public class BitmapImageRenderer implements ImageRenderer {
     public Rectangle2D getNativeBounds() {
         return new Rectangle2D.Double(0, 0, img.getWidth(), img.getHeight());
     }
+
+    @Override
+    public void setCacheInput(boolean enable) {
+        doCache = enable;
+        if (!enable) {
+            cachedContentType = null;
+            cachedImage = null;
+        }
+    }
+
+    @Override
+    public byte[] getCachedImage() {
+        return cachedImage;
+    }
+
+    @Override
+    public String getCachedContentType() {
+        return cachedContentType;
+    }
 }
index e78dd2ee140dfd91b5a2e158ba746ad1690d3174..01064dd8a4acff46fe0c4c833418cbba9c831c28 100644 (file)
@@ -272,6 +272,8 @@ public class DrawPaint {
                 return TRANSPARENT;
             }
 
+            Boolean cacheImage = (Boolean)graphics.getRenderingHint(Drawable.CACHE_IMAGE_SOURCE);
+            renderer.setCacheInput(cacheImage != null && cacheImage);
             renderer.loadImage(is, contentType);
 
             int alpha = fill.getAlpha();
@@ -336,7 +338,7 @@ public class DrawPaint {
             Shape s = (Shape)graphics.getRenderingHint(Drawable.GRADIENT_SHAPE);
 
             // TODO: check why original bitmaps scale/behave differently to vector based images
-            return new DrawTexturePaint(image, s, fill, flipX, flipY, renderer instanceof BitmapImageRenderer);
+            return new DrawTexturePaint(renderer, image, s, fill, flipX, flipY, renderer instanceof BitmapImageRenderer);
         } catch (IOException e) {
             LOG.atError().withThrowable(e).log("Can't load image data - using transparent color");
             return TRANSPARENT;
index 2d3db490691f2732223a9bcc21927a0f90dd6963..0557fa588b75ab983997860255672a561f64758d 100644 (file)
@@ -34,8 +34,11 @@ import java.awt.image.ColorModel;
 import org.apache.poi.sl.usermodel.Insets2D;
 import org.apache.poi.sl.usermodel.PaintStyle;
 import org.apache.poi.util.Dimension2DDouble;
+import org.apache.poi.util.Internal;
 
-/* package */ class DrawTexturePaint extends java.awt.TexturePaint {
+@Internal
+public class DrawTexturePaint extends java.awt.TexturePaint {
+    private final ImageRenderer imgRdr;
     private final PaintStyle.TexturePaint fill;
     private final Shape shape;
     private final double flipX, flipY;
@@ -44,9 +47,10 @@ import org.apache.poi.util.Dimension2DDouble;
     private static final Insets2D INSETS_EMPTY = new Insets2D(0,0,0,0);
 
 
-    DrawTexturePaint(BufferedImage txtr, Shape shape, PaintStyle.TexturePaint fill, double flipX, double flipY, boolean isBitmapSrc) {
+    DrawTexturePaint(ImageRenderer imgRdr, BufferedImage txtr, Shape shape, PaintStyle.TexturePaint fill, double flipX, double flipY, boolean isBitmapSrc) {
         // deactivate scaling/translation in super class, by specifying the dimension of the texture
         super(txtr, new Rectangle2D.Double(0,0,txtr.getWidth(),txtr.getHeight()));
+        this.imgRdr = imgRdr;
         this.fill = fill;
         this.shape = shape;
         this.flipX = flipX;
@@ -238,4 +242,16 @@ import org.apache.poi.util.Dimension2DDouble;
 
         return xform;
     }
+
+    public ImageRenderer getImageRenderer() {
+        return imgRdr;
+    }
+
+    public PaintStyle.TexturePaint getFill() {
+        return fill;
+    }
+
+    public Shape getAwtShape() {
+        return shape;
+    }
 }
index cb5f8592f9beccf9684c60bd376545605bcb475a..7f0a1a6c07c6a32cec9bda8470d679de10d87957 100644 (file)
@@ -50,6 +50,7 @@ public interface Drawable {
             case 13: return "BUFFERED_IMAGE";
             case 14: return "DEFAULT_CHARSET";
             case 15: return "EMF_FORCE_HEADER_BOUNDS";
+            case 16: return "CACHE_IMAGE_SOURCE";
             default: return "UNKNOWN_ID "+intKey();
             }
         }
@@ -166,6 +167,13 @@ public interface Drawable {
      */
     DrawableHint EMF_FORCE_HEADER_BOUNDS = new DrawableHint(15);
 
+    /**
+     * A boolean value to instruct the bitmap image renderer to keep the original image bytes.
+     * Defaults to {@code false} if unset.
+     */
+    DrawableHint CACHE_IMAGE_SOURCE = new DrawableHint(16);
+
+
     /**
      * Apply 2-D transforms before drawing this shape. This includes rotation and flipping.
      *
index 6f255c72d06c4723590864ae6b35d6fb5db82d9e..7c6e0762d8379e9a2bc198a6ccc48e7987163641 100644 (file)
@@ -162,4 +162,23 @@ public interface ImageRenderer {
      * @param defaultCharset the default charset
      */
     default void setDefaultCharset(Charset defaultCharset) {}
+
+
+    /**
+     * Dis-/Enables caching of input data for later retrieval.
+     * Opposed to {@link #getImage()}, which returns a {@link BufferedImage}, the cached image can be later
+     * used to embedded the original, unmodified data
+     * @param enable dis-/enables caching - this is an optional operation. {@code false} removes already cached data
+     */
+    default void setCacheInput(boolean enable) {}
+
+    /**
+     * @return the cached image data
+     */
+    default byte[] getCachedImage() { return null; }
+
+    /**
+     * @return the cached content type
+     */
+    default String getCachedContentType() { return null; }
 }
\ No newline at end of file
index 21f0824a2f88db85b71ef11ec77af0a36b781614..569ed80c0c39e3053a6e18abae20cb90c7d5fe1f 100644 (file)
@@ -163,7 +163,7 @@ public interface PaintStyle {
         /**
          * The stretch specifies the edges of a fill rectangle.<p>
          *
-         * Each edge of the fill rectangle is defined by a perentage offset from the corresponding edge
+         * Each edge of the fill rectangle is defined by a percentage offset from the corresponding edge
          * of the picture's bounding box. A positive percentage specifies an inset and a negative percentage
          * specifies an outset.<p>
          *
@@ -183,5 +183,9 @@ public interface PaintStyle {
             return null;
         }
 
+        /**
+         * @return the shape this texture paint is applied to
+         */
+        Shape getShape();
     }
 }