From 27c20b0e9a896d1b3dff0c17ecdc2c4a8f69e4fc Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Thu, 6 Jun 2019 22:32:41 +0000 Subject: [PATCH] Bug 60656 - EMF image support in slideshows git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1860732 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/hemf/draw/HemfDrawProperties.java | 24 +- .../apache/poi/hemf/draw/HemfGraphics.java | 90 ++++- .../poi/hemf/record/emf/HemfComment.java | 16 + .../apache/poi/hemf/record/emf/HemfDraw.java | 14 + .../apache/poi/hemf/record/emf/HemfFill.java | 7 +- .../apache/poi/hemf/record/emf/HemfMisc.java | 61 ++- .../hemf/record/emfplus/HemfPlusBrush.java | 237 +++++++++++- .../poi/hemf/record/emfplus/HemfPlusDraw.java | 225 +++++++++-- .../poi/hemf/record/emfplus/HemfPlusFont.java | 16 +- .../hemf/record/emfplus/HemfPlusHeader.java | 22 ++ .../hemf/record/emfplus/HemfPlusImage.java | 218 ++++++++++- .../poi/hemf/record/emfplus/HemfPlusMisc.java | 30 +- .../hemf/record/emfplus/HemfPlusObject.java | 58 ++- .../poi/hemf/record/emfplus/HemfPlusPath.java | 45 ++- .../poi/hemf/record/emfplus/HemfPlusPen.java | 353 +++++++++++------- .../hemf/record/emfplus/HemfPlusRecord.java | 11 +- .../hemf/record/emfplus/HemfPlusRegion.java | 14 +- .../hemf/usermodel/HemfEmbeddedIterator.java | 31 +- .../poi/hemf/usermodel/HemfPicture.java | 41 +- .../apache/poi/hwmf/draw/HwmfGraphics.java | 22 +- .../org/apache/poi/hwmf/record/HwmfDraw.java | 8 +- .../poi/hemf/usermodel/HemfPictureTest.java | 54 ++- 22 files changed, 1296 insertions(+), 301 deletions(-) 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 98b07d4c6b..2a05b16080 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/draw/HemfDrawProperties.java +++ b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfDrawProperties.java @@ -17,9 +17,10 @@ package org.apache.poi.hemf.draw; -import java.awt.Shape; import java.awt.geom.Path2D; +import java.awt.image.BufferedImage; +import org.apache.poi.hemf.record.emfplus.HemfPlusBrush.EmfPlusHatchStyle; import org.apache.poi.hwmf.draw.HwmfDrawProperties; public class HemfDrawProperties extends HwmfDrawProperties { @@ -27,6 +28,8 @@ public class HemfDrawProperties extends HwmfDrawProperties { /** Path for path bracket operations */ protected Path2D path = null; protected boolean usePathBracket = false; + private EmfPlusHatchStyle emfPlusBrushHatch; + private BufferedImage emfPlusImage; public HemfDrawProperties() { @@ -35,8 +38,11 @@ public class HemfDrawProperties extends HwmfDrawProperties { public HemfDrawProperties(HemfDrawProperties other) { super(other); path = (other.path != null) ? (Path2D)other.path.clone() : null; + usePathBracket = other.usePathBracket; + emfPlusBrushHatch = other.emfPlusBrushHatch; // TODO: check how to clone clip = other.clip; + emfPlusImage = other.emfPlusImage; } /** @@ -66,4 +72,20 @@ public class HemfDrawProperties extends HwmfDrawProperties { public void setUsePathBracket(boolean usePathBracket) { this.usePathBracket = usePathBracket; } + + public EmfPlusHatchStyle getEmfPlusBrushHatch() { + return emfPlusBrushHatch; + } + + public void setEmfPlusBrushHatch(EmfPlusHatchStyle emfPlusBrushHatch) { + this.emfPlusBrushHatch = emfPlusBrushHatch; + } + + public BufferedImage getEmfPlusImage() { + return emfPlusImage; + } + + public void setEmfPlusImage(BufferedImage emfPlusImage) { + this.emfPlusImage = emfPlusImage; + } } 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 c7e99965f5..10e4276533 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/draw/HemfGraphics.java +++ b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfGraphics.java @@ -22,12 +22,15 @@ import static org.apache.poi.hwmf.record.HwmfBrushStyle.BS_SOLID; import java.awt.Color; import java.awt.Graphics2D; +import java.awt.Paint; 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.HemfComment.EmfComment; 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.HwmfColorRef; @@ -37,12 +40,20 @@ import org.apache.poi.util.Internal; public class HemfGraphics extends HwmfGraphics { + public enum EmfRenderState { + INITIAL, + EMF_ONLY, + EMFPLUS_ONLY, + EMF_DCONTEXT + } + private static final HwmfColorRef WHITE = new HwmfColorRef(Color.WHITE); private static final HwmfColorRef LTGRAY = new HwmfColorRef(new Color(0x00C0C0C0)); private static final HwmfColorRef GRAY = new HwmfColorRef(new Color(0x00808080)); private static final HwmfColorRef DKGRAY = new HwmfColorRef(new Color(0x00404040)); private static final HwmfColorRef BLACK = new HwmfColorRef(Color.BLACK); + private EmfRenderState renderState = EmfRenderState.INITIAL; public HemfGraphics(Graphics2D graphicsCtx, Rectangle2D bbox) { super(graphicsCtx,bbox); @@ -62,8 +73,49 @@ public class HemfGraphics extends HwmfGraphics { : new HemfDrawProperties((HemfDrawProperties)oldProps); } + public EmfRenderState getRenderState() { + return renderState; + } + + public void setRenderState(EmfRenderState renderState) { + this.renderState = renderState; + } + public void draw(HemfRecord r) { - r.draw(this); + switch (renderState) { + case EMF_DCONTEXT: + // keep the dcontext state, if the next record is an EMF+ record + // only reset it, when we are processing EMF records again + if (!(r instanceof EmfComment)) { + renderState = EmfRenderState.INITIAL; + } + r.draw(this); + break; + case INITIAL: + r.draw(this); + break; + case EMF_ONLY: + case EMFPLUS_ONLY: + if ((r instanceof EmfComment) == (renderState == EmfRenderState.EMFPLUS_ONLY)) { + r.draw(this); + } + break; + default: + break; + } + } + + public void draw(HemfPlusRecord r) { + switch (renderState) { + case EMFPLUS_ONLY: + case EMF_DCONTEXT: + case INITIAL: + r.draw(this); + break; + case EMF_ONLY: + default: + break; + } } @Internal @@ -131,14 +183,36 @@ public class HemfGraphics extends HwmfGraphics { * @see HwmfGraphics#addObjectTableEntry(HwmfObjectTableEntry) */ public void addObjectTableEntry(HwmfObjectTableEntry entry, int index) { - if (index < 1) { - throw new IndexOutOfBoundsException("Object table entry index in EMF must be > 0 - invalid index: "+index); - } - + checkTableEntryIndex(index); objectIndexes.set(index); objectTable.put(index, entry); } + /** + * Gets a record which was registered earliser + * @param index the record index + * @return the record or {@code null} if it doesn't exist + */ + public HwmfObjectTableEntry getObjectTableEntry(int index) { + checkTableEntryIndex(index); + return objectTable.get(index); + } + + private void checkTableEntryIndex(int index) { + if (renderState != EmfRenderState.EMFPLUS_ONLY) { + // 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); + } + } + } + + @Override public void applyObjectTableEntry(int index) { if ((index & 0x80000000) != 0) { @@ -256,4 +330,10 @@ public class HemfGraphics extends HwmfGraphics { break; } } + + @Override + protected Paint getHatchedFill() { + // TODO: use EmfPlusHatchBrushData + return super.getHatchedFill(); + } } 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 a2cc75886c..1bd5bd6c25 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 @@ -26,6 +26,7 @@ import java.util.Iterator; import java.util.List; import java.util.function.Supplier; +import org.apache.poi.hemf.draw.HemfGraphics; import org.apache.poi.hemf.record.emfplus.HemfPlusRecord; import org.apache.poi.hemf.record.emfplus.HemfPlusRecordIterator; import org.apache.poi.hwmf.usermodel.HwmfPicture; @@ -102,6 +103,17 @@ public class HemfComment { return data; } + @Override + public void draw(HemfGraphics ctx) { + if (data instanceof EmfCommentDataPlus) { + if (ctx.getRenderState() == HemfGraphics.EmfRenderState.INITIAL) { + ctx.setRenderState(HemfGraphics.EmfRenderState.EMFPLUS_ONLY); + } + + ((EmfCommentDataPlus)data).draw(ctx); + } + } + @Override public String toString() { return "{ data: "+data+" }"; @@ -255,6 +267,10 @@ public class HemfComment { public List getRecords() { return Collections.unmodifiableList(records); } + + public void draw(HemfGraphics ctx) { + records.forEach(ctx::draw); + } } public static class EmfCommentDataBeginGroup implements EmfCommentData { 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 111f9e5e30..ae0fbd2d07 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 @@ -21,6 +21,7 @@ import static org.apache.poi.hwmf.record.HwmfDraw.boundsToString; import static org.apache.poi.hwmf.record.HwmfDraw.normalizeBounds; import java.awt.Shape; +import java.awt.geom.AffineTransform; import java.awt.geom.Arc2D; import java.awt.geom.Dimension2D; import java.awt.geom.Path2D; @@ -34,6 +35,7 @@ import org.apache.poi.hemf.draw.HemfGraphics; import org.apache.poi.hwmf.draw.HwmfGraphics.FillDrawStyle; import org.apache.poi.hwmf.record.HwmfDraw; import org.apache.poi.hwmf.record.HwmfDraw.WmfSelectObject; +import org.apache.poi.util.Internal; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; @@ -1153,4 +1155,16 @@ public class HemfDraw { ctx.draw((path) -> path.append(pi, true), fillDrawStyle); } + + + @Internal + public static String xformToString(AffineTransform xForm) { + return (xForm == null) ? "null" : + "{ scaleX: "+xForm.getScaleX()+ + ", shearX: "+xForm.getShearX()+ + ", transX: "+xForm.getTranslateX()+ + ", scaleY: "+xForm.getScaleY()+ + ", shearY: "+xForm.getShearY()+ + ", transY: "+xForm.getTranslateY()+" }"; + } } 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 a380d6c3fa..6979a37b71 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 @@ -19,6 +19,7 @@ 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.HemfDraw.xformToString; 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.pointToString; @@ -186,7 +187,7 @@ public class HemfFill { public String toString() { return "{ bounds: "+boundsToString(bounds)+ - ", xFormSrc: { scaleX: "+xFormSrc.getScaleX()+", shearX: "+xFormSrc.getShearX()+", transX: "+xFormSrc.getTranslateX()+", scaleY: "+xFormSrc.getScaleY()+", shearY: "+xFormSrc.getShearY()+", transY: "+xFormSrc.getTranslateY()+" }"+ + ", xFormSrc: " + xformToString(xFormSrc) + ", bkColorSrc: "+bkColorSrc+ ","+super.toString().substring(1); } @@ -705,6 +706,10 @@ public class HemfFill { xform.setTransform(m00, m10, m01, m11, m02, m12); + if (xform.isIdentity()) { + xform.setToIdentity(); + } + return 6 * LittleEndian.INT_SIZE; } 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 5e04e932d6..4ab50e4a00 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 @@ -18,6 +18,7 @@ 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.xformToString; import static org.apache.poi.hemf.record.emf.HemfFill.readBitmap; import static org.apache.poi.hemf.record.emf.HemfFill.readXForm; import static org.apache.poi.hemf.record.emf.HemfRecordIterator.HEADER_SIZE; @@ -621,14 +622,7 @@ public class HemfMisc { @Override public String toString() { - return - "{ xForm: " + - "{ scaleX: "+xForm.getScaleX()+ - ", shearX: "+xForm.getShearX()+ - ", transX: "+xForm.getTranslateX()+ - ", scaleY: "+xForm.getScaleY()+ - ", shearY: "+xForm.getShearY()+ - ", transY: "+xForm.getTranslateY()+" } }"; + return "{ xForm: " + xformToString(xForm)+" }"; } } @@ -695,7 +689,7 @@ public class HemfMisc { wsTrans.translate(-emfBounds.getHeight(), emfBounds.getHeight()); } } else { - wsTrans = adaptXForm(ctx.getTransform()); + wsTrans = adaptXForm(xForm, ctx.getTransform()); } tx = ctx.getTransform(); @@ -703,7 +697,7 @@ public class HemfMisc { break; case MWT_RIGHTMULTIPLY: tx = ctx.getTransform(); - tx.preConcatenate(adaptXForm(tx)); + tx.preConcatenate(adaptXForm(xForm, tx)); break; case MWT_IDENTITY: ctx.updateWindowMapMode(); @@ -713,40 +707,16 @@ public class HemfMisc { case MWT_SET: ctx.updateWindowMapMode(); tx = ctx.getTransform(); - tx.concatenate(adaptXForm(tx)); + tx.concatenate(adaptXForm(xForm, tx)); break; } ctx.setTransform(tx); } - /** - * adapt xform depending on the base transformation (... experimental ...) - */ - private AffineTransform adaptXForm(AffineTransform other) { - // normalize signed zero - Function nn = (d) -> (d == 0. ? 0. : d); - double yDiff = Math.signum(nn.apply(xForm.getTranslateY())) == Math.signum(nn.apply(other.getTranslateY())) ? 1. : -1.; - double xDiff = Math.signum(nn.apply(xForm.getTranslateX())) == Math.signum(nn.apply(other.getTranslateX())) ? 1. : -1.; - return new AffineTransform( - xForm.getScaleX() == 0 ? 1. : xForm.getScaleX(), - yDiff * xForm.getShearY(), - xDiff * xForm.getShearX(), - xForm.getScaleY() == 0. ? 1. : xForm.getScaleY(), - xForm.getTranslateX(), - xForm.getTranslateY() - ); - } - @Override public String toString() { return - "{ xForm: " + - "{ scaleX: "+xForm.getScaleX()+ - ", shearX: "+xForm.getShearX()+ - ", transX: "+xForm.getTranslateX()+ - ", scaleY: "+xForm.getScaleY()+ - ", shearY: "+xForm.getShearY()+ - ", transY: "+xForm.getTranslateY()+" }"+ + "{ xForm: " + xformToString(xForm) + ", modifyWorldTransformMode: '"+modifyWorldTransformMode+"' }"; } } @@ -825,4 +795,23 @@ public class HemfMisc { "}"; } } + + + /** + * adapt xform depending on the base transformation (... experimental ...) + */ + public static AffineTransform adaptXForm(AffineTransform xForm, AffineTransform other) { + // normalize signed zero + Function nn = (d) -> (d == 0. ? 0. : d); + double yDiff = Math.signum(nn.apply(xForm.getTranslateY())) == Math.signum(nn.apply(other.getTranslateY())) ? 1. : -1.; + double xDiff = Math.signum(nn.apply(xForm.getTranslateX())) == Math.signum(nn.apply(other.getTranslateX())) ? 1. : -1.; + return new AffineTransform( + xForm.getScaleX() == 0 ? 1. : xForm.getScaleX(), + yDiff * xForm.getShearY(), + xDiff * xForm.getShearX(), + xForm.getScaleY() == 0. ? 1. : xForm.getScaleY(), + xForm.getTranslateX(), + xForm.getTranslateY() + ); + } } 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 65c4a3d4d5..36d7b90ec7 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 @@ -17,27 +17,40 @@ package org.apache.poi.hemf.record.emfplus; +import static java.util.stream.Collectors.joining; +import static org.apache.poi.hemf.record.emf.HemfDraw.xformToString; import static org.apache.poi.hemf.record.emf.HemfFill.readXForm; import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readARGB; import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readPointF; import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF; +import static org.apache.poi.hwmf.record.HwmfDraw.boundsToString; +import static org.apache.poi.hwmf.record.HwmfDraw.pointToString; import java.awt.Color; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.IOException; +import java.util.Arrays; +import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; +import java.util.stream.Stream; +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.HemfPlusImage.EmfPlusImage; import org.apache.poi.hemf.record.emfplus.HemfPlusImage.EmfPlusWrapMode; import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectData; 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.hwmf.record.HwmfDraw; import org.apache.poi.util.BitField; import org.apache.poi.util.BitFieldFactory; +import org.apache.poi.util.Internal; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; @@ -68,58 +81,149 @@ public class HemfPlusBrush { } public enum EmfPlusHatchStyle { + /** Specifies equally spaced horizontal lines. */ HORIZONTAL(0X00000000), + /** Specifies equally spaced vertical lines. */ VERTICAL(0X00000001), + /** Specifies lines on a diagonal from upper left to lower right. */ FORWARD_DIAGONAL(0X00000002), + /** Specifies lines on a diagonal from upper right to lower left. */ BACKWARD_DIAGONAL(0X00000003), + /** Specifies crossing horizontal and vertical lines. */ LARGE_GRID(0X00000004), + /** Specifies crossing forward diagonal and backward diagonal lines with anti-aliasing. */ DIAGONAL_CROSS(0X00000005), + /** Specifies a 5-percent hatch, which is the ratio of foreground color to background color equal to 5:100. */ PERCENT_05(0X00000006), + /** Specifies a 10-percent hatch, which is the ratio of foreground color to background color equal to 10:100. */ PERCENT_10(0X00000007), + /** Specifies a 20-percent hatch, which is the ratio of foreground color to background color equal to 20:100. */ PERCENT_20(0X00000008), + /** Specifies a 25-percent hatch, which is the ratio of foreground color to background color equal to 25:100. */ PERCENT_25(0X00000009), + /** Specifies a 30-percent hatch, which is the ratio of foreground color to background color equal to 30:100. */ PERCENT_30(0X0000000A), + /** Specifies a 40-percent hatch, which is the ratio of foreground color to background color equal to 40:100. */ PERCENT_40(0X0000000B), + /** Specifies a 50-percent hatch, which is the ratio of foreground color to background color equal to 50:100. */ PERCENT_50(0X0000000C), + /** Specifies a 60-percent hatch, which is the ratio of foreground color to background color equal to 60:100. */ PERCENT_60(0X0000000D), + /** Specifies a 70-percent hatch, which is the ratio of foreground color to background color equal to 70:100. */ PERCENT_70(0X0000000E), + /** Specifies a 75-percent hatch, which is the ratio of foreground color to background color equal to 75:100. */ PERCENT_75(0X0000000F), + /** Specifies an 80-percent hatch, which is the ratio of foreground color to background color equal to 80:100. */ PERCENT_80(0X00000010), + /** Specifies a 90-percent hatch, which is the ratio of foreground color to background color equal to 90:100. */ PERCENT_90(0X00000011), + /** + * Specifies diagonal lines that slant to the right from top to bottom points with no anti-aliasing. + * They are spaced 50 percent further apart than lines in the FORWARD_DIAGONAL pattern + */ LIGHT_DOWNWARD_DIAGONAL(0X00000012), + /** + * Specifies diagonal lines that slant to the left from top to bottom points with no anti-aliasing. + * They are spaced 50 percent further apart than lines in the BACKWARD_DIAGONAL pattern. + */ LIGHT_UPWARD_DIAGONAL(0X00000013), + /** + * Specifies diagonal lines that slant to the right from top to bottom points with no anti-aliasing. + * They are spaced 50 percent closer and are twice the width of lines in the FORWARD_DIAGONAL pattern. + */ DARK_DOWNWARD_DIAGONAL(0X00000014), + /** + * Specifies diagonal lines that slant to the left from top to bottom points with no anti-aliasing. + * They are spaced 50 percent closer and are twice the width of lines in the BACKWARD_DIAGONAL pattern. + */ DARK_UPWARD_DIAGONAL(0X00000015), + /** + * Specifies diagonal lines that slant to the right from top to bottom points with no anti-aliasing. + * They have the same spacing between lines in WIDE_DOWNWARD_DIAGONAL pattern and FORWARD_DIAGONAL pattern, + * but WIDE_DOWNWARD_DIAGONAL has the triple line width of FORWARD_DIAGONAL. + */ WIDE_DOWNWARD_DIAGONAL(0X00000016), + /** + * Specifies diagonal lines that slant to the left from top to bottom points with no anti-aliasing. + * They have the same spacing between lines in WIDE_UPWARD_DIAGONAL pattern and BACKWARD_DIAGONAL pattern, + * but WIDE_UPWARD_DIAGONAL has the triple line width of WIDE_UPWARD_DIAGONAL. + */ WIDE_UPWARD_DIAGONAL(0X00000017), + /** Specifies vertical lines that are spaced 50 percent closer together than lines in the VERTICAL pattern. */ LIGHT_VERTICAL(0X00000018), + /** Specifies horizontal lines that are spaced 50 percent closer than lines in the HORIZONTAL pattern. */ LIGHT_HORIZONTAL(0X00000019), + /** + * Specifies vertical lines that are spaced 75 percent closer than lines in the VERTICAL pattern; + * or 25 percent closer than lines in the LIGHT_VERTICAL pattern. + */ NARROW_VERTICAL(0X0000001A), + /** + * Specifies horizontal lines that are spaced 75 percent closer than lines in the HORIZONTAL pattern; + * or 25 percent closer than lines in the LIGHT_HORIZONTAL pattern. + */ NARROW_HORIZONTAL(0X0000001B), + /** Specifies lines that are spaced 50 percent closer than lines in the VERTICAL pattern. */ DARK_VERTICAL(0X0000001C), + /** Specifies lines that are spaced 50 percent closer than lines in the HORIZONTAL pattern. */ DARK_HORIZONTAL(0X0000001D), + /** Specifies dashed diagonal lines that slant to the right from top to bottom points. */ DASHED_DOWNWARD_DIAGONAL(0X0000001E), + /** Specifies dashed diagonal lines that slant to the left from top to bottom points. */ DASHED_UPWARD_DIAGONAL(0X0000001F), + /** Specifies dashed horizontal lines. */ DASHED_HORIZONTAL(0X00000020), + /** Specifies dashed vertical lines. */ DASHED_VERTICAL(0X00000021), + /** Specifies a pattern of lines that has the appearance of confetti. */ SMALL_CONFETTI(0X00000022), + /** + * Specifies a pattern of lines that has the appearance of confetti, and is composed of larger pieces + * than the SMALL_CONFETTI pattern. + */ LARGE_CONFETTI(0X00000023), + /** Specifies horizontal lines that are composed of zigzags. */ ZIGZAG(0X00000024), + /** Specifies horizontal lines that are composed of tildes. */ WAVE(0X00000025), + /** + * Specifies a pattern of lines that has the appearance of layered bricks that slant to the left from + * top to bottom points. + */ DIAGONAL_BRICK(0X00000026), + /** Specifies a pattern of lines that has the appearance of horizontally layered bricks. */ HORIZONTAL_BRICK(0X00000027), + /** Specifies a pattern of lines that has the appearance of a woven material. */ WEAVE(0X00000028), + /** Specifies a pattern of lines that has the appearance of a plaid material. */ PLAID(0X00000029), + /** Specifies a pattern of lines that has the appearance of divots. */ DIVOT(0X0000002A), + /** Specifies crossing horizontal and vertical lines, each of which is composed of dots. */ DOTTED_GRID(0X0000002B), + /** Specifies crossing forward and backward diagonal lines, each of which is composed of dots. */ DOTTED_DIAMOND(0X0000002C), + /** + * Specifies a pattern of lines that has the appearance of diagonally layered + * shingles that slant to the right from top to bottom points. + */ SHINGLE(0X0000002D), + /** Specifies a pattern of lines that has the appearance of a trellis. */ TRELLIS(0X0000002E), + /** Specifies a pattern of lines that has the appearance of spheres laid adjacent to each other. */ SPHERE(0X0000002F), + /** Specifies crossing horizontal and vertical lines that are spaced 50 percent closer together than LARGE_GRID. */ SMALL_GRID(0X00000030), + /** Specifies a pattern of lines that has the appearance of a checkerboard. */ SMALL_CHECKER_BOARD(0X00000031), + /** + * Specifies a pattern of lines that has the appearance of a checkerboard, with squares that are twice the + * size of the squares in the SMALL_CHECKER_BOARD pattern. + */ LARGE_CHECKER_BOARD(0X00000032), + /** Specifies crossing forward and backward diagonal lines; the lines are not anti-aliased. */ OUTLINED_DIAMOND(0X00000033), + /** Specifies a pattern of lines that has the appearance of a checkerboard placed diagonally. */ SOLID_DIAMOND(0X00000034) ; @@ -204,17 +308,19 @@ public class HemfPlusBrush { BitField DO_NOT_TRANSFORM = BitFieldFactory.getInstance(0x00000100); long init(LittleEndianInputStream leis, long dataSize) throws IOException; + + void applyObject(HemfGraphics ctx, List continuedObjectData); } /** The EmfPlusBrush object specifies a graphics brush for filling regions. */ public static class EmfPlusBrush implements EmfPlusObjectData { - private final EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion(); + private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion(); private EmfPlusBrushType brushType; private EmfPlusBrushData brushData; @Override public long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException { - long size = version.init(leis); + long size = graphicsVersion.init(leis); brushType = EmfPlusBrushType.valueOf(leis.readInt()); size += LittleEndianConsts.INT_SIZE; @@ -224,6 +330,23 @@ public class HemfPlusBrush { return size; } + + @Override + public void applyObject(HemfGraphics ctx, List continuedObjectData) { + brushData.applyObject(ctx, continuedObjectData); + } + + @Override + public EmfPlusGraphicsVersion getGraphicsVersion() { + return graphicsVersion; + } + + @Override + public String toString() { + return + "{ brushType: '"+brushType+"'" + + ", brushData: "+brushData+" }"; + } } /** The EmfPlusSolidBrushData object specifies a solid color for a graphics brush. */ @@ -234,6 +357,17 @@ public class HemfPlusBrush { solidColor = readARGB(leis.readInt()); return LittleEndianConsts.INT_SIZE; } + + @Override + public void applyObject(HemfGraphics ctx, List continuedObjectData) { + HemfDrawProperties prop = ctx.getProperties(); + prop.setBackgroundColor(new HwmfColorRef(solidColor)); + } + + @Override + public String toString() { + return "{ solidColor: "+new HwmfColorRef(solidColor)+" }"; + } } @@ -247,6 +381,22 @@ public class HemfPlusBrush { backColor = readARGB(leis.readInt()); return 3*LittleEndianConsts.INT_SIZE; } + + @Override + public void applyObject(HemfGraphics ctx, List continuedObjectData) { + HemfDrawProperties prop = ctx.getProperties(); + prop.setBrushColor(new HwmfColorRef(foreColor)); + prop.setBackgroundColor(new HwmfColorRef(backColor)); + prop.setEmfPlusBrushHatch(style); + } + + @Override + public String toString() { + return + "{ style: '"+style+"'" + + ", foreColor: "+new HwmfColorRef(foreColor) + + ", backColor: "+new HwmfColorRef(backColor) + " }"; + } } /** The EmfPlusLinearGradientBrushData object specifies a linear gradient for a graphics brush. */ @@ -303,6 +453,32 @@ public class HemfPlusBrush { return size; } + + @Override + public void applyObject(HemfGraphics ctx, List continuedObjectData) { + HemfDrawProperties prop = ctx.getProperties(); + // TODO: implement + } + + @Override + public String toString() { + return + "{ flags: "+dataFlags+ + ", wrapMode: '"+wrapMode+"'"+ + ", rect: "+boundsToString(rect)+ + ", startColor: "+new HwmfColorRef(startColor)+ + ", endColor: "+new HwmfColorRef(endColor)+ + ", transform: "+xformToString(transform)+ + ", positions: "+ Arrays.toString(positions)+ + ", blendColors: "+ colorsToString(blendColors)+ + ", positionsV: "+ Arrays.toString(positionsV)+ + ", blendFactorsV: "+ Arrays.toString(blendFactorsV)+ + ", positionsH: "+ Arrays.toString(positionsH)+ + ", blendFactorsH: "+ Arrays.toString(blendFactorsH)+ + "}"; + } + + } /** The EmfPlusPathGradientBrushData object specifies a path gradient for a graphics brush. */ @@ -409,6 +585,31 @@ public class HemfPlusBrush { return size; } + + @Override + public void applyObject(HemfGraphics ctx, List continuedObjectData) { + + } + + @Override + public String toString() { + return + "{ flags: "+dataFlags+ + ", wrapMode: '"+wrapMode+"'"+ + ", centerColor: "+new HwmfColorRef(centerColor)+ + ", centerPoint: "+pointToString(centerPoint)+ + ", surroundingColor: "+colorsToString(surroundingColor)+ + ", boundaryPath: "+(boundaryPath == null ? "null" : boundaryPath)+ + ", boundaryPoints: "+pointsToString(boundaryPoints)+ + ", transform: "+xformToString(transform)+ + ", positions: "+Arrays.toString(positions)+ + ", blendColors: "+colorsToString(blendColors)+ + ", blendFactorsH: "+Arrays.toString(blendFactorsH)+ + ", focusScaleX: "+focusScaleX+ + ", focusScaleY: "+focusScaleY+ + "}" + ; + } } /** The EmfPlusTextureBrushData object specifies a texture image for a graphics brush. */ @@ -440,6 +641,24 @@ public class HemfPlusBrush { return size; } + + @Override + public void applyObject(HemfGraphics ctx, List continuedObjectData) { + image.applyObject(ctx, continuedObjectData); + HemfDrawProperties prop = ctx.getProperties(); + prop.setBrushBitmap(prop.getEmfPlusImage()); + prop.setBrushStyle(HwmfBrushStyle.BS_PATTERN); + } + + @Override + public String toString() { + return + "{ flags: "+dataFlags+ + ", wrapMode: '"+wrapMode+"'"+ + ", transform: "+xformToString(transform)+ + ", image: "+image+ + "]"; + } } private static int readPositions(LittleEndianInputStream leis, Consumer pos) { @@ -477,4 +696,18 @@ public class HemfPlusBrush { facs.accept(factors); return size + factors.length * LittleEndianConsts.INT_SIZE; } + + @Internal + public static String colorsToString(Color[] colors) { + return (colors == null ? "null" : + Stream.of(colors).map(HwmfColorRef::new).map(Object::toString). + collect(joining(",", "{", "}"))); + } + + @Internal + public static String pointsToString(Point2D[] points) { + return (points == null ? "null" : + Stream.of(points).map(HwmfDraw::pointToString). + collect(joining(",", "{", "}"))); + } } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusDraw.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusDraw.java index f35a1ea3ab..036b544fdc 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusDraw.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusDraw.java @@ -17,10 +17,18 @@ package org.apache.poi.hemf.record.emfplus; +import static java.util.stream.Collectors.joining; +import static org.apache.poi.hemf.record.emf.HemfDraw.xformToString; +import static org.apache.poi.hwmf.record.HwmfDraw.boundsToString; +import static org.apache.poi.hwmf.record.HwmfDraw.pointToString; + import java.awt.Color; 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.awt.image.BufferedImage; import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; @@ -30,8 +38,17 @@ import java.util.function.BiFunction; import org.apache.commons.math3.linear.LUDecomposition; import org.apache.commons.math3.linear.MatrixUtils; import org.apache.commons.math3.linear.RealMatrix; +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.hemf.record.emfplus.HemfPlusImage.EmfPlusImage; import org.apache.poi.hemf.record.emfplus.HemfPlusMisc.EmfPlusObjectId; +import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObject; +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.HwmfMisc.WmfSetBkMode.HwmfBkMode; +import org.apache.poi.hwmf.record.HwmfTernaryRasterOp; import org.apache.poi.util.BitField; import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.IOUtils; @@ -116,12 +133,45 @@ public class HemfPlusDraw { } } + public interface EmfPlusSolidColor { + /** + * If set, brushId specifies a color as an EmfPlusARGB object. + * If clear, brushId contains the index of an EmfPlusBrush object in the EMF+ Object Table. + */ + BitField SOLID_COLOR = BitFieldFactory.getInstance(0x8000); + + int getFlags(); + + int getBrushIdValue(); + + default boolean isSolidColor() { + return SOLID_COLOR.isSet(getFlags()); + } + + default int getBrushId() { + return (isSolidColor()) ? -1 : getBrushIdValue(); + } + + default Color getSolidColor() { + return (isSolidColor()) ? readARGB(getBrushIdValue()) : null; + } + + default void applyColor(HemfGraphics ctx) { + HemfDrawProperties prop = ctx.getProperties(); + if (isSolidColor()) { + prop.setBrushStyle(HwmfBrushStyle.BS_SOLID); + prop.setBrushColor(new HwmfColorRef(getSolidColor())); + } else { + ctx.applyObjectTableEntry(getBrushId()); + } + } + } /** * The EmfPlusDrawPath record specifies drawing a graphics path */ - public static class EmfPlusDrawPath implements HemfPlusRecord { + public static class EmfPlusDrawPath implements HemfPlusRecord, EmfPlusObjectId { private int flags; private int penId; @@ -148,18 +198,31 @@ public class HemfPlusDraw { return LittleEndianConsts.INT_SIZE; } + + @Override + public void draw(HemfGraphics ctx) { + ctx.applyObjectTableEntry(penId); + ctx.applyObjectTableEntry(getObjectId()); + + HemfDrawProperties prop = ctx.getProperties(); + final Path2D path = prop.getPath(); + if (path != null) { + ctx.draw(path); + } + } + + @Override + public String toString() { + return + "{ flags: "+flags+ + ", penId: "+penId+" }"; + } } /** * The EmfPlusFillRects record specifies filling the interiors of a series of rectangles. */ - public static class EmfPlusFillRects implements HemfPlusRecord, EmfPlusCompressed { - /** - * If set, brushId specifies a color as an EmfPlusARGB object. - * If clear, brushId contains the index of an EmfPlusBrush object in the EMF+ Object Table. - */ - private static final BitField SOLID_COLOR = BitFieldFactory.getInstance(0x8000); - + public static class EmfPlusFillRects implements HemfPlusRecord, EmfPlusCompressed, EmfPlusSolidColor { private int flags; private int brushId; private final ArrayList rectData = new ArrayList<>(); @@ -198,6 +261,29 @@ public class HemfPlusDraw { return size; } + + @Override + public void draw(HemfGraphics ctx) { + applyColor(ctx); + + Area area = new Area(); + rectData.stream().map(Area::new).forEach(area::add); + ctx.fill(area); + } + + @Override + public int getBrushIdValue() { + return brushId; + } + + @Override + public String toString() { + return + "{ flags: "+flags+ + ", brushId: "+brushId+ + ", rectData: "+rectData.stream().map(HwmfDraw::boundsToString).collect(joining(",", "{", "}"))+ + "}"; + } } public static class EmfPlusDrawImagePoints implements HemfPlusRecord, EmfPlusObjectId, EmfPlusCompressed, EmfPlusRelativePosition { @@ -302,6 +388,58 @@ public class HemfPlusDraw { return size; } + + @Override + public void draw(HemfGraphics ctx) { + HemfDrawProperties prop = ctx.getProperties(); + + ctx.applyObjectTableEntry(imageAttributesID); + ctx.applyObjectTableEntry(getObjectId()); + + AffineTransform txSaved = ctx.getTransform(), tx = new AffineTransform(txSaved); + try { + tx.concatenate(trans); + ctx.setTransform(tx); + + EmfPlusObject imgObj = (EmfPlusObject)ctx.getObjectTableEntry(getObjectId()); + EmfPlusImage img = imgObj.getObjectData(); + Rectangle2D srcBounds = img.getBounds(imgObj.getContinuedObject()); + BufferedImage bi = prop.getEmfPlusImage(); + + prop.setRasterOp(HwmfTernaryRasterOp.SRCCOPY); + prop.setBkMode(HwmfBkMode.TRANSPARENT); + + // the buffered image might be rescaled, so we need to calculate a new src rect to take + // the image data from + AffineTransform srcTx = new AffineTransform(); + srcTx.translate(-srcBounds.getX(), srcBounds.getY()); + srcTx.scale(bi.getWidth()/srcBounds.getWidth(), bi.getHeight()/srcBounds.getHeight()); + srcTx.translate(bi.getMinX(), bi.getMinY()); + + Rectangle2D biRect = srcTx.createTransformedShape(srcRect).getBounds2D(); + + // TODO: handle srcUnit + Rectangle2D destRect = new Rectangle2D.Double(0, 0, biRect.getWidth(), biRect.getHeight()); + ctx.drawImage(bi, srcRect, destRect); + } finally { + ctx.setTransform(txSaved); + } + } + + @Override + public String toString() { + return + "{ flags: "+flags+ + ", imageAttributesID: "+imageAttributesID+ + ", srcUnit: '"+srcUnit+"'"+ + ", srcRect: "+boundsToString(srcRect)+ + ", upperLeft: "+pointToString(upperLeft)+ + ", lowerLeft: "+pointToString(lowerLeft)+ + ", lowerRight: "+pointToString(lowerRight)+ + ", transform: "+xformToString(trans)+ + "}" + ; + } } /** The EmfPlusDrawImage record specifies drawing a scaled image. */ @@ -347,12 +485,34 @@ public class HemfPlusDraw { return size; } + + @Override + public void draw(HemfGraphics ctx) { + ctx.applyObjectTableEntry(imageAttributesID); + ctx.applyObjectTableEntry(getObjectId()); + + HemfDrawProperties prop = ctx.getProperties(); + prop.setRasterOp(HwmfTernaryRasterOp.SRCCOPY); + prop.setBkMode(HwmfBkMode.TRANSPARENT); + + ctx.drawImage(prop.getEmfPlusImage(), srcRect, rectData); + } + + @Override + public String toString() { + return + "{ flags: "+flags+ + ", imageAttributesID: "+imageAttributesID+ + ", srcUnit: '"+srcUnit+"'"+ + ", srcRect: "+boundsToString(srcRect)+ + ", rectData: "+boundsToString(rectData)+ + "}" + ; + } } /** The EmfPlusFillRegion record specifies filling the interior of a graphics region. */ - public static class EmfPlusFillRegion implements HemfPlusRecord { - private static final BitField SOLID_COLOR = BitFieldFactory.getInstance(0x8000); - + public static class EmfPlusFillRegion implements HemfPlusRecord, EmfPlusSolidColor, EmfPlusObjectId { private int flags; private int brushId; @@ -366,16 +526,9 @@ public class HemfPlusDraw { return flags; } - public boolean isSolidColor() { - return SOLID_COLOR.isSet(getFlags()); - } - - public int getBrushId() { - return (isSolidColor()) ? -1 : brushId; - } - - public Color getSolidColor() { - return (isSolidColor()) ? readARGB(brushId) : null; + @Override + public int getBrushIdValue() { + return brushId; } @Override @@ -390,6 +543,21 @@ public class HemfPlusDraw { return LittleEndianConsts.INT_SIZE; } + + @Override + public void draw(HemfGraphics ctx) { + applyColor(ctx); + ctx.applyObjectTableEntry(getObjectId()); + HemfDrawProperties prop = ctx.getProperties(); + ctx.fill(prop.getPath()); + } + + @Override + public String toString() { + return + "{ flags: "+flags+ + ", brushId: "+brushId+" }"; + } } /** The EmfPlusFillPath record specifies filling the interior of a graphics path. */ @@ -403,13 +571,7 @@ public class HemfPlusDraw { } /** The EmfPlusDrawDriverString record specifies text output with character positions. */ - public static class EmfPlusDrawDriverString implements HemfPlusRecord, EmfPlusObjectId { - /** - * If set, brushId specifies a color as an EmfPlusARGB object. - * If clear, brushId contains the index of an EmfPlusBrush object in the EMF+ Object Table. - */ - private static final BitField SOLID_COLOR = BitFieldFactory.getInstance(0x8000); - + public static class EmfPlusDrawDriverString implements HemfPlusRecord, EmfPlusObjectId, EmfPlusSolidColor { /** * If set, the positions of character glyphs SHOULD be specified in a character map lookup table. * If clear, the glyph positions SHOULD be obtained from an array of coordinates. @@ -453,6 +615,11 @@ public class HemfPlusDraw { return flags; } + @Override + public int getBrushIdValue() { + return brushId; + } + @Override public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { this.flags = flags; @@ -615,7 +782,7 @@ public class HemfPlusDraw { } static Color readARGB(int argb) { - return new Color( (argb >>> 8) & 0xFF, (argb >>> 16) & 0xFF, (argb >>> 24) & 0xFF, argb & 0xFF); + return new Color((argb >>> 16) & 0xFF, (argb >>> 8) & 0xFF, argb & 0xFF, (argb >>> 24) & 0xFF); } } 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 fc3550c433..a2201320b4 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 @@ -18,7 +18,9 @@ package org.apache.poi.hemf.record.emfplus; import java.io.IOException; +import java.util.List; +import org.apache.poi.hemf.draw.HemfGraphics; 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; @@ -60,7 +62,7 @@ public class HemfPlusFont { private static final BitField STRIKEOUT = BitFieldFactory.getInstance(0x00000008); - private final EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion(); + private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion(); private double emSize; private EmfPlusUnitType sizeUnit; private int styleFlags; @@ -70,7 +72,7 @@ public class HemfPlusFont { public long init(LittleEndianInputStream leis, long dataSize, HemfPlusObject.EmfPlusObjectType objectType, int flags) throws IOException { // An EmfPlusGraphicsVersion object that specifies the version of operating system graphics that was used // to create this object. - long size = version.init(leis); + long size = graphicsVersion.init(leis); // A 32-bit floating-point value that specifies the em size of the font in units specified by the SizeUnit field. emSize = leis.readFloat(); @@ -96,5 +98,15 @@ public class HemfPlusFont { return size; } + + @Override + public void applyObject(HemfGraphics ctx, List continuedObjectData) { + + } + + @Override + public EmfPlusGraphicsVersion getGraphicsVersion() { + return graphicsVersion; + } } } 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 ee196a78d9..aa93853173 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusHeader.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusHeader.java @@ -20,6 +20,8 @@ package org.apache.poi.hemf.record.emfplus; import java.io.IOException; +import org.apache.poi.hemf.draw.HemfGraphics; +import org.apache.poi.hemf.draw.HemfGraphics.EmfRenderState; import org.apache.poi.util.BitField; import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.Internal; @@ -85,6 +87,19 @@ public class HemfPlusHeader implements HemfPlusRecord { return version; } + /** + * If set, this flag indicates that this metafile is "dual-mode", which means that it contains two sets of records, + * each of which completely specifies the graphics content. If clear, the graphics content is specified by EMF+ + * records, and possibly EMF records that are preceded by an EmfPlusGetDC record. If this flag is set, EMF records + * alone SHOULD suffice to define the graphics content. Note that whether the "dual-mode" flag is set or not, some + * EMF records are always present, namely EMF control records and the EMF records that contain EMF+ records. + * + * @return {@code true} if dual-mode is enabled + */ + public boolean isEmfPlusDualMode() { + return (emfPlusFlags & 1) == 1; + } + public long getEmfPlusFlags() { return emfPlusFlags; } @@ -97,6 +112,13 @@ public class HemfPlusHeader implements HemfPlusRecord { return logicalDpiY; } + @Override + 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.EMFPLUS_ONLY); + } + @Override public String toString() { return "HemfPlusHeader{" + diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusImage.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusImage.java index 4374b1781f..ffa9d9c8e9 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusImage.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusImage.java @@ -20,18 +20,45 @@ package org.apache.poi.hemf.record.emfplus; import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readARGB; import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.geom.Dimension2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.ComponentColorModel; +import java.awt.image.DataBuffer; +import java.awt.image.DataBufferByte; +import java.awt.image.PixelInterleavedSampleModel; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.List; +import java.util.function.BiConsumer; +import javax.imageio.ImageIO; + +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; import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectType; +import org.apache.poi.hemf.usermodel.HemfPicture; +import org.apache.poi.hwmf.usermodel.HwmfPicture; import org.apache.poi.util.BitField; import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.IOUtils; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; +import org.apache.poi.util.Units; public class HemfPlusImage { + /** Maximum image dimension for converting embedded metafiles */ + private static final int MAX_IMAGE_SIZE = 1500; + /** The ImageDataType enumeration defines types of image data formats. */ public enum EmfPlusImageDataType { /** The type of image is not known. */ @@ -302,7 +329,7 @@ public class HemfPlusImage { leis.mark(LittleEndianConsts.INT_SIZE); long size = graphicsVersion.init(leis); - if (graphicsVersion.getGraphicsVersion() == null || graphicsVersion.getMetafileSignature() != 0xDBC01) { + if (isContinuedRecord()) { // CONTINUABLE is not always correctly set, so we check the version field if this record is continued imageDataType = EmfPlusImageDataType.CONTINUED; leis.reset(); @@ -385,8 +412,193 @@ public class HemfPlusImage { return size + fileSize; } + + @Override + public EmfPlusGraphicsVersion getGraphicsVersion() { + return graphicsVersion; + } + + public Rectangle2D getBounds(List continuedObjectData) { + try { + switch (getImageDataType()) { + case BITMAP: + if (getBitmapType() == EmfPlusBitmapDataType.PIXEL) { + return new Rectangle2D.Double(0, 0, bitmapWidth, bitmapHeight); + } else { + BufferedImage bi = ImageIO.read(new ByteArrayInputStream(getRawData(continuedObjectData))); + return new Rectangle2D.Double(bi.getMinX(), bi.getMinY(), bi.getWidth(), bi.getHeight()); + } + case METAFILE: + ByteArrayInputStream bis = new ByteArrayInputStream(getRawData(continuedObjectData)); + switch (getMetafileType()) { + case Wmf: + case WmfPlaceable: + HwmfPicture wmf = new HwmfPicture(bis); + return wmf.getBounds(); + case Emf: + case EmfPlusDual: + case EmfPlusOnly: + HemfPicture emf = new HemfPicture(bis); + return emf.getBounds(); + } + break; + default: + break; + } + } catch (Exception ignored) { + } + return new Rectangle2D.Double(1,1,1,1); + } + + public byte[] getRawData(List continuedObjectData) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + bos.write(getImageData()); + if (continuedObjectData != null) { + for (EmfPlusObjectData od : continuedObjectData) { + bos.write(((EmfPlusImage)od).getImageData()); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return bos.toByteArray(); + } + + @Override + public void applyObject(HemfGraphics ctx, List continuedObjectData) { + HemfDrawProperties prop = ctx.getProperties(); + BufferedImage bi = readImage(getRawData(continuedObjectData)); + prop.setEmfPlusImage(bi); + } + + /** + * Converts the gdi pixel data to a buffered image + * @param data the image data of all EmfPlusImage parts + * @return the BufferedImage + */ + public BufferedImage readGDIImage(final byte[] data) { + if (getImageDataType() != EmfPlusImageDataType.BITMAP || getBitmapType() != EmfPlusBitmapDataType.PIXEL) { + throw new RuntimeException("image data is not a GDI image"); + } + + final int width = getBitmapWidth(); + final int height = getBitmapHeight(); + final int stride = getBitmapStride(); + final EmfPlusPixelFormat pf = getPixelFormat(); + + int[] nBits, bOffs; + switch (pf) { + case ARGB_32BPP: + nBits = new int[]{8, 8, 8, 8}; + bOffs = new int[]{2, 1, 0, 3}; + break; + case RGB_24BPP: + nBits = new int[]{8, 8, 8}; + bOffs = new int[]{2, 1, 0}; + break; + default: + throw new RuntimeException("not yet implemented"); + } + + ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); + ComponentColorModel cm = new ComponentColorModel + (cs, nBits, pf.isAlpha(), pf.isPreMultiplied(), Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); + PixelInterleavedSampleModel csm = + new PixelInterleavedSampleModel(cm.getTransferType(), width, height, cm.getNumComponents(), stride, bOffs); + + DataBufferByte dbb = new DataBufferByte(data, data.length); + WritableRaster raster = (WritableRaster) Raster.createRaster(csm, dbb, null); + + return new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); + } + + private BufferedImage readImage(final byte[] data) { + // TODO: instead of returning a BufferedImage, we might return a pair of raw data + image renderer + // instead, so metafiles aren't pixelated, but directly written to the output graphics context + try { + switch (getImageDataType()) { + case BITMAP: { + BufferedImage bi = (getBitmapType() == EmfPlusBitmapDataType.PIXEL) + ? readGDIImage(data) + : ImageIO.read(new ByteArrayInputStream(data)); + +// final int w = bi.getWidth(); +// final int h = bi.getHeight(); +// +// int[] line = new int[w]; +// +// WritableRaster wr = bi.getRaster(); +// for (int row=0; row draw) { + int width = Units.pointsToPixel(dim.getWidth()); + // keep aspect ratio for height + int height = Units.pointsToPixel(dim.getHeight()); + double longSide = Math.max(width,height); + if (longSide > MAX_IMAGE_SIZE) { + double scale = MAX_IMAGE_SIZE / longSide; + width *= scale; + height *= scale; + } + + BufferedImage bufImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = bufImg.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + + draw.accept(g, new Rectangle2D.Double(0, 0, width, height)); + + g.dispose(); + + return bufImg; + } } + + public static class EmfPlusImageAttributes implements EmfPlusObjectData { private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion(); private EmfPlusWrapMode wrapMode; @@ -434,6 +646,10 @@ public class HemfPlusImage { public EmfPlusObjectClamp getObjectClamp() { return objectClamp; } + + @Override + public void applyObject(HemfGraphics ctx, List continuedObjectData) { + } } } 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 771758c81c..2d006563ab 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 @@ -17,6 +17,7 @@ package org.apache.poi.hemf.record.emfplus; +import static org.apache.poi.hemf.record.emf.HemfMisc.adaptXForm; import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF; import java.awt.geom.AffineTransform; @@ -24,10 +25,10 @@ import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.IOException; +import org.apache.poi.hemf.draw.HemfGraphics; import org.apache.poi.hemf.record.emf.HemfFill; import org.apache.poi.util.BitField; import org.apache.poi.util.BitFieldFactory; -import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; @@ -134,6 +135,12 @@ public class HemfPlusMisc { * SHOULD be processed. */ public static class EmfPlusGetDC extends EmfPlusFlagOnly { + @Override + public void draw(HemfGraphics ctx) { + if (ctx.getRenderState() == HemfGraphics.EmfRenderState.EMFPLUS_ONLY) { + ctx.setRenderState(HemfGraphics.EmfRenderState.EMF_DCONTEXT); + } + } } /** @@ -174,6 +181,18 @@ public class HemfPlusMisc { return HemfFill.readXForm(leis, matrixData); } + + public AffineTransform getMatrixData() { + return matrixData; + } + + @Override + public void draw(HemfGraphics ctx) { + ctx.updateWindowMapMode(); + AffineTransform tx = ctx.getTransform(); + tx.concatenate(getMatrixData()); + ctx.setTransform(tx); + } } /** @@ -185,6 +204,15 @@ public class HemfPlusMisc { public HemfPlusRecordType getEmfPlusRecordType() { return HemfPlusRecordType.multiplyWorldTransform; } + + @Override + public void draw(HemfGraphics ctx) { + ctx.updateWindowMapMode(); + AffineTransform tx = ctx.getTransform(); + tx.preConcatenate(adaptXForm(getMatrixData(), tx)); + tx.concatenate(getMatrixData()); + ctx.setTransform(tx); + } } /** 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 35845044da..4d23bab6e0 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 @@ -18,8 +18,11 @@ package org.apache.poi.hemf.record.emfplus; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.function.Supplier; +import org.apache.poi.hemf.draw.HemfGraphics; import org.apache.poi.hemf.record.emfplus.HemfPlusBrush.EmfPlusBrush; import org.apache.poi.hemf.record.emfplus.HemfPlusFont.EmfPlusFont; import org.apache.poi.hemf.record.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion; @@ -29,6 +32,8 @@ import org.apache.poi.hemf.record.emfplus.HemfPlusMisc.EmfPlusObjectId; import org.apache.poi.hemf.record.emfplus.HemfPlusPath.EmfPlusPath; import org.apache.poi.hemf.record.emfplus.HemfPlusPen.EmfPlusPen; import org.apache.poi.hemf.record.emfplus.HemfPlusRegion.EmfPlusRegion; +import org.apache.poi.hwmf.draw.HwmfGraphics; +import org.apache.poi.hwmf.record.HwmfObjectTableEntry; import org.apache.poi.util.BitField; import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.IOUtils; @@ -107,7 +112,7 @@ public class HemfPlusObject { * The EmfPlusObject record specifies an object for use in graphics operations. The object definition * can span multiple records), which is indicated by the value of the Flags field. */ - public static class EmfPlusObject implements HemfPlusRecord, EmfPlusObjectId { + public static class EmfPlusObject implements HemfPlusRecord, EmfPlusObjectId, HwmfObjectTableEntry { /** @@ -126,6 +131,7 @@ public class HemfPlusObject { // for debugging private int objectId; private EmfPlusObjectData objectData; + private List continuedObjectData; private int totalObjectSize; @Override @@ -174,10 +180,50 @@ public class HemfPlusObject { return size; } + + @Override + public void draw(HemfGraphics ctx) { + HwmfObjectTableEntry entry = ctx.getObjectTableEntry(getObjectId()); + if (objectData.isContinuedRecord()) { + EmfPlusObject other; + if (entry instanceof EmfPlusObject && objectData.getClass().isInstance((other = (EmfPlusObject)entry).getObjectData())) { + other.linkContinuedObject(objectData); + return; + } else { + throw new RuntimeException("can't find previous record for continued record"); + } + } + ctx.addObjectTableEntry(this, getObjectId()); + } + + @Override + public void applyObject(HwmfGraphics ctx) { + objectData.applyObject((HemfGraphics)ctx, continuedObjectData); + } + + void linkContinuedObject(EmfPlusObjectData continueObject) { + if (continuedObjectData == null) { + continuedObjectData = new ArrayList<>(); + } + continuedObjectData.add(continueObject); + } + + List getContinuedObject() { + return continuedObjectData; + } } public interface EmfPlusObjectData { long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException; + + void applyObject(HemfGraphics ctx, List continuedObjectData); + + EmfPlusGraphicsVersion getGraphicsVersion(); + + default boolean isContinuedRecord() { + EmfPlusGraphicsVersion gv = getGraphicsVersion(); + return (gv.getGraphicsVersion() == null || gv.getMetafileSignature() != 0xDBC01); + } } public static class EmfPlusUnknownData implements EmfPlusObjectData { @@ -195,5 +241,15 @@ public class HemfPlusObject { return dataSize; } + + @Override + public void applyObject(HemfGraphics ctx, List continuedObjectData) { + + } + + @Override + public EmfPlusGraphicsVersion getGraphicsVersion() { + return graphicsVersion; + } } } 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 86d2e48a59..c8405fece6 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 @@ -17,11 +17,15 @@ package org.apache.poi.hemf.record.emfplus; +import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.io.IOException; import java.util.Arrays; +import java.util.List; import java.util.function.BiFunction; +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.HemfPlusObject.EmfPlusObjectData; @@ -69,14 +73,14 @@ public class HemfPlusPath { private static final BitField POINT_RLE_COUNT = BitFieldFactory.getInstance(0x3F); - private final HemfPlusHeader.EmfPlusGraphicsVersion version = new HemfPlusHeader.EmfPlusGraphicsVersion(); + private final HemfPlusHeader.EmfPlusGraphicsVersion graphicsVersion = new HemfPlusHeader.EmfPlusGraphicsVersion(); private int pointFlags; private Point2D[] pathPoints; private byte[] pointTypes; @Override public long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException { - long size = version.init(leis); + long size = graphicsVersion.init(leis); // A 32-bit unsigned integer that specifies the number of points and associated point types that // are defined by this object. @@ -124,6 +128,11 @@ public class HemfPlusPath { return size; } + @Override + public HemfPlusHeader.EmfPlusGraphicsVersion getGraphicsVersion() { + return graphicsVersion; + } + public boolean isPointDashed(int index) { return POINT_TYPE_DASHED.isSet(pointTypes[index]); } @@ -144,6 +153,38 @@ public class HemfPlusPath { public int getFlags() { return pointFlags; } + + + + @Override + public void applyObject(HemfGraphics ctx, List continuedObjectData) { + HemfDrawProperties prop = ctx.getProperties(); + Path2D path = new Path2D.Double(Path2D.WIND_NON_ZERO); + prop.setPath(path); + + for (int idx=0; idx < pathPoints.length; idx++) { + Point2D p1 = pathPoints[idx]; + switch (getPointType(idx)) { + case START: + path.moveTo(p1.getX(), p1.getY()); + break; + case LINE: + path.lineTo(p1.getX(), p1.getY()); + break; + case BEZIER: { + Point2D p2 = pathPoints[++idx]; + Point2D p3 = pathPoints[++idx]; + path.curveTo(p1.getX(), p1.getY(), p2.getX(), p2.getY(), p3.getX(), p3.getY()); + break; + } + } + if (isPointClosed(idx)) { + path.closePath(); + } + } + } + + } 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 6dff00bdf7..c412b425a7 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 @@ -23,12 +23,17 @@ import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readPointF; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.io.IOException; +import java.util.List; import java.util.function.Consumer; +import org.apache.poi.hemf.draw.HemfDrawProperties; +import org.apache.poi.hemf.draw.HemfGraphics; +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; import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectType; import org.apache.poi.hemf.record.emfplus.HemfPlusPath.EmfPlusPath; +import org.apache.poi.hwmf.record.HwmfPenStyle; import org.apache.poi.util.BitField; import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.Internal; @@ -215,6 +220,13 @@ public class HemfPlusPen { } } + + @Internal + public interface EmfPlusCustomLineCap { + long init(LittleEndianInputStream leis) throws IOException; + } + + public static class EmfPlusPen implements EmfPlusObjectData { @@ -283,16 +295,17 @@ public class HemfPlusPen { private int type; private int penDataFlags; - private HemfPlusDraw.EmfPlusUnitType unitType; + private EmfPlusUnitType unitType; private double penWidth; - private AffineTransform trans; - private EmfPlusLineCapType startCap, endCap; - private EmfPlusLineJoin join; - private Double mitterLimit; - private EmfPlusLineStyle style; - EmfPlusDashedLineCapType dashedLineCapType; + private final AffineTransform trans = new AffineTransform(); + private EmfPlusLineCapType startCap = EmfPlusLineCapType.FLAT; + private EmfPlusLineCapType endCap = startCap; + private EmfPlusLineJoin join = EmfPlusLineJoin.ROUND; + private Double miterLimit = 1.; + private EmfPlusLineStyle style = EmfPlusLineStyle.SOLID; + private EmfPlusDashedLineCapType dashedLineCapType; private Double dashOffset; - private double[] dashedLineData; + private float[] dashedLineData; private EmfPlusPenAlignment penAlignment; private double[] compoundLineData; private EmfPlusCustomLineCap customStartCap; @@ -310,17 +323,16 @@ public class HemfPlusPen { penDataFlags = leis.readInt(); // A 32-bit unsigned integer that specifies the measuring units for the pen. // The value MUST be from the UnitType enumeration - unitType = HemfPlusDraw.EmfPlusUnitType.valueOf(leis.readInt()); + unitType = EmfPlusUnitType.valueOf(leis.readInt()); // A 32-bit floating-point value that specifies the width of the line drawn by the pen in the units specified // by the PenUnit field. If a zero width is specified, a minimum value is used, which is determined by the units. penWidth = leis.readFloat(); - size += 4* LittleEndianConsts.INT_SIZE; + size += 4*LittleEndianConsts.INT_SIZE; if (TRANSFORM.isSet(penDataFlags)) { // An optional EmfPlusTransformMatrix object that specifies a world space to device space transform for // the pen. This field MUST be present if the PenDataTransform flag is set in the PenDataFlags field of // the EmfPlusPenData object. - trans = new AffineTransform(); size += readXForm(leis, trans); } @@ -354,7 +366,7 @@ public class HemfPlusPen { // line walls on the inside the join to the intersection of the line walls outside the join. The miter // length can be large when the angle between two lines is small. This field MUST be present if the // PenDataMiterLimit flag is set in the PenDataFlags field of the EmfPlusPenData object. - mitterLimit = (double)leis.readFloat(); + miterLimit = (double)leis.readFloat(); size += LittleEndianConsts.INT_SIZE; } @@ -390,7 +402,7 @@ public class HemfPlusPen { } // An array of DashedLineDataSize floating-point values that specify the lengths of the dashes and spaces in a dashed line. - dashedLineData = new double[dashesSize]; + dashedLineData = new float[dashesSize]; for (int i=0; i setter, LittleEndianInputStream leis) throws IOException { EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion(); long size = version.init(leis); @@ -450,153 +467,209 @@ public class HemfPlusPen { return size; } - @Internal - public interface EmfPlusCustomLineCap { - long init(LittleEndianInputStream leis) throws IOException; + @Override + public void applyObject(HemfGraphics ctx, List continuedObjectData) { + final HemfDrawProperties prop = ctx.getProperties(); + // TOOD: + // - set width according unit type + // - provide logic for different start and end cap + // - provide standard caps like diamondd + // - support custom caps + + // workaround for too wide pens ... just arbitrary reduce high values ... + prop.setPenWidth(penWidth > 20 ? 1 : penWidth); + prop.setPenStyle(new HwmfPenStyle(){ + @Override + public HwmfLineCap getLineCap() { + // ignore endCap for now + switch(startCap) { + default: + case FLAT: + return HwmfLineCap.FLAT; + case ROUND: + return HwmfLineCap.ROUND; + case SQUARE: + return HwmfLineCap.SQUARE; + } + } + + @Override + public HwmfLineJoin getLineJoin() { + switch (join) { + default: + case BEVEL: + return HwmfLineJoin.BEVEL; + case ROUND: + return HwmfLineJoin.ROUND; + case MITER_CLIPPED: + case MITER: + return HwmfLineJoin.MITER; + } + } + + @Override + public HwmfLineDash getLineDash() { + return dashedLineData == null ? HwmfLineDash.SOLID : HwmfLineDash.USERSTYLE; + } + + @Override + public float[] getLineDashes() { + return dashedLineData; + } + + @Override + public boolean isAlternateDash() { + return (getLineDash() != HwmfLineDash.SOLID && dashOffset != null && dashOffset == 0); + } + + @Override + public boolean isGeometric() { + return (unitType == EmfPlusUnitType.World || unitType == EmfPlusUnitType.Display); + } + }); } + } - public static class EmfPlusPathArrowCap implements EmfPlusCustomLineCap { - /** - * If set, an EmfPlusFillPath object MUST be specified in the OptionalData field of the - * EmfPlusCustomLineCapData object for filling the custom line cap. - */ - private static final BitField FILL_PATH = BitFieldFactory.getInstance(0x00000001); - /** - * If set, an EmfPlusLinePath object MUST be specified in the OptionalData field of the - * EmfPlusCustomLineCapData object for outlining the custom line cap. - */ - private static final BitField LINE_PATH = BitFieldFactory.getInstance(0x00000002); - - - private int dataFlags; - private EmfPlusLineCapType baseCap; - private double baseInset; - private EmfPlusLineCapType startCap; - private EmfPlusLineCapType endCap; - private EmfPlusLineJoin join; - private double mitterLimit; - private double widthScale; - private final Point2D fillHotSpot = new Point2D.Double(); - private final Point2D lineHotSpot = new Point2D.Double(); - private EmfPlusPath fillPath; - private EmfPlusPath outlinePath; - - @Override - public long init(LittleEndianInputStream leis) throws IOException { - // A 32-bit unsigned integer that specifies the data in the OptionalData field. - // This value MUST be composed of CustomLineCapData flags - dataFlags = leis.readInt(); - - // A 32-bit unsigned integer that specifies the value from the LineCap enumeration on which - // the custom line cap is based. - baseCap = EmfPlusLineCapType.valueOf(leis.readInt()); - - // A 32-bit floating-point value that specifies the distance between the - // beginning of the line cap and the end of the line. - baseInset = leis.readFloat(); - - // A 32-bit unsigned integer that specifies the value in the LineCap enumeration that indicates the line - // cap used at the start/end of the line to be drawn. - startCap = EmfPlusLineCapType.valueOf(leis.readInt()); - endCap = EmfPlusLineCapType.valueOf(leis.readInt()); + public static class EmfPlusPathArrowCap implements EmfPlusCustomLineCap { + /** + * If set, an EmfPlusFillPath object MUST be specified in the OptionalData field of the + * EmfPlusCustomLineCapData object for filling the custom line cap. + */ + private static final BitField FILL_PATH = BitFieldFactory.getInstance(0x00000001); + /** + * If set, an EmfPlusLinePath object MUST be specified in the OptionalData field of the + * EmfPlusCustomLineCapData object for outlining the custom line cap. + */ + private static final BitField LINE_PATH = BitFieldFactory.getInstance(0x00000002); - // A 32-bit unsigned integer that specifies the value in the LineJoin enumeration, which specifies how - // to join two lines that are drawn by the same pen and whose ends meet. At the intersection of the two - // line ends, a line join makes the connection look more continuous. - join = EmfPlusLineJoin.valueOf(leis.readInt()); - // A 32-bit floating-point value that contains the limit of the thickness of the join on a mitered corner - // by setting the maximum allowed ratio of miter length to line width. - mitterLimit = leis.readFloat(); + private int dataFlags; + private EmfPlusLineCapType baseCap; + private double baseInset; + private EmfPlusLineCapType startCap; + private EmfPlusLineCapType endCap; + private EmfPlusLineJoin join; + private double mitterLimit; + private double widthScale; + private final Point2D fillHotSpot = new Point2D.Double(); + private final Point2D lineHotSpot = new Point2D.Double(); + private EmfPlusPath fillPath; + private EmfPlusPath outlinePath; - // A 32-bit floating-point value that specifies the amount by which to scale the custom line cap with - // respect to the width of the EmfPlusPen object that is used to draw the lines. - widthScale = leis.readFloat(); + @Override + public long init(LittleEndianInputStream leis) throws IOException { + // A 32-bit unsigned integer that specifies the data in the OptionalData field. + // This value MUST be composed of CustomLineCapData flags + dataFlags = leis.readInt(); - int size = 8* LittleEndianConsts.INT_SIZE; + // A 32-bit unsigned integer that specifies the value from the LineCap enumeration on which + // the custom line cap is based. + baseCap = EmfPlusLineCapType.valueOf(leis.readInt()); - // An EmfPlusPointF object that is not currently used. It MUST be set to {0.0, 0.0}. - size += readPointF(leis, fillHotSpot); - size += readPointF(leis, lineHotSpot); + // A 32-bit floating-point value that specifies the distance between the + // beginning of the line cap and the end of the line. + baseInset = leis.readFloat(); - if (FILL_PATH.isSet(dataFlags)) { - fillPath = new EmfPlusPath(); - size += fillPath.init(leis, -1, null, -1); - } + // A 32-bit unsigned integer that specifies the value in the LineCap enumeration that indicates the line + // cap used at the start/end of the line to be drawn. + startCap = EmfPlusLineCapType.valueOf(leis.readInt()); + endCap = EmfPlusLineCapType.valueOf(leis.readInt()); - if (LINE_PATH.isSet(dataFlags)) { - outlinePath = new EmfPlusPath(); - size += outlinePath.init(leis, -1, null, -1); - } + // A 32-bit unsigned integer that specifies the value in the LineJoin enumeration, which specifies how + // to join two lines that are drawn by the same pen and whose ends meet. At the intersection of the two + // line ends, a line join makes the connection look more continuous. + join = EmfPlusLineJoin.valueOf(leis.readInt()); + + // A 32-bit floating-point value that contains the limit of the thickness of the join on a mitered corner + // by setting the maximum allowed ratio of miter length to line width. + mitterLimit = leis.readFloat(); + + // A 32-bit floating-point value that specifies the amount by which to scale the custom line cap with + // respect to the width of the EmfPlusPen object that is used to draw the lines. + widthScale = leis.readFloat(); - return size; + int size = 8* LittleEndianConsts.INT_SIZE; + + // An EmfPlusPointF object that is not currently used. It MUST be set to {0.0, 0.0}. + size += readPointF(leis, fillHotSpot); + size += readPointF(leis, lineHotSpot); + + if (FILL_PATH.isSet(dataFlags)) { + fillPath = new EmfPlusPath(); + size += fillPath.init(leis, -1, null, -1); + } + + if (LINE_PATH.isSet(dataFlags)) { + outlinePath = new EmfPlusPath(); + size += outlinePath.init(leis, -1, null, -1); } + + return size; } + } - public static class EmfPlusAdjustableArrowCap implements EmfPlusCustomLineCap { - private double width; - private double height; - private double middleInset; - private boolean isFilled; - private EmfPlusLineCapType startCap; - private EmfPlusLineCapType endCap; - private EmfPlusLineJoin join; - private double mitterLimit; - private double widthScale; - private final Point2D fillHotSpot = new Point2D.Double(); - private final Point2D lineHotSpot = new Point2D.Double(); - - @Override - public long init(LittleEndianInputStream leis) throws IOException { - // A 32-bit floating-point value that specifies the width of the arrow cap. - // The width of the arrow cap is scaled by the width of the EmfPlusPen object that is used to draw the - // line being capped. For example, when drawing a capped line with a pen that has a width of 5 pixels, - // and the adjustable arrow cap object has a width of 3, the actual arrow cap is drawn 15 pixels wide. - width = leis.readFloat(); - - // A 32-bit floating-point value that specifies the height of the arrow cap. - // The height of the arrow cap is scaled by the width of the EmfPlusPen object that is used to draw the - // line being capped. For example, when drawing a capped line with a pen that has a width of 5 pixels, - // and the adjustable arrow cap object has a height of 3, the actual arrow cap is drawn 15 pixels high. - height = leis.readFloat(); - - // A 32-bit floating-point value that specifies the number of pixels between the outline of the arrow - // cap and the fill of the arrow cap. - middleInset = leis.readFloat(); - - // A 32-bit Boolean value that specifies whether the arrow cap is filled. - // If the arrow cap is not filled, only the outline is drawn. - isFilled = (leis.readInt() != 0); - - // A 32-bit unsigned integer that specifies the value in the LineCap enumeration that indicates - // the line cap to be used at the start/end of the line to be drawn. - startCap = EmfPlusLineCapType.valueOf(leis.readInt()); - endCap = EmfPlusLineCapType.valueOf(leis.readInt()); + public static class EmfPlusAdjustableArrowCap implements EmfPlusCustomLineCap { + private double width; + private double height; + private double middleInset; + private boolean isFilled; + private EmfPlusLineCapType startCap; + private EmfPlusLineCapType endCap; + private EmfPlusLineJoin join; + private double mitterLimit; + private double widthScale; + private final Point2D fillHotSpot = new Point2D.Double(); + private final Point2D lineHotSpot = new Point2D.Double(); - // 32-bit unsigned integer that specifies the value in the LineJoin enumeration that specifies how to - // join two lines that are drawn by the same pen and whose ends meet. At the intersection of the two - // line ends, a line join makes the connection look more continuous. - join = EmfPlusLineJoin.valueOf(leis.readInt()); + @Override + public long init(LittleEndianInputStream leis) throws IOException { + // A 32-bit floating-point value that specifies the width of the arrow cap. + // The width of the arrow cap is scaled by the width of the EmfPlusPen object that is used to draw the + // line being capped. For example, when drawing a capped line with a pen that has a width of 5 pixels, + // and the adjustable arrow cap object has a width of 3, the actual arrow cap is drawn 15 pixels wide. + width = leis.readFloat(); - // A 32-bit floating-point value that specifies the limit of the thickness of the join on a mitered - // corner by setting the maximum allowed ratio of miter length to line width. - mitterLimit = leis.readFloat(); + // A 32-bit floating-point value that specifies the height of the arrow cap. + // The height of the arrow cap is scaled by the width of the EmfPlusPen object that is used to draw the + // line being capped. For example, when drawing a capped line with a pen that has a width of 5 pixels, + // and the adjustable arrow cap object has a height of 3, the actual arrow cap is drawn 15 pixels high. + height = leis.readFloat(); - // A 32-bit floating-point value that specifies the amount by which to scale an EmfPlusCustomLineCap - // object with respect to the width of the graphics pen that is used to draw the lines. - widthScale = leis.readFloat(); + // A 32-bit floating-point value that specifies the number of pixels between the outline of the arrow + // cap and the fill of the arrow cap. + middleInset = leis.readFloat(); - int size = 9 * LittleEndianConsts.INT_SIZE; + // A 32-bit Boolean value that specifies whether the arrow cap is filled. + // If the arrow cap is not filled, only the outline is drawn. + isFilled = (leis.readInt() != 0); - // An EmfPlusPointF object that is not currently used. It MUST be set to {0.0, 0.0}. - size += readPointF(leis, fillHotSpot); + // A 32-bit unsigned integer that specifies the value in the LineCap enumeration that indicates + // the line cap to be used at the start/end of the line to be drawn. + startCap = EmfPlusLineCapType.valueOf(leis.readInt()); + endCap = EmfPlusLineCapType.valueOf(leis.readInt()); - // An EmfPlusPointF object that is not currently used. It MUST be set to {0.0, 0.0}. - size += readPointF(leis, lineHotSpot); + // 32-bit unsigned integer that specifies the value in the LineJoin enumeration that specifies how to + // join two lines that are drawn by the same pen and whose ends meet. At the intersection of the two + // line ends, a line join makes the connection look more continuous. + join = EmfPlusLineJoin.valueOf(leis.readInt()); - return size; - } - } + // A 32-bit floating-point value that specifies the limit of the thickness of the join on a mitered + // corner by setting the maximum allowed ratio of miter length to line width. + mitterLimit = leis.readFloat(); + // A 32-bit floating-point value that specifies the amount by which to scale an EmfPlusCustomLineCap + // object with respect to the width of the graphics pen that is used to draw the lines. + widthScale = leis.readFloat(); + + int size = 9 * LittleEndianConsts.INT_SIZE; + + // An EmfPlusPointF object that is not currently used. It MUST be set to {0.0, 0.0}. + size += readPointF(leis, fillHotSpot); + + // An EmfPlusPointF object that is not currently used. It MUST be set to {0.0, 0.0}. + size += readPointF(leis, lineHotSpot); + + 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 808c166360..c074b66704 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 @@ -20,7 +20,7 @@ package org.apache.poi.hemf.record.emfplus; import java.io.IOException; -import org.apache.poi.hemf.record.emf.HemfRecordType; +import org.apache.poi.hemf.draw.HemfGraphics; import org.apache.poi.util.Internal; import org.apache.poi.util.LittleEndianInputStream; @@ -45,4 +45,13 @@ public interface HemfPlusRecord { */ long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException; + + + /** + * Draws the record, the default redirects to the parent WMF record drawing + * @param ctx the drawing context + */ + default void draw(HemfGraphics ctx) { + } + } 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 d3f3002f99..7caeff1f3b 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 @@ -21,9 +21,11 @@ import static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF; import java.awt.geom.Rectangle2D; import java.io.IOException; +import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; +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; import org.apache.poi.hemf.record.emfplus.HemfPlusObject.EmfPlusObjectType; @@ -92,12 +94,12 @@ public class HemfPlusRegion { public static class EmfPlusRegion implements EmfPlusObjectData { - private final EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion(); + private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion(); private EmfPlusRegionNodeData regionNode; @Override public long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException { - long size = version.init(leis); + long size = graphicsVersion.init(leis); // A 32-bit unsigned integer that specifies the number of child nodes in the RegionNode field. int nodeCount = leis.readInt(); @@ -111,7 +113,15 @@ public class HemfPlusRegion { return size; } + @Override + public void applyObject(HemfGraphics ctx, List continuedObjectData) { + + } + @Override + public EmfPlusGraphicsVersion getGraphicsVersion() { + return graphicsVersion; + } } diff --git a/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfEmbeddedIterator.java b/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfEmbeddedIterator.java index 751f102b9e..3b3b7fa393 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfEmbeddedIterator.java +++ b/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfEmbeddedIterator.java @@ -267,36 +267,7 @@ public class HemfEmbeddedIterator implements Iterator { * Compress GDIs internal format to something useful */ private void compressGDIBitmap(EmfPlusImage img, HwmfEmbedded emb, HwmfEmbeddedType et) { - final int width = img.getBitmapWidth(); - final int height = img.getBitmapHeight(); - final int stride = img.getBitmapStride(); - final EmfPlusPixelFormat pf = img.getPixelFormat(); - - int[] nBits, bOffs; - switch (pf) { - case ARGB_32BPP: - nBits = new int[]{8, 8, 8, 8}; - bOffs = new int[]{2, 1, 0, 3}; - break; - case RGB_24BPP: - nBits = new int[]{8, 8, 8}; - bOffs = new int[]{2, 1, 0}; - break; - default: - throw new RuntimeException("not yet implemented"); - } - - ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_sRGB); - ComponentColorModel cm = new ComponentColorModel - (cs, nBits, pf.isAlpha(), pf.isPreMultiplied(), Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); - PixelInterleavedSampleModel csm = - new PixelInterleavedSampleModel(cm.getTransferType(), width, height, cm.getNumColorComponents(), stride, bOffs); - - byte d[] = emb.getRawData(); - WritableRaster raster = (WritableRaster) Raster.createRaster(csm, new DataBufferByte(d, d.length), null); - - BufferedImage bi = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); - + BufferedImage bi = img.readGDIImage(emb.getRawData()); try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); // use HwmfEmbeddedType literal for conversion 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 0915fe5f27..6e062cdc48 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java +++ b/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java @@ -46,7 +46,6 @@ import org.apache.poi.util.Units; */ @Internal public class HemfPicture implements Iterable { - private final LittleEndianInputStream stream; private final List records = new ArrayList<>(); private boolean isParsed = false; @@ -96,32 +95,52 @@ public class HemfPicture implements Iterable { } /** - * Return the image size in points + * Returns the bounding box in device-independent units. Usually this is taken from the placeable header. * - * @return the image size in points + * @return the bounding box */ - public Dimension2D getSize() { + public Rectangle2D getBounds() { HemfHeader header = (HemfHeader)getRecords().get(0); - final double coeff = (double) Units.EMU_PER_CENTIMETER / Units.EMU_PER_POINT / 10.; Rectangle2D dim = header.getFrameRectangle(); + double x = dim.getX(), y = dim.getY(); double width = dim.getWidth(), height = dim.getHeight(); - if (dim.isEmpty() || Math.rint(width*coeff) == 0 || Math.rint(height*coeff) == 0) { + if (dim.isEmpty() || Math.rint(width) == 0 || Math.rint(height) == 0) { for (HemfRecord r : getRecords()) { if (r instanceof HemfWindowing.EmfSetWindowExtEx) { - Dimension2D d = ((HemfWindowing.EmfSetWindowExtEx)r).getSize(); + HemfWindowing.EmfSetWindowExtEx extEx = (HemfWindowing.EmfSetWindowExtEx)r; + Dimension2D d = extEx.getSize(); width = d.getWidth(); height = d.getHeight(); // keep searching - sometimes there's another record } + if (r instanceof HemfWindowing.EmfSetWindowOrgEx) { + HemfWindowing.EmfSetWindowOrgEx orgEx = (HemfWindowing.EmfSetWindowOrgEx)r; + x = orgEx.getX(); + y = orgEx.getY(); + } } } - if (Math.rint(width*coeff) == 0 || Math.rint(height*coeff) == 0) { - width = 100; - height = 100; + return new Rectangle2D.Double(x, y, width, height); + } + + /** + * Return the image size in points + * + * @return the image size in points + */ + public Dimension2D getSize() { + final Rectangle2D bounds = getBounds(); + + if (bounds.isEmpty()) { + return new Dimension2DDouble(100,100); } - return new Dimension2DDouble(Math.abs(width*coeff), Math.abs(height*coeff)); + final double coeff = (double) Units.EMU_PER_CENTIMETER / Units.EMU_PER_POINT / 10.; + double width = Math.abs(bounds.getWidth()*coeff); + double height = Math.abs(bounds.getHeight()*coeff); + + return new Dimension2DDouble(width, height); } 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 744c995efe..a4c2774945 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java @@ -17,8 +17,10 @@ package org.apache.poi.hwmf.draw; +import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; +import java.awt.Composite; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.Paint; @@ -132,6 +134,9 @@ public class HwmfGraphics { public void fill(Shape shape) { HwmfDrawProperties prop = getProperties(); + + Composite old = graphicsCtx.getComposite(); + graphicsCtx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); if (prop.getBrushStyle() != HwmfBrushStyle.BS_NULL) { if (prop.getBkMode() == HwmfBkMode.OPAQUE) { graphicsCtx.setPaint(prop.getBackgroundColor().getColor()); @@ -141,20 +146,22 @@ public class HwmfGraphics { graphicsCtx.setPaint(getFill()); graphicsCtx.fill(shape); } + graphicsCtx.setComposite(old); draw(shape); } protected BasicStroke getStroke() { + HwmfDrawProperties prop = getProperties(); + HwmfPenStyle ps = prop.getPenStyle(); // TODO: fix line width calculation - float width = (float)getProperties().getPenWidth(); + float width = (float)prop.getPenWidth(); if (width == 0) { width = 1; } - HwmfPenStyle ps = getProperties().getPenStyle(); int cap = ps.getLineCap().awtFlag; int join = ps.getLineJoin().awtFlag; - float miterLimit = (float)getProperties().getPenMiterLimit(); + float miterLimit = (float)prop.getPenMiterLimit(); float[] dashes = ps.getLineDashes(); boolean dashAlt = ps.isAlternateDash(); // This value is not an integer index into the dash pattern array. @@ -602,7 +609,14 @@ public class HwmfGraphics { 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); + if (prop.getBkMode() == HwmfBkMode.OPAQUE) { + graphicsCtx.drawImage(img, 0, 0, prop.getBackgroundColor().getColor(), null); + } else { + Composite old = graphicsCtx.getComposite(); + graphicsCtx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER)); + graphicsCtx.drawImage(img, 0, 0, null); + graphicsCtx.setComposite(old); + } graphicsCtx.setTransform(at); graphicsCtx.setClip(clip); 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 733d0fca06..6f8b3aa045 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfDraw.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfDraw.java @@ -18,6 +18,7 @@ package org.apache.poi.hwmf.record; import java.awt.Shape; +import java.awt.geom.AffineTransform; import java.awt.geom.Arc2D; import java.awt.geom.Area; import java.awt.geom.Dimension2D; @@ -749,17 +750,17 @@ public class HwmfDraw { @Internal public static String pointToString(Point2D point) { - return "{ x: "+point.getX()+", y: "+point.getY()+" }"; + return (point == null) ? "null" : "{ x: "+point.getX()+", y: "+point.getY()+" }"; } @Internal public static String boundsToString(Rectangle2D bounds) { - return "{ x: "+bounds.getX()+", y: "+bounds.getY()+", w: "+bounds.getWidth()+", h: "+bounds.getHeight()+" }"; + return (bounds == null) ? "null" : "{ 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()+" }"; + return (dim == null) ? "null" : "{ w: "+dim.getWidth()+", h: "+dim.getHeight()+" }"; } @Internal @@ -772,5 +773,4 @@ public class HwmfDraw { Math.abs(bounds.getHeight()) ); } - } diff --git a/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/HemfPictureTest.java b/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/HemfPictureTest.java index 5caa4afe12..b3d5065325 100644 --- a/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/HemfPictureTest.java +++ b/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/HemfPictureTest.java @@ -24,22 +24,14 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.awt.geom.Point2D; -import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.FileWriter; -import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.stream.Stream; import org.apache.poi.POIDataSamples; import org.apache.poi.hemf.record.emf.HemfComment; @@ -64,11 +56,10 @@ public class HemfPictureTest { private static final POIDataSamples ss_samples = POIDataSamples.getSpreadSheetInstance(); private static final POIDataSamples sl_samples = POIDataSamples.getSlideShowInstance(); - /* - @Test +/* @Test @Ignore("Only for manual tests - need to add org.tukaani:xz:1.8 for this to work") public void paint() throws IOException { - byte buf[] = new byte[50_000_000]; + final byte buf[] = new byte[50_000_000]; // good test samples to validate rendering: // emfs/commoncrawl2/NB/NBWN2YH5VFCLZRFDQU7PB7IDD4UKY7DN_2.emf @@ -78,15 +69,16 @@ public class HemfPictureTest { final boolean writeLog = false; final boolean dumpRecords = false; - final boolean savePng = false; - final boolean dumpEmbedded = true; + final boolean savePng = true; + final boolean dumpEmbedded = false; Set passed = new HashSet<>(); - try (BufferedWriter sucWrite = parseEmfLog(passed, "emf-success.txt"); - BufferedWriter parseError = parseEmfLog(passed, "emf-parse.txt"); - BufferedWriter renderError = parseEmfLog(passed, "emf-render.txt"); - SevenZFile sevenZFile = new SevenZFile(new File("tmp/plus_emf.7z"))) { + try (BufferedWriter sucWrite = parseEmfLog(passed, "emf-success.txt") + ;BufferedWriter parseError = parseEmfLog(passed, "emf-parse.txt") + ;BufferedWriter renderError = parseEmfLog(passed, "emf-render.txt") + ;SevenZFile sevenZFile = new SevenZFile(new File("tmp/plus_emf.7z")) + ) { for (int idx=0;;idx++) { SevenZArchiveEntry entry = sevenZFile.getNextEntry(); if (entry == null) break; @@ -94,10 +86,14 @@ public class HemfPictureTest { if (entry.isDirectory() || !etName.endsWith(".emf") || passed.contains(etName)) continue; - // if (!etName.equals("emfs/commoncrawl2/2S/2SYMYPLNJURGCXJKLNZCJQGIBHVMQTRS_0.emf")) continue; - // emfs/commoncrawl2/ZJ/ZJT2BZPLQR7DKSKYLYL6GRDEUM2KIO5F_4.emf - // emfs/govdocs1/005/005203.ppt_3.emf + // KEEDHN6XES4EKK52E3AJHKCARNTQF7PO_0.emf takes ages, time is spent while drawing paths +// if (!etName.contains("KEEDHN6XES4EKK52E3AJHKCARNTQF7PO_0.emf")) continue; + + // F7GK5XOLERFURVTQALOCX3GJ6FH45LNQ strange colors + // ISS3ANIX2PL4PXR7SZSJSPBZI7YQQE3U stroke wrong +// if (!etName.contains("ISS3ANIX2PL4PXR7SZSJSPBZI7YQQE3U")) continue; + System.out.println(etName); @@ -129,9 +125,9 @@ public class HemfPictureTest { int embIdx = 0; for (HwmfEmbedded emb : emf.getEmbeddings()) { final File embName = new File("build/tmp", "emb_"+etName.replaceFirst(".+/", "").replace(".emf", "_"+embIdx + emb.getEmbeddedType().extension) ); -// try (FileOutputStream fos = new FileOutputStream(embName)) { -// fos.write(emb.getRawData()); -// } + try (FileOutputStream fos = new FileOutputStream(embName)) { + fos.write(emb.getRawData()); + } embIdx++; } } @@ -154,17 +150,19 @@ public class HemfPictureTest { BufferedImage bufImg = new BufferedImage((int)width, (int)height, BufferedImage.TYPE_INT_ARGB); g = bufImg.createGraphics(); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); - g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED); + g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); g.setComposite(AlphaComposite.Clear); g.fillRect(0, 0, (int)width, (int)height); g.setComposite(AlphaComposite.Src); + final File pngName = new File("build/tmp", etName.replaceFirst(".+/", "").replace(".emf", ".png")); + emf.draw(g, new Rectangle2D.Double(0, 0, width, height)); - final File pngName = new File("build/tmp", etName.replaceFirst(".+/", "").replace(".emf", ".png")); if (savePng) { ImageIO.write(bufImg, "PNG", pngName); } @@ -186,7 +184,7 @@ public class HemfPictureTest { } } } - } */ + } private static int hashException(Throwable e) { StringBuilder sb = new StringBuilder(); @@ -222,7 +220,7 @@ public class HemfPictureTest { } return Files.newBufferedWriter(log, StandardCharsets.UTF_8, soo); - } + }*/ @Test public void testBasicWindows() throws Exception { -- 2.39.5