Bug 60656 - EMF image support in slideshows

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1858625 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Andreas Beeker 2019-05-04 23:01:53 +00:00
parent dfbf788201
commit ea0e01acc1
27 changed files with 2696 additions and 164 deletions

View 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;
}
}

View 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);

View 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 {

View 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)

View 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;
}
}
}

View 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),

View File

@ -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;
}
}

View 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+"' }";
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View 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;

View 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),

View File

@ -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());
}
}
}

View 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);
}
}

View File

@ -142,7 +142,7 @@ public class HwmfGraphics {
graphicsCtx.fill(shape);
}
// draw(shape);
draw(shape);
}
protected BasicStroke getStroke() {

View 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

View File

@ -18,10 +18,13 @@
package org.apache.poi.hwmf.record;
import java.io.IOException;
import java.util.function.Supplier;
import org.apache.poi.hwmf.draw.HwmfGraphics;
import org.apache.poi.util.HexDump;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianCP950Reader;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInputStream;
@ -30,135 +33,140 @@ import org.apache.poi.util.LittleEndianInputStream;
* might not be directly accessible through WMF records
*/
public class HwmfEscape implements HwmfRecord {
private static final int MAX_OBJECT_SIZE = 0xFFFF;
public enum EscapeFunction {
/** Notifies the printer driver that the application has finished writing to a page. */
NEWFRAME(0x0001),
NEWFRAME(0x0001, WmfEscapeUnknownData::new),
/** Stops processing the current document. */
ABORTDOC(0x0002),
ABORTDOC(0x0002, WmfEscapeUnknownData::new),
/** Notifies the printer driver that the application has finished writing to a band. */
NEXTBAND(0x0003),
NEXTBAND(0x0003, WmfEscapeUnknownData::new),
/** Sets color table values. */
SETCOLORTABLE(0x0004),
SETCOLORTABLE(0x0004, WmfEscapeUnknownData::new),
/** Gets color table values. */
GETCOLORTABLE(0x0005),
GETCOLORTABLE(0x0005, WmfEscapeUnknownData::new),
/** Causes all pending output to be flushed to the output device. */
FLUSHOUT(0x0006),
FLUSHOUT(0x0006, WmfEscapeUnknownData::new),
/** Indicates that the printer driver SHOULD print text only, and no graphics. */
DRAFTMODE(0x0007),
DRAFTMODE(0x0007, WmfEscapeUnknownData::new),
/** Queries a printer driver to determine whether a specific escape function is supported on the output device it drives. */
QUERYESCSUPPORT(0x0008),
QUERYESCSUPPORT(0x0008, WmfEscapeUnknownData::new),
/** Sets the application-defined function that allows a print job to be canceled during printing. */
SETABORTPROC(0x0009),
SETABORTPROC(0x0009, WmfEscapeUnknownData::new),
/** Notifies the printer driver that a new print job is starting. */
STARTDOC(0x000A),
STARTDOC(0x000A, WmfEscapeUnknownData::new),
/** Notifies the printer driver that the current print job is ending. */
ENDDOC(0x000B),
ENDDOC(0x000B, WmfEscapeUnknownData::new),
/** Retrieves the physical page size currently selected on an output device. */
GETPHYSPAGESIZE(0x000C),
GETPHYSPAGESIZE(0x000C, WmfEscapeUnknownData::new),
/** Retrieves the offset from the upper-left corner of the physical page where the actual printing or drawing begins. */
GETPRINTINGOFFSET(0x000D),
GETPRINTINGOFFSET(0x000D, WmfEscapeUnknownData::new),
/** Retrieves the scaling factors for the x-axis and the y-axis of a printer. */
GETSCALINGFACTOR(0x000E),
GETSCALINGFACTOR(0x000E, WmfEscapeUnknownData::new),
/** Used to embed an enhanced metafile format (EMF) metafile within a WMF metafile. */
META_ESCAPE_ENHANCED_METAFILE(0x000F),
META_ESCAPE_ENHANCED_METAFILE(0x000F, WmfEscapeEMF::new),
/** Sets the width of a pen in pixels. */
SETPENWIDTH(0x0010),
SETPENWIDTH(0x0010, WmfEscapeUnknownData::new),
/** Sets the number of copies. */
SETCOPYCOUNT(0x0011),
SETCOPYCOUNT(0x0011, WmfEscapeUnknownData::new),
/** Sets the source, such as a particular paper tray or bin on a printer, for output forms. */
SETPAPERSOURCE(0x0012),
SETPAPERSOURCE(0x0012, WmfEscapeUnknownData::new),
/** This record passes through arbitrary data. */
PASSTHROUGH(0x0013),
PASSTHROUGH(0x0013, WmfEscapeUnknownData::new),
/** Gets information concerning graphics technology that is supported on a device. */
GETTECHNOLOGY(0x0014),
GETTECHNOLOGY(0x0014, WmfEscapeUnknownData::new),
/** Specifies the line-drawing mode to use in output to a device. */
SETLINECAP(0x0015),
SETLINECAP(0x0015, WmfEscapeUnknownData::new),
/** Specifies the line-joining mode to use in output to a device. */
SETLINEJOIN(0x0016),
SETLINEJOIN(0x0016, WmfEscapeUnknownData::new),
/** Sets the limit for the length of miter joins to use in output to a device. */
SETMITERLIMIT(0x0017),
SETMITERLIMIT(0x0017, WmfEscapeUnknownData::new),
/** Retrieves or specifies settings concerning banding on a device, such as the number of bands. */
BANDINFO(0x0018),
BANDINFO(0x0018, WmfEscapeUnknownData::new),
/** Draws a rectangle with a defined pattern. */
DRAWPATTERNRECT(0x0019),
DRAWPATTERNRECT(0x0019, WmfEscapeUnknownData::new),
/** Retrieves the physical pen size currently defined on a device. */
GETVECTORPENSIZE(0x001A),
GETVECTORPENSIZE(0x001A, WmfEscapeUnknownData::new),
/** Retrieves the physical brush size currently defined on a device. */
GETVECTORBRUSHSIZE(0x001B),
GETVECTORBRUSHSIZE(0x001B, WmfEscapeUnknownData::new),
/** Enables or disables double-sided (duplex) printing on a device. */
ENABLEDUPLEX(0x001C),
ENABLEDUPLEX(0x001C, WmfEscapeUnknownData::new),
/** Retrieves or specifies the source of output forms on a device. */
GETSETPAPERBINS(0x001D),
GETSETPAPERBINS(0x001D, WmfEscapeUnknownData::new),
/** Retrieves or specifies the paper orientation on a device. */
GETSETPRINTORIENT(0x001E),
GETSETPRINTORIENT(0x001E, WmfEscapeUnknownData::new),
/** Retrieves information concerning the sources of different forms on an output device. */
ENUMPAPERBINS(0x001F),
ENUMPAPERBINS(0x001F, WmfEscapeUnknownData::new),
/** Specifies the scaling of device-independent bitmaps (DIBs). */
SETDIBSCALING(0x0020),
SETDIBSCALING(0x0020, WmfEscapeUnknownData::new),
/** Indicates the start and end of an encapsulated PostScript (EPS) section. */
EPSPRINTING(0x0021),
EPSPRINTING(0x0021, WmfEscapeUnknownData::new),
/** Queries a printer driver for paper dimensions and other forms data. */
ENUMPAPERMETRICS(0x0022),
ENUMPAPERMETRICS(0x0022, WmfEscapeUnknownData::new),
/** Retrieves or specifies paper dimensions and other forms data on an output device. */
GETSETPAPERMETRICS(0x0023),
GETSETPAPERMETRICS(0x0023, WmfEscapeUnknownData::new),
/** Sends arbitrary PostScript data to an output device. */
POSTSCRIPT_DATA(0x0025),
POSTSCRIPT_DATA(0x0025, WmfEscapeUnknownData::new),
/** Notifies an output device to ignore PostScript data. */
POSTSCRIPT_IGNORE(0x0026),
POSTSCRIPT_IGNORE(0x0026, WmfEscapeUnknownData::new),
/** Gets the device units currently configured on an output device. */
GETDEVICEUNITS(0x002A),
GETDEVICEUNITS(0x002A, WmfEscapeUnknownData::new),
/** Gets extended text metrics currently configured on an output device. */
GETEXTENDEDTEXTMETRICS(0x0100),
GETEXTENDEDTEXTMETRICS(0x0100, WmfEscapeUnknownData::new),
/** Gets the font kern table currently defined on an output device. */
GETPAIRKERNTABLE(0x0102),
GETPAIRKERNTABLE(0x0102, WmfEscapeUnknownData::new),
/** Draws text using the currently selected font, background color, and text color. */
EXTTEXTOUT(0x0200),
EXTTEXTOUT(0x0200, WmfEscapeUnknownData::new),
/** Gets the font face name currently configured on a device. */
GETFACENAME(0x0201),
GETFACENAME(0x0201, WmfEscapeUnknownData::new),
/** Sets the font face name on a device. */
DOWNLOADFACE(0x0202),
DOWNLOADFACE(0x0202, WmfEscapeUnknownData::new),
/** Queries a printer driver about the support for metafiles on an output device. */
METAFILE_DRIVER(0x0801),
METAFILE_DRIVER(0x0801, WmfEscapeUnknownData::new),
/** Queries the printer driver about its support for DIBs on an output device. */
QUERYDIBSUPPORT(0x0C01),
QUERYDIBSUPPORT(0x0C01, WmfEscapeUnknownData::new),
/** Opens a path. */
BEGIN_PATH(0x1000),
BEGIN_PATH(0x1000, WmfEscapeUnknownData::new),
/** Defines a clip region that is bounded by a path. The input MUST be a 16-bit quantity that defines the action to take. */
CLIP_TO_PATH(0x1001),
CLIP_TO_PATH(0x1001, WmfEscapeUnknownData::new),
/** Ends a path. */
END_PATH(0x1002),
END_PATH(0x1002, WmfEscapeUnknownData::new),
/** The same as STARTDOC specified with a NULL document and output filename, data in raw mode, and a type of zero. */
OPEN_CHANNEL(0x100E),
OPEN_CHANNEL(0x100E, WmfEscapeUnknownData::new),
/** Instructs the printer driver to download sets of PostScript procedures. */
DOWNLOADHEADER(0x100F),
DOWNLOADHEADER(0x100F, WmfEscapeUnknownData::new),
/** The same as ENDDOC. See OPEN_CHANNEL. */
CLOSE_CHANNEL(0x1010),
CLOSE_CHANNEL(0x1010, WmfEscapeUnknownData::new),
/** Sends arbitrary data directly to a printer driver, which is expected to process this data only when in PostScript mode. */
POSTSCRIPT_PASSTHROUGH(0x1013),
POSTSCRIPT_PASSTHROUGH(0x1013, WmfEscapeUnknownData::new),
/** Sends arbitrary data directly to the printer driver. */
ENCAPSULATED_POSTSCRIPT(0x1014),
ENCAPSULATED_POSTSCRIPT(0x1014, WmfEscapeUnknownData::new),
/** Sets the printer driver to either PostScript or GDI mode. */
POSTSCRIPT_IDENTIFY(0x1015),
POSTSCRIPT_IDENTIFY(0x1015, WmfEscapeUnknownData::new),
/** Inserts a block of raw data into a PostScript stream. The input MUST be
a 32-bit quantity specifying the number of bytes to inject, a 16-bit quantity specifying the
injection point, and a 16-bit quantity specifying the page number, followed by the bytes to
inject. */
POSTSCRIPT_INJECTION(0x1016),
POSTSCRIPT_INJECTION(0x1016, WmfEscapeUnknownData::new),
/** Checks whether the printer supports a JPEG image. */
CHECKJPEGFORMAT(0x1017),
CHECKJPEGFORMAT(0x1017, WmfEscapeUnknownData::new),
/** Checks whether the printer supports a PNG image */
CHECKPNGFORMAT(0x1018),
CHECKPNGFORMAT(0x1018, WmfEscapeUnknownData::new),
/** Gets information on a specified feature setting for a PostScript printer driver. */
GET_PS_FEATURESETTING(0x1019),
GET_PS_FEATURESETTING(0x1019, WmfEscapeUnknownData::new),
/** Enables applications to write documents to a file or to a printer in XML Paper Specification (XPS) format. */
MXDC_ESCAPE(0x101A),
MXDC_ESCAPE(0x101A, WmfEscapeUnknownData::new),
/** Enables applications to include private procedures and other arbitrary data in documents. */
SPCLPASSTHROUGH2(0x11D8);
SPCLPASSTHROUGH2(0x11D8, WmfEscapeUnknownData::new);
int flag;
EscapeFunction(int flag) {
public int flag;
public final Supplier<? 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;
return 2*LittleEndianConsts.SHORT_SIZE+byteCount;
escapeData = escapeFunction.constructor.get();
size += escapeData.init(leis, byteCount, escapeFunction);
return size;
}
public EscapeFunction getEscapeFunction() {
return escapeFunction;
}
public <T 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;
}
}
}

View 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;
}
}

View 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;
}
}
}
/**

View 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 {

View File

@ -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;
}
}

View File

@ -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());
}
}
}

View File

@ -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;
}
}

View 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);
}
}

View 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 */
}

View 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);

Binary file not shown.