]> source.dussan.org Git - poi.git/commitdiff
Bug 60656 - EMF image support in slideshows
authorAndreas Beeker <kiwiwings@apache.org>
Sat, 4 May 2019 23:01:53 +0000 (23:01 +0000)
committerAndreas Beeker <kiwiwings@apache.org>
Sat, 4 May 2019 23:01:53 +0000 (23:01 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1858625 13f79535-47bb-0310-9956-ffa450edef68

27 files changed:
src/java/org/apache/poi/poifs/filesystem/FileMagic.java
src/java/org/apache/poi/util/LittleEndianInputStream.java
src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfComment.java
src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFill.java
src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfPalette.java
src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordType.java
src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusDraw.java [new file with mode: 0644]
src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusHeader.java
src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusMisc.java [new file with mode: 0644]
src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusObject.java [new file with mode: 0644]
src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordIterator.java
src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusRecordType.java
src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfEmbeddedIterator.java [new file with mode: 0644]
src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java
src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java
src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java
src/scratchpad/src/org/apache/poi/hwmf/record/HwmfEscape.java
src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java
src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMisc.java
src/scratchpad/src/org/apache/poi/hwmf/record/HwmfPlaceableHeader.java
src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfEmbedded.java [new file with mode: 0644]
src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfEmbeddedIterator.java [new file with mode: 0644]
src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfEmbeddedType.java [new file with mode: 0644]
src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java
src/scratchpad/testcases/org/apache/poi/hemf/usermodel/HemfPictureTest.java
src/scratchpad/testcases/org/apache/poi/hwmf/TestHwmfParsing.java
test-data/slideshow/nested_wmf.emf [new file with mode: 0644]

index 9cc0f96a18f049cea0f18bfa5b5f929736052350..79e8253b578a5cd19944475b35f1346323408712 100644 (file)
@@ -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("<!DOCTYP".getBytes(UTF_8),
-            "<html".getBytes(UTF_8),
-            "\n\r<html".getBytes(UTF_8),
-            "\r\n<html".getBytes(UTF_8),
-            "\r<html".getBytes(UTF_8),
-            "\n<html".getBytes(UTF_8),
-            "<HTML".getBytes(UTF_8),
-            "\r\n<HTML".getBytes(UTF_8),
-            "\n\r<HTML".getBytes(UTF_8),
-            "\r<HTML".getBytes(UTF_8),
-            "\n<HTML".getBytes(UTF_8)),
+    HTML("<!DOCTYP",
+         "<html","\n\r<html","\r\n<html","\r<html","\n<html",
+         "<HTML","\r\n<HTML","\n\r<HTML","\r<HTML","\n<HTML"),
     WORD2(new byte[]{ (byte)0xdb, (byte)0xa5, 0x2d, 0x00}),
+    /** JPEG image */
+    JPEG(
+        new byte[]{ (byte)0xFF, (byte)0xD8, (byte)0xFF, (byte)0xDB },
+        new byte[]{ (byte)0xFF, (byte)0xD8, (byte)0xFF, (byte)0xE0, '?', '?', 'J', 'F', 'I', 'F', 0x00, 0x01 },
+        new byte[]{ (byte)0xFF, (byte)0xD8, (byte)0xFF, (byte)0xEE },
+        new byte[]{ (byte)0xFF, (byte)0xD8, (byte)0xFF, (byte)0xE1, '?', '?', 'E', 'x', 'i', 'f', 0x00, 0x00 }),
+    /** GIF image */
+    GIF("GIF87a","GIF89a"),
+    /** PNG Image */
+    PNG(new byte[]{ (byte)0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A }),
+    /** TIFF Image */
+    TIFF("II*\u0000", "MM\u0000*" ),
     // keep UNKNOWN always as last enum!
     /** UNKNOWN magic */
     UNKNOWN(new byte[0]);
@@ -100,13 +104,17 @@ public enum FileMagic {
         this.magic = new byte[1][8];
         LittleEndian.putLong(this.magic[0], 0, magic);
     }
-    
+
     FileMagic(byte[]... magic) {
         this.magic = magic;
     }
     
-    FileMagic(String magic) {
-        this(magic.getBytes(LocaleUtil.CHARSET_1252));
+    FileMagic(String... magic) {
+        this.magic = new byte[magic.length][];
+        int i=0;
+        for (String s : magic) {
+            this.magic[i++] = s.getBytes(LocaleUtil.CHARSET_1252);
+        }
     }
 
     public static FileMagic valueOf(byte[] magic) {
@@ -123,9 +131,7 @@ public enum FileMagic {
     private static boolean findMagic(byte[] expected, byte[] actual) {
         int i=0;
         for (byte expectedByte : expected) {
-            byte actualByte = actual[i++];
-            if ((actualByte != expectedByte &&
-                    (expectedByte != 0x70 || (actualByte != 0x10 && actualByte != 0x20 && actualByte != 0x40)))) {
+            if (actual[i++] != expectedByte && expectedByte != '?') {
                 return false;
             }
         }
index af61d375f266f3dd6f60a5c87b10d9cc9ec7653f..a80597a267332bacfc165aece380459e9cfd3df3 100644 (file)
@@ -204,6 +204,9 @@ public class LittleEndianInputStream extends FilterInputStream implements Little
 
 
        public void skipFully(int len) throws IOException {
+       if (len == 0) {
+               return;
+               }
                long skipped = IOUtils.skipFully(this, len);
                if (skipped > Integer.MAX_VALUE) {
                        throw new IOException("can't skip further than "+Integer.MAX_VALUE);
index c9ea7481e135ccaf8d152ef15a5bbf813202d0f1..eee11cfc9f269be4a16c6a6d4ff3bca928328a3f 100644 (file)
@@ -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 {
index 8e19e8711661917e7f8cf1e922b9eb66998d5797..a380d6c3fa94066299d506f29a0c008ca691795e 100644 (file)
@@ -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 <java AffineTransform> = <xform>
 
         // m00 (scaleX) = eM11 (Horizontal scaling component)
index 9811cb2c3dd1a624730904bb7edeabd561fc2b24..3d002f00441b77a0dd77bfddf500ff5e77e37a9c 100644 (file)
@@ -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;
+        }
+    }
 }
index 9d25a9c145fcdbf1a7fac827e2399e7629075fb2..0066c8c16ad7f0e32962e1ef992ed2f08469f541 100644 (file)
@@ -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 (file)
index 0000000..8f9a13b
--- /dev/null
@@ -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<LittleEndianInputStream, Rectangle2D, Integer> 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<Rectangle2D> 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<LittleEndianInputStream, Rectangle2D, Integer> readRect = getReadRect();
+
+            rectData.ensureCapacity(count);
+
+            int size = 2 * LittleEndianConsts.INT_SIZE;
+            for (int i = 0; i<count; i++) {
+                Rectangle2D rect = new Rectangle2D.Double();
+                size += readRect.apply(leis, rect);
+                rectData.add(rect);
+            }
+
+            return size;
+        }
+    }
+
+    public static class EmfPlusDrawImagePoints implements HemfPlusRecord, EmfPlusObjectId, EmfPlusCompressed {
+        /**
+         * This bit indicates that the rendering of the image includes applying an effect.
+         * If set, an object of the Effect class MUST have been specified in an earlier EmfPlusSerializableObject record.
+         */
+        private static final BitField EFFECT = BitFieldFactory.getInstance(0x2000);
+
+        /**
+         * This bit indicates whether the PointData field specifies relative or absolute locations.
+         * If set, each element in PointData specifies a location in the coordinate space that is relative to the
+         * location specified by the previous element in the array. In the case of the first element in PointData,
+         * a previous location at coordinates (0,0) is assumed.
+         * If clear, PointData specifies absolute locations according to the {@link #isCompressed()} flag.
+         *
+         * Note If this flag is set, the {@link #isCompressed()} flag (above) is undefined and MUST be ignored.
+         */
+        private static final BitField POSITION = BitFieldFactory.getInstance(0x0800);
+
+        private int flags;
+        private int imageAttributesID;
+        private UnitType srcUnit;
+        private final Rectangle2D srcRect = new Rectangle2D.Double();
+        private final Point2D upperLeft = new Point2D.Double();
+        private final Point2D lowerRight = new Point2D.Double();
+        private final Point2D lowerLeft = new Point2D.Double();
+        private final AffineTransform trans = new AffineTransform();
+
+        @Override
+        public HemfPlusRecordType getEmfPlusRecordType() {
+            return HemfPlusRecordType.drawImagePoints;
+        }
+
+        @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 defines a portion of the image to be rendered.
+            size += readRectF(leis, srcRect);
+
+            // A 32-bit unsigned integer that specifies the number of points in the PointData array.
+            // Exactly 3 points MUST be specified.
+            int count = leis.readInt();
+            assert(count == 3);
+            size += LittleEndianConsts.INT_SIZE;
+
+            BiFunction<LittleEndianInputStream, Point2D, Integer> 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<Point2D> 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<glyphCount; i++) {
+                Point2D p = new Point2D.Double();
+                size += readPointF(leis, p);
+                glpyhPos.add(p);
+            }
+
+            if (hasMatrix) {
+                size += HemfFill.readXForm(leis, transformMatrix);
+            }
+
+
+            return size;
+        }
+    }
+
+    /** The EmfPlusDrawRects record specifies drawing a series of rectangles. */
+    public static class EmfPlusDrawRects implements HemfPlusRecord, EmfPlusObjectId, EmfPlusCompressed {
+        private int flags;
+        private final List<Rectangle2D> 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<LittleEndianInputStream, Rectangle2D, Integer> readRect = getReadRect();
+
+            for (int i=0; i<count; i++) {
+                Rectangle2D rect = new Rectangle2D.Double();
+                size += readRect.apply(leis, rect);
+            }
+
+            return size;
+        }
+    }
+
+
+    static double round10(double d) {
+        return new BigDecimal(d).setScale(10, BigDecimal.ROUND_HALF_UP).doubleValue();
+    }
+
+    static int readRectS(LittleEndianInputStream leis, Rectangle2D bounds) {
+        // A 16-bit signed integer that defines the ... coordinate
+        final int x = leis.readShort();
+        final int y = leis.readShort();
+        final int width = leis.readShort();
+        final int height = leis.readShort();
+        bounds.setRect(x, y, width, height);
+
+        return 4 * LittleEndianConsts.SHORT_SIZE;
+    }
+
+    static int readRectF(LittleEndianInputStream leis, Rectangle2D bounds) {
+        // A 32-bit floating-point that defines the ... coordinate
+        final double x = leis.readFloat();
+        final double y = leis.readFloat();
+        final double width = leis.readFloat();
+        final double height = leis.readFloat();
+        bounds.setRect(x, y, width, height);
+
+        return 4 * LittleEndianConsts.INT_SIZE;
+    }
+
+    /**
+     * The EmfPlusPoint object specifies an ordered pair of integer (X,Y) values that define an absolute
+     * location in a coordinate space.
+     */
+    static int readPointS(LittleEndianInputStream leis, Point2D point) {
+        double x = leis.readShort();
+        double y = leis.readShort();
+        point.setLocation(x,y);
+        return 2*LittleEndianConsts.SHORT_SIZE;
+    }
+
+    /**
+     * The EmfPlusPointF object specifies an ordered pair of floating-point (X,Y) values that define an
+     * absolute location in a coordinate space.
+     */
+    static int readPointF(LittleEndianInputStream leis, Point2D point) {
+        double x = leis.readFloat();
+        double y = leis.readFloat();
+        point.setLocation(x,y);
+        return 2*LittleEndianConsts.INT_SIZE;
+    }
+
+    /**
+     * The EmfPlusPointR object specifies an ordered pair of integer (X,Y) values that define a relative
+     * location in a coordinate space.
+     */
+    static int readPointR(LittleEndianInputStream leis, Point2D point) {
+        int[] p = { 0 };
+        int size = readEmfPlusInteger(leis, p);
+        double x = p[0];
+        size += readEmfPlusInteger(leis, p);
+        double y = p[0];
+        point.setLocation(x,y);
+        return size;
+    }
+
+    private static int readEmfPlusInteger(LittleEndianInputStream leis, int[] value) {
+        value[0] = leis.readByte();
+        // check for EmfPlusInteger7 value
+        if ((value[0] & 0x80) == 0) {
+            return LittleEndianConsts.BYTE_SIZE;
+        }
+        // ok we've read a EmfPlusInteger15
+        value[0] = ((value[0] << 8) | leis.readByte()) & 0x7FFF;
+        return LittleEndianConsts.SHORT_SIZE;
+    }
+}
index 6264750a9220ce0e9b951d82f9f5dd2bb0a14ef7..ee196a78d90749b5ad5c395a14975f1e9d828bec 100644 (file)
@@ -20,15 +20,40 @@ package org.apache.poi.hemf.record.emfplus;
 
 import java.io.IOException;
 
