From ea0e01acc11dbec8a120a7583535935abe3f8f6c Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Sat, 4 May 2019 23:01:53 +0000 Subject: [PATCH] Bug 60656 - EMF image support in slideshows git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1858625 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/poifs/filesystem/FileMagic.java | 46 +- .../poi/util/LittleEndianInputStream.java | 3 + .../poi/hemf/record/emf/HemfComment.java | 16 + .../apache/poi/hemf/record/emf/HemfFill.java | 2 +- .../poi/hemf/record/emf/HemfPalette.java | 55 ++ .../poi/hemf/record/emf/HemfRecordType.java | 2 +- .../poi/hemf/record/emfplus/HemfPlusDraw.java | 609 ++++++++++++++++++ .../hemf/record/emfplus/HemfPlusHeader.java | 69 +- .../poi/hemf/record/emfplus/HemfPlusMisc.java | 346 ++++++++++ .../hemf/record/emfplus/HemfPlusObject.java | 588 +++++++++++++++++ .../emfplus/HemfPlusRecordIterator.java | 3 +- .../record/emfplus/HemfPlusRecordType.java | 56 +- .../hemf/usermodel/HemfEmbeddedIterator.java | 342 ++++++++++ .../poi/hemf/usermodel/HemfPicture.java | 4 + .../apache/poi/hwmf/draw/HwmfGraphics.java | 2 +- .../apache/poi/hwmf/record/HwmfBitmapDib.java | 20 +- .../apache/poi/hwmf/record/HwmfEscape.java | 290 ++++++--- .../org/apache/poi/hwmf/record/HwmfFill.java | 61 +- .../org/apache/poi/hwmf/record/HwmfMisc.java | 20 +- .../poi/hwmf/record/HwmfPlaceableHeader.java | 8 +- .../poi/hwmf/usermodel/HwmfEmbedded.java | 49 ++ .../hwmf/usermodel/HwmfEmbeddedIterator.java | 140 ++++ .../poi/hwmf/usermodel/HwmfEmbeddedType.java | 37 ++ .../poi/hwmf/usermodel/HwmfPicture.java | 6 + .../poi/hemf/usermodel/HemfPictureTest.java | 56 +- .../org/apache/poi/hwmf/TestHwmfParsing.java | 30 +- test-data/slideshow/nested_wmf.emf | Bin 0 -> 40276 bytes 27 files changed, 2696 insertions(+), 164 deletions(-) create mode 100644 src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusDraw.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusMisc.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusObject.java create mode 100644 src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfEmbeddedIterator.java create mode 100644 src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfEmbedded.java create mode 100644 src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfEmbeddedIterator.java create mode 100644 src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfEmbeddedType.java create mode 100644 test-data/slideshow/nested_wmf.emf diff --git a/src/java/org/apache/poi/poifs/filesystem/FileMagic.java b/src/java/org/apache/poi/poifs/filesystem/FileMagic.java index 9cc0f96a18..79e8253b57 100644 --- a/src/java/org/apache/poi/poifs/filesystem/FileMagic.java +++ b/src/java/org/apache/poi/poifs/filesystem/FileMagic.java @@ -48,21 +48,21 @@ public enum FileMagic { 0x09, 0x00, // sid=0x0009 0x04, 0x00, // size=0x0004 0x00, 0x00, // unused - 0x70, 0x00 // 0x70 = multiple values + '?', 0x00 // '?' = multiple values }), /** BIFF3 raw stream - for Excel 3 */ BIFF3(new byte[]{ 0x09, 0x02, // sid=0x0209 0x06, 0x00, // size=0x0006 0x00, 0x00, // unused - 0x70, 0x00 // 0x70 = multiple values + '?', 0x00 // '?' = multiple values }), /** BIFF4 raw stream - for Excel 4 */ BIFF4(new byte[]{ 0x09, 0x04, // sid=0x0409 0x06, 0x00, // size=0x0006 0x00, 0x00, // unused - 0x70, 0x00 // 0x70 = multiple values + '?', 0x00 // '? = multiple values },new byte[]{ 0x09, 0x04, // sid=0x0409 0x06, 0x00, // size=0x0006 @@ -78,18 +78,22 @@ public enum FileMagic { /** PDF document */ PDF("%PDF"), /** Some different HTML documents */ - HTML(" Integer.MAX_VALUE) { throw new IOException("can't skip further than "+Integer.MAX_VALUE); 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 c9ea7481e1..eee11cfc9f 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 @@ -221,6 +221,10 @@ public class HemfComment { return privateData.length; } + public byte[] getPrivateData() { + return privateData; + } + @Override public String toString() { return "\""+new String(privateData, LocaleUtil.CHARSET_1252).replaceAll("\\p{Cntrl}", ".")+"\""; @@ -350,7 +354,15 @@ public class HemfComment { } public enum EmfFormatSignature { + /** + * The value of this member is the sequence of ASCII characters "FME ", + * which happens to be the reverse of the string "EMF", and it denotes EMF record data. + */ ENHMETA_SIGNATURE(0x464D4520), + /** + * The value of this member is the sequence of ASCII characters "FSPE", which happens to be the reverse + * of the string "EPSF", and it denotes encapsulated PostScript (EPS) format data. + */ EPS_SIGNATURE(0x46535045); int id; @@ -403,6 +415,10 @@ public class HemfComment { public byte[] getRawData() { return rawData; } + + public EmfFormatSignature getSignature() { + return signature; + } } public static class EmfCommentDataWMF implements EmfCommentData { 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 8e19e87116..a380d6c3fa 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 @@ -682,7 +682,7 @@ public class HemfFill { return 4 * LittleEndianConsts.INT_SIZE; } - static int readXForm(LittleEndianInputStream leis, AffineTransform xform) { + public static int readXForm(LittleEndianInputStream leis, AffineTransform xform) { // mapping = // m00 (scaleX) = eM11 (Horizontal scaling component) diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfPalette.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfPalette.java index 9811cb2c3d..3d002f0044 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfPalette.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfPalette.java @@ -151,4 +151,59 @@ public class HemfPalette { return 0; } } + + /** + * The EMR_SETICMMODE record specifies the mode of Image Color Management (ICM) for graphics operations. + */ + public static class EmfSetIcmMode implements HemfRecord { + /** The ICMMode enumeration defines values that specify when to turn on and off ICM. */ + public enum ICMMode { + /** + * Turns off Image Color Management (ICM) in the playback device context. + * Turns on old-style color correction of halftones. + */ + ICM_OFF(0x01), + /** + * Turns on ICM in the playback device context. + * Turns off old-style color correction of halftones. + */ + ICM_ON(0x02), + /** + * Queries the current state of color management in the playback device context. + */ + ICM_QUERY(0x03), + /** + * Turns off ICM in the playback device context, and turns off old-style color correction of halftones. + */ + ICM_DONE_OUTSIDEDC(0x04) + ; + + public final int id; + + ICMMode(int id) { + this.id = id; + } + + public static ICMMode valueOf(int id) { + for (ICMMode wrt : values()) { + if (wrt.id == id) return wrt; + } + return null; + } + + } + + private ICMMode icmMode; + + @Override + public HemfRecordType getEmfRecordType() { + return HemfRecordType.seticmmode; + } + + @Override + public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException { + icmMode = ICMMode.valueOf(leis.readInt()); + return LittleEndianConsts.INT_SIZE; + } + } } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordType.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordType.java index 9d25a9c145..0066c8c16a 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordType.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordType.java @@ -121,7 +121,7 @@ public enum HemfRecordType { extCreatePen(0x0000005F, HemfMisc.EmfExtCreatePen::new), polytextouta(0x00000060, HemfText.PolyTextOutA::new), polytextoutw(0x00000061, HemfText.PolyTextOutW::new), - seticmmode(0x00000062, UnimplementedHemfRecord::new), + seticmmode(0x00000062, HemfPalette.EmfSetIcmMode::new), createcolorspace(0x00000063, UnimplementedHemfRecord::new), setcolorspace(0x00000064, UnimplementedHemfRecord::new), deletecolorspace(0x00000065, UnimplementedHemfRecord::new), 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 new file mode 100644 index 0000000000..8f9a13ba02 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusDraw.java @@ -0,0 +1,609 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hemf.record.emfplus; + +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.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; +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.record.emf.HemfFill; +import org.apache.poi.hemf.record.emfplus.HemfPlusMisc.EmfPlusObjectId; +import org.apache.poi.util.BitField; +import org.apache.poi.util.BitFieldFactory; +import org.apache.poi.util.IOUtils; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianConsts; +import org.apache.poi.util.LittleEndianInputStream; +import org.apache.poi.util.StringUtil; + +public class HemfPlusDraw { + private static final int MAX_OBJECT_SIZE = 1_000_000; + + public enum UnitType { + /** Specifies a unit of logical distance within the world space. */ + World(0x00), + /** Specifies a unit of distance based on the characteristics of the physical display. */ + Display(0x01), + /** Specifies a unit of 1 pixel. */ + Pixel(0x02), + /** Specifies a unit of 1 printer's point, or 1/72 inch. */ + Point(0x03), + /** Specifies a unit of 1 inch. */ + Inch(0x04), + /** Specifies a unit of 1/300 inch. */ + Document(0x05), + /** Specifies a unit of 1 millimeter. */ + Millimeter(0x06) + ; + + public final int id; + + UnitType(int id) { + this.id = id; + } + + public static UnitType valueOf(int id) { + for (UnitType wrt : values()) { + if (wrt.id == id) return wrt; + } + return null; + } + + } + + public interface EmfPlusCompressed { + /** + * This bit indicates whether the data in the RectData field is compressed. + * If set, RectData contains an EmfPlusRect object. + * If clear, RectData contains an EmfPlusRectF object object. + */ + BitField COMPRESSED = BitFieldFactory.getInstance(0x4000); + + int getFlags(); + + /** + * The index in the EMF+ Object Table to associate with the object + * created by this record. The value MUST be zero to 63, inclusive. + */ + default boolean isCompressed() { + return COMPRESSED.isSet(getFlags()); + } + + default BiFunction getReadRect() { + return isCompressed() ? HemfPlusDraw::readRectS : HemfPlusDraw::readRectF; + } + } + + + + /** + * The EmfPlusDrawPath record specifies drawing a graphics path + */ + public static class EmfPlusDrawPath implements HemfPlusRecord { + private int flags; + private int penId; + + @Override + public HemfPlusRecordType getEmfPlusRecordType() { + return HemfPlusRecordType.drawPath; + } + + @Override + public int getFlags() { + return flags; + } + + @Override + public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { + this.flags = flags; + + // A 32-bit unsigned integer that specifies an index in the EMF+ Object Table for an EmfPlusPen object + // to use for drawing the EmfPlusPath. The value MUST be zero to 63, inclusive. + penId = leis.readInt(); + assert (0 <= penId && penId <= 63); + + assert (dataSize == LittleEndianConsts.INT_SIZE); + + return LittleEndianConsts.INT_SIZE; + } + } + + /** + * 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); + + private int flags; + private int brushId; + private final ArrayList rectData = new ArrayList<>(); + + @Override + public HemfPlusRecordType getEmfPlusRecordType() { + return HemfPlusRecordType.fillRects; + } + + @Override + public int getFlags() { + return flags; + } + + @Override + public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { + this.flags = flags; + + // A 32-bit unsigned integer that defines the brush, the content of which is + // determined by the S bit in the Flags field. + brushId = leis.readInt(); + + // A 32-bit unsigned integer that specifies the number of rectangles in the RectData field. + int count = leis.readInt(); + + BiFunction readRect = getReadRect(); + + rectData.ensureCapacity(count); + + int size = 2 * LittleEndianConsts.INT_SIZE; + for (int i = 0; i readPoint; + + if (POSITION.isSet(flags)) { + // If the POSITION flag is set in the Flags, the points specify relative locations. + readPoint = HemfPlusDraw::readPointR; + } else if (isCompressed()) { + // If the POSITION bit is clear and the COMPRESSED bit is set in the Flags field, the points + // specify absolute locations with integer values. + readPoint = HemfPlusDraw::readPointS; + } else { + // If the POSITION bit is clear and the COMPRESSED bit is clear in the Flags field, the points + // specify absolute locations with floating-point values. + readPoint = HemfPlusDraw::readPointF; + } + + // TODO: handle relative coordinates + + // An array of Count points that specify three points of a parallelogram. + // The three points represent the upper-left, upper-right, and lower-left corners of the parallelogram. + // The fourth point of the parallelogram is extrapolated from the first three. + // The portion of the image specified by the SrcRect field SHOULD have scaling and shearing transforms + // applied if necessary to fit inside the parallelogram. + + + // size += readPoint.apply(leis, upperLeft); + // size += readPoint.apply(leis, upperRight); + // size += readPoint.apply(leis, lowerLeft); + + size += readPoint.apply(leis, lowerLeft); + size += readPoint.apply(leis, lowerRight); + size += readPoint.apply(leis, upperLeft); + + // https://math.stackexchange.com/questions/2772737/how-to-transform-arbitrary-rectangle-into-specific-parallelogram + + RealMatrix para2normal = MatrixUtils.createRealMatrix(new double[][] { + { lowerLeft.getX(), lowerRight.getX(), upperLeft.getX() }, + { lowerLeft.getY(), lowerRight.getY(), upperLeft.getY() }, + { 1, 1, 1 } + }); + + RealMatrix rect2normal = MatrixUtils.createRealMatrix(new double[][]{ + { srcRect.getMinX(), srcRect.getMaxX(), srcRect.getMinX() }, + { srcRect.getMinY(), srcRect.getMinY(), srcRect.getMaxY() }, + { 1, 1, 1 } + }); + + RealMatrix normal2rect = new LUDecomposition(rect2normal).getSolver().getInverse(); + double[][] m = para2normal.multiply(normal2rect).getData(); + trans.setTransform(round10(m[0][0]), round10(m[1][0]), round10(m[0][1]), round10(m[1][1]), round10(m[0][2]), round10(m[1][2])); + + return size; + } + } + + /** The EmfPlusDrawImage record specifies drawing a scaled image. */ + public static class EmfPlusDrawImage implements HemfPlusRecord, EmfPlusObjectId, EmfPlusCompressed { + private int flags; + private int imageAttributesID; + private UnitType srcUnit; + private final Rectangle2D srcRect = new Rectangle2D.Double(); + private final Rectangle2D rectData = new Rectangle2D.Double(); + + @Override + public HemfPlusRecordType getEmfPlusRecordType() { + return HemfPlusRecordType.drawImage; + } + + @Override + public int getFlags() { + return flags; + } + + @Override + public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { + this.flags = flags; + + // A 32-bit unsigned integer that contains the index of the + // optional EmfPlusImageAttributes object in the EMF+ Object Table. + imageAttributesID = leis.readInt(); + + // A 32-bit signed integer that defines the units of the SrcRect field. + // It MUST be the UnitPixel value of the UnitType enumeration + srcUnit = UnitType.valueOf(leis.readInt()); + assert(srcUnit == UnitType.Pixel); + + int size = 2 * LittleEndianConsts.INT_SIZE; + + // An EmfPlusRectF object that specifies a portion of the image to be rendered. The portion of the image + // specified by this rectangle is scaled to fit the destination rectangle specified by the RectData field. + size += readRectF(leis, srcRect); + + // Either an EmfPlusRect or EmfPlusRectF object that defines the bounding box of the image. The portion of + // the image specified by the SrcRect field is scaled to fit this rectangle. + size += getReadRect().apply(leis, rectData); + + return size; + } + } + + /** 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); + + private int flags; + private final byte[] brushId = new byte[LittleEndianConsts.INT_SIZE]; + + @Override + public HemfPlusRecordType getEmfPlusRecordType() { + return HemfPlusRecordType.fillRegion; + } + + @Override + public int getFlags() { + return flags; + } + + public boolean isSolidColor() { + return SOLID_COLOR.isSet(getFlags()); + } + + public int getBrushId() { + return (isSolidColor()) ? -1 : LittleEndian.getInt(brushId); + } + + public Color getSolidColor() { + return (isSolidColor()) ? new Color(brushId[2], brushId[1], brushId[0], brushId[3]) : null; + } + + @Override + public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { + this.flags = flags; + + // A 32-bit unsigned integer that defines the brush, the content of which is determined by + // the SOLID_COLOR bit in the Flags field. + // If SOLID_COLOR is set, BrushId specifies a color as an EmfPlusARGB object. + // If clear, BrushId contains the index of an EmfPlusBrush object in the EMF+ Object Table. + leis.readFully(brushId); + + return LittleEndianConsts.INT_SIZE; + } + } + + /** The EmfPlusFillPath record specifies filling the interior of a graphics path. */ + public static class EmfPlusFillPath extends EmfPlusFillRegion { + + @Override + public HemfPlusRecordType getEmfPlusRecordType() { + return HemfPlusRecordType.fillPath; + } + + } + + /** 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); + + /** + * 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. + */ + private static final BitField CMAP_LOOKUP = BitFieldFactory.getInstance(0x0001); + + /** + * If set, the string SHOULD be rendered vertically. + * If clear, the string SHOULD be rendered horizontally. + */ + private static final BitField VERTICAL = BitFieldFactory.getInstance(0x0002); + + /** + * If set, character glyph positions SHOULD be calculated relative to the position of the first glyph. + * If clear, the glyph positions SHOULD be obtained from an array of coordinates. + */ + private static final BitField REALIZED_ADVANCE = BitFieldFactory.getInstance(0x0004); + + /** + * If set, less memory SHOULD be used to cache anti-aliased glyphs, which produces lower quality text rendering. + * If clear, more memory SHOULD be used, which produces higher quality text rendering. + */ + private static final BitField LIMIT_SUBPIXEL = BitFieldFactory.getInstance(0x0008); + + + + private int flags; + private int brushId; + private int optionsFlags; + private String glyphs; + private final List glpyhPos = new ArrayList<>(); + private final AffineTransform transformMatrix = new AffineTransform(); + + @Override + public HemfPlusRecordType getEmfPlusRecordType() { + return HemfPlusRecordType.drawDriverstring; + } + + @Override + public int getFlags() { + return flags; + } + + @Override + public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { + this.flags = flags; + + // A 32-bit unsigned integer that specifies either the foreground color of the text or a graphics brush, + // depending on the value of the SOLID_COLOR flag in the Flags. + brushId = leis.readInt(); + + // A 32-bit unsigned integer that specifies the spacing, orientation, and quality of rendering for the + // string. This value MUST be composed of DriverStringOptions flags + optionsFlags = leis.readInt(); + + // A 32-bit unsigned integer that specifies whether a transform matrix is present in the + // TransformMatrix field. + boolean hasMatrix = leis.readInt() == 1; + + // A 32-bit unsigned integer that specifies number of glyphs in the string. + int glyphCount = leis.readInt(); + + int size = 4 * LittleEndianConsts.INT_SIZE; + + // TOOD: implement Non-Cmap-Lookup correctly + + // If the CMAP_LOOKUP flag in the optionsFlags field is set, each value in this array specifies a + // Unicode character. Otherwise, each value specifies an index to a character glyph in the EmfPlusFont + // object specified by the ObjectId value in Flags field. + byte[] glyphBuf = IOUtils.toByteArray(leis, glyphCount*2, MAX_OBJECT_SIZE); + glyphs = StringUtil.getFromUnicodeLE(glyphBuf); + + size += glyphBuf.length; + + // An array of EmfPlusPointF objects that specify the output position of each character glyph. + // There MUST be GlyphCount elements, which have a one-to-one correspondence with the elements + // in the Glyphs array. + // + // Glyph positions are calculated from the position of the first glyph if the REALIZED_ADVANCE flag in + // Options flags is set. In this case, GlyphPos specifies the position of the first glyph only. + int glyphPosCnt = REALIZED_ADVANCE.isSet(optionsFlags) ? 1 : glyphCount; + for (int i=0; i rectData = new ArrayList<>(); + + @Override + public HemfPlusRecordType getEmfPlusRecordType() { + return HemfPlusRecordType.drawRects; + } + + @Override + public int getFlags() { + return flags; + } + + @Override + public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { + this.flags = flags; + + // A 32-bit unsigned integer that specifies the number of rectangles in the RectData member. + int count = leis.readInt(); + int size = LittleEndianConsts.INT_SIZE; + + BiFunction readRect = getReadRect(); + + for (int i=0; i constructor; + + EmfPlusObjectType(int id, Supplier constructor) { + this.id = id; + this.constructor = constructor; + } + + public static EmfPlusObjectType valueOf(int id) { + for (EmfPlusObjectType wrt : values()) { + if (wrt.id == id) return wrt; + } + return null; + } + } + + public enum EmfPlusImageDataType { + UNKNOWN(0x00000000), + BITMAP(0x00000001), + METAFILE(0x00000002), + CONTINUED(-1); + + public final int id; + + EmfPlusImageDataType(int id) { + this.id = id; + } + + public static EmfPlusImageDataType valueOf(int id) { + for (EmfPlusImageDataType wrt : values()) { + if (wrt.id == id) return wrt; + } + return null; + } + } + + public enum EmfPlusBitmapDataType { + PIXEL(0x00000000), + COMPRESSED(0x00000001); + + public final int id; + + EmfPlusBitmapDataType(int id) { + this.id = id; + } + + public static EmfPlusBitmapDataType valueOf(int id) { + for (EmfPlusBitmapDataType wrt : values()) { + if (wrt.id == id) return wrt; + } + return null; + } + } + + public enum EmfPlusPixelFormat { + UNDEFINED(0X00000000), + INDEXED_1BPP(0X00030101), + INDEXED_4BPP(0X00030402), + INDEXED_8BPP(0X00030803), + GRAYSCALE_16BPP(0X00101004), + RGB555_16BPP(0X00021005), + RGB565_16BPP(0X00021006), + ARGB1555_16BPP(0X00061007), + RGB_24BPP(0X00021808), + RGB_32BPP(0X00022009), + ARGB_32BPP(0X0026200A), + PARGB_32BPP(0X000E200B), + RGB_48BPP(0X0010300C), + ARGB_64BPP(0X0034400D), + PARGB_64BPP(0X001A400E), + ; + + private static final BitField CANONICAL = BitFieldFactory.getInstance(0x00200000); + private static final BitField EXTCOLORS = BitFieldFactory.getInstance(0x00100000); + private static final BitField PREMULTI = BitFieldFactory.getInstance(0x00080000); + private static final BitField ALPHA = BitFieldFactory.getInstance(0x00040000); + private static final BitField GDI = BitFieldFactory.getInstance(0x00020000); + private static final BitField PALETTE = BitFieldFactory.getInstance(0x00010000); + private static final BitField BPP = BitFieldFactory.getInstance(0x0000FF00); + private static final BitField INDEX = BitFieldFactory.getInstance(0x000000FF); + + public final int id; + + EmfPlusPixelFormat(int id) { + this.id = id; + } + + public static EmfPlusPixelFormat valueOf(int id) { + for (EmfPlusPixelFormat wrt : values()) { + if (wrt.id == id) return wrt; + } + return null; + } + + /** + * The pixel format enumeration index. + */ + public int getGDIEnumIndex() { + return id == -1 ? -1 : INDEX.getValue(id); + } + + /** + * The total number of bits per pixel. + */ + public int getBitsPerPixel() { + return id == -1 ? -1 : BPP.getValue(id); + } + + /** + * If set, the pixel values are indexes into a palette. + * If clear, the pixel values are actual colors. + */ + public boolean isPaletteIndexed() { + return id != -1 && PALETTE.isSet(id); + } + + /** + * If set, the pixel format is supported in Windows GDI. + * If clear, the pixel format is not supported in Windows GDI. + */ + public boolean isGDISupported() { + return id != -1 && GDI.isSet(id); + } + + /** + * If set, the pixel format includes an alpha transparency component. + * If clear, the pixel format does not include a component that specifies transparency. + */ + public boolean isAlpha() { + return id != -1 && ALPHA.isSet(id); + } + + /** + * If set, each color component in the pixel has been premultiplied by the pixel's alpha transparency value. + * If clear, each color component is multiplied by the pixel's alpha transparency value when the source pixel + * is blended with the destination pixel. + */ + public boolean isPreMultiplied() { + return id != -1 && PREMULTI.isSet(id); + } + + /** + * If set, the pixel format supports extended colors in 16-bits per channel. + * If clear, extended colors are not supported. + */ + public boolean isExtendedColors() { + return id != -1 && EXTCOLORS.isSet(id); + } + + /** + * If set, the pixel format is "canonical", which means that 32 bits per pixel are + * supported, with 24-bits for color components and an 8-bit alpha channel. + * If clear, the pixel format is not canonical. + */ + public boolean isCanonical() { + return id != -1 && CANONICAL.isSet(id); + } + } + + public enum EmfPlusMetafileDataType { + Wmf(0x00000001), + WmfPlaceable(0x00000002), + Emf(0x00000003), + EmfPlusOnly(0x00000004), + EmfPlusDual(0x00000005); + + public final int id; + + EmfPlusMetafileDataType(int id) { + this.id = id; + } + + public static EmfPlusMetafileDataType valueOf(int id) { + for (EmfPlusMetafileDataType wrt : values()) { + if (wrt.id == id) return wrt; + } + return null; + } + } + + /** + * The WrapMode enumeration defines how the pattern from a texture or gradient brush is tiled + * across a shape or at shape boundaries, when it is smaller than the area being filled. + */ + public enum EmfPlusWrapMode { + WRAP_MODE_TILE(0x00000000), + WRAP_MODE_TILE_FLIP_X(0x00000001), + WRAP_MODE_TILE_FLIP_Y(0x00000002), + WRAP_MODE_TILE_FLIP_XY(0x00000003), + WRAP_MODE_CLAMP(0x00000004) + ; + + public final int id; + + EmfPlusWrapMode(int id) { + this.id = id; + } + + public static EmfPlusWrapMode valueOf(int id) { + for (EmfPlusWrapMode wrt : values()) { + if (wrt.id == id) return wrt; + } + return null; + } + } + + public enum EmfPlusObjectClamp { + /** The object is clamped to a rectangle. */ + RectClamp(0x00000000), + /** The object is clamped to a bitmap. */ + BitmapClamp(0x00000001) + ; + + public final int id; + + EmfPlusObjectClamp(int id) { + this.id = id; + } + + public static EmfPlusObjectClamp valueOf(int id) { + for (EmfPlusObjectClamp wrt : values()) { + if (wrt.id == id) return wrt; + } + return null; + } + } + + /** + * 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 { + + + /** + * Indicates that the object definition continues on in the next EmfPlusObject + * record. This flag is never set in the final record that defines the object. + */ + private static final BitField CONTINUABLE = BitFieldFactory.getInstance(0x8000); + + /** + * Specifies the metafileType of object to be created by this record, from the + * ObjectType enumeration + */ + private static final BitField OBJECT_TYPE = BitFieldFactory.getInstance(0x7F00); + + private int flags; + // for debugging + private int objectId; + private EmfPlusObjectData objectData; + private int totalObjectSize; + + @Override + public HemfPlusRecordType getEmfPlusRecordType() { + return HemfPlusRecordType.object; + } + + @Override + public int getFlags() { + return flags; + } + + public EmfPlusObjectType getObjectType() { + return EmfPlusObjectType.valueOf(OBJECT_TYPE.getValue(flags)); + } + + public T getObjectData() { + return (T)objectData; + } + + @Override + public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException { + this.flags = flags; + + objectId = getObjectId(); + EmfPlusObjectType objectType = getObjectType(); + assert (objectType != null); + + int size = 0; + + totalObjectSize = 0; + int dataSize2 = (int) dataSize; + + if (CONTINUABLE.isSet(flags)) { + // If the record is continuable, when the continue bit is set, this field will be present. + // Continuing objects have multiple EMF+ records starting with EmfPlusContinuedObjectRecord. + // Each EmfPlusContinuedObjectRecord will contain a TotalObjectSize. Once TotalObjectSize number + // of bytes has been read, the next EMF+ record will not be treated as part of the continuing object. + totalObjectSize = leis.readInt(); + size += LittleEndianConsts.INT_SIZE; + dataSize2 -= LittleEndianConsts.INT_SIZE; + } + + objectData = objectType.constructor.get(); + size += objectData.init(leis, dataSize2, objectType, flags); + + return size; + } + } + + public interface EmfPlusObjectData { + long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException; + } + + public static class EmfPlusUnknownData implements EmfPlusObjectData { + private EmfPlusObjectType objectType; + private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion(); + private byte[] objectDataBytes; + + @Override + public long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException { + this.objectType = objectType; + + long size = graphicsVersion.init(leis); + + objectDataBytes = IOUtils.toByteArray(leis, dataSize - size, MAX_OBJECT_SIZE); + + return dataSize; + } + } + + public static class EmfPlusImage implements EmfPlusObjectData { + private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion(); + private EmfPlusImageDataType imageDataType; + private int bitmapWidth; + private int bitmapHeight; + private int bitmapStride; + private EmfPlusPixelFormat pixelFormat; + private EmfPlusBitmapDataType bitmapType; + private byte[] imageData; + private EmfPlusMetafileDataType metafileType; + private int metafileDataSize; + + public EmfPlusImageDataType getImageDataType() { + return imageDataType; + } + + public byte[] getImageData() { + return imageData; + } + + public EmfPlusPixelFormat getPixelFormat() { + return pixelFormat; + } + + public EmfPlusBitmapDataType getBitmapType() { + return bitmapType; + } + + public int getBitmapWidth() { + return bitmapWidth; + } + + public int getBitmapHeight() { + return bitmapHeight; + } + + public int getBitmapStride() { + return bitmapStride; + } + + public EmfPlusMetafileDataType getMetafileType() { + return metafileType; + } + + @Override + public long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException { + leis.mark(LittleEndianConsts.INT_SIZE); + long size = graphicsVersion.init(leis); + + if (graphicsVersion.getGraphicsVersion() == null || graphicsVersion.getMetafileSignature() != 0xDBC01) { + // CONTINUABLE is not always correctly set, so we check the version field if this record is continued + imageDataType = EmfPlusImageDataType.CONTINUED; + leis.reset(); + size = 0; + } else { + imageDataType = EmfPlusImageDataType.valueOf(leis.readInt()); + size += LittleEndianConsts.INT_SIZE; + } + + if (imageDataType == null) { + imageDataType = EmfPlusImageDataType.UNKNOWN; + } + + int fileSize; + switch (imageDataType) { + default: + case UNKNOWN: + case CONTINUED: + bitmapWidth = -1; + bitmapHeight = -1; + bitmapStride = -1; + bitmapType = null; + pixelFormat = null; + + fileSize = (int) (dataSize); + break; + + case BITMAP: + // A 32-bit signed integer that specifies the width in pixels of the area occupied by the bitmap. + // If the image is compressed, according to the Type field, this value is undefined and MUST be ignored. + bitmapWidth = leis.readInt(); + // A 32-bit signed integer that specifies the height in pixels of the area occupied by the bitmap. + // If the image is compressed, according to the Type field, this value is undefined and MUST be ignored. + bitmapHeight = leis.readInt(); + // A 32-bit signed integer that specifies the byte offset between the beginning of one scan-line + // and the next. This value is the number of bytes per pixel, which is specified in the PixelFormat + // field, multiplied by the width in pixels, which is specified in the Width field. + // The value of this field MUST be a multiple of four. If the image is compressed, according to the + // Type field, this value is undefined and MUST be ignored. + bitmapStride = leis.readInt(); + // A 32-bit unsigned integer that specifies the format of the pixels that make up the bitmap image. + // The supported pixel formats are specified in the PixelFormat enumeration + int pixelFormatInt = leis.readInt(); + // A 32-bit unsigned integer that specifies the metafileType of data in the BitmapData field. + // This value MUST be defined in the BitmapDataType enumeration + bitmapType = EmfPlusBitmapDataType.valueOf(leis.readInt()); + size += 5 * LittleEndianConsts.INT_SIZE; + + pixelFormat = (bitmapType == EmfPlusBitmapDataType.PIXEL) + ? EmfPlusPixelFormat.valueOf(pixelFormatInt) + : EmfPlusPixelFormat.UNDEFINED; + assert (pixelFormat != null); + + fileSize = (int) (dataSize - size); + + break; + + case METAFILE: + // A 32-bit unsigned integer that specifies the type of metafile that is embedded in the + // MetafileData field. This value MUST be defined in the MetafileDataType enumeration + metafileType = EmfPlusMetafileDataType.valueOf(leis.readInt()); + + // A 32-bit unsigned integer that specifies the size in bytes of the + // metafile data in the MetafileData field. + metafileDataSize = leis.readInt(); + + size += 2 * LittleEndianConsts.INT_SIZE; + + // ignore metafileDataSize, which might ignore a (placeable) header in front + // and also use the remaining bytes, which might contain padding bytes ... + fileSize = (int) (dataSize - size); + break; + } + + assert (fileSize <= dataSize - size); + + imageData = IOUtils.toByteArray(leis, fileSize, MAX_OBJECT_SIZE); + + // TODO: remove padding bytes between placeable WMF header and body? + + return size + fileSize; + } + } + + public static class EmfPlusImageAttributes implements EmfPlusObjectData { + private final EmfPlusGraphicsVersion graphicsVersion = new EmfPlusGraphicsVersion(); + private EmfPlusWrapMode wrapMode; + private Color clampColor; + private EmfPlusObjectClamp objectClamp; + + @Override + public long init(LittleEndianInputStream leis, long dataSize, EmfPlusObjectType objectType, int flags) throws IOException { + // An EmfPlusGraphicsVersion object that specifies the version of operating system graphics that + // was used to create this object. + long size = graphicsVersion.init(leis); + + // A 32-bit field that is not used and MUST be ignored. + leis.skip(LittleEndianConsts.INT_SIZE); + + // A 32-bit unsigned integer that specifies how to handle edge conditions with a value from the WrapMode enumeration + wrapMode = EmfPlusWrapMode.valueOf(leis.readInt()); + + // An EmfPlusARGB object that specifies the edge color to use when the WrapMode value is WrapModeClamp. + // This color is visible when the source rectangle processed by an EmfPlusDrawImage record is larger than the image itself. + byte[] buf = new byte[LittleEndianConsts.INT_SIZE]; + leis.readFully(buf); + clampColor = new Color(buf[2], buf[1], buf[0], buf[3]); + + // A 32-bit signed integer that specifies the object clamping behavior. It is not used until this object + // is applied to an image being drawn. This value MUST be one of the values defined in the following table. + objectClamp = EmfPlusObjectClamp.valueOf(leis.readInt()); + + // A value that SHOULD be set to zero and MUST be ignored upon receipt. + leis.skip(LittleEndianConsts.INT_SIZE); + + return size + 5*LittleEndianConsts.INT_SIZE; + } + + public EmfPlusGraphicsVersion getGraphicsVersion() { + return graphicsVersion; + } + + public EmfPlusWrapMode getWrapMode() { + return wrapMode; + } + + public Color getClampColor() { + return clampColor; + } + + public EmfPlusObjectClamp getObjectClamp() { + return objectClamp; + } + } +} diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordIterator.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordIterator.java index 4c6f0ad919..3cf7076b39 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordIterator.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordIterator.java @@ -50,7 +50,8 @@ public class HemfPlusRecordIterator implements Iterator { @Override public HemfPlusRecord next() { HemfPlusRecord toReturn = currentRecord; - final boolean isEOF = (limit == -1 || leis.getReadIndex()-startIdx >= limit); + // add the size for recordId/flags/recordSize/dataSize = 12 bytes + final boolean isEOF = (limit == -1 || (leis.getReadIndex()-startIdx)+12 >= limit); // (currentRecord instanceof HemfPlusMisc.EmfEof) currentRecord = isEOF ? null : _next(); return toReturn; diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordType.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordType.java index 2fc1926ff6..38263f4eff 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordType.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordType.java @@ -24,16 +24,16 @@ import org.apache.poi.util.Internal; @Internal public enum HemfPlusRecordType { header(0x4001, HemfPlusHeader::new), - eof(0x4002, UnimplementedHemfPlusRecord::new), + eof(0x4002, HemfPlusMisc.EmfPlusEOF::new), comment(0x4003, UnimplementedHemfPlusRecord::new), - getDC(0x4004, UnimplementedHemfPlusRecord::new), + getDC(0x4004, HemfPlusMisc.EmfPlusGetDC::new), multiFormatStart(0x4005, UnimplementedHemfPlusRecord::new), multiFormatSection(0x4006, UnimplementedHemfPlusRecord::new), multiFormatEnd(0x4007, UnimplementedHemfPlusRecord::new), - object(0x4008, UnimplementedHemfPlusRecord::new), + object(0x4008, HemfPlusObject.EmfPlusObject::new), clear(0x4009, UnimplementedHemfPlusRecord::new), - fillRects(0x400A, UnimplementedHemfPlusRecord::new), - drawRects(0x400B, UnimplementedHemfPlusRecord::new), + fillRects(0x400A, HemfPlusDraw.EmfPlusFillRects::new), + drawRects(0x400B, HemfPlusDraw.EmfPlusDrawRects::new), fillPolygon(0x400C, UnimplementedHemfPlusRecord::new), drawLines(0x400D, UnimplementedHemfPlusRecord::new), fillEllipse(0x400E, UnimplementedHemfPlusRecord::new), @@ -41,42 +41,42 @@ public enum HemfPlusRecordType { fillPie(0x4010, UnimplementedHemfPlusRecord::new), drawPie(0x4011, UnimplementedHemfPlusRecord::new), drawArc(0x4012, UnimplementedHemfPlusRecord::new), - fillRegion(0x4013, UnimplementedHemfPlusRecord::new), - fillPath(0x4014, UnimplementedHemfPlusRecord::new), - drawPath(0x4015, UnimplementedHemfPlusRecord::new), + fillRegion(0x4013, HemfPlusDraw.EmfPlusFillRegion::new), + fillPath(0x4014, HemfPlusDraw.EmfPlusFillPath::new), + drawPath(0x4015, HemfPlusDraw.EmfPlusDrawPath::new), fillClosedCurve(0x4016, UnimplementedHemfPlusRecord::new), drawClosedCurve(0x4017, UnimplementedHemfPlusRecord::new), drawCurve(0x4018, UnimplementedHemfPlusRecord::new), drawBeziers(0x4019, UnimplementedHemfPlusRecord::new), - drawImage(0x401A, UnimplementedHemfPlusRecord::new), - drawImagePoints(0x401B, UnimplementedHemfPlusRecord::new), + drawImage(0x401A, HemfPlusDraw.EmfPlusDrawImage::new), + drawImagePoints(0x401B, HemfPlusDraw.EmfPlusDrawImagePoints::new), drawString(0x401C, UnimplementedHemfPlusRecord::new), - setRenderingOrigin(0x401D, UnimplementedHemfPlusRecord::new), - setAntiAliasMode(0x401E, UnimplementedHemfPlusRecord::new), - setTextRenderingHint(0x401F, UnimplementedHemfPlusRecord::new), + setRenderingOrigin(0x401D, HemfPlusMisc.EmfPlusSetRenderingOrigin::new), + setAntiAliasMode(0x401E, HemfPlusMisc.EmfPlusSetAntiAliasMode::new), + setTextRenderingHint(0x401F, HemfPlusMisc.EmfPlusSetTextRenderingHint::new), setTextContrast(0x4020, UnimplementedHemfPlusRecord::new), - setInterpolationMode(0x4021, UnimplementedHemfPlusRecord::new), - setPixelOffsetMode(0x4022, UnimplementedHemfPlusRecord::new), - setComositingMode(0x4023, UnimplementedHemfPlusRecord::new), - setCompositingQuality(0x4024, UnimplementedHemfPlusRecord::new), - save(0x4025, UnimplementedHemfPlusRecord::new), - restore(0x4026, UnimplementedHemfPlusRecord::new), + setInterpolationMode(0x4021, HemfPlusMisc.EmfPlusSetInterpolationMode::new), + setPixelOffsetMode(0x4022, HemfPlusMisc.EmfPlusSetPixelOffsetMode::new), + setCompositingMode(0x4023, HemfPlusMisc.EmfPlusSetCompositingMode::new), + setCompositingQuality(0x4024, HemfPlusMisc.EmfPlusSetCompositingQuality::new), + save(0x4025, HemfPlusMisc.EmfPlusSave::new), + restore(0x4026, HemfPlusMisc.EmfPlusRestore::new), beginContainer(0x4027, UnimplementedHemfPlusRecord::new), beginContainerNoParams(0x428, UnimplementedHemfPlusRecord::new), endContainer(0x4029, UnimplementedHemfPlusRecord::new), - setWorldTransform(0x402A, UnimplementedHemfPlusRecord::new), - resetWorldTransform(0x402B, UnimplementedHemfPlusRecord::new), - multiplyWorldTransform(0x402C, UnimplementedHemfPlusRecord::new), + setWorldTransform(0x402A, HemfPlusMisc.EmfPlusSetWorldTransform::new), + resetWorldTransform(0x402B, HemfPlusMisc.EmfPlusResetWorldTransform::new), + multiplyWorldTransform(0x402C, HemfPlusMisc.EmfPlusMultiplyWorldTransform::new), translateWorldTransform(0x402D, UnimplementedHemfPlusRecord::new), scaleWorldTransform(0x402E, UnimplementedHemfPlusRecord::new), rotateWorldTransform(0x402F, UnimplementedHemfPlusRecord::new), - setPageTransform(0x4030, UnimplementedHemfPlusRecord::new), - resetClip(0x4031, UnimplementedHemfPlusRecord::new), - setClipRect(0x4032, UnimplementedHemfPlusRecord::new), - setClipRegion(0x4033, UnimplementedHemfPlusRecord::new), - setClipPath(0x4034, UnimplementedHemfPlusRecord::new), + setPageTransform(0x4030, HemfPlusMisc.EmfPlusSetPageTransform::new), + resetClip(0x4031, HemfPlusMisc.EmfPlusResetClip::new), + setClipRect(0x4032, HemfPlusMisc.EmfPlusSetClipRect::new), + setClipRegion(0x4033, HemfPlusMisc.EmfPlusSetClipRegion::new), + setClipPath(0x4034, HemfPlusMisc.EmfPlusSetClipPath::new), offsetClip(0x4035, UnimplementedHemfPlusRecord::new), - drawDriverstring(0x4036, UnimplementedHemfPlusRecord::new), + drawDriverstring(0x4036, HemfPlusDraw.EmfPlusDrawDriverString::new), strokeFillPath(0x4037, UnimplementedHemfPlusRecord::new), serializableObject(0x4038, UnimplementedHemfPlusRecord::new), setTSGraphics(0x4039, UnimplementedHemfPlusRecord::new), diff --git a/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfEmbeddedIterator.java b/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfEmbeddedIterator.java new file mode 100644 index 0000000000..2d5699deba --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfEmbeddedIterator.java @@ -0,0 +1,342 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hemf.usermodel; + +import java.awt.Transparency; +import java.awt.color.ColorSpace; +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.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +import javax.imageio.ImageIO; + +import org.apache.poi.hemf.record.emf.HemfComment; +import org.apache.poi.hemf.record.emf.HemfRecord; +import org.apache.poi.hemf.record.emfplus.HemfPlusObject; +import org.apache.poi.hwmf.record.HwmfBitmapDib; +import org.apache.poi.hwmf.record.HwmfFill; +import org.apache.poi.hwmf.usermodel.HwmfEmbedded; +import org.apache.poi.hwmf.usermodel.HwmfEmbeddedType; +import org.apache.poi.poifs.filesystem.FileMagic; + +public class HemfEmbeddedIterator implements Iterator { + + private final Deque> iterStack = new ArrayDeque<>(); + private Object current; + + public HemfEmbeddedIterator(HemfPicture emf) { + this(emf.getRecords().iterator()); + } + + public HemfEmbeddedIterator(Iterator recordIterator) { + iterStack.add(recordIterator); + } + + @Override + public boolean hasNext() { + if (iterStack.isEmpty()) { + return false; + } + + if (current != null) { + // don't search twice and potentially skip items + return true; + } + + Iterator iter; + do { + iter = iterStack.peek(); + while (iter.hasNext()) { + Object obj = iter.next(); + if (obj instanceof HemfComment.EmfComment) { + HemfComment.EmfCommentData cd = ((HemfComment.EmfComment)obj).getCommentData(); + if ( + cd instanceof HemfComment.EmfCommentDataWMF || + cd instanceof HemfComment.EmfCommentDataGeneric + ) { + current = obj; + return true; + } + + if (cd instanceof HemfComment.EmfCommentDataMultiformats) { + Iterator iter2 = ((HemfComment.EmfCommentDataMultiformats)cd).getFormats().iterator(); + if (iter2.hasNext()) { + iterStack.push(iter2); + continue; + } + } + + if (cd instanceof HemfComment.EmfCommentDataPlus) { + Iterator iter2 = ((HemfComment.EmfCommentDataPlus)cd).getRecords().iterator(); + if (iter2.hasNext()) { + iter = iter2; + iterStack.push(iter2); + continue; + } + } + } + + if (obj instanceof HemfComment.EmfCommentDataFormat) { + current = obj; + return true; + } + + if (obj instanceof HemfPlusObject.EmfPlusObject && ((HemfPlusObject.EmfPlusObject)obj).getObjectType() == HemfPlusObject.EmfPlusObjectType.IMAGE) { + current = obj; + return true; + } + + if (obj instanceof HwmfFill.WmfStretchDib) { + HwmfBitmapDib bitmap = ((HwmfFill.WmfStretchDib) obj).getBitmap(); + if (bitmap.isValid()) { + current = obj; + return true; + } + } + } + iterStack.pop(); + } while (!iterStack.isEmpty()); + + return false; + } + + @Override + public HwmfEmbedded next() { + HwmfEmbedded emb; + if ((emb = checkEmfCommentDataWMF()) != null) { + return emb; + } + if ((emb = checkEmfCommentDataGeneric()) != null) { + return emb; + } + if ((emb = checkEmfCommentDataFormat()) != null) { + return emb; + } + if ((emb = checkEmfPlusObject()) != null) { + return emb; + } + if ((emb = checkWmfStretchDib()) != null) { + return emb; + } + + return null; + } + + private HwmfEmbedded checkEmfCommentDataWMF() { + if (!(current instanceof HemfComment.EmfCommentDataWMF && ((HemfComment.EmfComment)current).getCommentData() instanceof HemfComment.EmfCommentDataWMF)) { + return null; + } + + HemfComment.EmfCommentDataWMF wmf = (HemfComment.EmfCommentDataWMF)((HemfComment.EmfComment)current).getCommentData(); + HwmfEmbedded emb = new HwmfEmbedded(); + emb.setEmbeddedType(HwmfEmbeddedType.WMF); + emb.setData(wmf.getWMFData()); + current = null; + return emb; + } + + private HwmfEmbedded checkEmfCommentDataGeneric() { + if (!(current instanceof HemfComment.EmfComment && ((HemfComment.EmfComment)current).getCommentData() instanceof HemfComment.EmfCommentDataGeneric)) { + return null; + } + HemfComment.EmfCommentDataGeneric cdg = (HemfComment.EmfCommentDataGeneric)((HemfComment.EmfComment)current).getCommentData(); + HwmfEmbedded emb = new HwmfEmbedded(); + emb.setEmbeddedType(HwmfEmbeddedType.UNKNOWN); + emb.setData(cdg.getPrivateData()); + current = null; + return emb; + } + + private HwmfEmbedded checkEmfCommentDataFormat() { + if (!(current instanceof HemfComment.EmfCommentDataFormat)) { + return null; + } + HemfComment.EmfCommentDataFormat cdf = (HemfComment.EmfCommentDataFormat)current; + HwmfEmbedded emb = new HwmfEmbedded(); + boolean isEmf = (cdf.getSignature() == HemfComment.EmfFormatSignature.ENHMETA_SIGNATURE); + emb.setEmbeddedType(isEmf ? HwmfEmbeddedType.EMF : HwmfEmbeddedType.EPS); + emb.setData(cdf.getRawData()); + current = null; + return emb; + } + + private HwmfEmbedded checkWmfStretchDib() { + if (!(current instanceof HwmfFill.WmfStretchDib)) { + return null; + } + HwmfEmbedded emb = new HwmfEmbedded(); + emb.setData(((HwmfFill.WmfStretchDib) current).getBitmap().getBMPData()); + emb.setEmbeddedType(HwmfEmbeddedType.BMP); + current = null; + return emb; + } + + private HwmfEmbedded checkEmfPlusObject() { + if (!(current instanceof HemfPlusObject.EmfPlusObject)) { + return null; + } + + HemfPlusObject.EmfPlusObject epo = (HemfPlusObject.EmfPlusObject)current; + assert(epo.getObjectType() == HemfPlusObject.EmfPlusObjectType.IMAGE); + HemfPlusObject.EmfPlusImage img = epo.getObjectData(); + assert(img.getImageDataType() != null); + + HwmfEmbedded emb = getEmfPlusImageData(); + + HwmfEmbeddedType et; + switch (img.getImageDataType()) { + case BITMAP: + if (img.getBitmapType() == HemfPlusObject.EmfPlusBitmapDataType.COMPRESSED) { + switch (FileMagic.valueOf(emb.getRawData())) { + case JPEG: + et = HwmfEmbeddedType.JPEG; + break; + case GIF: + et = HwmfEmbeddedType.GIF; + break; + case PNG: + et = HwmfEmbeddedType.PNG; + break; + case TIFF: + et = HwmfEmbeddedType.TIFF; + break; + default: + et = HwmfEmbeddedType.BITMAP; + break; + } + } else { + et = HwmfEmbeddedType.PNG; + compressGDIBitmap(img, emb, et); + } + break; + case METAFILE: + assert(img.getMetafileType() != null); + switch (img.getMetafileType()) { + case Wmf: + case WmfPlaceable: + et = HwmfEmbeddedType.WMF; + break; + case Emf: + case EmfPlusDual: + case EmfPlusOnly: + et = HwmfEmbeddedType.EMF; + break; + default: + et = HwmfEmbeddedType.UNKNOWN; + break; + } + break; + default: + et = HwmfEmbeddedType.UNKNOWN; + break; + } + emb.setEmbeddedType(et); + + return emb; + } + + /** + * Compress GDIs internal format to something useful + */ + private void compressGDIBitmap(HemfPlusObject.EmfPlusImage img, HwmfEmbedded emb, HwmfEmbeddedType et) { + final int width = img.getBitmapWidth(); + final int height = img.getBitmapHeight(); + final int stride = img.getBitmapStride(); + final HemfPlusObject.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); + + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + // use HwmfEmbeddedType literal for conversion + ImageIO.write(bi, et.toString(), bos); + emb.setData(bos.toByteArray()); + } catch (IOException e) { + // TODO: throw appropriate exception + throw new RuntimeException(e); + } + } + + + private HwmfEmbedded getEmfPlusImageData() { + HemfPlusObject.EmfPlusObject epo = (HemfPlusObject.EmfPlusObject)current; + assert(epo.getObjectType() == HemfPlusObject.EmfPlusObjectType.IMAGE); + + final int objectId = epo.getObjectId(); + + HwmfEmbedded emb = new HwmfEmbedded(); + + HemfPlusObject.EmfPlusImage img = (HemfPlusObject.EmfPlusImage)epo.getObjectData(); + assert(img.getImageDataType() != null); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + for (;;) { + bos.write(img.getImageData()); + + current = null; + //noinspection ConstantConditions + if (hasNext() && + (current instanceof HemfPlusObject.EmfPlusObject) && + ((epo = (HemfPlusObject.EmfPlusObject) current).getObjectId() == objectId) + ) { + img = (HemfPlusObject.EmfPlusImage)epo.getObjectData(); + } else { + return emb; + } + } + } catch (IOException e) { + // ByteArrayOutputStream doesn't throw IOException + return null; + } finally { + emb.setData(bos.toByteArray()); + } + } +} 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 6d53ae23ea..0915fe5f27 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java +++ b/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java @@ -35,6 +35,7 @@ import org.apache.poi.hemf.record.emf.HemfHeader; import org.apache.poi.hemf.record.emf.HemfRecord; import org.apache.poi.hemf.record.emf.HemfRecordIterator; import org.apache.poi.hemf.record.emf.HemfWindowing; +import org.apache.poi.hwmf.usermodel.HwmfEmbedded; import org.apache.poi.util.Dimension2DDouble; import org.apache.poi.util.Internal; import org.apache.poi.util.LittleEndianInputStream; @@ -158,4 +159,7 @@ public class HemfPicture implements Iterable { } } + public Iterable getEmbeddings() { + return () -> new HemfEmbeddedIterator(HemfPicture.this); + } } 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 766f9020df..744c995efe 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java @@ -142,7 +142,7 @@ public class HwmfGraphics { graphicsCtx.fill(shape); } -// draw(shape); + draw(shape); } protected BasicStroke getStroke() { diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java index 1632525d8e..2ef22a407a 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java @@ -25,6 +25,7 @@ import java.awt.LinearGradientPaint; import java.awt.MultipleGradientPaint; import java.awt.RenderingHints; import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -460,12 +461,29 @@ public class HwmfBitmapDib { } public BufferedImage getImage() { + return getImage(null, null, false); + } + + public BufferedImage getImage(Color foreground, Color background, boolean hasAlpha) { + BufferedImage bi; try { - return ImageIO.read(getBMPStream()); + bi = ImageIO.read(getBMPStream()); } catch (IOException|RuntimeException e) { logger.log(POILogger.ERROR, "invalid bitmap data - returning placeholder image"); return getPlaceholder(); } + + if (foreground != null && background != null && headerBitCount == HwmfBitmapDib.BitCount.BI_BITCOUNT_1) { + IndexColorModel cmOld = (IndexColorModel)bi.getColorModel(); + int transPixel = hasAlpha ? (((cmOld.getRGB(0) & 0xFFFFFF) == 0) ? 0 : 1) : -1; + int transferType = bi.getData().getTransferType(); + int fg = foreground.getRGB(), bg = background.getRGB(); + int[] cmap = { (transPixel == 0 ? bg : fg), (transPixel == 1 ? bg : fg) }; + IndexColorModel cmNew = new IndexColorModel(1, cmap.length, cmap, 0, hasAlpha, transPixel, transferType); + bi = new BufferedImage(cmNew, bi.getRaster(), false, null); + } + + return bi; } @Override diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfEscape.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfEscape.java index 0d00934530..4462370350 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfEscape.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfEscape.java @@ -18,10 +18,13 @@ package org.apache.poi.hwmf.record; import java.io.IOException; +import java.util.function.Supplier; import org.apache.poi.hwmf.draw.HwmfGraphics; import org.apache.poi.util.HexDump; import org.apache.poi.util.IOUtils; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.LittleEndianCP950Reader; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; @@ -30,135 +33,140 @@ import org.apache.poi.util.LittleEndianInputStream; * might not be directly accessible through WMF records */ public class HwmfEscape implements HwmfRecord { - + private static final int MAX_OBJECT_SIZE = 0xFFFF; + public enum EscapeFunction { /** Notifies the printer driver that the application has finished writing to a page. */ - NEWFRAME(0x0001), + NEWFRAME(0x0001, WmfEscapeUnknownData::new), /** Stops processing the current document. */ - ABORTDOC(0x0002), + ABORTDOC(0x0002, WmfEscapeUnknownData::new), /** Notifies the printer driver that the application has finished writing to a band. */ - NEXTBAND(0x0003), + NEXTBAND(0x0003, WmfEscapeUnknownData::new), /** Sets color table values. */ - SETCOLORTABLE(0x0004), + SETCOLORTABLE(0x0004, WmfEscapeUnknownData::new), /** Gets color table values. */ - GETCOLORTABLE(0x0005), + GETCOLORTABLE(0x0005, WmfEscapeUnknownData::new), /** Causes all pending output to be flushed to the output device. */ - FLUSHOUT(0x0006), + FLUSHOUT(0x0006, WmfEscapeUnknownData::new), /** Indicates that the printer driver SHOULD print text only, and no graphics. */ - DRAFTMODE(0x0007), + DRAFTMODE(0x0007, WmfEscapeUnknownData::new), /** Queries a printer driver to determine whether a specific escape function is supported on the output device it drives. */ - QUERYESCSUPPORT(0x0008), + QUERYESCSUPPORT(0x0008, WmfEscapeUnknownData::new), /** Sets the application-defined function that allows a print job to be canceled during printing. */ - SETABORTPROC(0x0009), + SETABORTPROC(0x0009, WmfEscapeUnknownData::new), /** Notifies the printer driver that a new print job is starting. */ - STARTDOC(0x000A), + STARTDOC(0x000A, WmfEscapeUnknownData::new), /** Notifies the printer driver that the current print job is ending. */ - ENDDOC(0x000B), + ENDDOC(0x000B, WmfEscapeUnknownData::new), /** Retrieves the physical page size currently selected on an output device. */ - GETPHYSPAGESIZE(0x000C), + GETPHYSPAGESIZE(0x000C, WmfEscapeUnknownData::new), /** Retrieves the offset from the upper-left corner of the physical page where the actual printing or drawing begins. */ - GETPRINTINGOFFSET(0x000D), + GETPRINTINGOFFSET(0x000D, WmfEscapeUnknownData::new), /** Retrieves the scaling factors for the x-axis and the y-axis of a printer. */ - GETSCALINGFACTOR(0x000E), + GETSCALINGFACTOR(0x000E, WmfEscapeUnknownData::new), /** Used to embed an enhanced metafile format (EMF) metafile within a WMF metafile. */ - META_ESCAPE_ENHANCED_METAFILE(0x000F), + META_ESCAPE_ENHANCED_METAFILE(0x000F, WmfEscapeEMF::new), /** Sets the width of a pen in pixels. */ - SETPENWIDTH(0x0010), + SETPENWIDTH(0x0010, WmfEscapeUnknownData::new), /** Sets the number of copies. */ - SETCOPYCOUNT(0x0011), + SETCOPYCOUNT(0x0011, WmfEscapeUnknownData::new), /** Sets the source, such as a particular paper tray or bin on a printer, for output forms. */ - SETPAPERSOURCE(0x0012), + SETPAPERSOURCE(0x0012, WmfEscapeUnknownData::new), /** This record passes through arbitrary data. */ - PASSTHROUGH(0x0013), + PASSTHROUGH(0x0013, WmfEscapeUnknownData::new), /** Gets information concerning graphics technology that is supported on a device. */ - GETTECHNOLOGY(0x0014), + GETTECHNOLOGY(0x0014, WmfEscapeUnknownData::new), /** Specifies the line-drawing mode to use in output to a device. */ - SETLINECAP(0x0015), + SETLINECAP(0x0015, WmfEscapeUnknownData::new), /** Specifies the line-joining mode to use in output to a device. */ - SETLINEJOIN(0x0016), + SETLINEJOIN(0x0016, WmfEscapeUnknownData::new), /** Sets the limit for the length of miter joins to use in output to a device. */ - SETMITERLIMIT(0x0017), + SETMITERLIMIT(0x0017, WmfEscapeUnknownData::new), /** Retrieves or specifies settings concerning banding on a device, such as the number of bands. */ - BANDINFO(0x0018), + BANDINFO(0x0018, WmfEscapeUnknownData::new), /** Draws a rectangle with a defined pattern. */ - DRAWPATTERNRECT(0x0019), + DRAWPATTERNRECT(0x0019, WmfEscapeUnknownData::new), /** Retrieves the physical pen size currently defined on a device. */ - GETVECTORPENSIZE(0x001A), + GETVECTORPENSIZE(0x001A, WmfEscapeUnknownData::new), /** Retrieves the physical brush size currently defined on a device. */ - GETVECTORBRUSHSIZE(0x001B), + GETVECTORBRUSHSIZE(0x001B, WmfEscapeUnknownData::new), /** Enables or disables double-sided (duplex) printing on a device. */ - ENABLEDUPLEX(0x001C), + ENABLEDUPLEX(0x001C, WmfEscapeUnknownData::new), /** Retrieves or specifies the source of output forms on a device. */ - GETSETPAPERBINS(0x001D), + GETSETPAPERBINS(0x001D, WmfEscapeUnknownData::new), /** Retrieves or specifies the paper orientation on a device. */ - GETSETPRINTORIENT(0x001E), + GETSETPRINTORIENT(0x001E, WmfEscapeUnknownData::new), /** Retrieves information concerning the sources of different forms on an output device. */ - ENUMPAPERBINS(0x001F), + ENUMPAPERBINS(0x001F, WmfEscapeUnknownData::new), /** Specifies the scaling of device-independent bitmaps (DIBs). */ - SETDIBSCALING(0x0020), + SETDIBSCALING(0x0020, WmfEscapeUnknownData::new), /** Indicates the start and end of an encapsulated PostScript (EPS) section. */ - EPSPRINTING(0x0021), + EPSPRINTING(0x0021, WmfEscapeUnknownData::new), /** Queries a printer driver for paper dimensions and other forms data. */ - ENUMPAPERMETRICS(0x0022), + ENUMPAPERMETRICS(0x0022, WmfEscapeUnknownData::new), /** Retrieves or specifies paper dimensions and other forms data on an output device. */ - GETSETPAPERMETRICS(0x0023), + GETSETPAPERMETRICS(0x0023, WmfEscapeUnknownData::new), /** Sends arbitrary PostScript data to an output device. */ - POSTSCRIPT_DATA(0x0025), + POSTSCRIPT_DATA(0x0025, WmfEscapeUnknownData::new), /** Notifies an output device to ignore PostScript data. */ - POSTSCRIPT_IGNORE(0x0026), + POSTSCRIPT_IGNORE(0x0026, WmfEscapeUnknownData::new), /** Gets the device units currently configured on an output device. */ - GETDEVICEUNITS(0x002A), + GETDEVICEUNITS(0x002A, WmfEscapeUnknownData::new), /** Gets extended text metrics currently configured on an output device. */ - GETEXTENDEDTEXTMETRICS(0x0100), + GETEXTENDEDTEXTMETRICS(0x0100, WmfEscapeUnknownData::new), /** Gets the font kern table currently defined on an output device. */ - GETPAIRKERNTABLE(0x0102), + GETPAIRKERNTABLE(0x0102, WmfEscapeUnknownData::new), /** Draws text using the currently selected font, background color, and text color. */ - EXTTEXTOUT(0x0200), + EXTTEXTOUT(0x0200, WmfEscapeUnknownData::new), /** Gets the font face name currently configured on a device. */ - GETFACENAME(0x0201), + GETFACENAME(0x0201, WmfEscapeUnknownData::new), /** Sets the font face name on a device. */ - DOWNLOADFACE(0x0202), + DOWNLOADFACE(0x0202, WmfEscapeUnknownData::new), /** Queries a printer driver about the support for metafiles on an output device. */ - METAFILE_DRIVER(0x0801), + METAFILE_DRIVER(0x0801, WmfEscapeUnknownData::new), /** Queries the printer driver about its support for DIBs on an output device. */ - QUERYDIBSUPPORT(0x0C01), + QUERYDIBSUPPORT(0x0C01, WmfEscapeUnknownData::new), /** Opens a path. */ - BEGIN_PATH(0x1000), + BEGIN_PATH(0x1000, WmfEscapeUnknownData::new), /** Defines a clip region that is bounded by a path. The input MUST be a 16-bit quantity that defines the action to take. */ - CLIP_TO_PATH(0x1001), + CLIP_TO_PATH(0x1001, WmfEscapeUnknownData::new), /** Ends a path. */ - END_PATH(0x1002), + END_PATH(0x1002, WmfEscapeUnknownData::new), /** The same as STARTDOC specified with a NULL document and output filename, data in raw mode, and a type of zero. */ - OPEN_CHANNEL(0x100E), + OPEN_CHANNEL(0x100E, WmfEscapeUnknownData::new), /** Instructs the printer driver to download sets of PostScript procedures. */ - DOWNLOADHEADER(0x100F), + DOWNLOADHEADER(0x100F, WmfEscapeUnknownData::new), /** The same as ENDDOC. See OPEN_CHANNEL. */ - CLOSE_CHANNEL(0x1010), + CLOSE_CHANNEL(0x1010, WmfEscapeUnknownData::new), /** Sends arbitrary data directly to a printer driver, which is expected to process this data only when in PostScript mode. */ - POSTSCRIPT_PASSTHROUGH(0x1013), + POSTSCRIPT_PASSTHROUGH(0x1013, WmfEscapeUnknownData::new), /** Sends arbitrary data directly to the printer driver. */ - ENCAPSULATED_POSTSCRIPT(0x1014), + ENCAPSULATED_POSTSCRIPT(0x1014, WmfEscapeUnknownData::new), /** Sets the printer driver to either PostScript or GDI mode. */ - POSTSCRIPT_IDENTIFY(0x1015), + POSTSCRIPT_IDENTIFY(0x1015, WmfEscapeUnknownData::new), /** Inserts a block of raw data into a PostScript stream. The input MUST be a 32-bit quantity specifying the number of bytes to inject, a 16-bit quantity specifying the injection point, and a 16-bit quantity specifying the page number, followed by the bytes to inject. */ - POSTSCRIPT_INJECTION(0x1016), + POSTSCRIPT_INJECTION(0x1016, WmfEscapeUnknownData::new), /** Checks whether the printer supports a JPEG image. */ - CHECKJPEGFORMAT(0x1017), + CHECKJPEGFORMAT(0x1017, WmfEscapeUnknownData::new), /** Checks whether the printer supports a PNG image */ - CHECKPNGFORMAT(0x1018), + CHECKPNGFORMAT(0x1018, WmfEscapeUnknownData::new), /** Gets information on a specified feature setting for a PostScript printer driver. */ - GET_PS_FEATURESETTING(0x1019), + GET_PS_FEATURESETTING(0x1019, WmfEscapeUnknownData::new), /** Enables applications to write documents to a file or to a printer in XML Paper Specification (XPS) format. */ - MXDC_ESCAPE(0x101A), + MXDC_ESCAPE(0x101A, WmfEscapeUnknownData::new), /** Enables applications to include private procedures and other arbitrary data in documents. */ - SPCLPASSTHROUGH2(0x11D8); + SPCLPASSTHROUGH2(0x11D8, WmfEscapeUnknownData::new); - int flag; - EscapeFunction(int flag) { + public int flag; + public final Supplier constructor; + + + EscapeFunction(int flag, Supplier constructor) { this.flag = flag; + this.constructor = constructor; } static EscapeFunction valueOf(int flag) { @@ -169,21 +177,18 @@ public class HwmfEscape implements HwmfRecord { } } + public interface HwmfEscapeData { + public int init(LittleEndianInputStream leis, long recordSize, EscapeFunction escapeFunction) throws IOException; + } + + /** * A 16-bit unsigned integer that defines the escape function. The * value MUST be from the MetafileEscapes enumeration. */ private EscapeFunction escapeFunction; - /** - * A 16-bit unsigned integer that specifies the size, in bytes, of the - * EscapeData field. - */ - private int byteCount; - /** - * An array of bytes of size ByteCount. - */ - private byte[] escapeData; - + private HwmfEscapeData escapeData; + @Override public HwmfRecordType getWmfRecordType() { return HwmfRecordType.escape; @@ -192,10 +197,22 @@ public class HwmfEscape implements HwmfRecord { @Override public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException { escapeFunction = EscapeFunction.valueOf(leis.readUShort()); - byteCount = leis.readUShort(); - escapeData = IOUtils.toByteArray(leis,byteCount); + // A 16-bit unsigned integer that specifies the size, in bytes, of the EscapeData field. + int byteCount = leis.readUShort(); + int size = 2*LittleEndianConsts.SHORT_SIZE; - return 2*LittleEndianConsts.SHORT_SIZE+byteCount; + escapeData = escapeFunction.constructor.get(); + size += escapeData.init(leis, byteCount, escapeFunction); + + return size; + } + + public EscapeFunction getEscapeFunction() { + return escapeFunction; + } + + public T getEscapeData() { + return (T)escapeData; } @Override @@ -206,7 +223,126 @@ public class HwmfEscape implements HwmfRecord { public String toString() { StringBuilder sb = new StringBuilder(); sb.append("escape - function: "+escapeFunction+"\n"); - sb.append(HexDump.dump(escapeData, 0, 0)); + sb.append(escapeData.toString()); return sb.toString(); } + + public static class WmfEscapeUnknownData implements HwmfEscapeData { + EscapeFunction escapeFunction; + private byte[] escapeDataBytes; + + public byte[] getEscapeDataBytes() { + return escapeDataBytes; + } + + @Override + public int init(LittleEndianInputStream leis, long recordSize, EscapeFunction escapeFunction) throws IOException { + this.escapeFunction = escapeFunction; + escapeDataBytes = IOUtils.toByteArray(leis,recordSize,MAX_OBJECT_SIZE); + return 0; + } + + @Override + public String toString() { + return HexDump.dump(escapeDataBytes, 0, 0); + } + } + + public static class WmfEscapeEMF implements HwmfEscapeData { + // The magic for EMF parts, i.e. the byte sequence for "WMFC" + private static final int EMF_COMMENT_IDENTIFIER = 0x43464D57; + + int commentIdentifier; + int commentType; + int version; + int checksum; + int flags; + int commentRecordCount; + int currentRecordSize; + int remainingBytes; + int emfRecordSize; + byte[] emfData; + + + @Override + public int init(LittleEndianInputStream leis, long recordSize, EscapeFunction escapeFunction) throws IOException { + if (recordSize < LittleEndianConsts.INT_SIZE) { + return 0; + } + + // A 32-bit unsigned integer that defines this record as a WMF Comment record. + int commentIdentifier = leis.readInt(); + + if (commentIdentifier != EMF_COMMENT_IDENTIFIER) { + // there are some WMF implementation using this record as a MFCOMMENT or similar + // if the commentIdentifier doesn't match, then return immediately + return LittleEndianConsts.INT_SIZE; + } + + // A 32-bit unsigned integer that identifies the type of comment in this record. + // This value MUST be 0x00000001. + commentType = leis.readInt(); + assert(commentType == 0x00000001); + + // A 32-bit unsigned integer that specifies EMF metafile interoperability. This SHOULD be 0x00010000. + version = leis.readInt(); + + // A 16-bit unsigned integer used to validate the correctness of the embedded EMF stream. + // This value MUST be the one's-complement of the result of applying an XOR operation to all WORDs in the EMF stream. + checksum = leis.readUShort(); + + // This 32-bit unsigned integer is unused and MUST be set to zero. + flags = leis.readInt(); + assert(flags == 0); + + // A 32-bit unsigned integer that specifies the total number of consecutive META_ESCAPE_ENHANCED_METAFILE + // records that contain the embedded EMF metafile. + commentRecordCount = leis.readInt(); + + // A 32-bit unsigned integer that specifies the size, in bytes, of the EnhancedMetafileData field. + // This value MUST be less than or equal to 8,192. + currentRecordSize = leis.readInt(); + assert(0 <= currentRecordSize && currentRecordSize <= 0x2000); + + // A 32-bit unsigned integer that specifies the number of bytes in the EMF stream that remain to be + // processed after this record. Those additional EMF bytes MUST follow in the EnhancedMetafileData + // fields of subsequent META_ESCAPE_ENHANDED_METAFILE escape records. + remainingBytes = leis.readInt(); + + // A 32-bit unsigned integer that specifies the total size of the EMF stream embedded in this + // sequence of META_ESCAPE_ENHANCED_METAFILE records. + emfRecordSize = leis.readInt(); + + + // A segment of an EMF file. The bytes in consecutive META_ESCAPE_ENHANCED_METAFILE records + // MUST be concatenated to represent the entire embedded EMF file. + emfData = IOUtils.toByteArray(leis, currentRecordSize, MAX_OBJECT_SIZE); + + return LittleEndianConsts.INT_SIZE*8+ LittleEndianConsts.SHORT_SIZE+emfData.length; + } + + public boolean isValid() { + return commentIdentifier == EMF_COMMENT_IDENTIFIER; + } + + public int getCommentRecordCount() { + return commentRecordCount; + } + + public int getCurrentRecordSize() { + return currentRecordSize; + } + + public int getRemainingBytes() { + return remainingBytes; + } + + public int getEmfRecordSize() { + return emfRecordSize; + } + + public byte[] getEmfData() { + return emfData; + } + } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java index 3b420e401d..920e302e2a 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java @@ -20,6 +20,7 @@ package org.apache.poi.hwmf.record; import static org.apache.poi.hwmf.record.HwmfDraw.boundsToString; import static org.apache.poi.hwmf.record.HwmfDraw.readPointS; +import java.awt.Color; import java.awt.Shape; import java.awt.geom.Path2D; import java.awt.geom.Point2D; @@ -29,6 +30,7 @@ import java.io.IOException; import org.apache.poi.hwmf.draw.HwmfDrawProperties; import org.apache.poi.hwmf.draw.HwmfGraphics; +import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; @@ -37,7 +39,29 @@ public class HwmfFill { * A record which contains an image (to be extracted) */ public interface HwmfImageRecord { - BufferedImage getImage(); + + default BufferedImage getImage() { + return getImage(Color.BLACK, new Color(0x00FFFFFF, true), true); + } + + /** + * Provide an image using the fore-/background color, in case of a 1-bit pattern + * @param foreground the foreground color + * @param background the background color + * @param hasAlpha if true, the background color is rendered transparent - see {@link HwmfMisc.WmfSetBkMode.HwmfBkMode} + * @return the image + * + * @since POI 4.1.1 + */ + BufferedImage getImage(Color foreground, Color background, boolean hasAlpha); + + /** + * @return the raw BMP data + * + * @see BMP format + * @since POI 4.1.1 + */ + byte[] getBMPData(); } /** @@ -497,7 +521,9 @@ public class HwmfFill { HwmfDrawProperties prop = ctx.getProperties(); prop.setRasterOp(rasterOperation); if (bitmap.isValid()) { - ctx.drawImage(getImage(), srcBounds, dstBounds); + BufferedImage bi = bitmap.getImage(prop.getPenColor().getColor(), prop.getBackgroundColor().getColor(), + prop.getBkMode() == HwmfBkMode.TRANSPARENT); + ctx.drawImage(bi, srcBounds, dstBounds); } else if (!dstBounds.isEmpty()) { BufferedImage bi = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); ctx.drawImage(bi, new Rectangle2D.Double(0,0,100,100), dstBounds); @@ -505,8 +531,17 @@ public class HwmfFill { } @Override - public BufferedImage getImage() { - return bitmap.getImage(); + public BufferedImage getImage(Color foreground, Color background, boolean hasAlpha) { + return bitmap.getImage(foreground,background,hasAlpha); + } + + public HwmfBitmapDib getBitmap() { + return bitmap; + } + + @Override + public byte[] getBMPData() { + return bitmap.getBMPData(); } @Override @@ -631,8 +666,13 @@ public class HwmfFill { } @Override - public BufferedImage getImage() { - return dib.getImage(); + public BufferedImage getImage(Color foreground, Color background, boolean hasAlpha) { + return dib.getImage(foreground,background,hasAlpha); + } + + @Override + public byte[] getBMPData() { + return dib.getBMPData(); } } @@ -738,8 +778,13 @@ public class HwmfFill { } @Override - public BufferedImage getImage() { - return (target != null && target.isValid()) ? target.getImage() : null; + public BufferedImage getImage(Color foreground, Color background, boolean hasAlpha) { + return (target != null && target.isValid()) ? target.getImage(foreground,background,hasAlpha) : null; + } + + @Override + public byte[] getBMPData() { + return (target != null && target.isValid()) ? target.getBMPData() : null; } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMisc.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMisc.java index eef15e30d7..bd9264b6ac 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMisc.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMisc.java @@ -17,6 +17,7 @@ package org.apache.poi.hwmf.record; +import java.awt.Color; import java.awt.geom.Dimension2D; import java.awt.image.BufferedImage; import java.io.IOException; @@ -25,6 +26,7 @@ import org.apache.poi.hwmf.draw.HwmfDrawProperties; import org.apache.poi.hwmf.draw.HwmfGraphics; import org.apache.poi.hwmf.record.HwmfFill.ColorUsage; import org.apache.poi.hwmf.record.HwmfFill.HwmfImageRecord; +import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode; import org.apache.poi.util.Dimension2DDouble; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.LittleEndianInputStream; @@ -459,19 +461,31 @@ public class HwmfMisc { } HwmfDrawProperties prop = ctx.getProperties(); prop.setBrushStyle(style); - prop.setBrushBitmap(getImage()); + prop.setBrushBitmap(getImage(prop.getBrushColor().getColor(), prop.getBackgroundColor().getColor(), + prop.getBkMode() == HwmfBkMode.TRANSPARENT)); } @Override - public BufferedImage getImage() { + public BufferedImage getImage(Color foreground, Color background, boolean hasAlpha) { if (patternDib != null && patternDib.isValid()) { - return patternDib.getImage(); + return patternDib.getImage(foreground, background, hasAlpha); } else if (pattern16 != null) { return pattern16.getImage(); } else { return null; } } + + @Override + public byte[] getBMPData() { + if (patternDib != null && patternDib.isValid()) { + return patternDib.getBMPData(); + } else if (pattern16 != null) { + return null; + } else { + return null; + } + } } /** diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfPlaceableHeader.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfPlaceableHeader.java index e2d6a3a5e1..82aba4b12d 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfPlaceableHeader.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfPlaceableHeader.java @@ -66,7 +66,13 @@ public class HwmfPlaceableHeader { * This value can be used to determine whether the metafile has become corrupted. */ leis.readShort(); - + + // sometimes the placeable header is filled/aligned to dwords. + // check for padding 0 bytes. + leis.mark(LittleEndianConsts.INT_SIZE); + if (leis.readShort() != 0) { + leis.reset(); + } } public static HwmfPlaceableHeader readHeader(LittleEndianInputStream leis) throws IOException { diff --git a/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfEmbedded.java b/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfEmbedded.java new file mode 100644 index 0000000000..2bb9187988 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfEmbedded.java @@ -0,0 +1,49 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hwmf.usermodel; + +import org.apache.poi.util.Beta; + +/** + * An embedded resource - this class hides the logic of chained emf+ object records and other internals. + * Consider its API as unstable for now, i.e. there's no guarantee for backward compatibility + */ +@Beta +public class HwmfEmbedded { + + private HwmfEmbeddedType embeddedType; + private byte[] data; + + public HwmfEmbeddedType getEmbeddedType() { + return embeddedType; + } + + public byte[] getRawData() { + return data; + } + + public void setEmbeddedType(HwmfEmbeddedType embeddedType) { + this.embeddedType = embeddedType; + } + + public void setData(byte[] data) { + this.data = data; + } +} + + diff --git a/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfEmbeddedIterator.java b/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfEmbeddedIterator.java new file mode 100644 index 0000000000..5d5b88ab33 --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfEmbeddedIterator.java @@ -0,0 +1,140 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hwmf.usermodel; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +import org.apache.poi.hwmf.record.HwmfEscape; +import org.apache.poi.hwmf.record.HwmfEscape.EscapeFunction; +import org.apache.poi.hwmf.record.HwmfEscape.WmfEscapeEMF; +import org.apache.poi.hwmf.record.HwmfFill.HwmfImageRecord; +import org.apache.poi.hwmf.record.HwmfRecord; + +public class HwmfEmbeddedIterator implements Iterator { + + private final Deque> iterStack = new ArrayDeque<>(); + private Object current; + + public HwmfEmbeddedIterator(HwmfPicture wmf) { + this(wmf.getRecords().iterator()); + } + + public HwmfEmbeddedIterator(Iterator recordIterator) { + iterStack.add(recordIterator); + } + + @Override + public boolean hasNext() { + if (iterStack.isEmpty()) { + return false; + } + + if (current != null) { + // don't search twice and potentially skip items + return true; + } + + Iterator iter; + do { + iter = iterStack.peek(); + while (iter.hasNext()) { + Object obj = iter.next(); + if (obj instanceof HwmfImageRecord) { + current = obj; + return true; + } + if (obj instanceof HwmfEscape && ((HwmfEscape)obj).getEscapeFunction() == EscapeFunction.META_ESCAPE_ENHANCED_METAFILE) { + WmfEscapeEMF emfData = ((HwmfEscape)obj).getEscapeData(); + if (emfData.isValid()) { + current = obj; + return true; + } + } + } + iterStack.pop(); + } while (!iterStack.isEmpty()); + + return false; + } + + @Override + public HwmfEmbedded next() { + HwmfEmbedded emb; + if ((emb = checkHwmfImageRecord()) != null) { + return emb; + } + if ((emb = checkHwmfEscapeRecord()) != null) { + return emb; + } + return null; + } + + private HwmfEmbedded checkHwmfImageRecord() { + if (!(current instanceof HwmfImageRecord)) { + return null; + } + + HwmfImageRecord hir = (HwmfImageRecord)current; + current = null; + + HwmfEmbedded emb = new HwmfEmbedded(); + emb.setEmbeddedType(HwmfEmbeddedType.BMP); + emb.setData(hir.getBMPData()); + + return emb; + } + + + private HwmfEmbedded checkHwmfEscapeRecord() { + if (!(current instanceof HwmfEscape)) { + return null; + } + final HwmfEscape esc = (HwmfEscape)current; + assert(esc.getEscapeFunction() == EscapeFunction.META_ESCAPE_ENHANCED_METAFILE); + + WmfEscapeEMF img = esc.getEscapeData(); + assert(img.isValid()); + current = null; + + final HwmfEmbedded emb = new HwmfEmbedded(); + emb.setEmbeddedType(HwmfEmbeddedType.EMF); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + for (;;) { + bos.write(img.getEmfData()); + + current = null; + if (img.getRemainingBytes() > 0 && hasNext() && (current instanceof HwmfEscape)) { + img = ((HwmfEscape)current).getEscapeData(); + } else { + return emb; + } + } + } catch (IOException e) { + // ByteArrayOutputStream doesn't throw IOException + return null; + } finally { + emb.setData(bos.toByteArray()); + } + } +} diff --git a/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfEmbeddedType.java b/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfEmbeddedType.java new file mode 100644 index 0000000000..e9e96edb1c --- /dev/null +++ b/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfEmbeddedType.java @@ -0,0 +1,37 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hwmf.usermodel; + +public enum HwmfEmbeddedType { + BITMAP(".bitmap"), + WMF(".wmf"), + EMF(".emf"), + EPS(".eps"), + JPEG(".jpg"), + GIF(".gif"), + TIFF(".tiff"), + PNG(".png"), + BMP(".bmp"), + UNKNOWN(".dat"); + + public final String extension; + + HwmfEmbeddedType(String extension) { + this.extension = extension; + } +} diff --git a/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java b/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java index bf91dc864b..da61291066 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java @@ -127,8 +127,10 @@ public class HwmfPicture { ctx.scale(graphicsBounds.getWidth()/wmfBounds.getWidth(), graphicsBounds.getHeight()/wmfBounds.getHeight()); HwmfGraphics g = new HwmfGraphics(ctx, wmfBounds); + int idx = 0; for (HwmfRecord r : records) { r.draw(g); + idx++; } } finally { ctx.setTransform(at); @@ -184,4 +186,8 @@ public class HwmfPicture { double coeff = Units.POINT_DPI/inch; return new Dimension((int)Math.round(bounds.getWidth()*coeff), (int)Math.round(bounds.getHeight()*coeff)); } + + public Iterable getEmbeddings() { + return () -> new HwmfEmbeddedIterator(HwmfPicture.this); + } } 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 ba7e1e3ad8..d7862c451f 100644 --- a/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/HemfPictureTest.java +++ b/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/HemfPictureTest.java @@ -35,6 +35,7 @@ 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; @@ -51,6 +52,8 @@ import org.apache.poi.hemf.record.emf.HemfRecordType; import org.apache.poi.hemf.record.emf.HemfText; import org.apache.poi.hwmf.record.HwmfRecord; import org.apache.poi.hwmf.record.HwmfText; +import org.apache.poi.hwmf.usermodel.HwmfEmbedded; +import org.apache.poi.hwmf.usermodel.HwmfEmbeddedType; import org.apache.poi.hwmf.usermodel.HwmfPicture; import org.apache.poi.util.IOUtils; import org.apache.poi.util.RecordFormatException; @@ -73,16 +76,17 @@ public class HemfPictureTest { // emfs/govdocs1/844/844795.ppt_2.emf // emfs/commoncrawl2/TO/TOYZSTNUSW5OFCFUQ6T5FBLIDLCRF3NH_0.emf - final boolean writeLog = true; + final boolean writeLog = false; final boolean dumpRecords = false; - final boolean savePng = true; + final boolean savePng = false; + final boolean dumpEmbedded = true; 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/render_emf.7z"))) { + SevenZFile sevenZFile = new SevenZFile(new File("tmp/plus_emf.7z"))) { for (int idx=0;;idx++) { SevenZArchiveEntry entry = sevenZFile.getNextEntry(); if (entry == null) break; @@ -90,6 +94,11 @@ 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 + System.out.println(etName); int size = sevenZFile.read(buf); @@ -116,6 +125,18 @@ public class HemfPictureTest { dumpRecords(emf); } + if (dumpEmbedded) { + 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()); +// } + embIdx++; + } + } + + Graphics2D g = null; try { Dimension2D dim = emf.getSize(); @@ -194,7 +215,7 @@ public class HemfPictureTest { if (Files.exists(log)) { soo = StandardOpenOption.APPEND; try (Stream stream = Files.lines(log)) { - stream.forEach((s) -> passed.add(s.split("\\s")[0])); + stream.filter(s -> !s.startsWith("#")).forEach((s) -> passed.add(s.split("\\s")[0])); } } else { soo = StandardOpenOption.CREATE; @@ -380,7 +401,28 @@ public class HemfPictureTest { } } - /* - govdocs1 064213.doc-0.emf contains an example of extextouta - */ + @Test + public void nestedWmfEmf() throws Exception { + try (InputStream is = sl_samples.openResourceAsStream("nested_wmf.emf")) { + HemfPicture emf1 = new HemfPicture(is); + List embeds = new ArrayList<>(); + emf1.getEmbeddings().forEach(embeds::add); + assertEquals(1, embeds.size()); + assertEquals(HwmfEmbeddedType.WMF, embeds.get(0).getEmbeddedType()); + + HwmfPicture wmf = new HwmfPicture(new ByteArrayInputStream(embeds.get(0).getRawData())); + embeds.clear(); + wmf.getEmbeddings().forEach(embeds::add); + assertEquals(3, embeds.size()); + assertEquals(HwmfEmbeddedType.EMF, embeds.get(0).getEmbeddedType()); + + HemfPicture emf2 = new HemfPicture(new ByteArrayInputStream(embeds.get(0).getRawData())); + embeds.clear(); + emf2.getEmbeddings().forEach(embeds::add); + assertTrue(embeds.isEmpty()); + } + } + + + /* govdocs1 064213.doc-0.emf contains an example of extextouta */ } \ No newline at end of file diff --git a/src/scratchpad/testcases/org/apache/poi/hwmf/TestHwmfParsing.java b/src/scratchpad/testcases/org/apache/poi/hwmf/TestHwmfParsing.java index 7632284296..5b0651a93f 100644 --- a/src/scratchpad/testcases/org/apache/poi/hwmf/TestHwmfParsing.java +++ b/src/scratchpad/testcases/org/apache/poi/hwmf/TestHwmfParsing.java @@ -47,6 +47,7 @@ import org.apache.poi.hwmf.record.HwmfFont; import org.apache.poi.hwmf.record.HwmfRecord; import org.apache.poi.hwmf.record.HwmfRecordType; import org.apache.poi.hwmf.record.HwmfText; +import org.apache.poi.hwmf.usermodel.HwmfEmbedded; import org.apache.poi.hwmf.usermodel.HwmfPicture; import org.apache.poi.sl.usermodel.PictureData; import org.apache.poi.sl.usermodel.PictureData.PictureType; @@ -82,8 +83,10 @@ public class TestHwmfParsing { @Test @Ignore("This is work-in-progress and not a real unit test ...") public void paint() throws IOException { - File f = samples.getFile("santa.wmf"); - // File f = new File("bla.wmf"); + boolean dumpEmbedded = true; + +// File f = samples.getFile("santa.wmf"); + File f = new File("testme.wmf"); FileInputStream fis = new FileInputStream(f); HwmfPicture wmf = new HwmfPicture(fis); fis.close(); @@ -92,12 +95,10 @@ public class TestHwmfParsing { int width = Units.pointsToPixel(dim.getWidth()); // keep aspect ratio for height int height = Units.pointsToPixel(dim.getHeight()); - double max = Math.max(width, height); - if (max > 1500) { - width *= 1500/max; - height *= 1500/max; - } - + double scale = (width > height) ? 1500 / width : 1500 / width; + 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); @@ -110,6 +111,17 @@ public class TestHwmfParsing { g.dispose(); ImageIO.write(bufImg, "PNG", new File("bla.png")); + + if (dumpEmbedded) { + int embIdx = 0; + for (HwmfEmbedded emb : wmf.getEmbeddings()) { + final File embName = new File("build/tmp", "emb_"+embIdx + emb.getEmbeddedType().extension); + try (FileOutputStream fos = new FileOutputStream(embName)) { + fos.write(emb.getRawData()); + } + embIdx++; + } + } } @Test @@ -190,7 +202,7 @@ public class TestHwmfParsing { int width = Units.pointsToPixel(dim.getWidth()); // keep aspect ratio for height int height = Units.pointsToPixel(dim.getHeight()); - + BufferedImage bufImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); Graphics2D g = bufImg.createGraphics(); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); diff --git a/test-data/slideshow/nested_wmf.emf b/test-data/slideshow/nested_wmf.emf new file mode 100644 index 0000000000000000000000000000000000000000..1752b81f516a3bc00b65b63bb9dae9f7ce1f4d6e GIT binary patch literal 40276 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