From 0a84f6881f5a4dc1354274dc49e38d283ca764ad Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Tue, 30 Oct 2018 22:35:45 +0000 Subject: [PATCH] #60656 - Support export file that contains emf and render it correctly git-svn-id: https://svn.apache.org/repos/asf/poi/branches/hemf@1845291 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/hemf/draw/HemfDrawProperties.java | 9 - .../apache/poi/hemf/draw/HemfGraphics.java | 102 ++------ .../apache/poi/hemf/record/emf/HemfDraw.java | 6 +- .../apache/poi/hemf/record/emf/HemfFill.java | 128 +++++----- .../poi/hemf/record/emf/HemfHeader.java | 32 +-- .../apache/poi/hemf/record/emf/HemfText.java | 2 +- .../poi/hemf/record/emf/HemfWindowing.java | 10 +- .../poi/hemf/usermodel/HemfPicture.java | 2 +- .../poi/hwmf/draw/HwmfDrawProperties.java | 22 ++ .../apache/poi/hwmf/draw/HwmfGraphics.java | 218 +++++++++++++++--- .../apache/poi/hwmf/record/HwmfBitmapDib.java | 2 +- .../org/apache/poi/hwmf/record/HwmfDraw.java | 8 + .../org/apache/poi/hwmf/record/HwmfFill.java | 36 ++- .../poi/hwmf/record/HwmfRegionMode.java | 59 +++++ .../org/apache/poi/hwmf/record/HwmfText.java | 4 +- .../apache/poi/hwmf/record/HwmfWindowing.java | 1 + 16 files changed, 410 insertions(+), 231 deletions(-) create mode 100644 src/scratchpad/src/org/apache/poi/hwmf/record/HwmfRegionMode.java 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 f22f5a460f..98b07d4c6b 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/draw/HemfDrawProperties.java +++ b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfDrawProperties.java @@ -26,7 +26,6 @@ public class HemfDrawProperties extends HwmfDrawProperties { /** Path for path bracket operations */ protected Path2D path = null; - protected Shape clip = null; protected boolean usePathBracket = false; @@ -67,12 +66,4 @@ public class HemfDrawProperties extends HwmfDrawProperties { public void setUsePathBracket(boolean usePathBracket) { this.usePathBracket = usePathBracket; } - - public Shape getClip() { - return clip; - } - - public void setClip(Shape shape) { - clip = shape; - } } 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 1d947f24ff..c7e99965f5 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/draw/HemfGraphics.java +++ b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfGraphics.java @@ -22,16 +22,13 @@ import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_SOLID; import java.awt.Color; import java.awt.Graphics2D; -import java.awt.Shape; -import java.awt.geom.AffineTransform; -import java.awt.geom.Area; import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.function.Consumer; -import org.apache.poi.hemf.record.emf.HemfFill; import org.apache.poi.hemf.record.emf.HemfRecord; +import org.apache.poi.hwmf.draw.HwmfDrawProperties; import org.apache.poi.hwmf.draw.HwmfGraphics; import org.apache.poi.hwmf.record.HwmfColorRef; import org.apache.poi.hwmf.record.HwmfObjectTableEntry; @@ -47,36 +44,22 @@ public class HemfGraphics extends HwmfGraphics { private static final HwmfColorRef BLACK = new HwmfColorRef(Color.BLACK); - private final AffineTransform initTrans; - 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); - initTrans = new AffineTransform(graphicsCtx.getTransform()); } @Override public HemfDrawProperties getProperties() { - if (prop == null) { - prop = new HemfDrawProperties(); - } - return (HemfDrawProperties)prop; + return (HemfDrawProperties)super.getProperties(); } @Override - public void saveProperties() { - final HemfDrawProperties oldProp = getProperties(); - oldProp.setClip(graphicsCtx.getClip()); - propStack.add(oldProp); - prop = new HemfDrawProperties(oldProp); - } - - @Override - public void restoreProperties(int index) { - super.restoreProperties(index); - HemfDrawProperties newProp = getProperties(); - graphicsCtx.setClip(newProp.getClip()); + protected HemfDrawProperties newProperties(HwmfDrawProperties oldProps) { + return (oldProps == null) + ? new HemfDrawProperties() + : new HemfDrawProperties((HemfDrawProperties)oldProps); } public void draw(HemfRecord r) { @@ -94,8 +77,12 @@ public class HemfGraphics extends HwmfGraphics { } else { path = new Path2D.Double(); path.setWindingRule(prop.getWindingRule()); + } + + // add dummy move-to at start, to handle invalid emfs not containing that move-to + if (path.getCurrentPoint() == null) { Point2D pnt = prop.getLocation(); - path.moveTo(pnt.getX(),pnt.getY()); + path.moveTo(pnt.getX(), pnt.getY()); } try { @@ -108,7 +95,12 @@ public class HemfGraphics extends HwmfGraphics { pathConsumer.accept(path); } - prop.setLocation(path.getCurrentPoint()); + Point2D curPnt = path.getCurrentPoint(); + if (curPnt == null) { + return; + } + + prop.setLocation(curPnt); if (!useBracket) { switch (fillDraw) { case FILL: @@ -264,64 +256,4 @@ public class HemfGraphics extends HwmfGraphics { break; } } - - /** - * @return the initial AffineTransform, when this graphics context was created - */ - public AffineTransform getInitTransform() { - return new AffineTransform(initTrans); - } - - /** - * @return the current AffineTransform - */ - public AffineTransform getTransform() { - return new AffineTransform(graphicsCtx.getTransform()); - } - - /** - * Set the current AffineTransform - * @param tx the current AffineTransform - */ - public void setTransform(AffineTransform tx) { - graphicsCtx.setTransform(tx); - } - - public void setClip(Shape clip, HemfFill.HemfRegionMode regionMode) { - Shape oldClip = graphicsCtx.getClip(); - - switch (regionMode) { - case RGN_AND: - graphicsCtx.clip(clip); - break; - case RGN_OR: - if (oldClip == null) { - graphicsCtx.setClip(clip); - } else { - Area area = new Area(oldClip); - area.add(new Area(clip)); - graphicsCtx.setClip(area); - } - break; - case RGN_XOR: - if (oldClip == null) { - graphicsCtx.setClip(clip); - } else { - Area area = new Area(oldClip); - area.exclusiveOr(new Area(clip)); - graphicsCtx.setClip(area); - } - break; - case RGN_DIFF: - if (oldClip != null) { - Area area = new Area(oldClip); - area.subtract(new Area(clip)); - graphicsCtx.setClip(area); - } - break; - case RGN_COPY: - graphicsCtx.setClip(clip); - break; - } - } } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfDraw.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfDraw.java index 103f90a052..d18f0d4119 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfDraw.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfDraw.java @@ -1051,7 +1051,11 @@ public class HemfDraw { @Override public void draw(HemfGraphics ctx) { final HemfDrawProperties prop = ctx.getProperties(); - final Path2D path = (Path2D)prop.getPath().clone(); + final Path2D origPath = prop.getPath(); + if (origPath.getCurrentPoint() == null) { + return; + } + final Path2D path = (Path2D)origPath.clone(); path.closePath(); path.setWindingRule(ctx.getProperties().getWindingRule()); ctx.fill(path); 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 7b172d5506..810c9325a3 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 @@ -20,10 +20,11 @@ package org.apache.poi.hemf.record.emf; import static org.apache.poi.hemf.record.emf.HemfDraw.readPointL; import static org.apache.poi.hemf.record.emf.HemfDraw.readRectL; import static org.apache.poi.hemf.record.emf.HemfRecordIterator.HEADER_SIZE; +import static org.apache.poi.hwmf.record.HwmfDraw.boundsToString; +import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Area; -import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.ByteArrayInputStream; @@ -36,11 +37,11 @@ import org.apache.poi.hemf.draw.HemfDrawProperties; import org.apache.poi.hemf.draw.HemfGraphics; import org.apache.poi.hwmf.draw.HwmfGraphics; import org.apache.poi.hwmf.record.HwmfBitmapDib; -import org.apache.poi.hwmf.record.HwmfBrushStyle; import org.apache.poi.hwmf.record.HwmfColorRef; import org.apache.poi.hwmf.record.HwmfDraw; import org.apache.poi.hwmf.record.HwmfFill; import org.apache.poi.hwmf.record.HwmfFill.ColorUsage; +import org.apache.poi.hwmf.record.HwmfRegionMode; import org.apache.poi.hwmf.record.HwmfTernaryRasterOp; import org.apache.poi.util.IOUtils; import org.apache.poi.util.LittleEndian; @@ -50,47 +51,6 @@ import org.apache.poi.util.LittleEndianInputStream; public class HemfFill { private static final int MAX_RECORD_LENGTH = 10_000_000; - public enum HemfRegionMode { - /** - * The new clipping region includes the intersection (overlapping areas) - * of the current clipping region and the current path (or new region). - */ - RGN_AND(0x01), - /** - * The new clipping region includes the union (combined areas) - * of the current clipping region and the current path (or new region). - */ - RGN_OR(0x02), - /** - * The new clipping region includes the union of the current clipping region - * and the current path (or new region) but without the overlapping areas - */ - RGN_XOR(0x03), - /** - * The new clipping region includes the areas of the current clipping region - * with those of the current path (or new region) excluded. - */ - RGN_DIFF(0x04), - /** - * The new clipping region is the current path (or the new region). - */ - RGN_COPY(0x05); - - int flag; - HemfRegionMode(int flag) { - this.flag = flag; - } - - public static HemfRegionMode valueOf(int flag) { - for (HemfRegionMode rm : values()) { - if (rm.flag == flag) return rm; - } - return null; - } - - } - - /** * The EMR_SETPOLYFILLMODE record defines polygon fill mode. */ @@ -105,7 +65,7 @@ public class HemfFill { public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { // A 32-bit unsigned integer that specifies the polygon fill mode and // MUST be in the PolygonFillMode enumeration. - polyfillMode = HwmfPolyfillMode.valueOf((int)leis.readUInt()); + polyFillMode = HwmfPolyfillMode.valueOf((int)leis.readUInt()); return LittleEndianConsts.INT_SIZE; } } @@ -133,7 +93,7 @@ public class HemfFill { * optionally in combination with a brush pattern, according to a specified raster operation, stretching or * compressing the output to fit the dimensions of the destination, if necessary. */ - public static class EmfStretchBlt extends HwmfFill.WmfBitBlt implements HemfRecord { + public static class EmfStretchBlt extends HwmfFill.WmfStretchDib implements HemfRecord { protected final Rectangle2D bounds = new Rectangle2D.Double(); /** An XForm object that specifies a world-space to page-space transform to apply to the source bitmap. */ @@ -142,14 +102,6 @@ public class HemfFill { /** A WMF ColorRef object that specifies the background color of the source bitmap. */ protected final HwmfColorRef bkColorSrc = new HwmfColorRef(); - /** - * A 32-bit unsigned integer that specifies how to interpret values in the color table in - * the source bitmap header. This value MUST be in the DIBColors enumeration - */ - protected int usageSrc; - - protected final HwmfBitmapDib bitmap = new HwmfBitmapDib(); - @Override public HemfRecordType getEmfRecordType() { return HemfRecordType.stretchBlt; @@ -168,7 +120,7 @@ public class HemfFill { // rectangle and optionally a brush pattern, to achieve the final color. int rasterOpIndex = (int)leis.readUInt(); - rasterOperation = HwmfTernaryRasterOp.valueOf(rasterOpIndex); + rasterOperation = HwmfTernaryRasterOp.valueOf(rasterOpIndex >>> 16); size += LittleEndianConsts.INT_SIZE; @@ -179,7 +131,7 @@ public class HemfFill { size += bkColorSrc.init(leis); - usageSrc = (int)leis.readUInt(); + colorUsage = ColorUsage.valueOf((int)leis.readUInt()); // A 32-bit unsigned integer that specifies the offset, in bytes, from the // start of this record to the source bitmap header in the BitmapBuffer field. @@ -188,7 +140,7 @@ public class HemfFill { // A 32-bit unsigned integer that specifies the size, in bytes, of the source bitmap header. final int cbBmiSrc = (int)leis.readUInt(); size += 3*LittleEndianConsts.INT_SIZE; - if (size <= recordSize) { + if (size >= recordSize) { return size; } @@ -198,9 +150,12 @@ public class HemfFill { // A 32-bit unsigned integer that specifies the size, in bytes, of the source bitmap bits. final int cbBitsSrc = (int)leis.readUInt(); - size += 2*LittleEndianConsts.INT_SIZE; + if (size >= recordSize) { + return size; + } + if (srcEqualsDstDimension()) { srcBounds.setRect(srcPnt.getX(), srcPnt.getY(), dstBounds.getWidth(), dstBounds.getHeight()); } else { @@ -219,14 +174,20 @@ public class HemfFill { return false; } + @Override + public void draw(HemfGraphics ctx) { + HemfDrawProperties prop = ctx.getProperties(); + prop.setBackgroundColor(this.bkColorSrc); + super.draw(ctx); + } + @Override public String toString() { return - "{ bounds: { x: "+bounds.getX()+", y: "+bounds.getY()+", w: "+bounds.getWidth()+", h: "+bounds.getHeight()+"}"+ + "{ bounds: "+boundsToString(bounds)+ ", xFormSrc: { scaleX: "+xFormSrc.getScaleX()+", shearX: "+xFormSrc.getShearX()+", transX: "+xFormSrc.getTranslateX()+", scaleY: "+xFormSrc.getScaleY()+", shearY: "+xFormSrc.getShearY()+", transY: "+xFormSrc.getTranslateY()+" }"+ ", bkColorSrc: "+bkColorSrc+ - ", usageSrc: "+usageSrc+", " - + super.toString().substring(1); + ","+super.toString().substring(1); } } @@ -279,7 +240,8 @@ public class HemfFill { // These codes define how the color data of the source rectangle is to be combined with the color data // of the destination rectangle and optionally a brush pattern, to achieve the final color. // The value MUST be in the WMF Ternary Raster Operation enumeration - rasterOperation = HwmfTernaryRasterOp.valueOf(leis.readInt()); + int rasterOpIndex = (int)leis.readUInt(); + rasterOperation = HwmfTernaryRasterOp.valueOf(rasterOpIndex >>> 16); // A 32-bit signed integer that specifies the logical width of the destination rectangle. int cxDest = leis.readInt(); @@ -291,7 +253,7 @@ public class HemfFill { size += 8*LittleEndianConsts.INT_SIZE; - size += readBitmap(leis, dib, startIdx, offBmiSrc, cbBmiSrc, offBitsSrc, cbBitsSrc); + size += readBitmap(leis, bitmap, startIdx, offBmiSrc, cbBmiSrc, offBitsSrc, cbBitsSrc); return size; } @@ -346,7 +308,7 @@ public class HemfFill { ctx.fill(getShape()); } - protected Area getShape() { + protected Shape getShape() { return getRgnShape(rgnRects); } } @@ -371,7 +333,7 @@ public class HemfFill { return size; } - protected Area getShape() { + protected Shape getShape() { return getRgnShape(rgnRects); } } @@ -408,13 +370,13 @@ public class HemfFill { return size; } - protected Area getShape() { + protected Shape getShape() { return getRgnShape(rgnRects); } } public static class EmfExtSelectClipRgn implements HemfRecord { - protected HemfRegionMode regionMode; + protected HwmfRegionMode regionMode; protected final List rgnRects = new ArrayList<>(); @Override @@ -427,20 +389,43 @@ public class HemfFill { // A 32-bit unsigned integer that specifies the size of region data in bytes long rgnDataSize = leis.readUInt(); // A 32-bit unsigned integer that specifies the way to use the region. - regionMode = HemfRegionMode.valueOf((int)leis.readUInt()); + regionMode = HwmfRegionMode.valueOf((int)leis.readUInt()); long size = 2* LittleEndianConsts.INT_SIZE; // If RegionMode is RGN_COPY, this data can be omitted and the clip region // SHOULD be set to the default (NULL) clip region. - if (regionMode != HemfRegionMode.RGN_COPY) { + if (regionMode != HwmfRegionMode.RGN_COPY) { size += readRgnData(leis, rgnRects); } return size; } - protected Area getShape() { + protected Shape getShape() { return getRgnShape(rgnRects); } + + @Override + public void draw(HemfGraphics ctx) { + HemfDrawProperties prop = ctx.getProperties(); + ctx.setClip(getShape(), regionMode, true); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("{ regionMode: '"+regionMode+"'"); + sb.append(", regions: ["); + boolean isFirst = true; + for (Rectangle2D r : rgnRects) { + if (!isFirst) { + sb.append(","); + } + isFirst = false; + sb.append(boundsToString(r)); + } + sb.append("]}"); + return sb.toString(); + } } public static class EmfAlphaBlend implements HemfRecord { @@ -717,7 +702,10 @@ public class HemfFill { return 6 * LittleEndian.INT_SIZE; } - protected static Area getRgnShape(List rgnRects) { + protected static Shape getRgnShape(List rgnRects) { + if (rgnRects.size() == 1) { + return rgnRects.get(0); + } final Area frame = new Area(); rgnRects.forEach((rct) -> frame.add(new Area(rct))); return frame; diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfHeader.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfHeader.java index 79e3b4943c..4b01a5a256 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfHeader.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfHeader.java @@ -20,6 +20,8 @@ package org.apache.poi.hemf.record.emf; import static org.apache.poi.hemf.record.emf.HemfDraw.readDimensionInt; import static org.apache.poi.hemf.record.emf.HemfDraw.readRectL; import static org.apache.poi.hemf.record.emf.HemfRecordIterator.HEADER_SIZE; +import static org.apache.poi.hwmf.record.HwmfDraw.boundsToString; +import static org.apache.poi.hwmf.record.HwmfDraw.dimToString; import java.awt.geom.Dimension2D; import java.awt.geom.Rectangle2D; @@ -119,21 +121,21 @@ public class HemfHeader implements HemfRecord { @Override public String toString() { return "HemfHeader{" + - "boundsRectangle=" + boundsRectangle + - ", frameRectangle=" + frameRectangle + - ", bytes=" + bytes + - ", records=" + records + - ", handles=" + handles + - ", description=" + description + - ", nPalEntries=" + nPalEntries + - ", hasExtension1=" + hasExtension1 + - ", cbPixelFormat=" + cbPixelFormat + - ", offPixelFormat=" + offPixelFormat + - ", bOpenGL=" + bOpenGL + - ", hasExtension2=" + hasExtension2 + - ", deviceDimension=" + deviceDimension + - ", microDimension=" + microDimension + - ", milliDimension=" + milliDimension + + "boundsRectangle: " + boundsToString(boundsRectangle) + + ", frameRectangle: " + boundsToString(frameRectangle) + + ", bytes: " + bytes + + ", records: " + records + + ", handles: " + handles + + ", description: '" + description + "'" + + ", nPalEntries: " + nPalEntries + + ", hasExtension1: " + hasExtension1 + + ", cbPixelFormat: " + cbPixelFormat + + ", offPixelFormat: " + offPixelFormat + + ", bOpenGL: " + bOpenGL + + ", hasExtension2: " + hasExtension2 + + ", deviceDimension: " + dimToString(deviceDimension) + + ", microDimension: " + dimToString(microDimension) + + ", milliDimension: " + dimToString(milliDimension) + '}'; } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfText.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfText.java index 9f36812179..4a8508961b 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfText.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfText.java @@ -191,7 +191,7 @@ public class HemfText { @Override public void draw(HwmfGraphics ctx) { - ctx.drawString(rawTextBytes, reference, bounds, dx, isUnicode()); + ctx.drawString(rawTextBytes, reference, bounds, options, dx, isUnicode()); } @Override 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 294bfdcb99..955bbd9f03 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 @@ -24,7 +24,7 @@ import java.io.IOException; import org.apache.poi.hemf.draw.HemfDrawProperties; import org.apache.poi.hemf.draw.HemfGraphics; -import org.apache.poi.hemf.record.emf.HemfFill.HemfRegionMode; +import org.apache.poi.hwmf.record.HwmfRegionMode; import org.apache.poi.hwmf.record.HwmfWindowing; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; @@ -188,7 +188,7 @@ public class HemfWindowing { * device context, combining the new region with any existing clipping region using the specified mode. */ public static class EmfSelectClipPath implements HemfRecord { - protected HemfRegionMode regionMode; + protected HwmfRegionMode regionMode; @Override public HemfRecordType getEmfRecordType() { @@ -199,15 +199,15 @@ public class HemfWindowing { public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { // A 32-bit unsigned integer that specifies the way to use the path. // The value MUST be in the RegionMode enumeration - regionMode = HemfRegionMode.valueOf(leis.readInt()); + regionMode = HwmfRegionMode.valueOf(leis.readInt()); return LittleEndianConsts.INT_SIZE; } @Override public void draw(HemfGraphics ctx) { - HemfDrawProperties props = ctx.getProperties(); - ctx.setClip(props.getPath(), regionMode); + HemfDrawProperties prop = ctx.getProperties(); + ctx.setClip(prop.getPath(), regionMode, false); } @Override 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 b8e863a85a..8d49fdd0e1 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java +++ b/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java @@ -113,7 +113,7 @@ public class HemfPicture implements Iterable { height = 100; } - return new Dimension2DDouble(width*coeff, height*coeff); + return new Dimension2DDouble(Math.abs(width*coeff), Math.abs(height*coeff)); } public void draw(Graphics2D ctx, Rectangle2D graphicsBounds) { diff --git a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfDrawProperties.java b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfDrawProperties.java index 176c8e1bce..9a58e57014 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfDrawProperties.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfDrawProperties.java @@ -19,6 +19,7 @@ package org.apache.poi.hwmf.draw; import java.awt.Color; import java.awt.Shape; +import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.geom.Path2D; import java.awt.geom.Point2D; @@ -67,6 +68,8 @@ public class HwmfDrawProperties { private HwmfTextAlignment textAlignAsian; private HwmfTextVerticalAlignment textVAlignAsian; private HwmfTernaryRasterOp rasterOp; + protected Shape clip; + protected final AffineTransform transform = new AffineTransform(); public HwmfDrawProperties() { window = new Rectangle2D.Double(0, 0, 1, 1); @@ -89,6 +92,7 @@ public class HwmfDrawProperties { textAlignAsian = HwmfTextAlignment.RIGHT; textVAlignAsian = HwmfTextVerticalAlignment.TOP; rasterOp = HwmfTernaryRasterOp.PATCOPY; + clip = null; } public HwmfDrawProperties(HwmfDrawProperties other) { @@ -126,6 +130,8 @@ public class HwmfDrawProperties { this.textAlignAsian = other.textAlignAsian; this.textVAlignAsian = other.textVAlignAsian; this.rasterOp = other.rasterOp; + this.transform.setTransform(other.transform); + this.clip = other.clip; } public void setViewportExt(double width, double height) { @@ -387,4 +393,20 @@ public class HwmfDrawProperties { public void setRasterOp(HwmfTernaryRasterOp rasterOp) { this.rasterOp = rasterOp; } + + public AffineTransform getTransform() { + return transform; + } + + public void setTransform(AffineTransform transform) { + this.transform.setTransform(transform); + } + + public Shape getClip() { + return clip; + } + + public void setClip(Shape clip) { + this.clip = clip; + } } 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 0038298946..9ba07dd006 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java @@ -29,6 +29,7 @@ import java.awt.font.FontRenderContext; import java.awt.font.TextAttribute; import java.awt.font.TextLayout; import java.awt.geom.AffineTransform; +import java.awt.geom.Area; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; @@ -50,7 +51,9 @@ import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode; import org.apache.poi.hwmf.record.HwmfObjectTableEntry; import org.apache.poi.hwmf.record.HwmfPenStyle; import org.apache.poi.hwmf.record.HwmfPenStyle.HwmfLineDash; +import org.apache.poi.hwmf.record.HwmfRegionMode; import org.apache.poi.hwmf.record.HwmfText; +import org.apache.poi.hwmf.record.HwmfText.WmfExtTextOutOptions; import org.apache.poi.sl.draw.DrawFactory; import org.apache.poi.sl.draw.DrawFontManager; import org.apache.poi.util.LocaleUtil; @@ -66,11 +69,12 @@ public class HwmfGraphics { protected final Graphics2D graphicsCtx; protected final BitSet objectIndexes = new BitSet(); protected final TreeMap objectTable = new TreeMap<>(); + protected final AffineTransform initialAT = new AffineTransform(); + private static final Charset DEFAULT_CHARSET = LocaleUtil.CHARSET_1252; /** Bounding box from the placeable header */ private final Rectangle2D bbox; - private final AffineTransform initialAT; /** * Initialize a graphics context for wmf rendering @@ -81,16 +85,20 @@ public class HwmfGraphics { public HwmfGraphics(Graphics2D graphicsCtx, Rectangle2D bbox) { this.graphicsCtx = graphicsCtx; this.bbox = (Rectangle2D)bbox.clone(); - this.initialAT = graphicsCtx.getTransform(); + this.initialAT.setTransform(graphicsCtx.getTransform()); } public HwmfDrawProperties getProperties() { if (prop == null) { - prop = new HwmfDrawProperties(); + prop = newProperties(null); } return prop; } + protected HwmfDrawProperties newProperties(HwmfDrawProperties oldProps) { + return (oldProps == null) ? new HwmfDrawProperties() : new HwmfDrawProperties(oldProps); + } + public void draw(Shape shape) { HwmfPenStyle ps = getProperties().getPenStyle(); if (ps == null) { @@ -119,14 +127,18 @@ public class HwmfGraphics { } public void fill(Shape shape) { - if (getProperties().getBrushStyle() != HwmfBrushStyle.BS_NULL) { -// GeneralPath gp = new GeneralPath(shape); -// gp.setWindingRule(getProperties().getPolyfillMode().awtFlag); + HwmfDrawProperties prop = getProperties(); + if (prop.getBrushStyle() != HwmfBrushStyle.BS_NULL) { + if (prop.getBkMode() == HwmfBkMode.OPAQUE) { + graphicsCtx.setPaint(prop.getBackgroundColor().getColor()); + graphicsCtx.fill(shape); + } + graphicsCtx.setPaint(getFill()); graphicsCtx.fill(shape); } - draw(shape); +// draw(shape); } protected BasicStroke getStroke() { @@ -264,8 +276,10 @@ public class HwmfGraphics { public void saveProperties() { final HwmfDrawProperties p = getProperties(); assert(p != null); + p.setTransform(graphicsCtx.getTransform()); + p.setClip(graphicsCtx.getClip()); propStack.add(p); - prop = new HwmfDrawProperties(p); + prop = newProperties(p); } /** @@ -291,7 +305,16 @@ public class HwmfGraphics { // roll to last when curIdx == 0 stackIndex = propStack.size()-1; } - prop = propStack.get(stackIndex); + + // The playback device context is restored by popping state information off a stack that was created by + // prior SAVEDC records + // ... so because being a stack, we will remove all entries having a greater index than the stackIndex + for (int i=propStack.size()-1; i>=stackIndex; i--) { + prop = propStack.remove(i); + } + + graphicsCtx.setTransform(prop.getTransform()); + graphicsCtx.setClip(prop.getClip()); } /** @@ -338,13 +361,14 @@ public class HwmfGraphics { } } - public void drawString(byte[] text, Point2D reference, Rectangle2D clip) { - drawString(text, reference, clip, null, false); + public void drawString(byte[] text, Point2D reference) { + drawString(text, reference, null, null, null, false); } - public void drawString(byte[] text, Point2D reference, Rectangle2D clip, List dx, boolean isUnicode) { + public void drawString(byte[] text, Point2D reference, Rectangle2D clip, WmfExtTextOutOptions opts, List dx, boolean isUnicode) { + final HwmfDrawProperties prop = getProperties(); - HwmfFont font = getProperties().getFont(); + HwmfFont font = prop.getFont(); if (font == null || text == null || text.length == 0) { return; } @@ -446,21 +470,31 @@ public class HwmfGraphics { Point2D dst = new Point2D.Double(); tx.transform(src, dst); - // TODO: implement clipping on bounds + final Shape clipShape = graphicsCtx.getClip(); final AffineTransform at = graphicsCtx.getTransform(); try { + if (clip != null) { + graphicsCtx.translate(-clip.getCenterX(), -clip.getCenterY()); + graphicsCtx.rotate(angle); + graphicsCtx.translate(clip.getCenterX(), clip.getCenterY()); + if (prop.getBkMode() == HwmfBkMode.OPAQUE && opts.isOpaque()) { + graphicsCtx.setPaint(prop.getBackgroundColor().getColor()); + graphicsCtx.fill(clip); + } + if (opts.isClipped()) { + graphicsCtx.setClip(clip); + } + graphicsCtx.setTransform(at); + } + graphicsCtx.translate(reference.getX(), reference.getY()); graphicsCtx.rotate(angle); graphicsCtx.translate(dst.getX(), dst.getY()); - if (getProperties().getBkMode() == HwmfBkMode.OPAQUE && clip != null) { - // TODO: validate bounds - graphicsCtx.setBackground(getProperties().getBackgroundColor().getColor()); - graphicsCtx.fill(new Rectangle2D.Double(0, 0, clip.getWidth(), clip.getHeight())); - } - graphicsCtx.setColor(getProperties().getTextColor().getColor()); + graphicsCtx.setColor(prop.getTextColor().getColor()); graphicsCtx.drawString(as.getIterator(), 0, 0); } finally { graphicsCtx.setTransform(at); + graphicsCtx.setClip(clipShape); } } @@ -498,18 +532,136 @@ public class HwmfGraphics { } public void drawImage(BufferedImage img, Rectangle2D srcBounds, Rectangle2D dstBounds) { - // prop.getRasterOp(); - graphicsCtx.drawImage(img, - (int)dstBounds.getX(), - (int)dstBounds.getY(), - (int)(dstBounds.getX()+dstBounds.getWidth()), - (int)(dstBounds.getY()+dstBounds.getHeight()), - (int)srcBounds.getX(), - (int)srcBounds.getY(), - (int)(srcBounds.getX()+srcBounds.getWidth()), - (int)(srcBounds.getY()+srcBounds.getHeight()), - null, // getProperties().getBackgroundColor().getColor(), - null - ); + HwmfDrawProperties prop = getProperties(); + + // handle raster op + // currently the raster op as described in https://docs.microsoft.com/en-us/windows/desktop/gdi/ternary-raster-operations + // are not supported, as we would need to extract the destination image area from the underlying buffered image + // and therefore would make it mandatory that the graphics context must be from a buffered image + // furthermore I doubt the purpose of bitwise image operations on non-black/white images + switch (prop.getRasterOp()) { + case D: + // keep destination, i.e. do nothing + break; + case PATCOPY: + graphicsCtx.setPaint(getFill()); + graphicsCtx.fill(dstBounds); + break; + case BLACKNESS: + graphicsCtx.setPaint(Color.BLACK); + graphicsCtx.fill(dstBounds); + break; + case WHITENESS: + graphicsCtx.setPaint(Color.WHITE); + graphicsCtx.fill(dstBounds); + break; + default: + case SRCCOPY: + final Shape clip = graphicsCtx.getClip(); + + // add clipping in case of a source subimage, i.e. a clipped source image + // some dstBounds are horizontal or vertical flipped, so we need to normalize the images + Rectangle2D normalized = new Rectangle2D.Double( + dstBounds.getWidth() >= 0 ? dstBounds.getMinX() : dstBounds.getMaxX(), + dstBounds.getHeight() >= 0 ? dstBounds.getMinY() : dstBounds.getMaxY(), + Math.abs(dstBounds.getWidth()), + Math.abs(dstBounds.getHeight())); + graphicsCtx.clip(normalized); + final AffineTransform at = graphicsCtx.getTransform(); + + final Rectangle2D imgBounds = new Rectangle2D.Double(0,0,img.getWidth(),img.getHeight()); + final boolean isImgBounds = (srcBounds.equals(new Rectangle2D.Double())); + final Rectangle2D srcBounds2 = isImgBounds ? imgBounds : srcBounds; + + // TODO: apply emf transform + graphicsCtx.translate(dstBounds.getX(), dstBounds.getY()); + graphicsCtx.scale(dstBounds.getWidth()/srcBounds2.getWidth(), dstBounds.getHeight()/srcBounds2.getHeight()); + graphicsCtx.translate(-srcBounds2.getX(), -srcBounds2.getY()); + + graphicsCtx.drawImage(img, 0, 0, prop.getBackgroundColor().getColor(), null); + + graphicsCtx.setTransform(at); + graphicsCtx.setClip(clip); + break; + } + + } + + /** + * @return the initial AffineTransform, when this graphics context was created + */ + public AffineTransform getInitTransform() { + return new AffineTransform(initialAT); + } + + /** + * @return the current AffineTransform + */ + public AffineTransform getTransform() { + return new AffineTransform(graphicsCtx.getTransform()); + } + + /** + * Set the current AffineTransform + * @param tx the current AffineTransform + */ + public void setTransform(AffineTransform tx) { + graphicsCtx.setTransform(tx); + } + + private static int clipCnt = 0; + + public void setClip(Shape clip, HwmfRegionMode regionMode, boolean useInitialAT) { + final AffineTransform at = graphicsCtx.getTransform(); + if (useInitialAT) { + graphicsCtx.setTransform(initialAT); + } + final Shape oldClip = graphicsCtx.getClip(); + final boolean isEmpty = clip.getBounds2D().isEmpty(); + switch (regionMode) { + case RGN_AND: + if (!isEmpty) { + graphicsCtx.clip(clip); + } + break; + case RGN_OR: + if (!isEmpty) { + if (oldClip == null) { + graphicsCtx.setClip(clip); + } else { + Area area = new Area(oldClip); + area.add(new Area(clip)); + graphicsCtx.setClip(area); + } + } + break; + case RGN_XOR: + if (!isEmpty) { + if (oldClip == null) { + graphicsCtx.setClip(clip); + } else { + Area area = new Area(oldClip); + area.exclusiveOr(new Area(clip)); + graphicsCtx.setClip(area); + } + } + break; + case RGN_DIFF: + if (!isEmpty) { + if (oldClip != null) { + Area area = new Area(oldClip); + area.subtract(new Area(clip)); + graphicsCtx.setClip(area); + } + } + break; + case RGN_COPY: { + graphicsCtx.setClip(isEmpty ? null : clip); + break; + } + } + if (useInitialAT) { + graphicsCtx.setTransform(at); + } } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java index e6a8e302a3..1a7e33ec11 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java @@ -413,7 +413,7 @@ public class HwmfBitmapDib { return new ByteArrayInputStream(getBMPData()); } - private byte[] getBMPData() { + public byte[] getBMPData() { if (imageData == null) { throw new RecordFormatException("bitmap not initialized ... need to call init() before"); } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfDraw.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfDraw.java index 107d440499..f3507fa103 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfDraw.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfDraw.java @@ -20,6 +20,7 @@ package org.apache.poi.hwmf.record; import java.awt.Shape; import java.awt.geom.Arc2D; import java.awt.geom.Area; +import java.awt.geom.Dimension2D; import java.awt.geom.Ellipse2D; import java.awt.geom.Line2D; import java.awt.geom.Path2D; @@ -744,4 +745,11 @@ public class HwmfDraw { public static String boundsToString(Rectangle2D bounds) { return "{ x: "+bounds.getX()+", y: "+bounds.getY()+", w: "+bounds.getWidth()+", h: "+bounds.getHeight()+" }"; } + + @Internal + public static String dimToString(Dimension2D dim) { + return "{ w: "+dim.getWidth()+", h: "+dim.getHeight()+" }"; + } + + } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java index e335db4577..131699f6b3 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java @@ -27,6 +27,7 @@ import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.IOException; +import org.apache.poi.hwmf.draw.HwmfDrawProperties; import org.apache.poi.hwmf.draw.HwmfGraphics; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; @@ -222,7 +223,7 @@ public class HwmfFill { * An unsigned integer that defines polygon fill mode. * This MUST be one of the values: ALTERNATE = 0x0001, WINDING = 0x0002 */ - protected HwmfPolyfillMode polyfillMode; + protected HwmfPolyfillMode polyFillMode; @Override public HwmfRecordType getWmfRecordType() { @@ -231,13 +232,18 @@ public class HwmfFill { @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { - polyfillMode = HwmfPolyfillMode.valueOf(leis.readUShort() & 3); + polyFillMode = HwmfPolyfillMode.valueOf(leis.readUShort() & 3); return LittleEndianConsts.SHORT_SIZE; } @Override public void draw(HwmfGraphics ctx) { - ctx.getProperties().setPolyfillMode(polyfillMode); + ctx.getProperties().setPolyfillMode(polyFillMode); + } + + @Override + public String toString() { + return "{ polyFillMode: '"+ polyFillMode +"' }"; } } @@ -458,7 +464,7 @@ public class HwmfFill { * A variable-sized DeviceIndependentBitmap Object (section 2.2.2.9) that is the * source of the color data. */ - protected final HwmfBitmapDib dib = new HwmfBitmapDib(); + protected final HwmfBitmapDib bitmap = new HwmfBitmapDib(); @Override public HwmfRecordType getWmfRecordType() { @@ -481,22 +487,36 @@ public class HwmfFill { size += readBounds2(leis, srcBounds); size += readBounds2(leis, dstBounds); - size += dib.init(leis, (int)(recordSize-6-size)); + size += bitmap.init(leis, (int)(recordSize-6-size)); return size; } @Override public void draw(HwmfGraphics ctx) { - if (dib.isValid()) { - ctx.getProperties().setRasterOp(rasterOperation); + HwmfDrawProperties prop = ctx.getProperties(); + prop.setRasterOp(rasterOperation); + if (bitmap.isValid()) { ctx.drawImage(getImage(), srcBounds, dstBounds); + } else { + BufferedImage bi = new BufferedImage((int)dstBounds.getWidth(), (int)dstBounds.getHeight(), BufferedImage.TYPE_INT_ARGB); + ctx.drawImage(bi, dstBounds, dstBounds); } } @Override public BufferedImage getImage() { - return dib.getImage(); + return bitmap.getImage(); + } + + @Override + public String toString() { + return + "{ rasterOperation: '"+rasterOperation+"'"+ + ", colorUsage: '"+colorUsage+"'"+ + ", srcBounds: "+boundsToString(srcBounds)+ + ", dstBounds: "+boundsToString(dstBounds)+ + "}"; } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfRegionMode.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfRegionMode.java new file mode 100644 index 0000000000..3a56e52e70 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfRegionMode.java @@ -0,0 +1,59 @@ +/* ==================================================================== + 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.record; + +import org.apache.poi.hemf.record.emf.HemfFill; + +public enum HwmfRegionMode { + /** + * The new clipping region includes the intersection (overlapping areas) + * of the current clipping region and the current path (or new region). + */ + RGN_AND(0x01), + /** + * The new clipping region includes the union (combined areas) + * of the current clipping region and the current path (or new region). + */ + RGN_OR(0x02), + /** + * The new clipping region includes the union of the current clipping region + * and the current path (or new region) but without the overlapping areas + */ + RGN_XOR(0x03), + /** + * The new clipping region includes the areas of the current clipping region + * with those of the current path (or new region) excluded. + */ + RGN_DIFF(0x04), + /** + * The new clipping region is the current path (or the new region). + */ + RGN_COPY(0x05); + + int flag; + HwmfRegionMode(int flag) { + this.flag = flag; + } + + public static HwmfRegionMode valueOf(int flag) { + for (HwmfRegionMode rm : values()) { + if (rm.flag == flag) return rm; + } + return null; + } +} 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 024e1dedf2..f7708ab25c 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java @@ -191,7 +191,7 @@ public class HwmfText { @Override public void draw(HwmfGraphics ctx) { - ctx.drawString(getTextBytes(), reference, null); + ctx.drawString(getTextBytes(), reference); } public String getText(Charset charset) { @@ -396,7 +396,7 @@ public class HwmfText { @Override public void draw(HwmfGraphics ctx) { - ctx.drawString(rawTextBytes, reference, bounds, dx, false); + ctx.drawString(rawTextBytes, reference, bounds, options, dx, false); } public String getText(Charset charset) throws IOException { diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfWindowing.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfWindowing.java index 88915f8ad2..a3fb60bd1d 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfWindowing.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfWindowing.java @@ -433,6 +433,7 @@ public class HwmfWindowing { @Override public void applyObject(HwmfGraphics ctx) { + ctx.setClip(bounds, HwmfRegionMode.RGN_AND, true); } @Override