+import org.apache.poi.util.BitField;
+import org.apache.poi.util.BitFieldFactory;
 import org.apache.poi.util.Internal;
 import org.apache.poi.util.LittleEndianConsts;
 import org.apache.poi.util.LittleEndianInputStream;
 
 @Internal
 public class HemfPlusHeader implements HemfPlusRecord {
+    /**
+     * The GraphicsVersion enumeration defines versions of operating system graphics that are used to
+     * create EMF+ metafiles.
+     */
+    public enum GraphicsVersion {
+        V1(0x0001),
+        V1_1(0x0002)
+        ;
+
+        public final int id;
+
+        GraphicsVersion(int id) {
+            this.id = id;
+        }
+
+        public static GraphicsVersion valueOf(int id) {
+            for (GraphicsVersion wrt : values()) {
+                if (wrt.id == id) return wrt;
+            }
+            return null;
+        }
+    }
+
 
     private int flags;
-    private long version; //hack for now; replace with EmfPlusGraphicsVersion object
+    private final EmfPlusGraphicsVersion version = new EmfPlusGraphicsVersion();
     private long emfPlusFlags;
     private long logicalDpiX;
     private long logicalDpiY;
@@ -45,11 +70,9 @@ public class HemfPlusHeader implements HemfPlusRecord {
     @Override
     public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException {
         this.flags = flags;
-        version = leis.readUInt();
+        version.init(leis);
 
-        // verify MetafileSignature (20 bits) == 0xDBC01 and
-        // GraphicsVersion (12 bits) in (1 or 2)
-        assert((version & 0xFFFFFA00) == 0xDBC01000L && ((version & 0x3FF) == 1 || (version & 0x3FF) == 2));
+        assert(version.getMetafileSignature() == 0xDBC01 && version.getGraphicsVersion() != null);
 
         emfPlusFlags = leis.readUInt();
 
@@ -58,7 +81,7 @@ public class HemfPlusHeader implements HemfPlusRecord {
         return 4* LittleEndianConsts.INT_SIZE;
     }
 
-    public long getVersion() {
+    public EmfPlusGraphicsVersion getVersion() {
         return version;
     }
 
@@ -84,4 +107,38 @@ public class HemfPlusHeader implements HemfPlusRecord {
                 ", logicalDpiY=" + logicalDpiY +
                 '}';
     }
+
+    public static class EmfPlusGraphicsVersion {
+        private static final BitField METAFILE_SIGNATURE = BitFieldFactory.getInstance(0xFFFFF000);
+
+        private static final BitField GRAPHICS_VERSION = BitFieldFactory.getInstance(0x00000FFF);
+
+
+        private int metafileSignature;
+        private GraphicsVersion graphicsVersion;
+
+
+        public int getMetafileSignature() {
+            return metafileSignature;
+        }
+
+        public GraphicsVersion getGraphicsVersion() {
+            return graphicsVersion;
+        }
+
+        public long init(LittleEndianInputStream leis) throws IOException {
+            int val = leis.readInt();
+            // A value that identifies the type of metafile. The value for an EMF+ metafile is 0xDBC01.
+            metafileSignature = METAFILE_SIGNATURE.getValue(val);
+            // The version of operating system graphics. This value MUST be defined in the GraphicsVersion enumeration
+            graphicsVersion = GraphicsVersion.valueOf(GRAPHICS_VERSION.getValue(val));
+
+            return LittleEndianConsts.INT_SIZE;
+        }
+
+        public String toString() {
+            return "{ metafileSignature=0x"+Integer.toHexString(metafileSignature)+
+                    " , graphicsVersion='"+graphicsVersion+"' }";
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusMisc.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusMisc.java
new file mode 100644 (file)
index 0000000..771758c
--- /dev/null
@@ -0,0 +1,346 @@
+/* ====================================================================
+   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 static org.apache.poi.hemf.record.emfplus.HemfPlusDraw.readRectF;
+
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.io.IOException;
+
+import org.apache.poi.hemf.record.emf.HemfFill;
+import org.apache.poi.util.BitField;
+import org.apache.poi.util.BitFieldFactory;
+import org.apache.poi.util.LittleEndian;
+import org.apache.poi.util.LittleEndianConsts;
+import org.apache.poi.util.LittleEndianInputStream;
+
+public class HemfPlusMisc {
+    public interface EmfPlusObjectId {
+        BitField OBJECT_ID = BitFieldFactory.getInstance(0x00FF);
+
+        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 int getObjectId() {
+            return OBJECT_ID.getValue(getFlags());
+        }
+    }
+
+    public enum CombineMode {
+        CombineModeReplace(0x00000000),
+        CombineModeIntersect(0x00000001),
+        CombineModeUnion(0x00000002),
+        CombineModeXOR(0x00000003),
+        CombineModeExclude(0x00000004),
+        CombineModeComplement(0x00000005)
+        ;
+
+        public final int id;
+
+        CombineMode(int id) {
+            this.id = id;
+        }
+
+        public static CombineMode valueOf(int id) {
+            for (CombineMode wrt : values()) {
+                if (wrt.id == id) return wrt;
+            }
+            return null;
+        }
+    }
+
+
+    public static abstract class EmfPlusFlagOnly implements HemfPlusRecord {
+        private int flags;
+        private HemfPlusRecordType recordType;
+
+        @Override
+        public int getFlags() {
+            return flags;
+        }
+
+        @Override
+        public final HemfPlusRecordType getEmfPlusRecordType() {
+            return recordType;
+        }
+
+        @Override
+        public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException {
+            this.flags = flags;
+            assert(dataSize == 0);
+            recordType = HemfPlusRecordType.getById(recordId);
+            return 0;
+        }
+    }
+
+    public static class EmfPlusEOF extends EmfPlusFlagOnly {
+    }
+
+    /**
+     * The EmfPlusSetPixelOffsetMode record specifies how pixels are centered with respect to the
+     * coordinates of the drawing surface.
+     */
+    public static class EmfPlusSetPixelOffsetMode extends EmfPlusFlagOnly {
+    }
+
+    /**
+     * The EmfPlusSetAntiAliasMode record specifies the anti-aliasing mode for text output.
+     */
+    public static class EmfPlusSetAntiAliasMode extends EmfPlusFlagOnly {
+    }
+
+    /**
+     * The EmfPlusSetCompositingMode record specifies how source colors are combined with background colors.
+     */
+    public static class EmfPlusSetCompositingMode extends EmfPlusFlagOnly {
+    }
+
+    /**
+     * The EmfPlusSetCompositingQuality record specifies the desired level of quality for creating
+     * composite images from multiple objects.
+     */
+    public static class EmfPlusSetCompositingQuality extends EmfPlusFlagOnly {
+    }
+
+    /**
+     * The EmfPlusSetInterpolationMode record specifies how image scaling, including stretching and
+     * shrinking, is performed.
+     */
+    public static class EmfPlusSetInterpolationMode extends EmfPlusFlagOnly {
+    }
+
+    /**
+     * The EmfPlusGetDC record specifies that subsequent EMF records encountered in the metafile
+     * SHOULD be processed.
+     */
+    public static class EmfPlusGetDC extends EmfPlusFlagOnly {
+    }
+
+    /**
+     * The EmfPlusSetTextRenderingHint record specifies the quality of text rendering, including the type
+     * of anti-aliasing.
+     */
+    public static class EmfPlusSetTextRenderingHint extends EmfPlusFlagOnly {
+    }
+
+    /**
+     * The EmfPlusResetWorldTransform record resets the current world space transform to the identify matrix.
+     */
+    public static class EmfPlusResetWorldTransform extends EmfPlusFlagOnly {
+    }
+
+
+    /**
+     * The EmfPlusSetWorldTransform record sets the world transform according to the values in a
+     * specified transform matrix.
+     */
+    public static class EmfPlusSetWorldTransform implements HemfPlusRecord {
+        private int flags;
+        private final AffineTransform matrixData = new AffineTransform();
+
+        @Override
+        public HemfPlusRecordType getEmfPlusRecordType() {
+            return HemfPlusRecordType.setWorldTransform;
+        }
+
+        @Override
+        public int getFlags() {
+            return flags;
+        }
+
+        @Override
+        public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException {
+            this.flags = flags;
+
+            return HemfFill.readXForm(leis, matrixData);
+        }
+    }
+
+    /**
+     * The EmfPlusMultiplyWorldTransform record multiplies the current world space transform by a
+     * specified transform matrix.
+     */
+    public static class EmfPlusMultiplyWorldTransform extends EmfPlusSetWorldTransform {
+        @Override
+        public HemfPlusRecordType getEmfPlusRecordType() {
+            return HemfPlusRecordType.multiplyWorldTransform;
+        }
+    }
+
+    /**
+     * The EmfPlusSetPageTransform record specifies scaling factors and units for converting page space
+     * coordinates to device space coordinates.
+     */
+    public static class EmfPlusSetPageTransform implements HemfPlusRecord {
+        private int flags;
+        private double pageScale;
+
+        @Override
+        public HemfPlusRecordType getEmfPlusRecordType() {
+            return HemfPlusRecordType.setPageTransform;
+        }
+
+        @Override
+        public int getFlags() {
+            return flags;
+        }
+
+        @Override
+        public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException {
+            this.flags = flags;
+            pageScale = leis.readFloat();
+            return LittleEndianConsts.INT_SIZE;
+        }
+    }
+
+    /**
+     * The EmfPlusSetClipRegion record combines the current clipping region with another graphics region.
+     */
+    public static class EmfPlusSetClipRegion extends EmfPlusSetClipPath {
+
+    }
+
+
+    /**
+     * The EmfPlusSetClipPath record combines the current clipping region with a graphics path.
+     */
+    public static class EmfPlusSetClipPath extends EmfPlusFlagOnly implements EmfPlusObjectId {
+        private static final BitField COMBINE_MODE = BitFieldFactory.getInstance(0x0F00);
+
+        public CombineMode getCombineMode() {
+            return CombineMode.valueOf(COMBINE_MODE.getValue(getFlags()));
+        }
+    }
+
+    /** The EmfPlusSetClipRect record combines the current clipping region with a rectangle. */
+    public static class EmfPlusSetClipRect implements HemfPlusRecord {
+        private static final BitField COMBINE_MODE = BitFieldFactory.getInstance(0x0F00);
+
+        private int flags;
+        private final Rectangle2D clipRect = new Rectangle2D.Double();
+
+        @Override
+        public HemfPlusRecordType getEmfPlusRecordType() {
+            return HemfPlusRecordType.setClipRect;
+        }
+
+        @Override
+        public int getFlags() {
+            return flags;
+        }
+
+        public CombineMode getCombineMode() {
+            return CombineMode.valueOf(COMBINE_MODE.getValue(getFlags()));
+        }
+
+        @Override
+        public long init(LittleEndianInputStream leis, long dataSize, long recordId, int flags) throws IOException {
+            this.flags = flags;
+
+            // An EmfPlusRectF object that defines the rectangle to use in the CombineMode operation.
+            return readRectF(leis, clipRect);
+        }
+    }
+
+
+    /** The EmfPlusResetClip record resets the current clipping region for the world space to infinity. */
+    public static class EmfPlusResetClip extends EmfPlusFlagOnly {
+
+    }
+
+    /**
+     * The EmfPlusSave record saves the graphics state, identified by a specified index, on a stack of saved
+     * graphics states.
+     */
+    public static class EmfPlusSave implements HemfPlusRecord {
+        private int flags;
+        private int stackIndex;
+
+        @Override
+        public HemfPlusRecordType getEmfPlusRecordType() {
+            return HemfPlusRecordType.save;
+        }
+
+        @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 a level to associate with the graphics state.
+            // The level value can be used by a subsequent EmfPlusRestore record operation to retrieve the graphics state.
+            stackIndex = leis.readInt();
+
+            return LittleEndianConsts.INT_SIZE;
+        }
+    }
+
+    /**
+     * The EmfPlusRestore record restores the graphics state, identified by a specified index, from a stack
+     * of saved graphics states.
+     */
+    public static class EmfPlusRestore extends EmfPlusSave {
+        @Override
+        public HemfPlusRecordType getEmfPlusRecordType() {
+            return HemfPlusRecordType.restore;
+        }
+    }
+
+    /** The EmfPlusSetRenderingOrigin record specifies the rendering origin for graphics output. */
+    public static class EmfPlusSetRenderingOrigin implements HemfPlusRecord {
+        int flags;
+        Point2D origin = new Point2D.Double();
+
+        @Override
+        public HemfPlusRecordType getEmfPlusRecordType() {
+            return HemfPlusRecordType.setRenderingOrigin;
+        }
+
+        @Override
+        public int getFlags() {
+            return flags;
+        }
+
+        public Point2D getOrigin() {
+            return origin;
+        }
+
+        @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 horizontal coordinate value of the rendering origin.
+            double x = leis.readUInt();
+            // A 32-bit unsigned integer that defines the vertical coordinate value of the rendering origin.
+            double y = leis.readUInt();
+
+            origin.setLocation(x,y);
+
+            return LittleEndianConsts.INT_SIZE*2;
+        }
+    }
+
+}
diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusObject.java b/src/scratchpad/src/org/apache/poi/hemf/record/emfplus/HemfPlusObject.java
new file mode 100644 (file)
index 0000000..5e30ba5
--- /dev/null
@@ -0,0 +1,588 @@
+/* ====================================================================
+   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.io.IOException;
+import java.util.function.Supplier;
+
+import org.apache.poi.hemf.record.emfplus.HemfPlusHeader.EmfPlusGraphicsVersion;
+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.LittleEndianConsts;
+import org.apache.poi.util.LittleEndianInputStream;
+
+public class HemfPlusObject {
+    private static final int MAX_OBJECT_SIZE = 50_000_000;
+
+    /**
+     * The ObjectType enumeration defines types of graphics objects that can be created and used in graphics operations.
+     */
+    public enum EmfPlusObjectType {
+        /**
+         * The object is not a valid object.
+         */
+        INVALID(0x00000000, EmfPlusUnknownData::new),
+        /**
+         * Brush objects fill graphics regions.
+         */
+        BRUSH(0x00000001, EmfPlusUnknownData::new),
+        /**
+         * Pen objects draw graphics lines.
+         */
+        PEN(0x00000002, EmfPlusUnknownData::new),
+        /**
+         * Path objects specify sequences of lines, curves, and shapes.
+         */
+        PATH(0x00000003, EmfPlusUnknownData::new),
+        /**
+         * Region objects specify areas of the output surface.
+         */
+        REGION(0x00000004, EmfPlusUnknownData::new),
+        /**
+         * Image objects encapsulate bitmaps and metafiles.
+         */
+        IMAGE(0x00000005, EmfPlusImage::new),
+        /**
+         * Font objects specify font properties, including typeface style, em size, and font family.
+         */
+        FONT(0x00000006, EmfPlusUnknownData::new),
+        /**
+         * String format objects specify text layout, including alignment, orientation, tab stops, clipping,
+         * and digit substitution for languages that do not use Western European digits.
+         */
+        STRING_FORMAT(0x00000007, EmfPlusUnknownData::new),
+        /**
+         * Image attribute objects specify operations on pixels during image rendering, including color
+         * adjustment, grayscale adjustment, gamma correction, and color mapping.
+         */
+        IMAGE_ATTRIBUTES(0x00000008, EmfPlusImageAttributes::new),
+        /**
+         * Custom line cap objects specify shapes to draw at the ends of a graphics line, including
+         * squares, circles, and diamonds.
+         */
+        CUSTOM_LINE_CAP(0x00000009, EmfPlusUnknownData::new);
+
+        public final int id;
+        public final Supplier<? extends EmfPlusObjectData> constructor;
+
+        EmfPlusObjectType(int id, Supplier<? extends EmfPlusObjectData> 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 extends EmfPlusObjectData> 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;
+        }
+    }
+}
index 4c6f0ad9198544611aeab11bbd3789d1515b6ed6..3cf7076b3986333b02a171bf23d84a91bec82072 100644 (file)
@@ -50,7 +50,8 @@ public class HemfPlusRecordIterator implements Iterator<HemfPlusRecord> {
     @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;
index 2fc1926ff6319795e0c55c55b5df47cb1037d36e..38263f4effac611f9685d951bfd401a8b3eb6744 100644 (file)
@@ -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 (file)
index 0000000..2d5699d
--- /dev/null
@@ -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<HwmfEmbedded> {
+
+    private final Deque<Iterator<?>> iterStack = new ArrayDeque<>();
+    private Object current;
+
+    public HemfEmbeddedIterator(HemfPicture emf) {
+        this(emf.getRecords().iterator());
+    }
+
+    public HemfEmbeddedIterator(Iterator<HemfRecord> 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());
+        }
+    }
+}
index 6d53ae23ea4f28220b9c8d5ff9834e3323d3a72e..0915fe5f27f49b24002009c7b99f068d7e3d68b3 100644 (file)
@@ -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<HemfRecord> {
         }
     }
 
