From 38b415e26af27aa7a22d1d1ea13eb87f445a9066 Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Sun, 1 Nov 2020 15:37:20 +0000 Subject: [PATCH] #64716 - wmf display error EMF: workaround for invalid EMF header bounds EMF: add option to PPTX2PNG / DrawableHint to fallback to force EMF header bounds EMF: use RGB instead of HSL gradiants git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1883051 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/common/usermodel/PictureType.java | 35 +++++ .../apache/poi/sl/draw/DrawPictureShape.java | 17 +-- src/java/org/apache/poi/sl/draw/Drawable.java | 12 ++ .../poi/poifs/crypt/dsig/SignatureLine.java | 61 ++------- .../org/apache/poi/xslf/util/PPTX2PNG.java | 9 +- .../poi/hemf/record/emf/HemfComment.java | 19 +++ .../poi/hemf/record/emf/HemfRecord.java | 4 + .../poi/hemf/record/emf/HemfWindowing.java | 48 +++++++ .../hemf/record/emfplus/HemfPlusBrush.java | 67 ++++++--- .../hemf/record/emfplus/HemfPlusHeader.java | 6 + .../poi/hemf/record/emfplus/HemfPlusMisc.java | 5 + .../hemf/record/emfplus/HemfPlusRecord.java | 4 + .../poi/hemf/usermodel/HemfPicture.java | 129 +++++++++++++----- 13 files changed, 296 insertions(+), 120 deletions(-) diff --git a/src/java/org/apache/poi/common/usermodel/PictureType.java b/src/java/org/apache/poi/common/usermodel/PictureType.java index 4aceb624a0..d3ed67fc2e 100644 --- a/src/java/org/apache/poi/common/usermodel/PictureType.java +++ b/src/java/org/apache/poi/common/usermodel/PictureType.java @@ -17,6 +17,8 @@ package org.apache.poi.common.usermodel; +import org.apache.poi.poifs.filesystem.FileMagic; + /** * General enum class to define a picture format/type * @@ -65,4 +67,37 @@ public enum PictureType { this.contentType = contentType; this.extension = extension; } + + public String getContentType() { + return contentType; + } + + public String getExtension() { + return extension; + } + + public static PictureType valueOf(FileMagic fm) { + switch (fm) { + case BMP: + return PictureType.BMP; + case GIF: + return PictureType.GIF; + case JPEG: + return PictureType.JPEG; + case PNG: + return PictureType.PNG; + case XML: + // this is quite fuzzy, to suppose all XMLs are SVGs when handling pictures ... + return PictureType.SVG; + case WMF: + return PictureType.WMF; + case EMF: + return PictureType.EMF; + case TIFF: + return PictureType.TIFF; + default: + case UNKNOWN: + return PictureType.UNKNOWN; + } + } } diff --git a/src/java/org/apache/poi/sl/draw/DrawPictureShape.java b/src/java/org/apache/poi/sl/draw/DrawPictureShape.java index 695f4ec708..2e25d75a6a 100644 --- a/src/java/org/apache/poi/sl/draw/DrawPictureShape.java +++ b/src/java/org/apache/poi/sl/draw/DrawPictureShape.java @@ -27,6 +27,8 @@ import java.util.ServiceLoader; import java.util.function.Supplier; import java.util.stream.StreamSupport; +import org.apache.poi.common.usermodel.PictureType; +import org.apache.poi.poifs.filesystem.FileMagic; import org.apache.poi.sl.usermodel.PictureData; import org.apache.poi.sl.usermodel.PictureShape; import org.apache.poi.sl.usermodel.RectAlign; @@ -36,11 +38,6 @@ import org.apache.poi.util.POILogger; public class DrawPictureShape extends DrawSimpleShape { private static final POILogger LOG = POILogFactory.getLogger(DrawPictureShape.class); - private static final String[] KNOWN_RENDERER = { - "org.apache.poi.hwmf.draw.HwmfImageRenderer", - "org.apache.poi.hemf.draw.HemfImageRenderer", - "org.apache.poi.xslf.draw.SVGImageRenderer" - }; public DrawPictureShape(PictureShape shape) { super(shape); @@ -60,10 +57,14 @@ public class DrawPictureShape extends DrawSimpleShape { } try { - String ct = data.getContentType(); + byte[] dataBytes = data.getData(); + + PictureType type = PictureType.valueOf(FileMagic.valueOf(dataBytes)); + String ct = (type == PictureType.UNKNOWN) ? data.getContentType() : type.getContentType(); + ImageRenderer renderer = getImageRenderer(graphics, ct); if (renderer.canRender(ct)) { - renderer.loadImage(data.getData(), ct); + renderer.loadImage(dataBytes, ct); renderer.drawImage(graphics, anchor, insets); return; } @@ -92,7 +93,7 @@ public class DrawPictureShape extends DrawSimpleShape { // the fallback is the BitmapImageRenderer, at least it gracefully handles invalid images final Supplier getFallback = () -> { - LOG.log(POILogger.WARN, "No suiteable image renderer found for content-type '"+ + LOG.log(POILogger.WARN, "No suitable image renderer found for content-type '"+ contentType+"' - include poi-scratchpad (for wmf/emf) or poi-ooxml (for svg) jars!"); return fallback; }; diff --git a/src/java/org/apache/poi/sl/draw/Drawable.java b/src/java/org/apache/poi/sl/draw/Drawable.java index 919e1a6f25..cb5f8592f9 100644 --- a/src/java/org/apache/poi/sl/draw/Drawable.java +++ b/src/java/org/apache/poi/sl/draw/Drawable.java @@ -49,6 +49,7 @@ public interface Drawable { case 12: return "CURRENT_SLIDE"; case 13: return "BUFFERED_IMAGE"; case 14: return "DEFAULT_CHARSET"; + case 15: return "EMF_FORCE_HEADER_BOUNDS"; default: return "UNKNOWN_ID "+intKey(); } } @@ -153,6 +154,17 @@ public interface Drawable { */ DrawableHint DEFAULT_CHARSET = new DrawableHint(14); + /** + * A boolean value to force the usage of the bounding box, which is specified in the EMF header. + * Defaults to {@code FALSE} - in this case the records are scanned for window and + * viewport records to determine the initial bounding box by using the following + * condition: {@code isValid(viewport) ? viewport : isValid(window) ? window : headerBounds } + *

