git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1858625 13f79535-47bb-0310-9956-ffa450edef68tags/REL_4_1_1
@@ -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; | |||
} | |||
} |
@@ -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); |
@@ -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 { |
@@ -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) |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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), |
@@ -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; | |||
} | |||
} |
@@ -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+"' }"; | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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; |
@@ -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), |
@@ -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()); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -142,7 +142,7 @@ public class HwmfGraphics { | |||
graphicsCtx.fill(shape); | |||
} | |||
// draw(shape); | |||
draw(shape); | |||
} | |||
protected BasicStroke getStroke() { |
@@ -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 |
@@ -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; | |||
escapeData = escapeFunction.constructor.get(); | |||
size += escapeData.init(leis, byteCount, escapeFunction); | |||
return size; | |||
} | |||
public EscapeFunction getEscapeFunction() { | |||
return escapeFunction; | |||
} | |||
return 2*LittleEndianConsts.SHORT_SIZE+byteCount; | |||
public <T extends HwmfEscapeData> T getEscapeData() { | |||
return (T)escapeData; | |||
} | |||
@Override | |||
@@ -206,7 +223,126 @@ public class HwmfEscape implements HwmfRecord { | |||
public String toString() { | |||
StringBuilder sb = new StringBuilder(); | |||
sb.append("escape - function: "+escapeFunction+"\n"); | |||
sb.append(HexDump.dump(escapeData, 0, 0)); | |||
sb.append(escapeData.toString()); | |||
return sb.toString(); | |||
} | |||
public static class WmfEscapeUnknownData implements HwmfEscapeData { | |||
EscapeFunction escapeFunction; | |||
private byte[] escapeDataBytes; | |||
public byte[] getEscapeDataBytes() { | |||
return escapeDataBytes; | |||
} | |||
@Override | |||
public int init(LittleEndianInputStream leis, long recordSize, EscapeFunction escapeFunction) throws IOException { | |||
this.escapeFunction = escapeFunction; | |||
escapeDataBytes = IOUtils.toByteArray(leis,recordSize,MAX_OBJECT_SIZE); | |||
return 0; | |||
} | |||
@Override | |||
public String toString() { | |||
return HexDump.dump(escapeDataBytes, 0, 0); | |||
} | |||
} | |||
public static class WmfEscapeEMF implements HwmfEscapeData { | |||
// The magic for EMF parts, i.e. the byte sequence for "WMFC" | |||
private static final int EMF_COMMENT_IDENTIFIER = 0x43464D57; | |||
int commentIdentifier; | |||
int commentType; | |||
int version; | |||
int checksum; | |||
int flags; | |||
int commentRecordCount; | |||
int currentRecordSize; | |||
int remainingBytes; | |||
int emfRecordSize; | |||
byte[] emfData; | |||
@Override | |||
public int init(LittleEndianInputStream leis, long recordSize, EscapeFunction escapeFunction) throws IOException { | |||
if (recordSize < LittleEndianConsts.INT_SIZE) { | |||
return 0; | |||
} | |||
// A 32-bit unsigned integer that defines this record as a WMF Comment record. | |||
int commentIdentifier = leis.readInt(); | |||
if (commentIdentifier != EMF_COMMENT_IDENTIFIER) { | |||
// there are some WMF implementation using this record as a MFCOMMENT or similar | |||
// if the commentIdentifier doesn't match, then return immediately | |||
return LittleEndianConsts.INT_SIZE; | |||
} | |||
// A 32-bit unsigned integer that identifies the type of comment in this record. | |||
// This value MUST be 0x00000001. | |||
commentType = leis.readInt(); | |||
assert(commentType == 0x00000001); | |||
// A 32-bit unsigned integer that specifies EMF metafile interoperability. This SHOULD be 0x00010000. | |||
version = leis.readInt(); | |||
// A 16-bit unsigned integer used to validate the correctness of the embedded EMF stream. | |||
// This value MUST be the one's-complement of the result of applying an XOR operation to all WORDs in the EMF stream. | |||
checksum = leis.readUShort(); | |||
// This 32-bit unsigned integer is unused and MUST be set to zero. | |||
flags = leis.readInt(); | |||
assert(flags == 0); | |||
// A 32-bit unsigned integer that specifies the total number of consecutive META_ESCAPE_ENHANCED_METAFILE | |||
// records that contain the embedded EMF metafile. | |||
commentRecordCount = leis.readInt(); | |||
// A 32-bit unsigned integer that specifies the size, in bytes, of the EnhancedMetafileData field. | |||
// This value MUST be less than or equal to 8,192. | |||
currentRecordSize = leis.readInt(); | |||
assert(0 <= currentRecordSize && currentRecordSize <= 0x2000); | |||
// A 32-bit unsigned integer that specifies the number of bytes in the EMF stream that remain to be | |||
// processed after this record. Those additional EMF bytes MUST follow in the EnhancedMetafileData | |||
// fields of subsequent META_ESCAPE_ENHANDED_METAFILE escape records. | |||
remainingBytes = leis.readInt(); | |||
// A 32-bit unsigned integer that specifies the total size of the EMF stream embedded in this | |||
// sequence of META_ESCAPE_ENHANCED_METAFILE records. | |||
emfRecordSize = leis.readInt(); | |||
// A segment of an EMF file. The bytes in consecutive META_ESCAPE_ENHANCED_METAFILE records | |||
// MUST be concatenated to represent the entire embedded EMF file. | |||
emfData = IOUtils.toByteArray(leis, currentRecordSize, MAX_OBJECT_SIZE); | |||
return LittleEndianConsts.INT_SIZE*8+ LittleEndianConsts.SHORT_SIZE+emfData.length; | |||
} | |||
public boolean isValid() { | |||
return commentIdentifier == EMF_COMMENT_IDENTIFIER; | |||
} | |||
public int getCommentRecordCount() { | |||
return commentRecordCount; | |||
} | |||
public int getCurrentRecordSize() { | |||
return currentRecordSize; | |||
} | |||
public int getRemainingBytes() { | |||
return remainingBytes; | |||
} | |||
public int getEmfRecordSize() { | |||
return emfRecordSize; | |||
} | |||
public byte[] getEmfData() { | |||
return emfData; | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} | |||
@@ -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; | |||
} | |||
} | |||
} | |||
/** |
@@ -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 { |
@@ -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; | |||
} | |||
} | |||
@@ -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()); | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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 */ | |||
} |
@@ -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); |