From 1562175343efa0fd12ce6df3d6e887cd78ce61ee Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Fri, 29 Nov 2019 00:39:35 +0000 Subject: [PATCH] Bug 60656 - Emf image support in slideshows - use Rectangle2D instead of Dimension2D for image bounds - fix shearing transformation - fix rendering of font attributes (bold/italic/...) - emf+: needs its own object table and properties table - emf+: add linear gradient handler - emf+: handle brush data of pens - wmf/emf/emf+: position right aligned text correctly - emf+: use emf+ instead of emf records in dual-mode - emf+: handle region data and operations correctly - emf/+: map font weight to awt font weight correctly git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1870566 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/sl/draw/BitmapImageRenderer.java | 11 +- .../org/apache/poi/sl/draw/DrawPaint.java | 3 +- src/java/org/apache/poi/sl/draw/Drawable.java | 2 + .../org/apache/poi/sl/draw/ImageRenderer.java | 11 +- src/java/org/apache/poi/util/Units.java | 17 ++ .../poi/xslf/draw/SVGImageRenderer.java | 6 +- .../poi/hemf/draw/HemfDrawProperties.java | 40 ++++ .../apache/poi/hemf/draw/HemfGraphics.java | 171 +++++++++++++--- .../poi/hemf/draw/HemfImageRenderer.java | 37 ++-- .../poi/hemf/record/emf/HemfComment.java | 4 + .../apache/poi/hemf/record/emf/HemfFill.java | 4 +- .../apache/poi/hemf/record/emf/HemfFont.java | 24 +++ .../apache/poi/hemf/record/emf/HemfMisc.java | 3 + .../hemf/record/emf/HemfRecordIterator.java | 6 +- .../hemf/record/emfplus/HemfPlusBrush.java | 183 +++++++++++++++--- .../poi/hemf/record/emfplus/HemfPlusDraw.java | 92 ++++++--- .../poi/hemf/record/emfplus/HemfPlusFont.java | 15 +- .../hemf/record/emfplus/HemfPlusHeader.java | 17 +- .../poi/hemf/record/emfplus/HemfPlusMisc.java | 45 +++-- .../hemf/record/emfplus/HemfPlusObject.java | 4 +- .../poi/hemf/record/emfplus/HemfPlusPath.java | 18 +- .../poi/hemf/record/emfplus/HemfPlusPen.java | 26 ++- .../hemf/record/emfplus/HemfPlusRecord.java | 4 + .../record/emfplus/HemfPlusRecordType.java | 2 +- .../hemf/record/emfplus/HemfPlusRegion.java | 150 ++++++++++++-- .../poi/hemf/usermodel/HemfPicture.java | 13 +- .../apache/poi/hwmf/draw/HwmfGraphics.java | 56 +++++- .../poi/hwmf/draw/HwmfGraphicsState.java | 72 +++++++ .../poi/hwmf/draw/HwmfImageRenderer.java | 12 +- .../poi/hwmf/record/HwmfBrushStyle.java | 6 +- .../org/apache/poi/hwmf/record/HwmfFont.java | 4 +- .../poi/hwmf/record/HwmfRegionMode.java | 19 +- .../org/apache/poi/hwmf/record/HwmfText.java | 5 +- .../poi/hwmf/usermodel/HwmfPicture.java | 40 ++-- 34 files changed, 917 insertions(+), 205 deletions(-) create mode 100644 src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphicsState.java diff --git a/src/java/org/apache/poi/sl/draw/BitmapImageRenderer.java b/src/java/org/apache/poi/sl/draw/BitmapImageRenderer.java index 64535411fe..3f6c886859 100644 --- a/src/java/org/apache/poi/sl/draw/BitmapImageRenderer.java +++ b/src/java/org/apache/poi/sl/draw/BitmapImageRenderer.java @@ -17,7 +17,6 @@ package org.apache.poi.sl.draw; -import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; @@ -255,10 +254,10 @@ public class BitmapImageRenderer implements ImageRenderer { } @Override - public Dimension getDimension() { + public Rectangle2D getBounds() { return (img == null) - ? new Dimension(0,0) - : new Dimension(img.getWidth(),img.getHeight()); + ? new Rectangle2D.Double() + : new Rectangle2D.Double(0, 0, img.getWidth(), img.getHeight()); } @Override @@ -316,7 +315,9 @@ public class BitmapImageRenderer implements ImageRenderer { AffineTransform at = new AffineTransform(sx, 0, 0, sy, tx, ty) ; Shape clipOld = graphics.getClip(); - if (isClipped) graphics.clip(anchor.getBounds2D()); + if (isClipped) { + graphics.clip(anchor.getBounds2D()); + } graphics.drawRenderedImage(img, at); graphics.setClip(clipOld); diff --git a/src/java/org/apache/poi/sl/draw/DrawPaint.java b/src/java/org/apache/poi/sl/draw/DrawPaint.java index f674aa1317..60a861cc49 100644 --- a/src/java/org/apache/poi/sl/draw/DrawPaint.java +++ b/src/java/org/apache/poi/sl/draw/DrawPaint.java @@ -583,8 +583,7 @@ public class DrawPaint { * * @return an array containing the 3 HSL values. */ - private static double[] RGB2HSL(Color color) - { + public static double[] RGB2HSL(Color color) { // Get RGB values in the range 0 - 1 float[] rgb = color.getRGBColorComponents( null ); diff --git a/src/java/org/apache/poi/sl/draw/Drawable.java b/src/java/org/apache/poi/sl/draw/Drawable.java index d75b46dc40..fd0ea1b6fa 100644 --- a/src/java/org/apache/poi/sl/draw/Drawable.java +++ b/src/java/org/apache/poi/sl/draw/Drawable.java @@ -46,6 +46,8 @@ public interface Drawable { case 9: return "FONT_MAP"; case 10: return "GSAVE"; case 11: return "GRESTORE"; + case 12: return "CURRENT_SLIDE"; + case 13: return "BUFFERED_IMAGE"; default: return "UNKNOWN_ID "+intKey(); } } diff --git a/src/java/org/apache/poi/sl/draw/ImageRenderer.java b/src/java/org/apache/poi/sl/draw/ImageRenderer.java index 5ab71380ca..0289fce7bf 100644 --- a/src/java/org/apache/poi/sl/draw/ImageRenderer.java +++ b/src/java/org/apache/poi/sl/draw/ImageRenderer.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.io.InputStream; import org.apache.poi.common.usermodel.GenericRecord; +import org.apache.poi.util.Dimension2DDouble; /** * Classes can implement this interfaces to support other formats, for @@ -105,10 +106,18 @@ public interface ImageRenderer { */ Rectangle2D getNativeBounds(); + /** + * @return the bounds of the buffered image in pixel + */ + Rectangle2D getBounds(); + /** * @return the dimension of the buffered image in pixel */ - Dimension2D getDimension(); + default Dimension2D getDimension() { + Rectangle2D r = getBounds(); + return new Dimension2DDouble(Math.abs(r.getWidth()), Math.abs(r.getHeight())); + } /** * @param alpha the alpha [0..1] to be added to the image (possibly already containing an alpha channel) diff --git a/src/java/org/apache/poi/util/Units.java b/src/java/org/apache/poi/util/Units.java index e645ca56b5..599e443f63 100644 --- a/src/java/org/apache/poi/util/Units.java +++ b/src/java/org/apache/poi/util/Units.java @@ -17,6 +17,7 @@ package org.apache.poi.util; import java.awt.geom.Dimension2D; +import java.awt.geom.Rectangle2D; public class Units { /** @@ -158,6 +159,22 @@ public class Units { return new Dimension2DDouble(width, height); } + public static Rectangle2D pointsToPixel(Rectangle2D pointsDim) { + double x = pointsDim.getX() * PIXEL_DPI / POINT_DPI; + double y = pointsDim.getY() * PIXEL_DPI / POINT_DPI; + double width = pointsDim.getWidth() * PIXEL_DPI / POINT_DPI; + double height = pointsDim.getHeight() * PIXEL_DPI / POINT_DPI; + return new Rectangle2D.Double(x, y, width, height); + } + + public static Rectangle2D pixelToPoints(Rectangle2D pointsDim) { + double x = pointsDim.getX() * POINT_DPI / PIXEL_DPI; + double y = pointsDim.getY() * POINT_DPI / PIXEL_DPI; + double width = pointsDim.getWidth() * POINT_DPI / PIXEL_DPI; + double height = pointsDim.getHeight() * POINT_DPI / PIXEL_DPI; + return new Rectangle2D.Double(x, y, width, height); + } + public static int charactersToEMU(double characters) { return (int) characters * EMU_PER_CHARACTER; } diff --git a/src/ooxml/java/org/apache/poi/xslf/draw/SVGImageRenderer.java b/src/ooxml/java/org/apache/poi/xslf/draw/SVGImageRenderer.java index 5f92c10195..ac357759fc 100644 --- a/src/ooxml/java/org/apache/poi/xslf/draw/SVGImageRenderer.java +++ b/src/ooxml/java/org/apache/poi/xslf/draw/SVGImageRenderer.java @@ -42,7 +42,6 @@ import org.apache.batik.util.XMLResourceDescriptor; import org.apache.poi.sl.draw.Drawable; import org.apache.poi.sl.draw.ImageRenderer; import org.apache.poi.sl.usermodel.PictureData; -import org.apache.poi.util.Dimension2DDouble; import org.w3c.dom.Document; public class SVGImageRenderer implements ImageRenderer { @@ -76,9 +75,8 @@ public class SVGImageRenderer implements ImageRenderer { } @Override - public Dimension2D getDimension() { - Rectangle2D r = svgRoot.getPrimitiveBounds(); - return new Dimension2DDouble(Math.ceil(r.getWidth()), Math.ceil(r.getHeight())); + public Rectangle2D getBounds() { + return svgRoot.getPrimitiveBounds(); } @Override diff --git a/src/scratchpad/src/org/apache/poi/hemf/draw/HemfDrawProperties.java b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfDrawProperties.java index 0d6a8ead1e..5ec47cc054 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/draw/HemfDrawProperties.java +++ b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfDrawProperties.java @@ -17,10 +17,13 @@ package org.apache.poi.hemf.draw; +import java.awt.Color; import java.awt.geom.AffineTransform; import java.awt.geom.Path2D; +import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.function.BiConsumer; import org.apache.poi.hemf.record.emfplus.HemfPlusBrush.EmfPlusHatchStyle; @@ -47,6 +50,10 @@ public class HemfDrawProperties extends HwmfDrawProperties { private final List transXForm = new ArrayList<>(); private final List transOper = new ArrayList<>(); + private Rectangle2D brushRect; + private List> brushColorsV; + private List> brushColorsH; + public HemfDrawProperties() { } @@ -60,6 +67,15 @@ public class HemfDrawProperties extends HwmfDrawProperties { emfPlusImage = other.emfPlusImage; transXForm.addAll(other.transXForm); transOper.addAll(other.transOper); + if (other.brushRect != null) { + brushRect = (Rectangle2D)other.brushRect.clone(); + } + if (other.brushColorsV != null) { + brushColorsV = new ArrayList<>(other.brushColorsV); + } + if (other.brushColorsH != null) { + brushColorsH = new ArrayList<>(other.brushColorsH); + } } /** @@ -139,4 +155,28 @@ public class HemfDrawProperties extends HwmfDrawProperties { List getTransOper() { return transOper; } + + public Rectangle2D getBrushRect() { + return brushRect; + } + + public void setBrushRect(Rectangle2D brushRect) { + this.brushRect = brushRect; + } + + public List> getBrushColorsV() { + return brushColorsV; + } + + public void setBrushColorsV(List> brushColorsV) { + this.brushColorsV = brushColorsV; + } + + public List> getBrushColorsH() { + return brushColorsH; + } + + public void setBrushColorsH(List> brushColorsH) { + this.brushColorsH = brushColorsH; + } } diff --git a/src/scratchpad/src/org/apache/poi/hemf/draw/HemfGraphics.java b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfGraphics.java index f39e043135..41aa52aea8 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/draw/HemfGraphics.java +++ b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfGraphics.java @@ -20,16 +20,25 @@ package org.apache.poi.hemf.draw; import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_NULL; import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_SOLID; +import java.awt.AlphaComposite; import java.awt.Color; +import java.awt.Composite; import java.awt.Graphics2D; +import java.awt.LinearGradientPaint; +import java.awt.MultipleGradientPaint; import java.awt.Paint; +import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; +import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; import java.util.function.Consumer; +import java.util.stream.Stream; import org.apache.poi.hemf.draw.HemfDrawProperties.TransOperand; import org.apache.poi.hemf.record.emf.HemfComment.EmfComment; @@ -37,7 +46,9 @@ import org.apache.poi.hemf.record.emf.HemfRecord; import org.apache.poi.hemf.record.emfplus.HemfPlusRecord; import org.apache.poi.hwmf.draw.HwmfDrawProperties; import org.apache.poi.hwmf.draw.HwmfGraphics; +import org.apache.poi.hwmf.record.HwmfBrushStyle; import org.apache.poi.hwmf.record.HwmfColorRef; +import org.apache.poi.hwmf.record.HwmfMisc; import org.apache.poi.hwmf.record.HwmfObjectTableEntry; import org.apache.poi.hwmf.record.HwmfPenStyle; import org.apache.poi.util.Internal; @@ -58,11 +69,14 @@ public class HemfGraphics extends HwmfGraphics { private static final HwmfColorRef BLACK = new HwmfColorRef(Color.BLACK); private EmfRenderState renderState = EmfRenderState.INITIAL; + private final Map plusObjectTable = new HashMap<>(); + private final Map plusPropStack = new HashMap<>(); public HemfGraphics(Graphics2D graphicsCtx, Rectangle2D bbox) { super(graphicsCtx,bbox); // add dummy entry for object ind ex 0, as emf is 1-based objectIndexes.set(0); + getProperties().setBkMode(HwmfMisc.WmfSetBkMode.HwmfBkMode.TRANSPARENT); } @Override @@ -87,14 +101,8 @@ public class HemfGraphics extends HwmfGraphics { public void draw(HemfRecord r) { switch (getRenderState()) { + default: case EMF_DCONTEXT: - // This state specifies that subsequent EMF records encountered in the metafile SHOULD be processed. - // EMF records cease being processed when the next EMF+ record is encountered. - if (r instanceof EmfComment) { - setRenderState(EmfRenderState.EMFPLUS_ONLY); - } - r.draw(this); - break; case INITIAL: r.draw(this); break; @@ -108,8 +116,6 @@ public class HemfGraphics extends HwmfGraphics { r.draw(this); } break; - default: - break; } } @@ -171,10 +177,7 @@ public class HemfGraphics extends HwmfGraphics { /** * Adds or sets an record of type {@link HwmfObjectTableEntry} to the object table. - * If the {@code index} is less than 1, the method acts the same as - * {@link HwmfGraphics#addObjectTableEntry(HwmfObjectTableEntry)}, otherwise the - * index is used to access the object table. - * As the table is filled successively, the index must be between 1 and size+1 + * The index must be > 0 * * @param entry the record to be stored * @param index the index to be overwritten, regardless if its content was unset before @@ -182,42 +185,74 @@ public class HemfGraphics extends HwmfGraphics { * @see HwmfGraphics#addObjectTableEntry(HwmfObjectTableEntry) */ public void addObjectTableEntry(HwmfObjectTableEntry entry, int index) { - checkTableEntryIndex(index); + // in EMF the index must > 0 + if (index < 1) { + throw new IndexOutOfBoundsException("Object table entry index in EMF must be > 0 - invalid index: "+index); + } objectIndexes.set(index); objectTable.put(index, entry); } /** - * Gets a record which was registered earliser + * Adds or sets an record of type {@link HwmfObjectTableEntry} to the plus object table. + * The index must be in the range [0..63] + * + * @param entry the record to be stored + * @param index the index to be overwritten, regardless if its content was unset before + * + * @see HwmfGraphics#addObjectTableEntry(HwmfObjectTableEntry) + */ + public void addPlusObjectTableEntry(HwmfObjectTableEntry entry, int index) { + // in EMF+ the index must be between 0 and 63 + if (index < 0 || index > 63) { + throw new IndexOutOfBoundsException("Object table entry index in EMF+ must be [0..63] - invalid index: "+index); + } + plusObjectTable.put(index, entry); + } + + /** + * Gets a record which was registered earlier * @param index the record index * @return the record or {@code null} if it doesn't exist */ public HwmfObjectTableEntry getObjectTableEntry(int index) { - checkTableEntryIndex(index); + // in EMF the index must > 0 + if (index < 1) { + throw new IndexOutOfBoundsException("Object table entry index in EMF must be > 0 - invalid index: "+index); + } return objectTable.get(index); } - private void checkTableEntryIndex(int index) { - if (renderState != EmfRenderState.EMFPLUS_ONLY && renderState != EmfRenderState.EMF_DCONTEXT) { - // in EMF the index must > 0 - if (index < 1) { - throw new IndexOutOfBoundsException("Object table entry index in EMF must be > 0 - invalid index: "+index); - } - } else { - // in EMF+ the index must be between 0 and 63 - if (index < 0 || index > 63) { - throw new IndexOutOfBoundsException("Object table entry index in EMF+ must be [0..63] - invalid index: "+index); - } + public HwmfObjectTableEntry getPlusObjectTableEntry(int index) { + // in EMF+ the index must be between 0 and 63 + if (index < 0 || index > 63) { + throw new IndexOutOfBoundsException("Object table entry index in EMF+ must be [0..63] - invalid index: "+index); } + return plusObjectTable.get(index); } - @Override public void applyObjectTableEntry(int index) { if ((index & 0x80000000) != 0) { selectStockObject(index); } else { - super.applyObjectTableEntry(index); + HwmfObjectTableEntry ote = objectTable.get(index); + if (ote == null) { + throw new NoSuchElementException("EMF reference exception - object table entry on index "+index+" was deleted before."); + } + ote.applyObject(this); + } + } + + public void applyPlusObjectTableEntry(int index) { + if ((index & 0x80000000) != 0) { + selectStockObject(index); + } else { + HwmfObjectTableEntry ote = plusObjectTable.get(index); + if (ote == null) { + throw new NoSuchElementException("EMF+ reference exception - plus object table entry on index "+index+" was deleted before."); + } + ote.applyObject(this); } } @@ -351,4 +386,82 @@ public class HemfGraphics extends HwmfGraphics { graphicsCtx.setTransform(tx); } + + @Override + public void fill(Shape shape) { + HemfDrawProperties prop = getProperties(); + + Composite old = graphicsCtx.getComposite(); + graphicsCtx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); + if (prop.getBrushStyle() != HwmfBrushStyle.BS_NULL) { + if (prop.getBkMode() == HwmfMisc.WmfSetBkMode.HwmfBkMode.OPAQUE) { + graphicsCtx.setPaint(prop.getBackgroundColor().getColor()); + graphicsCtx.fill(shape); + } + + graphicsCtx.setPaint(getFill()); + graphicsCtx.fill(shape); + } + graphicsCtx.setComposite(old); + } + + + @Override + protected Paint getLinearGradient() { + HemfDrawProperties prop = getProperties(); + Rectangle2D rect = prop.getBrushRect(); + List> colorsH = prop.getBrushColorsH(); + assert(rect != null && colorsH != null); + + // TODO: handle ColorsV list with a custom GradientPaint + // for an idea on how to handle 2d-gradients google "bilinear color interpolation". + // basically use two linear interpolations for x/y or vertical/horizontal axis. + // the resulting two colors need to be interpolated by 50%. + + return new LinearGradientPaint( + new Point2D.Double(rect.getMinX(),rect.getCenterY()), + new Point2D.Double(rect.getMaxX(),rect.getCenterY()), + toArray(colorsH.stream().map(Map.Entry::getKey), colorsH.size()), + colorsH.stream().map(Map.Entry::getValue).toArray(Color[]::new), + MultipleGradientPaint.CycleMethod.NO_CYCLE, + MultipleGradientPaint.ColorSpaceType.SRGB, + prop.getBrushTransform() + ); + } + + private static float[] toArray(Stream numbers, int size) { + float[] arr = new float[size]; + final int[] i = {0}; + numbers.forEach(n -> arr[i[0]++] = n.floatValue()); + return arr; + } + + /** + * Saves the current properties to the plus stack + */ + public void savePlusProperties(int index) { + final HemfDrawProperties p = getProperties(); + assert(p != null); + p.setTransform(graphicsCtx.getTransform()); + p.setClip(graphicsCtx.getClip()); + plusPropStack.put(index,p); + prop = newProperties(p); + } + + /** + * Restores the properties from the plus stack + * + * @param index the index of the previously saved properties + */ + public void restorePlusProperties(int index) { + if (!plusPropStack.containsKey(index)) { + return; + } + + prop = new HemfDrawProperties(plusPropStack.get(index)); + + graphicsCtx.setTransform(prop.getTransform()); + graphicsCtx.setClip(prop.getClip()); + } + } diff --git a/src/scratchpad/src/org/apache/poi/hemf/draw/HemfImageRenderer.java b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfImageRenderer.java index e41fe8d69e..42cef834a9 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/draw/HemfImageRenderer.java +++ b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfImageRenderer.java @@ -19,11 +19,9 @@ package org.apache.poi.hemf.draw; import static org.apache.poi.hwmf.draw.HwmfImageRenderer.getOuterBounds; -import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.RenderingHints; -import java.awt.Shape; import java.awt.geom.Dimension2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; @@ -33,6 +31,7 @@ import java.io.InputStream; import org.apache.poi.common.usermodel.GenericRecord; import org.apache.poi.hemf.usermodel.HemfPicture; +import org.apache.poi.hwmf.draw.HwmfGraphicsState; import org.apache.poi.hwmf.draw.HwmfImageRenderer; import org.apache.poi.sl.draw.BitmapImageRenderer; import org.apache.poi.sl.draw.EmbeddedExtractor; @@ -66,11 +65,6 @@ public class HemfImageRenderer implements ImageRenderer, EmbeddedExtractor { image = new HemfPicture(new ByteArrayInputStream(data)); } - @Override - public Dimension2D getDimension() { - return Units.pointsToPixel(image == null ? new Dimension() : image.getSize()); - } - @Override public void setAlpha(double alpha) { this.alpha = alpha; @@ -110,20 +104,20 @@ public class HemfImageRenderer implements ImageRenderer, EmbeddedExtractor { return false; } - boolean isClipped = true; - if (clip == null) { - isClipped = false; - clip = new Insets(0,0,0,0); - } - - Shape clipOld = graphics.getClip(); - if (isClipped) { - graphics.clip(anchor); - } + HwmfGraphicsState graphicsState = new HwmfGraphicsState(); + graphicsState.backup(graphics); - image.draw(graphics, getOuterBounds(anchor, clip)); + try { + if (clip != null) { + graphics.clip(anchor); + } else { + clip = new Insets(0, 0, 0, 0); + } - graphics.setClip(clipOld); + image.draw(graphics, getOuterBounds(anchor, clip)); + } finally { + graphicsState.restore(graphics); + } return true; } @@ -142,4 +136,9 @@ public class HemfImageRenderer implements ImageRenderer, EmbeddedExtractor { public Rectangle2D getNativeBounds() { return image.getBounds(); } + + @Override + public Rectangle2D getBounds() { + return Units.pointsToPixel(image == null ? new Rectangle2D.Double() : image.getBoundsInPoints()); + } } 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 9129ba4852..d476c21bcc 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 @@ -29,6 +29,7 @@ import java.util.function.Supplier; import org.apache.poi.common.usermodel.GenericRecord; import org.apache.poi.hemf.draw.HemfGraphics; +import org.apache.poi.hemf.draw.HemfGraphics.EmfRenderState; import org.apache.poi.hemf.record.emfplus.HemfPlusRecord; import org.apache.poi.hemf.record.emfplus.HemfPlusRecordIterator; import org.apache.poi.hwmf.usermodel.HwmfPicture; @@ -300,6 +301,9 @@ public class HemfComment { @Override public void draw(HemfGraphics ctx) { + // This state specifies that subsequent EMF records encountered in the metafile SHOULD be processed. + // EMF records cease being processed when the next EMF+ record is encountered. + ctx.setRenderState(EmfRenderState.EMFPLUS_ONLY); records.forEach(ctx::draw); } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFill.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFill.java index 514db1d309..d6b8228ea6 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFill.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFill.java @@ -862,7 +862,9 @@ public class HemfFill { // m12 (translateY) = eDy (The vertical translation component, in logical units.) double m12 = leis.readFloat(); - xform.setTransform(m00, m10, m01, m11, m02, m12); + // TODO: not sure, why the shearing has to be inverted here, + // probably because of the different world/user space transformation + xform.setTransform(m00, -m10, -m01, m11, m02, m12); if (xform.isIdentity()) { xform.setToIdentity(); diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFont.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFont.java index f7106053a7..66229fb536 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFont.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFont.java @@ -492,6 +492,30 @@ public class HemfFont extends HwmfFont { ); } + public void setHeight(double height) { + this.height = height; + } + + public void setWeight(int weight) { + this.weight = weight; + } + + public void setItalic(boolean italic) { + this.italic = italic; + } + + public void setUnderline(boolean underline) { + this.underline = underline; + } + + public void setStrikeOut(boolean strikeOut) { + this.strikeOut = strikeOut; + } + + public void setTypeface(String typeface) { + this.facename = typeface; + } + @Override protected int readString(LittleEndianInputStream leis, StringBuilder sb, int limit) throws IOException { sb.setLength(0); diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfMisc.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfMisc.java index 6c43fbdb8f..e9056f682c 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfMisc.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfMisc.java @@ -559,6 +559,9 @@ public class HemfMisc { public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { final int startIdx = leis.getReadIndex(); + // An unsigned integer that specifies the index of the extended logical pen object in + // the EMF object table. This index MUST be saved so that this object can be + // reused or modified. penIndex = (int) leis.readUInt(); // A 32-bit unsigned integer that specifies the offset from the start of this diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordIterator.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordIterator.java index dfa68670e1..dc5a2e3d57 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordIterator.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordIterator.java @@ -72,10 +72,12 @@ public class HemfRecordIterator implements Iterator { final HemfRecord record = type.constructor.get(); try { - long remBytes = recordSize-HEADER_SIZE; + long remBytes = recordSize - HEADER_SIZE; long readBytes = record.init(stream, remBytes, recordId); assert (readBytes <= remBytes); - stream.skipFully((int)(remBytes-readBytes)); + stream.skipFully((int) (remBytes - readBytes)); + } catch (RecordFormatException e) { + throw e; } catch (IOException|RuntimeException e) { throw new RecordFormatException(e); } 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 e8e2f4262d..4aeaf43e94 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 @@ -29,12 +29,18 @@ import java.awt.geom.Rectangle2D; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.AbstractMap; +import java.util.Arrays; 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; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.apache.poi.common.usermodel.GenericRecord; import org.apache.poi.hemf.draw.HemfDrawProperties; @@ -47,6 +53,7 @@ import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectType; import org.apache.poi.hemf.record.emfplus.HemfPlusPath.EmfPlusPath; import org.apache.poi.hwmf.record.HwmfBrushStyle; import org.apache.poi.hwmf.record.HwmfColorRef; +import org.apache.poi.sl.draw.DrawPaint; import org.apache.poi.util.BitField; import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.GenericRecordJsonWriter; @@ -310,7 +317,19 @@ public class HemfPlusBrush { long init(LittleEndianInputStream leis, long dataSize) throws IOException; + /** + * Apply brush data to graphics properties + * @param ctx the graphics context + * @param continuedObjectData the list continued object data + */ void applyObject(HemfGraphics ctx, List continuedObjectData); + + /** + * Apply brush data to pen properties + * @param ctx the graphics context + * @param continuedObjectData the list continued object data + */ + void applyPen(HemfGraphics ctx, List continuedObjectData); } /** The EmfPlusBrush object specifies a graphics brush for filling regions. */ @@ -347,6 +366,13 @@ public class HemfPlusBrush { brushData.applyObject(ctx, continuedObjectData); } + + public void applyPen(HemfGraphics ctx, List continuedObjectData) { + EmfPlusBrushData brushData = getBrushData(continuedObjectData); + brushData.applyPen(ctx, continuedObjectData); + } + + @Override public EmfPlusGraphicsVersion getGraphicsVersion() { return graphicsVersion; @@ -372,7 +398,6 @@ public class HemfPlusBrush { return brushData; } - public byte[] getRawData(List continuedObjectData) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); try { @@ -416,11 +441,17 @@ public class HemfPlusBrush { @Override public void applyObject(HemfGraphics ctx, List continuedObjectData) { HemfDrawProperties prop = ctx.getProperties(); - prop.setBackgroundColor(new HwmfColorRef(solidColor)); + prop.setBrushColor(new HwmfColorRef(solidColor)); prop.setBrushTransform(null); prop.setBrushStyle(HwmfBrushStyle.BS_SOLID); } + @Override + public void applyPen(HemfGraphics ctx, List continuedObjectData) { + HemfDrawProperties prop = ctx.getProperties(); + prop.setPenColor(new HwmfColorRef(solidColor)); + } + @Override public String toString() { return GenericRecordJsonWriter.marshal(this); @@ -457,6 +488,12 @@ public class HemfPlusBrush { prop.setEmfPlusBrushHatch(style); } + @Override + public void applyPen(HemfGraphics ctx, List continuedObjectData) { + HemfDrawProperties prop = ctx.getProperties(); + prop.setPenColor(new HwmfColorRef(foreColor)); + } + @Override public String toString() { return GenericRecordJsonWriter.marshal(this); @@ -484,12 +521,15 @@ public class HemfPlusBrush { private Rectangle2D rect = new Rectangle2D.Double(); private Color startColor, endColor; private AffineTransform transform; - private double[] positions; + private float[] positions; private Color[] blendColors; - private double[] positionsV; - private double[] blendFactorsV; - private double[] positionsH; - private double[] blendFactorsH; + private float[] positionsV; + private float[] blendFactorsV; + 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" }; @Override public long init(LittleEndianInputStream leis, long dataSize) throws IOException { @@ -518,16 +558,13 @@ public class HemfPlusBrush { size += readXForm(leis, (transform = new AffineTransform())); } - final boolean isPreset = PRESET_COLORS.isSet(dataFlags); - final boolean blendH = BLEND_FACTORS_H.isSet(dataFlags); - final boolean blendV = BLEND_FACTORS_V.isSet(dataFlags); - if (isPreset && (blendH || blendV)) { + if (isPreset() && (isBlendH() || isBlendV())) { throw new RuntimeException("invalid combination of preset colors and blend factors v/h"); } - size += (isPreset) ? readColors(leis, d -> positions = d, c -> blendColors = c) : 0; - size += (blendV) ? readFactors(leis, d -> positionsV = d, f -> blendFactorsV = f) : 0; - size += (blendH) ? readFactors(leis, d -> positionsH = d, f -> blendFactorsH = f) : 0; + size += (isPreset()) ? readColors(leis, d -> positions = d, c -> blendColors = c) : 0; + size += (isBlendV()) ? readFactors(leis, d -> positionsV = d, f -> blendFactorsV = f) : 0; + size += (isBlendH()) ? readFactors(leis, d -> positionsH = d, f -> blendFactorsH = f) : 0; return size; } @@ -535,7 +572,26 @@ public class HemfPlusBrush { @Override public void applyObject(HemfGraphics ctx, List continuedObjectData) { HemfDrawProperties prop = ctx.getProperties(); - // TODO: implement + prop.setBrushStyle(HwmfBrushStyle.BS_LINEAR_GRADIENT); + prop.setBrushRect(rect); + prop.setBrushTransform(transform); + + // Preset colors and BlendH/V are mutual exclusive + if (isPreset()) { + setColorProps(prop::setBrushColorsH, positions, this::getBlendColorAt); + } else { + setColorProps(prop::setBrushColorsH, positionsH, this::getBlendHColorAt); + } + setColorProps(prop::setBrushColorsV, positionsV, this::getBlendVColorAt); + + if (!(isPreset() || isBlendH() || isBlendV())) { + prop.setBrushColorsH(Arrays.asList(kv(0f,startColor), kv(1f,endColor))); + } + } + + @Override + public void applyPen(HemfGraphics ctx, List continuedObjectData) { + } @Override @@ -551,7 +607,7 @@ public class HemfPlusBrush { @Override public Map> getGenericProperties() { final Map> m = new LinkedHashMap<>(); - m.put("flags", () -> dataFlags); + m.put("flags", GenericRecordUtil.getBitsAsString(() -> dataFlags, FLAG_MASKS, FLAG_NAMES)); m.put("wrapMode", () -> wrapMode); m.put("rect", () -> rect); m.put("startColor", () -> startColor); @@ -565,6 +621,67 @@ public class HemfPlusBrush { m.put("blendFactorsH", () -> blendFactorsH); return Collections.unmodifiableMap(m); } + + private boolean isPreset() { + return PRESET_COLORS.isSet(dataFlags); + } + + private boolean isBlendH() { + return BLEND_FACTORS_H.isSet(dataFlags); + } + + private boolean isBlendV() { + return BLEND_FACTORS_V.isSet(dataFlags); + } + + 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 getBlendVColorAt(int index) { + return kv(positionsV[index],interpolateColors(blendFactorsV[index])); + } + + 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) { + if (positions == null) { + setter.accept(null); + } else { + setter.accept(IntStream.range(0, positions.length).boxed().map(sup).collect(Collectors.toList())); + } + } + + private Color interpolateColors(final double factor) { + // https://stackoverflow.com/questions/1416560/hsl-interpolation + + final double[] hslStart = DrawPaint.RGB2HSL(startColor); + final double[] hslStop = DrawPaint.RGB2HSL(endColor); + + BiFunction linearInter = (start, stop) -> + start.doubleValue()+(stop.doubleValue()-start.doubleValue())*factor; + + double alpha = linearInter.apply(startColor.getAlpha(),endColor.getAlpha()); + 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.; + + 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; + + return DrawPaint.HSL2RGB(hue, sat, lum, alpha/255.); + } } /** The EmfPlusPathGradientBrushData object specifies a path gradient for a graphics brush. */ @@ -577,9 +694,9 @@ public class HemfPlusBrush { private EmfPlusPath boundaryPath; private Point2D[] boundaryPoints; private AffineTransform transform; - private double[] positions; + private float[] positions; private Color[] blendColors; - private double[] blendFactorsH; + private float[] blendFactorsH; private Double focusScaleX, focusScaleY; @Override @@ -597,8 +714,12 @@ public class HemfPlusBrush { // that appears at the center point of the brush. The color of the brush changes gradually from the // boundary color to the center color as it moves from the boundary to the center point. centerColor = readARGB(leis.readInt()); - int size = 3*LittleEndianConsts.INT_SIZE; + + if (wrapMode == null) { + return size; + } + size += readPointF(leis, centerPoint); // An unsigned 32-bit integer that specifies the number of colors specified in the SurroundingColor field. @@ -608,10 +729,10 @@ public class HemfPlusBrush { // An array of SurroundingColorCount EmfPlusARGB objects that specify the colors for discrete points on the // boundary of the brush. surroundingColor = new Color[colorCount]; - for (int i=0; i continuedObjectData) { + + } + @Override public String toString() { return GenericRecordJsonWriter.marshal(this); @@ -747,6 +873,11 @@ public class HemfPlusBrush { prop.setBrushTransform(transform); } + @Override + public void applyPen(HemfGraphics ctx, List continuedObjectData) { + + } + @Override public String toString() { return GenericRecordJsonWriter.marshal(this); @@ -768,11 +899,11 @@ public class HemfPlusBrush { } } - private static int readPositions(LittleEndianInputStream leis, Consumer pos) { + private static int readPositions(LittleEndianInputStream leis, Consumer pos) { final int count = leis.readInt(); int size = LittleEndianConsts.INT_SIZE; - double[] positions = new double[count]; + float[] positions = new float[count]; for (int i=0; i pos, Consumer cols) { + private static int readColors(LittleEndianInputStream leis, Consumer pos, Consumer cols) { int[] count = { 0 }; int size = readPositions(leis, p -> { count[0] = p.length; pos.accept(p); }); Color[] colors = new Color[count[0]]; @@ -793,10 +924,10 @@ public class HemfPlusBrush { return size + colors.length * LittleEndianConsts.INT_SIZE; } - private static int readFactors(LittleEndianInputStream leis, Consumer pos, Consumer facs) { + private static int readFactors(LittleEndianInputStream leis, Consumer pos, Consumer facs) { int[] count = { 0 }; int size = readPositions(leis, p -> { count[0] = p.length; pos.accept(p); }); - double[] factors = new double[count[0]]; + float[] factors = new float[count[0]]; for (int i=0; i glpyhPos = new ArrayList<>(); + private final List glyphPos = new ArrayList<>(); private final AffineTransform transformMatrix = new AffineTransform(); @Override public HemfPlusRecordType getEmfPlusRecordType() { - return HemfPlusRecordType.drawDriverstring; + return HemfPlusRecordType.drawDriverString; } @Override @@ -689,7 +692,7 @@ public class HemfPlusDraw { // A 32-bit unsigned integer that specifies whether a transform matrix is present in the // TransformMatrix field. - boolean hasMatrix = leis.readInt() == 1; + int matrixPresent = leis.readInt(); // A 32-bit unsigned integer that specifies number of glyphs in the string. int glyphCount = leis.readInt(); @@ -716,10 +719,10 @@ public class HemfPlusDraw { for (int i=0; i { + byte[] buf = new String(new int[]{glyphIter.next()}, 0, 1).getBytes(Charsets.UTF_16LE); + ctx.drawString(buf, buf.length, p, null, null, null, null, true); + }); + } + } + @Override public String toString() { return GenericRecordJsonWriter.marshal(this); @@ -739,7 +767,7 @@ public class HemfPlusDraw { "brushId", this::getBrushId, "optionsFlags", getBitsAsString(() -> optionsFlags, OPTIONS_MASK, OPTIONS_NAMES), "glyphs", () -> glyphs, - "glyphPos", () -> glpyhPos, + "glyphPos", () -> glyphPos, "transform", () -> transformMatrix ); } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusFont.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusFont.java index e0a2fc759f..857a8e93f5 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusFont.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusFont.java @@ -22,7 +22,10 @@ import java.util.List; import java.util.Map; import java.util.function.Supplier; +import org.apache.poi.common.usermodel.fonts.FontHeader; +import org.apache.poi.hemf.draw.HemfDrawProperties; import org.apache.poi.hemf.draw.HemfGraphics; +import org.apache.poi.hemf.record.emf.HemfFont; import org.apache.poi.hemf.record.emfplus.HemfPlusDraw.EmfPlusUnitType; import org.apache.poi.hemf.record.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion; import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectData; @@ -106,7 +109,17 @@ public class HemfPlusFont { @Override public void applyObject(HemfGraphics ctx, List continuedObjectData) { - + HemfDrawProperties prop = ctx.getProperties(); + HemfFont font = new HemfFont(); + font.initDefaults(); + font.setTypeface(family); + // TODO: check how to calculate the font size + font.setHeight(emSize); + font.setStrikeOut(STRIKEOUT.isSet(styleFlags)); + font.setUnderline(UNDERLINE.isSet(styleFlags)); + font.setWeight(BOLD.isSet(styleFlags) ? 700 : FontHeader.REGULAR_WEIGHT); + font.setItalic(ITALIC.isSet(styleFlags)); + prop.setFont(font); } @Override 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 4fcfeb7d0a..256c72ea49 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 @@ -18,7 +18,7 @@ package org.apache.poi.hemf.record.emfplus; -import static org.apache.poi.util.GenericRecordUtil.getBitsAsString; +import static org.apache.poi.util.GenericRecordUtil.getEnumBitsAsString; import java.io.IOException; import java.util.Map; @@ -60,8 +60,13 @@ public class HemfPlusHeader implements HemfPlusRecord { } } - private static final int[] FLAGS_MASK = { 0x0001 }; - private static final String[] FLAGS_NAMES = { "DUAL_MODE" }; + private static final int[] FLAGS_MASK = { 0x0000, 0x0001 }; + private static final String[] FLAGS_NAMES = { "EMF_PLUS_MODE", "DUAL_MODE" }; + + private static final int[] EMFFLAGS_MASK = { 0x0000, 0x0001 }; + private static final String[] EMFFLAGS_NAMES = { "CONTEXT_PRINTER", "CONTEXT_VIDEO" }; + + private int flags; private final EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion(); @@ -125,7 +130,7 @@ public class HemfPlusHeader implements HemfPlusRecord { public void draw(HemfGraphics ctx) { // currently EMF is better supported than EMF+ ... so if there's a complete set of EMF records available, // disable EMF+ rendering for now - ctx.setRenderState(isEmfPlusDualMode() ? EmfRenderState.EMF_ONLY : EmfRenderState.EMF_DCONTEXT); + ctx.setRenderState(EmfRenderState.EMF_DCONTEXT); } @Override @@ -136,9 +141,9 @@ public class HemfPlusHeader implements HemfPlusRecord { @Override public Map> getGenericProperties() { return GenericRecordUtil.getGenericProperties( - "flags", this::getFlags, + "flags", getEnumBitsAsString(this::getFlags, FLAGS_MASK, FLAGS_NAMES), "version", this::getVersion, - "emfPlusFlags", getBitsAsString(this::getEmfPlusFlags, FLAGS_MASK, FLAGS_NAMES), + "emfPlusFlags", getEnumBitsAsString(this::getEmfPlusFlags, EMFFLAGS_MASK, EMFFLAGS_NAMES), "logicalDpiX", this::getLogicalDpiX, "logicalDpiY", this::getLogicalDpiY ); 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 f3ad69aae5..2a8866be48 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 @@ -20,6 +20,7 @@ package org.apache.poi.hemf.record.emfplus; import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF; import static org.apache.poi.util.GenericRecordUtil.getBitsAsString; +import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; @@ -30,6 +31,7 @@ import java.util.function.Supplier; import org.apache.poi.hemf.draw.HemfDrawProperties; import org.apache.poi.hemf.draw.HemfGraphics; import org.apache.poi.hemf.record.emf.HemfFill; +import org.apache.poi.hwmf.record.HwmfRegionMode; import org.apache.poi.util.BitField; import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.GenericRecordUtil; @@ -53,18 +55,20 @@ public class HemfPlusMisc { } public enum CombineMode { - CombineModeReplace(0x00000000), - CombineModeIntersect(0x00000001), - CombineModeUnion(0x00000002), - CombineModeXOR(0x00000003), - CombineModeExclude(0x00000004), - CombineModeComplement(0x00000005) + REPLACE(0x00000000, HwmfRegionMode.RGN_COPY), + INTERSECT(0x00000001, HwmfRegionMode.RGN_AND), + UNION(0x00000002, HwmfRegionMode.RGN_OR), + XOR(0x00000003, HwmfRegionMode.RGN_XOR), + EXCLUDE(0x00000004, HwmfRegionMode.RGN_DIFF), + COMPLEMENT(0x00000005, HwmfRegionMode.RGN_COMPLEMENT) ; public final int id; + public final HwmfRegionMode regionMode; - CombineMode(int id) { + CombineMode(int id, HwmfRegionMode regionMode) { this.id = id; + this.regionMode = regionMode; } public static CombineMode valueOf(int id) { @@ -303,6 +307,14 @@ public class HemfPlusMisc { public CombineMode getCombineMode() { return CombineMode.valueOf(COMBINE_MODE.getValue(getFlags())); } + + @Override + public void draw(HemfGraphics ctx) { + HemfDrawProperties prop = ctx.getProperties(); + ctx.applyPlusObjectTableEntry(getObjectId()); + Shape clip = prop.getPath(); + ctx.setClip(clip, clip == null ? HwmfRegionMode.RGN_COPY : getCombineMode().regionMode, false); + } } /** The EmfPlusSetClipRect record combines the current clipping region with a rectangle. */ @@ -389,6 +401,11 @@ public class HemfPlusMisc { return LittleEndianConsts.INT_SIZE; } + @Override + public void draw(HemfGraphics ctx) { + ctx.savePlusProperties(getStackIndex()); + } + @Override public Map> getGenericProperties() { return GenericRecordUtil.getGenericProperties( @@ -407,6 +424,11 @@ public class HemfPlusMisc { public HemfPlusRecordType getEmfPlusRecordType() { return HemfPlusRecordType.restore; } + + @Override + public void draw(HemfGraphics ctx) { + ctx.restorePlusProperties(getStackIndex()); + } } /** The EmfPlusSetRenderingOrigin record specifies the rendering origin for graphics output. */ @@ -432,10 +454,11 @@ public class HemfPlusMisc { public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { this.flags = flags; - // A 32-bit unsigned integer that defines the horizontal coordinate value of the rendering origin. - double x = leis.readUInt(); - // A 32-bit unsigned integer that defines the vertical coordinate value of the rendering origin. - double y = leis.readUInt(); + // error in the MS-EMFPLUS docs - its a signed integer instead of an unsigned + // A 32-bit signed integer that defines the horizontal coordinate value of the rendering origin. + int x = leis.readInt(); + // A 32-bit signed integer that defines the vertical coordinate value of the rendering origin. + int y = leis.readInt(); origin.setLocation(x,y); diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusObject.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusObject.java index 34c6b40067..06335f0ad0 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusObject.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusObject.java @@ -196,7 +196,7 @@ public class HemfPlusObject { public void draw(HemfGraphics ctx) { if (objectData.isContinuedRecord()) { EmfPlusObject other; - HwmfObjectTableEntry entry = ctx.getObjectTableEntry(getObjectId()); + HwmfObjectTableEntry entry = ctx.getPlusObjectTableEntry(getObjectId()); if (entry instanceof EmfPlusObject && objectData.getClass().isInstance((other = (EmfPlusObject)entry).getObjectData()) ) { @@ -205,7 +205,7 @@ public class HemfPlusObject { throw new RuntimeException("can't find previous record for continued record"); } } else { - ctx.addObjectTableEntry(this, getObjectId()); + ctx.addPlusObjectTableEntry(this, getObjectId()); } } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusPath.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusPath.java index 71d71b73eb..ef29a9d63d 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusPath.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusPath.java @@ -35,6 +35,7 @@ import org.apache.poi.hemf.draw.HemfDrawProperties; import org.apache.poi.hemf.draw.HemfGraphics; import org.apache.poi.hemf.record.emfplus.HemfPlusDraw.EmfPlusCompressed; import org.apache.poi.hemf.record.emfplus.HemfPlusDraw.EmfPlusRelativePosition; +import org.apache.poi.hemf.record.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion; import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectData; import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectType; import org.apache.poi.util.BitField; @@ -87,7 +88,7 @@ public class HemfPlusPath { private static final int[] TYPE_MASKS = { 0x10, 0x20, 0x80 }; private static final String[] TYPE_NAMES = { "DASHED", "MARKER", "CLOSE" }; - private final HemfPlusHeader.EmfPlusGraphicsVersion graphicsVersion = new HemfPlusHeader.EmfPlusGraphicsVersion(); + private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion(); private int pointFlags; private Point2D[] pathPoints; private byte[] pointTypes; @@ -143,7 +144,7 @@ public class HemfPlusPath { } @Override - public HemfPlusHeader.EmfPlusGraphicsVersion getGraphicsVersion() { + public EmfPlusGraphicsVersion getGraphicsVersion() { return graphicsVersion; } @@ -175,9 +176,15 @@ public class HemfPlusPath { @Override public void applyObject(HemfGraphics ctx, List continuedObjectData) { HemfDrawProperties prop = ctx.getProperties(); - Path2D path = new Path2D.Double(Path2D.WIND_NON_ZERO); - prop.setPath(path); + prop.setPath(getPath()); + } + + public Path2D getPath() { + return getPath(Path2D.WIND_NON_ZERO); + } + public Path2D getPath(int windingRule) { + Path2D path = new Path2D.Double(windingRule); for (int idx=0; idx < pathPoints.length; idx++) { Point2D p1 = pathPoints[idx]; switch (getPointType(idx)) { @@ -198,10 +205,11 @@ public class HemfPlusPath { path.closePath(); } } + return path; } @Override - public EmfPlusObjectType getGenericRecordType() { + public Enum getGenericRecordType() { return EmfPlusObjectType.PATH; } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusPen.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusPen.java index 4540aab3b0..7f03fb37d2 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusPen.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusPen.java @@ -34,6 +34,7 @@ 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.emfplus.HemfPlusBrush.EmfPlusBrush; import org.apache.poi.hemf.record.emfplus.HemfPlusDraw.EmfPlusUnitType; import org.apache.poi.hemf.record.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion; import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectData; @@ -330,6 +331,8 @@ public class HemfPlusPen { private EmfPlusCustomLineCap customStartCap; private EmfPlusCustomLineCap customEndCap; + private final EmfPlusBrush brush = new EmfPlusBrush(); + @Override public long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException { // An EmfPlusGraphicsVersion object that specifies the version of operating system graphics that @@ -463,6 +466,8 @@ public class HemfPlusPen { size += initCustomCap(c -> customEndCap = c, leis); } + size += brush.init(leis, dataSize-size, EmfPlusObjectType.BRUSH, 0); + return size; } @@ -472,8 +477,12 @@ public class HemfPlusPen { } private long initCustomCap(Consumer setter, LittleEndianInputStream leis) throws IOException { + int CustomStartCapSize = leis.readInt(); + int size = LittleEndianConsts.INT_SIZE; + EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion(); - long size = version.init(leis); + size += version.init(leis); + assert(version.getGraphicsVersion() != null); boolean adjustableArrow = (leis.readInt() != 0); size += LittleEndianConsts.INT_SIZE; @@ -492,11 +501,11 @@ public class HemfPlusPen { // TOOD: // - set width according unit type // - provide logic for different start and end cap - // - provide standard caps like diamondd + // - provide standard caps like diamond // - support custom caps - // workaround for too wide pens ... just arbitrary reduce high values ... - prop.setPenWidth(penWidth > 20 ? 1 : penWidth); + brush.applyPen(ctx, continuedObjectData); + prop.setPenWidth(penWidth); prop.setPenStyle(new HwmfPenStyle(){ @Override public HwmfLineCap getLineCap() { @@ -573,6 +582,7 @@ public class HemfPlusPen { m.put("compoundLineData", () -> compoundLineData); m.put("customStartCap", () -> customStartCap); m.put("customEndCap", () -> customEndCap); + m.put("brush", () -> brush); return Collections.unmodifiableMap(m); } } @@ -645,13 +655,17 @@ public class HemfPlusPen { size += readPointF(leis, lineHotSpot); if (FILL_PATH.isSet(dataFlags)) { + int fillSize = leis.readInt(); + size += LittleEndianConsts.INT_SIZE; fillPath = new EmfPlusPath(); - size += fillPath.init(leis, -1, null, -1); + size += fillPath.init(leis, fillSize, EmfPlusObjectType.PATH, -1); } if (LINE_PATH.isSet(dataFlags)) { + int pathSize = leis.readInt(); + size += LittleEndianConsts.INT_SIZE; outlinePath = new EmfPlusPath(); - size += outlinePath.init(leis, -1, null, -1); + size += outlinePath.init(leis, pathSize, EmfPlusObjectType.PATH, -1); } return size; 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 608bba7e7f..af1cf0935f 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 @@ -55,4 +55,8 @@ public interface HemfPlusRecord extends GenericRecord { default void draw(HemfGraphics ctx) { } + @Override + default Enum getGenericRecordType() { + return getEmfPlusRecordType(); + } } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordType.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordType.java index 38263f4eff..acbba48668 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordType.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordType.java @@ -76,7 +76,7 @@ public enum HemfPlusRecordType { setClipRegion(0x4033, HemfPlusMisc.EmfPlusSetClipRegion::new), setClipPath(0x4034, HemfPlusMisc.EmfPlusSetClipPath::new), offsetClip(0x4035, UnimplementedHemfPlusRecord::new), - drawDriverstring(0x4036, HemfPlusDraw.EmfPlusDrawDriverString::new), + drawDriverString(0x4036, HemfPlusDraw.EmfPlusDrawDriverString::new), strokeFillPath(0x4037, UnimplementedHemfPlusRecord::new), serializableObject(0x4038, UnimplementedHemfPlusRecord::new), setTSGraphics(0x4039, UnimplementedHemfPlusRecord::new), diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRegion.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRegion.java index f8e43fc646..8862e856b9 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRegion.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRegion.java @@ -19,14 +19,19 @@ package org.apache.poi.hemf.record.emfplus; import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF; +import java.awt.Shape; +import java.awt.geom.Area; +import java.awt.geom.Path2D; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; 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.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion; import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectData; @@ -42,49 +47,51 @@ public class HemfPlusRegion { * Specifies a region node with child nodes. A Boolean AND operation SHOULD be applied to the left and right * child nodes specified by an EmfPlusRegionNodeChildNodes object */ - AND(0X00000001, EmfPlusRegionNode::new), + AND(0X00000001, EmfPlusRegionNode::new, Area::intersect), /** * Specifies a region node with child nodes. A Boolean OR operation SHOULD be applied to the left and right * child nodes specified by an EmfPlusRegionNodeChildNodes object. */ - OR(0X00000002, EmfPlusRegionNode::new), + OR(0X00000002, EmfPlusRegionNode::new, Area::add), /** * Specifies a region node with child nodes. A Boolean XOR operation SHOULD be applied to the left and right * child nodes specified by an EmfPlusRegionNodeChildNodes object. */ - XOR(0X00000003, EmfPlusRegionNode::new), + XOR(0X00000003, EmfPlusRegionNode::new, Area::exclusiveOr), /** * Specifies a region node with child nodes. A Boolean operation, defined as "the part of region 1 that is excluded * from region 2", SHOULD be applied to the left and right child nodes specified by an EmfPlusRegionNodeChildNodes object. */ - EXCLUDE(0X00000004, EmfPlusRegionNode::new), + EXCLUDE(0X00000004, EmfPlusRegionNode::new, Area::subtract), /** * Specifies a region node with child nodes. A Boolean operation, defined as "the part of region 2 that is excluded * from region 1", SHOULD be applied to the left and right child nodes specified by an EmfPlusRegionNodeChildNodes object. */ - COMPLEMENT(0X00000005, EmfPlusRegionNode::new), + COMPLEMENT(0X00000005, EmfPlusRegionNode::new, Area::subtract), /** * Specifies a region node with no child nodes. * The RegionNodeData field SHOULD specify a boundary with an EmfPlusRectF object. */ - RECT(0X10000000, EmfPlusRegionRect::new), + RECT(0X10000000, EmfPlusRegionRect::new, null), /** * Specifies a region node with no child nodes. * The RegionNodeData field SHOULD specify a boundary with an EmfPlusRegionNodePath object */ - PATH(0X10000001, EmfPlusRegionPath::new), + PATH(0X10000001, EmfPlusRegionPath::new, null), /** Specifies a region node with no child nodes. The RegionNodeData field SHOULD NOT be present. */ - EMPTY(0X10000002, EmfPlusRegionEmpty::new), + EMPTY(0X10000002, EmfPlusRegionEmpty::new, null), /** Specifies a region node with no child nodes, and its bounds are not defined. */ - INFINITE(0X10000003, EmfPlusRegionInfinite::new) + INFINITE(0X10000003, EmfPlusRegionInfinite::new, null) ; public final int id; public final Supplier constructor; + public final BiConsumer operation; - EmfPlusRegionNodeDataType(int id, Supplier constructor) { + EmfPlusRegionNodeDataType(int id, Supplier constructor, BiConsumer operation) { this.id = id; this.constructor = constructor; + this.operation = operation; } public static EmfPlusRegionNodeDataType valueOf(int id) { @@ -95,6 +102,7 @@ public class HemfPlusRegion { } } + /** The EmfPlusRegion object specifies line and curve segments that define a nonrectilinear shape. */ public static class EmfPlusRegion implements EmfPlusObjectData { private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion(); @@ -111,14 +119,16 @@ public class HemfPlusRegion { // An array of RegionNodeCount+1 EmfPlusRegionNode objects. Regions are specified as a binary tree of // region nodes, and each node MUST either be a terminal node or specify one or two child nodes. // RegionNode MUST contain at least one element. - size += readNode(leis, d -> regionNode = d); + size += readNode(leis, this::setRegionNode); return size; } @Override public void applyObject(HemfGraphics ctx, List continuedObjectData) { - + HemfDrawProperties prop = ctx.getProperties(); + Shape shape = regionNode.getShape(); + prop.setPath(shape == null ? null : new Path2D.Double(shape)); } @Override @@ -131,11 +141,19 @@ public class HemfPlusRegion { return EmfPlusObjectType.REGION; } + private void setRegionNode(EmfPlusRegionNodeData regionNode) { + this.regionNode = regionNode; + } + + public EmfPlusRegionNodeData getRegionNode() { + return regionNode; + } + @Override public Map> getGenericProperties() { return GenericRecordUtil.getGenericProperties( "graphicsVersion", this::getGraphicsVersion, - "regionNode", () -> regionNode + "regionNode", this::getRegionNode ); } } @@ -143,6 +161,8 @@ public class HemfPlusRegion { public interface EmfPlusRegionNodeData extends GenericRecord { long init(LittleEndianInputStream leis) throws IOException; + Shape getShape(); + default void setNodeType(EmfPlusRegionNodeDataType type) {} } public static class EmfPlusRegionPath extends EmfPlusPath implements EmfPlusRegionNodeData { @@ -150,6 +170,16 @@ public class HemfPlusRegion { int dataSize = leis.readInt(); return super.init(leis, dataSize, EmfPlusObjectType.PATH, 0) + LittleEndianConsts.INT_SIZE; } + + @Override + public Shape getShape() { + return getPath(); + } + + @Override + public EmfPlusRegionNodeDataType getGenericRecordType() { + return EmfPlusRegionNodeDataType.PATH; + } } public static class EmfPlusRegionInfinite implements EmfPlusRegionNodeData { @@ -162,6 +192,16 @@ public class HemfPlusRegion { public Map> getGenericProperties() { return null; } + + @Override + public Shape getShape() { + return null; + } + + @Override + public EmfPlusRegionNodeDataType getGenericRecordType() { + return EmfPlusRegionNodeDataType.INFINITE; + } } public static class EmfPlusRegionEmpty implements EmfPlusRegionNodeData { @@ -174,6 +214,16 @@ public class HemfPlusRegion { public Map> getGenericProperties() { return null; } + + @Override + public Shape getShape() { + return new Rectangle2D.Double(0,0,0,0); + } + + @Override + public EmfPlusRegionNodeDataType getGenericRecordType() { + return EmfPlusRegionNodeDataType.EMPTY; + } } public static class EmfPlusRegionRect implements EmfPlusRegionNodeData { @@ -188,25 +238,90 @@ public class HemfPlusRegion { public Map> getGenericProperties() { return GenericRecordUtil.getGenericProperties("rect", () -> rect); } + + @Override + public Shape getShape() { + return rect; + } + + @Override + public EmfPlusRegionNodeDataType getGenericRecordType() { + return EmfPlusRegionNodeDataType.RECT; + } } + /** The EmfPlusRegionNode object specifies nodes of a graphics region. */ public static class EmfPlusRegionNode implements EmfPlusRegionNodeData { private EmfPlusRegionNodeData left, right; + private EmfPlusRegionNodeDataType nodeType; @Override public long init(LittleEndianInputStream leis) throws IOException { - long size = readNode(leis, n -> left = n); - size += readNode(leis, n -> right = n); + long size = readNode(leis, this::setLeft); + size += readNode(leis, this::setRight); return size; } + private void setLeft(EmfPlusRegionNodeData left) { + this.left = left; + } + + private void setRight(EmfPlusRegionNodeData right) { + this.right = right; + } + + public EmfPlusRegionNodeData getLeft() { + return left; + } + + public EmfPlusRegionNodeData getRight() { + return right; + } + + public EmfPlusRegionNodeDataType getNodeType() { + return nodeType; + } + + @Override + public void setNodeType(EmfPlusRegionNodeDataType nodeType) { + this.nodeType = nodeType; + } + @Override public Map> getGenericProperties() { return GenericRecordUtil.getGenericProperties( - "left", () -> left, - "right", () -> right + "nodeType", this::getNodeType, + "left", this::getLeft, + "right", this::getRight ); } + + @Override + public Shape getShape() { + boolean com = (nodeType == EmfPlusRegionNodeDataType.COMPLEMENT); + final Shape leftShape = (com ? right : left).getShape(); + final Shape rightShape = (com ? left : right).getShape(); + + if (leftShape == null) { + return rightShape; + } else if (rightShape == null) { + return leftShape; + } + + // TODO: check Area vs. Path manipulation + Area leftArea = new Area(leftShape); + Area rightArea = new Area(rightShape); + + assert(nodeType.operation != null); + nodeType.operation.accept(leftArea, rightArea); + + return leftArea; + } + + @Override + public EmfPlusRegionNodeDataType getGenericRecordType() { + return nodeType; + } } private static long readNode(LittleEndianInputStream leis, Consumer con) throws IOException { @@ -216,6 +331,7 @@ public class HemfPlusRegion { assert(type != null); EmfPlusRegionNodeData nd = type.constructor.get(); con.accept(nd); + nd.setNodeType(type); return LittleEndianConsts.INT_SIZE + nd.init(leis); } } 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 547ba87f4c..ed87177882 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java +++ b/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java @@ -130,14 +130,23 @@ public class HemfPicture implements Iterable, GenericRecord { return new Rectangle2D.Double(x, y, width, height); } + /** + * Return the image bounds in points + * + * @return the image bounds in points + */ + public Rectangle2D getBoundsInPoints() { + return Units.pixelToPoints(getHeader().getBoundsRectangle()); + } + /** * Return the image size in points * * @return the image size in points */ public Dimension2D getSize() { - final Rectangle2D b = getHeader().getBoundsRectangle(); - return Units.pixelToPoints(new Dimension2DDouble(abs(b.getWidth()), abs(b.getHeight()))); + final Rectangle2D b = getBoundsInPoints(); + return new Dimension2DDouble(abs(b.getWidth()), abs(b.getHeight())); } private static double minX(Rectangle2D bounds) { diff --git a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java index ef5f4117d0..d1485c3975 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java @@ -93,12 +93,27 @@ public class HwmfGraphics { } } - protected final List propStack = new LinkedList<>(); + private static final Float[] WEIGHT_MAP = { + 900f, TextAttribute.WEIGHT_ULTRABOLD, + 800f, TextAttribute.WEIGHT_EXTRABOLD, + 750f, TextAttribute.WEIGHT_HEAVY, + 700f, TextAttribute.WEIGHT_BOLD, + 600f, TextAttribute.WEIGHT_DEMIBOLD, + 500f, TextAttribute.WEIGHT_MEDIUM, + 450f, TextAttribute.WEIGHT_SEMIBOLD, + 400f, TextAttribute.WEIGHT_REGULAR, + 300f, TextAttribute.WEIGHT_DEMILIGHT, + 200f, TextAttribute.WEIGHT_LIGHT, + 1f, TextAttribute.WEIGHT_EXTRA_LIGHT + }; + + + private final List propStack = new LinkedList<>(); protected HwmfDrawProperties prop; protected final Graphics2D graphicsCtx; protected final BitSet objectIndexes = new BitSet(); protected final TreeMap objectTable = new TreeMap<>(); - protected final AffineTransform initialAT = new AffineTransform(); + private final AffineTransform initialAT = new AffineTransform(); private static final Charset DEFAULT_CHARSET = LocaleUtil.CHARSET_1252; @@ -207,9 +222,14 @@ public class HwmfGraphics { case BS_DIBPATTERNPT: return getPatternPaint(); case BS_SOLID: return getSolidFill(); case BS_HATCHED: return getHatchedFill(); + case BS_LINEAR_GRADIENT: return getLinearGradient(); } } + protected Paint getLinearGradient() { + return null; + } + protected Paint getSolidFill() { return getProperties().getBrushColor().getColor(); } @@ -440,8 +460,13 @@ public class HwmfGraphics { } int trimLen; - for (trimLen=0; trimLen= WEIGHT_MAP[i]) { + awtFW = WEIGHT_MAP[i+1]; + break; + } + } + as.addAttribute(TextAttribute.WEIGHT, awtFW); } private double getFontHeight(HwmfFont font) { @@ -661,7 +695,11 @@ public class HwmfGraphics { // of the referenced image and can be also negative Composite old = graphicsCtx.getComposite(); graphicsCtx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); - img.drawImage(graphicsCtx, normBounds, getSubImageInsets(srcBounds, img.getNativeBounds())); + + boolean useDeviceBounds = (img instanceof HwmfImageRenderer); + + img.drawImage(graphicsCtx, normBounds, + getSubImageInsets(srcBounds, useDeviceBounds ? img.getNativeBounds() : img.getBounds())); graphicsCtx.setComposite(old); graphicsCtx.setTransform(oldTrans); @@ -683,9 +721,9 @@ public class HwmfGraphics { // Todo: check if we need to normalize srcBounds x/y, in case of flipped images // for now we assume the width/height is positive int left = (int)Math.round((srcBounds.getX()-nativeBounds.getX())/nativeBounds.getWidth()*100_000.); - int top = (int)Math.round((srcBounds.getY()-nativeBounds.getY())/nativeBounds.getWidth()*100_000.); + int top = (int)Math.round((srcBounds.getY()-nativeBounds.getY())/nativeBounds.getHeight()*100_000.); int right = (int)Math.round((nativeBounds.getMaxX()-srcBounds.getMaxX())/nativeBounds.getWidth()*100_000.); - int bottom = (int)Math.round((nativeBounds.getMaxY()-srcBounds.getMaxY())/nativeBounds.getWidth()*100_000.); + int bottom = (int)Math.round((nativeBounds.getMaxY()-srcBounds.getMaxY())/nativeBounds.getHeight()*100_000.); return new Insets(top, left, bottom, right); } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphicsState.java b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphicsState.java new file mode 100644 index 0000000000..e04046f4c2 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphicsState.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.hwmf.draw; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.geom.AffineTransform; + +import org.apache.poi.util.Internal; + +/** + * An util class for saving the state of a {@link java.awt.Graphics2D} object + */ +@Internal +public class HwmfGraphicsState { + private Color background; + private Shape clip; + private Color color; + private Composite composite; + private Font font; + private Paint paint; + private Stroke stroke; + private AffineTransform trans; + + /** + * Saves the state of the graphics2D object + */ + public void backup(Graphics2D graphics2D) { + background = graphics2D.getBackground(); + clip = graphics2D.getClip(); + color = graphics2D.getColor(); + composite = graphics2D.getComposite(); + font = graphics2D.getFont(); + paint = graphics2D.getPaint(); + stroke = graphics2D.getStroke(); + trans = graphics2D.getTransform(); + } + + /** + * Retrieves the state into the graphics2D object + */ + public void restore(Graphics2D graphics2D) { + graphics2D.setBackground(background); + graphics2D.setClip(clip); + graphics2D.setColor(color); + graphics2D.setComposite(composite); + graphics2D.setFont(font); + graphics2D.setPaint(paint); + graphics2D.setStroke(stroke); + graphics2D.setTransform(trans); + } +} diff --git a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfImageRenderer.java b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfImageRenderer.java index 7ac2af934b..68abb2432c 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfImageRenderer.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfImageRenderer.java @@ -21,7 +21,6 @@ import java.awt.Dimension; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.RenderingHints; -import java.awt.Shape; import java.awt.geom.Dimension2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; @@ -114,20 +113,22 @@ public class HwmfImageRenderer implements ImageRenderer, EmbeddedExtractor { return false; } + HwmfGraphicsState graphicsState = new HwmfGraphicsState(); + graphicsState.backup(graphics); + boolean isClipped = true; if (clip == null) { isClipped = false; clip = new Insets(0,0,0,0); } - Shape clipOld = graphics.getClip(); if (isClipped) { graphics.clip(anchor); } image.draw(graphics, getOuterBounds(anchor, clip)); - graphics.setClip(clipOld); + graphicsState.restore(graphics); return true; } @@ -179,4 +180,9 @@ public class HwmfImageRenderer implements ImageRenderer, EmbeddedExtractor { public Rectangle2D getNativeBounds() { return image.getBounds(); } + + @Override + public Rectangle2D getBounds() { + return Units.pointsToPixel(image == null ? new Rectangle2D.Double() : image.getBoundsInPoints()); + } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBrushStyle.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBrushStyle.java index 903ba592ac..2d2c74577c 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBrushStyle.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBrushStyle.java @@ -64,7 +64,11 @@ public enum HwmfBrushStyle { /** * Not supported */ - BS_MONOPATTERN(0x0009); + BS_MONOPATTERN(0x0009), + /** + * (POI arbitrary:) EMF/EMF+ specific value for linear gradient paint + */ + BS_LINEAR_GRADIENT(0x0100); int flag; HwmfBrushStyle(int flag) { diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFont.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFont.java index 14432432db..076fe00f54 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFont.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFont.java @@ -288,7 +288,7 @@ public class HwmfFont implements FontInfo, GenericRecord { * For all height comparisons, the font mapper SHOULD find the largest physical * font that does not exceed the requested size. */ - protected int height; + protected double height; /** * A 16-bit signed integer that defines the average width, in logical units, of @@ -433,7 +433,7 @@ public class HwmfFont implements FontInfo, GenericRecord { facename = "SansSerif"; } - public int getHeight() { + public double getHeight() { return height; } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfRegionMode.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfRegionMode.java index d6bc3fcb07..903cc39eee 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfRegionMode.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfRegionMode.java @@ -45,7 +45,11 @@ public enum HwmfRegionMode { /** * The new clipping region is the current path (or the new region). */ - RGN_COPY(0x05, HwmfRegionMode::copyOp); + RGN_COPY(0x05, HwmfRegionMode::copyOp), + /** + * This is the opposite of {@link #RGN_DIFF}, and only made-up for compatibility with EMF+ + */ + RGN_COMPLEMENT(-1, HwmfRegionMode::complementOp); private final int flag; private final BiFunction op; @@ -125,4 +129,17 @@ public enum HwmfRegionMode { private static Shape copyOp(final Shape oldClip, final Shape newClip) { return (newClip == null || newClip.getBounds2D().isEmpty()) ? null : newClip; } + + private static Shape complementOp(final Shape oldClip, final Shape newClip) { + assert(newClip != null); + if (newClip.getBounds2D().isEmpty()) { + return oldClip; + } else if (oldClip == null) { + return newClip; + } else { + Area newArea = new Area(newClip); + newArea.subtract(new Area(oldClip)); + return newArea; + } + } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java index 533c85a821..f96e02199a 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java @@ -232,7 +232,10 @@ public class HwmfText { @Override public Map> getGenericProperties() { - return GenericRecordUtil.getGenericProperties("text", () -> getText(StandardCharsets.US_ASCII)); + return GenericRecordUtil.getGenericProperties( + "text", () -> getText(StandardCharsets.US_ASCII), + "reference", () -> reference + ); } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java b/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java index c5a4bad522..a33ea0ff1d 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java @@ -36,6 +36,7 @@ import java.util.function.Supplier; import org.apache.poi.common.usermodel.GenericRecord; import org.apache.poi.hwmf.draw.HwmfDrawProperties; import org.apache.poi.hwmf.draw.HwmfGraphics; +import org.apache.poi.hwmf.draw.HwmfGraphicsState; import org.apache.poi.hwmf.record.HwmfHeader; import org.apache.poi.hwmf.record.HwmfPlaceableHeader; import org.apache.poi.hwmf.record.HwmfRecord; @@ -127,8 +128,8 @@ public class HwmfPicture implements Iterable, GenericRecord { } public void draw(Graphics2D ctx, Rectangle2D graphicsBounds) { - final Shape clip = ctx.getClip(); - final AffineTransform at = ctx.getTransform(); + HwmfGraphicsState state = new HwmfGraphicsState(); + state.backup(ctx); try { Rectangle2D wmfBounds = getBounds(); Rectangle2D innerBounds = getInnnerBounds(); @@ -137,13 +138,10 @@ public class HwmfPicture implements Iterable, GenericRecord { } // scale output bounds to image bounds - ctx.translate(graphicsBounds.getX(), graphicsBounds.getY()); - ctx.scale(graphicsBounds.getWidth()/wmfBounds.getWidth(), graphicsBounds.getHeight()/wmfBounds.getHeight()); + ctx.translate(graphicsBounds.getCenterX(), graphicsBounds.getCenterY()); + ctx.scale(graphicsBounds.getWidth()/innerBounds.getWidth(), graphicsBounds.getHeight()/innerBounds.getHeight()); + ctx.translate(-innerBounds.getCenterX(), -innerBounds.getCenterY()); - ctx.translate(-wmfBounds.getX(), -wmfBounds.getY()); - ctx.translate(innerBounds.getCenterX(), innerBounds.getCenterY()); - ctx.scale(wmfBounds.getWidth()/innerBounds.getWidth(), wmfBounds.getHeight()/innerBounds.getHeight()); - ctx.translate(-wmfBounds.getCenterX(), -wmfBounds.getCenterY()); HwmfGraphics g = new HwmfGraphics(ctx, innerBounds); HwmfDrawProperties prop = g.getProperties(); @@ -162,8 +160,7 @@ public class HwmfPicture implements Iterable, GenericRecord { idx++; } } finally { - ctx.setTransform(at); - ctx.setClip(clip); + state.restore(ctx); } } @@ -214,19 +211,30 @@ public class HwmfPicture implements Iterable, GenericRecord { public HwmfHeader getHeader() { return header; } - + /** - * Return the image size in points + * Return the image bound in points * - * @return the image size in points + * @return the image bound in points */ - public Dimension2D getSize() { + public Rectangle2D getBoundsInPoints() { double inch = (placeableHeader == null) ? 1440 : placeableHeader.getUnitsPerInch(); Rectangle2D bounds = getBounds(); - + //coefficient to translate from WMF dpi to 72dpi double coeff = Units.POINT_DPI/inch; - return new Dimension2DDouble(bounds.getWidth()*coeff, bounds.getHeight()*coeff); + return AffineTransform.getScaleInstance(coeff, coeff).createTransformedShape(bounds).getBounds2D(); + } + + + /** + * Return the image size in points + * + * @return the image size in points + */ + public Dimension2D getSize() { + Rectangle2D bounds = getBoundsInPoints(); + return new Dimension2DDouble(bounds.getWidth(), bounds.getHeight()); } public Iterable getEmbeddings() { -- 2.39.5