+    public Iterable<HwmfEmbedded> getEmbeddings() {
+        return () -> new HemfEmbeddedIterator(HemfPicture.this);
+    }
 }
index 766f9020dff0220c3de6e62eaf525bdf89d7b800..744c995efe6c4563355a476542515eaefb1f020c 100644 (file)
@@ -142,7 +142,7 @@ public class HwmfGraphics {
             graphicsCtx.fill(shape);
         }
 
-//        draw(shape);
+        draw(shape);
     }
 
     protected BasicStroke getStroke() {
index 1632525d8e49ee2cf0e776d9ccb72c86623153c0..2ef22a407aa7d0e562a4d5ae516768b260938f23 100644 (file)
@@ -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
index 0d00934530e12959471245ed4aa178c42376b914..44623703503449eead7ad50b4211d650ee013945 100644 (file)
 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<? extends HwmfEscape.HwmfEscapeData> constructor;
+
+
+        EscapeFunction(int flag, Supplier<? extends HwmfEscape.HwmfEscapeData> 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;
+
+        escapeData = escapeFunction.constructor.get();
+        size += escapeData.init(leis, byteCount, escapeFunction);
+
+        return size;
+    }
+
+    public EscapeFunction getEscapeFunction() {
+        return escapeFunction;
+    }
 
-        return 2*LittleEndianConsts.SHORT_SIZE+byteCount;
+    public <T extends HwmfEscapeData> 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;
+        }
+    }
 }