+ * This is a workaround switch, which might be removed in future releases, when the bounding box + * determination for the special cases is fixed. + * In most cases it's recommended to leave the default value. + */ + DrawableHint EMF_FORCE_HEADER_BOUNDS = new DrawableHint(15); /** * Apply 2-D transforms before drawing this shape. This includes rotation and flipping. diff --git a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureLine.java b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureLine.java index 8d9ffe4024..075ac95a6a 100644 --- a/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureLine.java +++ b/src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureLine.java @@ -321,34 +321,11 @@ public abstract class SignatureLine { */ protected byte[] plainPng() throws IOException { byte[] plain = getPlainSignature(); - PictureType pictureType; - switch (FileMagic.valueOf(plain)) { - case PNG: - return plain; - case BMP: - pictureType = PictureType.BMP; - break; - case EMF: - pictureType = PictureType.EMF; - break; - case GIF: - pictureType = PictureType.GIF; - break; - case JPEG: - pictureType = PictureType.JPEG; - break; - case XML: - pictureType = PictureType.SVG; - break; - case TIFF: - pictureType = PictureType.TIFF; - break; - default: - throw new IllegalArgumentException("Unsupported picture format"); + PictureType pictureType = PictureType.valueOf(FileMagic.valueOf(plain)); + if (pictureType == PictureType.UNKNOWN) { + throw new IllegalArgumentException("Unsupported picture format"); } - - ImageRenderer rnd = DrawPictureShape.getImageRenderer(null, pictureType.contentType); if (rnd == null) { throw new UnsupportedOperationException(pictureType + " can't be rendered - did you provide poi-scratchpad and its dependencies (batik et al.)"); @@ -375,11 +352,8 @@ public abstract class SignatureLine { /** * Generate the image for a signature line - * @param caption three lines separated by "\n" - usually something like "First name Last name\nRole\nname of the key" - * @param inputImage the plain signature - supported formats are PNG,GIF,JPEG,(SVG),EMF,WMF. - * for SVG,EMF,WMF poi-scratchpad needs to be in the class-/modulepath - * if {@code null}, the inputImage is not rendered - * @param invalidText for invalid signature images, use the given text + * @param showSignature show signature image - use {@code false} for placeholder images in to-be-signed documents + * @param showInvalidStamp print invalid stamp over the signature * @return the signature image in PNG format as byte array */ protected byte[] generateImage(boolean showSignature, boolean showInvalidStamp) throws IOException { @@ -462,28 +436,11 @@ public abstract class SignatureLine { private void determineContentType() { FileMagic fm = FileMagic.valueOf(plainSignature); - switch (fm) { - case GIF: - contentType = PictureType.GIF.contentType; - break; - case PNG: - contentType = PictureType.PNG.contentType; - break; - case JPEG: - contentType = PictureType.JPEG.contentType; - break; - case XML: - contentType = PictureType.SVG.contentType; - break; - case EMF: - contentType = PictureType.EMF.contentType; - break; - case WMF: - contentType = PictureType.WMF.contentType; - break; - default: - throw new IllegalArgumentException("unknown image type"); + PictureType type = PictureType.valueOf(fm); + if (type == PictureType.UNKNOWN) { + throw new IllegalArgumentException("unknown image type"); } + contentType = type.contentType; } } 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 46d3da99fb..f4c57325da 100644 --- a/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java +++ b/src/ooxml/java/org/apache/poi/xslf/util/PPTX2PNG.java @@ -75,7 +75,8 @@ public final class PPTX2PNG { " some files (usually wmf) don't have a header, i.e. an identifiable file magic\n" + " -textAsShapes text elements are saved as shapes in SVG, necessary for variable spacing\n" + " often found in math formulas\n" + - " -charset sets the default charset to be used, defaults to Windows-1252"; + " -charset sets the default charset to be used, defaults to Windows-1252\n" + + " -emfHeaderBounds force the usage of the emf header bounds to calculate the bounding box"; System.out.println(msg); // no System.exit here, as we also run in junit tests! @@ -104,6 +105,7 @@ public final class PPTX2PNG { private FileMagic defaultFileType = FileMagic.OLE2; private boolean textAsShapes = false; private Charset charset = LocaleUtil.CHARSET_1252; + private boolean emfHeaderBounds = false; private PPTX2PNG() { } @@ -189,7 +191,9 @@ public final class PPTX2PNG { charset = LocaleUtil.CHARSET_1252; } break; - + case "-emfheaderbounds": + emfHeaderBounds = true; + break; default: file = new File(args[i]); break; @@ -279,6 +283,7 @@ public final class PPTX2PNG { graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); graphics.setRenderingHint(Drawable.DEFAULT_CHARSET, getDefaultCharset()); + graphics.setRenderingHint(Drawable.EMF_FORCE_HEADER_BOUNDS, emfHeaderBounds); graphics.scale(scale / lenSide, scale / lenSide); diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfComment.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfComment.java index 0d055d340f..83e8db7598 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfComment.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfComment.java @@ -101,6 +101,9 @@ public class HemfComment { */ default void draw(HemfGraphics ctx) {} + default void calcBounds(Rectangle2D bounds, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) { } + + @Override default HemfCommentRecordType getGenericRecordType() { return getCommentRecordType(); @@ -131,6 +134,11 @@ public class HemfComment { data.draw(ctx); } + @Override + public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) { + data.calcBounds(window, viewport, renderState); + } + @Override public String toString() { return GenericRecordJsonWriter.marshal(this); @@ -332,6 +340,17 @@ public class HemfComment { records.forEach(ctx::draw); } + @Override + public void calcBounds(Rectangle2D window, Rectangle2D viewport, EmfRenderState[] renderState) { + renderState[0] = EmfRenderState.EMFPLUS_ONLY; + for (HemfPlusRecord r : records) { + r.calcBounds(window, viewport, renderState); + if (!window.isEmpty() && !viewport.isEmpty()) { + break; + } + } + } + @Override public Map> getGenericProperties() { return null; diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecord.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecord.java index 0ab50e819d..fe6a6ccf68 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecord.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecord.java @@ -18,6 +18,7 @@ package org.apache.poi.hemf.record.emf; +import java.awt.geom.Rectangle2D; import java.io.IOException; import java.util.Map; import java.util.function.Supplier; @@ -56,6 +57,9 @@ public interface HemfRecord extends GenericRecord { } } + default void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) { + } + /** * Sets the header reference, in case the record needs to refer to it * @param header the emf header diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfWindowing.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfWindowing.java index 6167cd6028..46e28209f8 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfWindowing.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfWindowing.java @@ -22,6 +22,7 @@ import static org.apache.poi.hemf.record.emf.HemfDraw.readPointL; import static org.apache.poi.hwmf.record.HwmfDraw.normalizeBounds; import java.awt.geom.Dimension2D; +import java.awt.geom.Rectangle2D; import java.io.IOException; import java.util.Map; import java.util.function.Supplier; @@ -56,6 +57,13 @@ public class HemfWindowing { public HemfRecordType getGenericRecordType() { return getEmfRecordType(); } + + @Override + public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) { + double x = window.getX(); + double y = window.getY(); + window.setRect(x,y,size.getWidth(),size.getHeight()); + } } /** @@ -76,6 +84,13 @@ public class HemfWindowing { public HemfRecordType getGenericRecordType() { return getEmfRecordType(); } + + @Override + public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) { + double w = window.getWidth(); + double h = window.getHeight(); + window.setRect(origin.getX(),origin.getY(),w,h); + } } /** @@ -96,6 +111,13 @@ public class HemfWindowing { public HemfRecordType getGenericRecordType() { return getEmfRecordType(); } + + @Override + public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) { + double x = viewport.getX(); + double y = viewport.getY(); + viewport.setRect(x,y,extents.getWidth(),extents.getHeight()); + } } /** @@ -116,6 +138,13 @@ public class HemfWindowing { public HemfRecordType getGenericRecordType() { return getEmfRecordType(); } + + @Override + public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) { + double w = viewport.getWidth(); + double h = viewport.getHeight(); + viewport.setRect(origin.getX(), origin.getY(), w, h); + } } /** @@ -200,6 +229,16 @@ public class HemfWindowing { public HemfRecordType getGenericRecordType() { return getEmfRecordType(); } + + @Override + public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) { + double x = viewport.getX(); + double y = viewport.getY(); + double w = viewport.getWidth(); + double h = viewport.getHeight(); + viewport.setRect(x,y,w * scale.getWidth(),h * scale.getHeight()); + } + } /** @@ -221,6 +260,15 @@ public class HemfWindowing { public HemfRecordType getGenericRecordType() { return getEmfRecordType(); } + + @Override + public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) { + double x = window.getX(); + double y = window.getY(); + double w = window.getWidth(); + double h = window.getHeight(); + window.setRect(x,y,w * scale.getWidth(),h * scale.getHeight()); + } } /** diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusBrush.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusBrush.java index 34bda1e4be..91f8d31c95 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusBrush.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusBrush.java @@ -35,7 +35,6 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -519,7 +518,7 @@ public class HemfPlusBrush { public static class EmfPlusLinearGradientBrushData implements EmfPlusBrushData { private int dataFlags; private EmfPlusWrapMode wrapMode; - private Rectangle2D rect = new Rectangle2D.Double(); + private final Rectangle2D rect = new Rectangle2D.Double(); private Color startColor, endColor; private AffineTransform blendTransform; private float[] positions; @@ -529,8 +528,8 @@ public class HemfPlusBrush { private float[] positionsH; private float[] blendFactorsH; - private static int[] FLAG_MASKS = { 0x02, 0x04, 0x08, 0x10, 0x80 }; - private static String[] FLAG_NAMES = { "TRANSFORM", "PRESET_COLORS", "BLEND_FACTORS_H", "BLEND_FACTORS_V", "BRUSH_DATA_IS_GAMMA_CORRECTED" }; + private static final int[] FLAG_MASKS = {0x02, 0x04, 0x08, 0x10, 0x80}; + private static final String[] FLAG_NAMES = {"TRANSFORM", "PRESET_COLORS", "BLEND_FACTORS_H", "BLEND_FACTORS_V", "BRUSH_DATA_IS_GAMMA_CORRECTED"}; @Override public long init(LittleEndianInputStream leis, long dataSize) throws IOException { @@ -543,7 +542,7 @@ public class HemfPlusBrush { // gradient is repeated. wrapMode = EmfPlusWrapMode.valueOf(leis.readInt()); - int size = 2*LittleEndianConsts.INT_SIZE; + int size = 2 * LittleEndianConsts.INT_SIZE; size += readRectF(leis, rect); // An EmfPlusARGB object that specifies the color at the starting/ending boundary point of the linear gradient brush. @@ -551,9 +550,9 @@ public class HemfPlusBrush { endColor = readARGB(leis.readInt()); // skip reserved1/2 fields - leis.skipFully(2*LittleEndianConsts.INT_SIZE); + leis.skipFully(2 * LittleEndianConsts.INT_SIZE); - size += 4*LittleEndianConsts.INT_SIZE; + size += 4 * LittleEndianConsts.INT_SIZE; if (TRANSFORM.isSet(dataFlags)) { size += readXForm(leis, (blendTransform = new AffineTransform())); @@ -586,7 +585,7 @@ public class HemfPlusBrush { setColorProps(prop::setBrushColorsV, positionsV, this::getBlendVColorAt); if (!(isPreset() || isBlendH() || isBlendV())) { - prop.setBrushColorsH(Arrays.asList(kv(0f,startColor), kv(1f,endColor))); + prop.setBrushColorsH(Arrays.asList(kv(0f, startColor), kv(1f, endColor))); } } @@ -607,7 +606,7 @@ public class HemfPlusBrush { @Override public Map> getGenericProperties() { - final Map> m = new LinkedHashMap<>(); + final Map> m = new LinkedHashMap<>(); m.put("flags", GenericRecordUtil.getBitsAsString(() -> dataFlags, FLAG_MASKS, FLAG_NAMES)); m.put("wrapMode", () -> wrapMode); m.put("rect", () -> rect); @@ -635,24 +634,24 @@ public class HemfPlusBrush { return BLEND_FACTORS_V.isSet(dataFlags); } - private Map.Entry getBlendColorAt(int index) { + private Map.Entry getBlendColorAt(int index) { return kv(positions[index], blendColors[index]); } - private Map.Entry getBlendHColorAt(int index) { - return kv(positionsH[index],interpolateColors(blendFactorsH[index])); + private Map.Entry getBlendHColorAt(int index) { + return kv(positionsH[index], interpolateColors(blendFactorsH[index])); } - private Map.Entry getBlendVColorAt(int index) { - return kv(positionsV[index],interpolateColors(blendFactorsV[index])); + private Map.Entry getBlendVColorAt(int index) { + return kv(positionsV[index], interpolateColors(blendFactorsV[index])); } - private static Map.Entry kv(Float position, Color color) { + private static Map.Entry kv(Float position, Color color) { return new AbstractMap.SimpleEntry<>(position, color); } private static void setColorProps( - Consumer>> setter, float[] positions, Function> sup) { + Consumer>> setter, float[] positions, Function> sup) { if (positions == null) { setter.accept(null); } else { @@ -661,8 +660,26 @@ public class HemfPlusBrush { } private Color interpolateColors(final double factor) { - // https://stackoverflow.com/questions/1416560/hsl-interpolation + return interpolateColorsRGB(factor); + } + private Color interpolateColorsRGB(final double factor) { + // TODO: check IS_GAMMA_CORRECTED flag and maybe don't convert into scRGB + double[] start = DrawPaint.RGB2SCRGB(startColor); + double[] end = DrawPaint.RGB2SCRGB(endColor); + + // compute the interpolated color in linear space + int a = (int)Math.round(startColor.getAlpha() + factor * (endColor.getAlpha() - startColor.getAlpha())); + double r = start[0] + factor * (end[0] - start[0]); + double g = start[1] + factor * (end[1] - start[1]); + double b = start[2] + factor * (end[2] - start[2]); + + Color inter = DrawPaint.SCRGB2RGB(r,g,b); + return new Color(inter.getRed(), inter.getGreen(), inter.getBlue(), a); + } + + /* + private Color interpolateColorsHSL(final double factor) { final double[] hslStart = DrawPaint.RGB2HSL(startColor); final double[] hslStop = DrawPaint.RGB2HSL(endColor); @@ -673,16 +690,24 @@ public class HemfPlusBrush { double sat = linearInter.apply(hslStart[1],hslStop[1]); double lum = linearInter.apply(hslStart[2],hslStop[2]); - double hue1 = (hslStart[0]+hslStop[0])/2.; - double hue2 = (hslStart[0]+hslStop[0]+360.)/2.; + // find closest match - decide if need to go clockwise or counter-clockwise + // https://stackoverflow.com/questions/1416560/hsl-interpolation + double hueMidCW = (hslStart[0]+hslStop[0])/2.; + double hueMidCCW = (hslStart[0]+hslStop[0]+360.)/2.; Function hueDelta = (hue) -> Math.min(Math.abs(hslStart[0]-hue), Math.abs(hslStop[0]-hue)); - double hue = hueDelta.apply(hue1) < hueDelta.apply(hue2) ? hue1 : hue2; + double hslDiff; + if (hueDelta.apply(hueMidCW) > hueDelta.apply(hueMidCCW)) { + hslDiff = (hslStart[0] < hslStop[0]) ? hslStop[0]-hslStart[0] : (360-hslStart[0])+hslStop[0]; + } else { + hslDiff = (hslStart[0] < hslStop[0]) ? -hslStart[0]-(360-hslStop[0]) : -(hslStart[0]-hslStop[0]); + } + double hue = (hslStart[0]+hslDiff*factor)%360.; return DrawPaint.HSL2RGB(hue, sat, lum, alpha/255.); - } + } */ } /** The EmfPlusPathGradientBrushData object specifies a path gradient for a graphics brush. */ diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusHeader.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusHeader.java index a492985c9b..a271e77de1 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusHeader.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusHeader.java @@ -20,6 +20,7 @@ package org.apache.poi.hemf.record.emfplus; import static org.apache.poi.util.GenericRecordUtil.getEnumBitsAsString; +import java.awt.geom.Rectangle2D; import java.io.IOException; import java.util.Map; import java.util.function.Supplier; @@ -134,6 +135,11 @@ public class HemfPlusHeader implements HemfPlusRecord { ctx.setRenderState(EmfRenderState.EMF_DCONTEXT); } + @Override + public void calcBounds(Rectangle2D window, Rectangle2D viewport, EmfRenderState[] renderState) { + renderState[0] = EmfRenderState.EMF_DCONTEXT; + } + @Override public String toString() { return GenericRecordJsonWriter.marshal(this); diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusMisc.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusMisc.java index 104863f8cd..eaaf0a88f1 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusMisc.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusMisc.java @@ -163,6 +163,11 @@ public class HemfPlusMisc { public void draw(HemfGraphics ctx) { ctx.setRenderState(HemfGraphics.EmfRenderState.EMF_DCONTEXT); } + + @Override + public void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) { + renderState[0] = HemfGraphics.EmfRenderState.EMF_DCONTEXT; + } } /** diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecord.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecord.java index 3ac27f0dec..99210ad7ab 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecord.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecord.java @@ -18,6 +18,7 @@ package org.apache.poi.hemf.record.emfplus; +import java.awt.geom.Rectangle2D; import java.io.IOException; import org.apache.poi.common.usermodel.GenericRecord; @@ -55,6 +56,9 @@ public interface HemfPlusRecord extends GenericRecord { default void draw(HemfGraphics ctx) { } + default void calcBounds(Rectangle2D window, Rectangle2D viewport, HemfGraphics.EmfRenderState[] renderState) { + } + @Override default HemfPlusRecordType getGenericRecordType() { return getEmfPlusRecordType(); diff --git a/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java b/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java index c94fb8ac4b..deae850ceb 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java +++ b/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java @@ -19,6 +19,8 @@ package org.apache.poi.hemf.usermodel; import static java.lang.Math.abs; +import static org.apache.poi.hemf.draw.HemfGraphics.EmfRenderState.EMFPLUS_ONLY; +import static org.apache.poi.hemf.draw.HemfGraphics.EmfRenderState.EMF_ONLY; import java.awt.Graphics2D; import java.awt.Shape; @@ -36,14 +38,14 @@ import java.util.function.Consumer; import java.util.function.Supplier; import org.apache.poi.common.usermodel.GenericRecord; -import org.apache.poi.hemf.draw.HemfDrawProperties; import org.apache.poi.hemf.draw.HemfGraphics; +import org.apache.poi.hemf.record.emf.HemfComment; import org.apache.poi.hemf.record.emf.HemfHeader; import org.apache.poi.hemf.record.emf.HemfRecord; import org.apache.poi.hemf.record.emf.HemfRecordIterator; -import org.apache.poi.hemf.record.emf.HemfWindowing; import org.apache.poi.hwmf.usermodel.HwmfCharsetAware; import org.apache.poi.hwmf.usermodel.HwmfEmbedded; +import org.apache.poi.sl.draw.Drawable; import org.apache.poi.util.Dimension2DDouble; import org.apache.poi.util.Internal; import org.apache.poi.util.LittleEndianInputStream; @@ -114,26 +116,36 @@ public class HemfPicture implements Iterable, GenericRecord { */ public Rectangle2D getBounds() { Rectangle2D dim = getHeader().getFrameRectangle(); - double x = dim.getX(), y = dim.getY(); - double width = dim.getWidth(), height = dim.getHeight(); - if (dim.isEmpty() || Math.rint(width) == 0 || Math.rint(height) == 0) { - for (HemfRecord r : getRecords()) { - if (r instanceof HemfWindowing.EmfSetWindowExtEx) { - HemfWindowing.EmfSetWindowExtEx extEx = (HemfWindowing.EmfSetWindowExtEx)r; - Dimension2D d = extEx.getSize(); - width = d.getWidth(); - height = d.getHeight(); - // keep searching - sometimes there's another record - } - if (r instanceof HemfWindowing.EmfSetWindowOrgEx) { - HemfWindowing.EmfSetWindowOrgEx orgEx = (HemfWindowing.EmfSetWindowOrgEx)r; - x = orgEx.getX(); - y = orgEx.getY(); - } + boolean isInvalid = ReluctantRectangle2D.isEmpty(dim); + if (isInvalid) { + Rectangle2D lastDim = new ReluctantRectangle2D(); + getInnerBounds(lastDim, new ReluctantRectangle2D()); + if (!lastDim.isEmpty()) { + return lastDim; } } + return dim; + } - return new Rectangle2D.Double(x, y, width, height); + public void getInnerBounds(Rectangle2D window, Rectangle2D viewport) { + HemfGraphics.EmfRenderState[] renderState = { HemfGraphics.EmfRenderState.INITIAL }; + for (HemfRecord r : getRecords()) { + if ( + (renderState[0] == EMF_ONLY && r instanceof HemfComment.EmfComment) || + (renderState[0] == EMFPLUS_ONLY && !(r instanceof HemfComment.EmfComment)) + ) { + continue; + } + + try { + r.calcBounds(window, viewport, renderState); + } catch (RuntimeException ignored) { + } + + if (!window.isEmpty() && !viewport.isEmpty()) { + break; + } + } } /** @@ -154,32 +166,36 @@ public class HemfPicture implements Iterable, GenericRecord { final Rectangle2D b = getBoundsInPoints(); return new Dimension2DDouble(abs(b.getWidth()), abs(b.getHeight())); } - - private static double minX(Rectangle2D bounds) { - return Math.min(bounds.getMinX(), bounds.getMaxX()); - } - - private static double minY(Rectangle2D bounds) { - return Math.min(bounds.getMinY(), bounds.getMaxY()); - } - public void draw(Graphics2D ctx, Rectangle2D graphicsBounds) { final Shape clip = ctx.getClip(); final AffineTransform at = ctx.getTransform(); try { Rectangle2D emfBounds = getHeader().getBoundsRectangle(); + Rectangle2D winBounds = new ReluctantRectangle2D(); + Rectangle2D viewBounds = new ReluctantRectangle2D(); + getInnerBounds(winBounds, viewBounds); + + Boolean forceHeader = (Boolean)ctx.getRenderingHint(Drawable.EMF_FORCE_HEADER_BOUNDS); + if (forceHeader == null) { + forceHeader = false; + } + // this is a compromise ... sometimes winBounds are totally off :( + // but mostly they fit better than the header bounds + Rectangle2D b = + !viewBounds.isEmpty() && !forceHeader + ? viewBounds + : !winBounds.isEmpty() && !forceHeader + ? winBounds + : emfBounds; - // scale output bounds to image bounds ctx.translate(graphicsBounds.getCenterX(), graphicsBounds.getCenterY()); - ctx.scale(graphicsBounds.getWidth()/emfBounds.getWidth(), graphicsBounds.getHeight()/emfBounds.getHeight()); - ctx.translate(-emfBounds.getCenterX(), -emfBounds.getCenterY()); + ctx.scale( + graphicsBounds.getWidth()/b.getWidth(), + graphicsBounds.getHeight()/b.getHeight() + ); + ctx.translate(-b.getCenterX(),-b.getCenterY()); - HemfGraphics g = new HemfGraphics(ctx, emfBounds); - HemfDrawProperties prop = g.getProperties(); - prop.setWindowOrg(emfBounds.getX(), emfBounds.getY()); - prop.setWindowExt(emfBounds.getWidth(), emfBounds.getHeight()); - prop.setViewportOrg(emfBounds.getX(), emfBounds.getY()); - prop.setViewportExt(emfBounds.getWidth(), emfBounds.getHeight()); + HemfGraphics g = new HemfGraphics(ctx, b); for (HemfRecord r : getRecords()) { try { @@ -214,4 +230,43 @@ public class HemfPicture implements Iterable, GenericRecord { public Charset getDefaultCharset() { return defaultCharset; } + + + private static class ReluctantRectangle2D extends Rectangle2D.Double { + private boolean offsetSet = false; + private boolean rangeSet = false; + + public ReluctantRectangle2D() { + super(-1,-1,0,0); + } + + @Override + public void setRect(double x, double y, double w, double h) { + if (offsetSet && rangeSet) { + return; + } + super.setRect( + offsetSet ? this.x : x, + offsetSet ? this.y : y, + rangeSet ? this.width : w, + rangeSet ? this.height : h); + offsetSet |= (x != -1 || y != -1); + rangeSet |= (w != 0 || h != 0); + } + + @Override + public boolean isEmpty() { + return isEmpty(this); + } + + public static boolean isEmpty(Rectangle2D r) { + double w = Math.rint(r.getWidth()); + double h = Math.rint(r.getHeight()); + return + (w <= 0.0) || (h <= 0.0) || + (r.getX() == -1 && r.getY() == -1) || + // invalid emf bound have sometimes 1,1 as dimension + (w == 1 && h == 1); + } + } }