index 3b420e401d652f8be864d20bb3f29b3c5efe6205..920e302e2a6b9e75c459c8cd009962c28d3d1e27 100644 (file)
@@ -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 <a href="https://en.wikipedia.org/wiki/BMP_file_format">BMP format</a>
+         * @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;
         }
     }
 
index eef15e30d7a2b1f766ee61d01e882aff66727b86..bd9264b6ac2b9afdb88430d733b3713b0c48c556 100644 (file)
@@ -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;
+            }
+        }
     }
 
     /**
index e2d6a3a5e1f75ec829c7a93cd9a8d8b9775dd800..82aba4b12d5f203d8621dcc537854a2d8ad28a81 100644 (file)
@@ -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 (file)
index 0000000..2bb9187
--- /dev/null
@@ -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 (file)
index 0000000..5d5b88a
--- /dev/null
@@ -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<HwmfEmbedded> {
+
+    private final Deque<Iterator<?>> iterStack = new ArrayDeque<>();
+    private Object current;
+
+    public HwmfEmbeddedIterator(HwmfPicture wmf) {
+        this(wmf.getRecords().iterator());
+    }
+
+    public HwmfEmbeddedIterator(Iterator<HwmfRecord> 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 (file)
index 0000000..e9e96ed
--- /dev/null
@@ -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;
+    }
+}
index bf91dc864bb946436fa6b535c5934b21a50e13e1..da61291066da00735ae6f74f239895dde3594964 100644 (file)
@@ -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<HwmfEmbedded> getEmbeddings() {
+        return () -> new HwmfEmbeddedIterator(HwmfPicture.this);
+    }
 }
index ba7e1e3ad83853f250dfaa454fafb7aa7bc62e7d..d7862c451fd55c82139b5015e6c883b8351b870e 100644 (file)
@@ -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<String> 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<String> 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<HwmfEmbedded> 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
index 763228429638a67e084acac0169e707e1896fc91..5b0651a93f0be4962fbb8ccf3c476d8db56d6f5b 100644 (file)
@@ -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 (file)
index 0000000..1752b81
Binary files /dev/null and b/test-data/slideshow/nested_wmf.emf differ