public HemfGraphics(Graphics2D graphicsCtx, Rectangle2D bbox) {
super(graphicsCtx,bbox);
// add dummy entry for object index 0, as emf is 1-based
- addObjectTableEntry((ctx)->{});
+ objectIndexes.set(0);
}
@Override
final Path2D path;
if (useBracket) {
path = prop.getPath();
+ if (path.getCurrentPoint() == null) {
+ // workaround if a path has been started and no MoveTo command
+ // has been specified before the first lineTo/splineTo
+ final Point2D loc = prop.getLocation();
+ path.moveTo(loc.getX(), loc.getY());
+ }
} else {
path = new Path2D.Double();
Point2D pnt = prop.getLocation();
*/
public void addObjectTableEntry(HwmfObjectTableEntry entry, int index) {
if (index < 1) {
- super.addObjectTableEntry(entry);
- return;
- }
-
- if (index > objectTable.size()) {
- throw new IllegalStateException("object table hasn't grown to this index yet");
+ throw new IndexOutOfBoundsException("Object table entry index in EMF must be > 0 - invalid index: "+index);
}
- if (index == objectTable.size()) {
- objectTable.add(entry);
- } else {
- objectTable.set(index, entry);
- }
+ objectIndexes.set(index);
+ objectTable.put(index, entry);
}
@Override
import org.apache.poi.hemf.record.emfplus.HemfPlusRecord;
import org.apache.poi.hemf.record.emfplus.HemfPlusRecordIterator;
+import org.apache.poi.hwmf.usermodel.HwmfPicture;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.Internal;
import org.apache.poi.util.LittleEndianConsts;
*/
@Internal
public class HemfComment {
- private static final int MAX_RECORD_LENGTH = 2_000_000;
+ private static final int MAX_RECORD_LENGTH = HwmfPicture.MAX_RECORD_LENGTH;
public enum HemfCommentRecordType {
emfGeneric(-1, EmfCommentDataGeneric::new, false),
} else {
// A 32-bit unsigned integer from the RecordType enumeration that identifies this record
// as a comment record. This value MUST be 0x00000046.
- type = leis.readUInt();
+ try {
+ type = leis.readUInt();
+ } catch (RuntimeException e) {
+ // EOF
+ return null;
+ }
assert(type == HemfRecordType.comment.id);
// A 32-bit unsigned integer that specifies the size in bytes of this record in the
// metafile. This value MUST be a multiple of 4 bytes.
for (EmfCommentDataFormat fmt : formats) {
int skip = fmt.offData-(leis.getReadIndex()-startIdx);
leis.skipFully(skip);
- fmt.rawData = new byte[fmt.sizeData];
- leis.readFully(fmt.rawData);
+ fmt.rawData = IOUtils.safelyAllocate(fmt.sizeData, MAX_RECORD_LENGTH);
+ int readBytes = leis.read(fmt.rawData);
+ if (readBytes < fmt.sizeData) {
+ // EOF
+ break;
+ }
}
return leis.getReadIndex()-startIdx;
}
@Override
- public long init(final LittleEndianInputStream leis, final long dataSize)
- throws IOException {
+ public long init(final LittleEndianInputStream leis, final long dataSize) throws IOException {
final int startIdx = leis.getReadIndex();
final int commentIdentifier = (int)leis.readUInt();
assert(commentIdentifier == HemfCommentRecordType.emfPublic.id);
int winMetafileSize = (int)leis.readUInt();
byte[] winMetafile = IOUtils.safelyAllocate(winMetafileSize, MAX_RECORD_LENGTH);
- leis.readFully(winMetafile);
+ // some emf comments are truncated, so we don't use readFully here
+ leis.read(winMetafile);
return leis.getReadIndex()-startIdx;
}
public void draw(HemfGraphics ctx) {
final HemfDrawProperties prop = ctx.getProperties();
final Path2D path = prop.getPath();
- if (path != null) {
+ if (path != null && path.getCurrentPoint() != null) {
path.closePath();
prop.setLocation(path.getCurrentPoint());
}
}
private static void polyTo(final HemfGraphics ctx, final Path2D poly) {
- final PathIterator pi = poly.getPathIterator(null);
- if (pi.isDone()) {
- // ignore empty polys
+ if (poly.getCurrentPoint() == null) {
return;
}
- // ignore dummy start point (moveTo)
+ final PathIterator pi = poly.getPathIterator(null);
+ // ignore empty polys and dummy start point (moveTo)
pi.next();
- assert (!pi.isDone());
+ if (pi.isDone()) {
+ return;
+ }
ctx.draw((path) -> path.append(pi, true));
}
return undefinedSpace1;
}
- final LittleEndianInputStream leisDib;
+ final int dibSize = cbBmi+cbBits;
if (undefinedSpace2 == 0) {
- leisDib = leis;
- } else {
- final ByteArrayOutputStream bos = new ByteArrayOutputStream(cbBmi+cbBits);
- final long cbBmiSrcAct = IOUtils.copy(leis, bos, cbBmi);
- assert (cbBmiSrcAct == cbBmi);
- leis.skipFully(undefinedSpace2);
- final long cbBitsSrcAct = IOUtils.copy(leis, bos, cbBits);
- assert (cbBitsSrcAct == cbBits);
- leisDib = new LittleEndianInputStream(new ByteArrayInputStream(bos.toByteArray()));
+ return undefinedSpace1 + bitmap.init(leis, dibSize);
}
- final int dibSize = cbBmi+cbBits;
+
+ final ByteArrayOutputStream bos = new ByteArrayOutputStream(cbBmi+cbBits);
+ final long cbBmiSrcAct = IOUtils.copy(leis, bos, cbBmi);
+ assert (cbBmiSrcAct == cbBmi);
+ leis.skipFully(undefinedSpace2);
+ final long cbBitsSrcAct = IOUtils.copy(leis, bos, cbBits);
+ assert (cbBitsSrcAct == cbBits);
+
+ final LittleEndianInputStream leisDib = new LittleEndianInputStream(new ByteArrayInputStream(bos.toByteArray()));
final int dibSizeAct = bitmap.init(leisDib, dibSize);
assert (dibSizeAct <= dibSize);
return undefinedSpace1 + cbBmi + undefinedSpace2 + cbBits;
static long readRgnData(final LittleEndianInputStream leis, final List<Rectangle2D> rgnRects) {
// *** RegionDataHeader ***
// A 32-bit unsigned integer that specifies the size of this object in bytes. This MUST be 0x00000020.
- long rgnHdrSiez = leis.readUInt();
- assert(rgnHdrSiez == 0x20);
+ long rgnHdrSize = leis.readUInt();
+ assert(rgnHdrSize == 0x20);
// A 32-bit unsigned integer that specifies the region type. This SHOULD be RDH_RECTANGLES (0x00000001)
long rgnHdrType = leis.readUInt();
assert(rgnHdrType == 1);
@Override
public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException {
// A 128-bit WMF RectL object, which specifies bounding rectangle, in device units
- return readRectL(leis, bounds);
+ return (recordSize == 0) ? 0 : readRectL(leis, bounds);
}
@Override
package org.apache.poi.hemf.record.emf;
-import static org.apache.poi.hemf.record.emf.HemfDraw.readDimensionFloat;
import static org.apache.poi.hemf.record.emf.HemfDraw.readDimensionInt;
import static org.apache.poi.hemf.record.emf.HemfDraw.readRectL;
+import static org.apache.poi.hemf.record.emf.HemfRecordIterator.HEADER_SIZE;
import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import org.apache.poi.util.Dimension2DDouble;
import org.apache.poi.util.Internal;
-import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInputStream;
private long bytes;
private long records;
private int handles;
- private long nDescription;
- private long offDescription;
+ private String description;
private long nPalEntries;
private boolean hasExtension1;
private long cbPixelFormat;
return handles;
}
- public long getnDescription() {
- return nDescription;
- }
-
- public long getOffDescription() {
- return offDescription;
- }
+ public String getDescription() { return description; }
public long getnPalEntries() {
return nPalEntries;
", bytes=" + bytes +
", records=" + records +
", handles=" + handles +
- ", nDescription=" + nDescription +
- ", offDescription=" + offDescription +
+ ", description=" + description +
", nPalEntries=" + nPalEntries +
", hasExtension1=" + hasExtension1 +
", cbPixelFormat=" + cbPixelFormat +
throw new IOException("Not a valid EMF header. Record type:"+recordId);
}
+ int startIdx = leis.getReadIndex();
+
//bounds
long size = readRectL(leis, boundsRectangle);
size += readRectL(leis, frameRectangle);
//reserved
leis.skipFully(LittleEndianConsts.SHORT_SIZE);
- nDescription = leis.readUInt();
- offDescription = leis.readUInt();
+ int nDescription = (int)leis.readUInt();
+ int offDescription = (int)leis.readUInt();
nPalEntries = leis.readUInt();
size += 8*LittleEndianConsts.INT_SIZE;
size += readDimensionInt(leis, deviceDimension);
size += readDimensionInt(leis, milliDimension);
+ if (nDescription > 0 && offDescription > 0) {
+ int skip = (int)(offDescription - (size + HEADER_SIZE));
+ leis.mark(skip+nDescription*2);
+ leis.skipFully(skip);
+ byte[] buf = new byte[(nDescription-1)*2];
+ leis.readFully(buf);
+ description = new String(buf, StandardCharsets.UTF_16LE).replace((char)0, ' ').trim();
+ leis.reset();
+ }
+
if (size+12 <= recordSize) {
hasExtension1 = true;
cbPixelFormat = leis.readUInt();
hasExtension2 = true;
size += readDimensionInt(leis, microDimension);
}
+
return size;
}
}
import org.apache.poi.util.LittleEndianInputStream;
public class HemfMisc {
- private static final int MAX_RECORD_LENGTH = 10_000_000;
-
public static class EmfEof implements HemfRecord {
protected final List<PaletteEntry> palette = new ArrayList<>();
public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException {
return readPointL(leis, origin);
}
+
+ @Override
+ public String toString() {
+ return "{ x: "+origin.getX()+", y: "+origin.getY()+" }";
+ }
}
public static class EmfSetWorldTransform implements HemfRecord {
", modifyWorldTransformMode: "+modifyWorldTransformMode+" }";
}
}
+
+ public static class EmfCreateMonoBrush16 extends EmfCreatePen {
+ protected HwmfFill.ColorUsage colorUsage;
+ protected final HwmfBitmapDib bitmap = new HwmfBitmapDib();
+
+ @Override
+ public HemfRecordType getEmfRecordType() {
+ return HemfRecordType.createMonoBrush16;
+ }
+
+ @Override
+ public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException {
+ final int startIdx = leis.getReadIndex();
+
+ penIndex = (int) leis.readUInt();
+
+ // A 32-bit unsigned integer that specifies how to interpret values in the color
+ // table in the DIB header. This value MUST be in the DIBColors enumeration
+ colorUsage = HwmfFill.ColorUsage.valueOf((int) leis.readUInt());
+
+ // A 32-bit unsigned integer that specifies the offset from the start of this
+ // record to the DIB header, if the record contains a DIB.
+ int offBmi = (int) leis.readUInt();
+
+ // A 32-bit unsigned integer that specifies the size of the DIB header, if the
+ // record contains a DIB.
+ int cbBmi = (int) leis.readUInt();
+
+ // A 32-bit unsigned integer that specifies the offset from the start of this
+ // record to the DIB bits, if the record contains a DIB.
+ int offBits = (int) leis.readUInt();
+
+ // A 32-bit unsigned integer that specifies the size of the DIB bits, if the record
+ // contains a DIB.
+ int cbBits = (int) leis.readUInt();
+
+ int size = 6 * LittleEndianConsts.INT_SIZE;
+
+ size += readBitmap(leis, bitmap, startIdx, offBmi, cbBmi, offBits, cbBits);
+
+ return size;
+ }
+ }
}
if (currentRecord != null && HemfRecordType.eof == currentRecord.getEmfRecordType()) {
return null;
}
- long recordId = stream.readUInt();
- long recordSize = stream.readUInt();
+
+ final long recordId, recordSize;
+ try {
+ recordId = stream.readUInt();
+ recordSize = stream.readUInt();
+ } catch (RuntimeException e) {
+ // EOF
+ return null;
+ }
HemfRecordType type = HemfRecordType.getById(recordId);
if (type == null) {
polyPolyline16(0x0000005A, HemfDraw.EmfPolyPolyline16::new),
polyPolygon16(0x0000005B, HemfDraw.EmfPolyPolygon16::new),
polyDraw16(0x0000005C, HemfDraw.EmfPolyDraw16::new),
- createmonobrush16(0x0000005D, UnimplementedHemfRecord::new),
+ createMonoBrush16(0x0000005D, HemfMisc.EmfCreateMonoBrush16::new),
createDibPatternBrushPt(0x0000005E, HemfMisc.EmfCreateDibPatternBrushPt::new),
extCreatePen(0x0000005F, HemfMisc.EmfExtCreatePen::new),
polytextouta(0x00000060, HemfText.PolyTextOutA::new),
int offDx = (int)leis.readUInt();
size += LittleEndianConsts.INT_SIZE;
- int undefinedSpace1 = (int)(offString - (size + HEADER_SIZE));
- assert (undefinedSpace1 >= 0);
- leis.skipFully(undefinedSpace1);
- size += undefinedSpace1;
-
- rawTextBytes = IOUtils.safelyAllocate(stringLength*(isUnicode()?2:1), MAX_RECORD_LENGTH);
- leis.readFully(rawTextBytes);
- size += rawTextBytes.length;
-
- dx.clear();
- if (offDx > 0) {
- int undefinedSpace2 = (int) (offDx - (size + HEADER_SIZE));
- assert (undefinedSpace2 >= 0);
- leis.skipFully(undefinedSpace2);
- size += undefinedSpace2;
-
- // An array of 32-bit unsigned integers that specify the output spacing between the origins of adjacent
- // character cells in logical units. The location of this field is specified by the value of offDx
- // in bytes from the start of this record. If spacing is defined, this field contains the same number
- // of values as characters in the output string.
- //
- // If the Options field of the EmrText object contains the ETO_PDY flag, then this buffer
- // contains twice as many values as there are characters in the output string, one
- // horizontal and one vertical offset for each, in that order.
- //
- // If ETO_RTLREADING is specified, characters are laid right to left instead of left to right.
- // No other options affect the interpretation of this field.
- while (size < recordSize) {
- dx.add((int) leis.readUInt());
- size += LittleEndianConsts.INT_SIZE;
+ // handle dx before string and other way round
+ for (char op : ((offDx < offString) ? "ds" : "sd").toCharArray()) {
+ switch (op) {
+ case 'd': {
+ dx.clear();
+ int undefinedSpace2 = (int) (offDx - (size + HEADER_SIZE));
+ if (offDx > 0 && undefinedSpace2 >= 0 && offDx-HEADER_SIZE < recordSize) {
+ leis.skipFully(undefinedSpace2);
+ size += undefinedSpace2;
+
+ // An array of 32-bit unsigned integers that specify the output spacing between the origins of adjacent
+ // character cells in logical units. The location of this field is specified by the value of offDx
+ // in bytes from the start of this record. If spacing is defined, this field contains the same number
+ // of values as characters in the output string.
+ //
+ // If the Options field of the EmrText object contains the ETO_PDY flag, then this buffer
+ // contains twice as many values as there are characters in the output string, one
+ // horizontal and one vertical offset for each, in that order.
+ //
+ // If ETO_RTLREADING is specified, characters are laid right to left instead of left to right.
+ // No other options affect the interpretation of this field.
+ final int maxSize = (int)Math.min((offDx < offString) ? (offString-HEADER_SIZE) : recordSize, recordSize);
+ while (size <= maxSize-LittleEndianConsts.INT_SIZE) {
+ dx.add((int) leis.readUInt());
+ size += LittleEndianConsts.INT_SIZE;
+ }
+ }
+ if (dx.size() < stringLength) {
+ // invalid dx array
+ dx.clear();
+ }
+ break;
+ }
+ default:
+ case 's': {
+ int undefinedSpace1 = (int)(offString - (size + HEADER_SIZE));
+ if (offString > 0 && undefinedSpace1 >= 0 && offString-HEADER_SIZE < recordSize) {
+ leis.skipFully(undefinedSpace1);
+ size += undefinedSpace1;
+
+ final int maxSize = (int)Math.min(recordSize-size, stringLength * (isUnicode() ? 2 : 1));
+ rawTextBytes = IOUtils.safelyAllocate(maxSize, MAX_RECORD_LENGTH);
+ leis.readFully(rawTextBytes);
+ size += maxSize;
+ break;
+ }
+ }
}
}
@Override
public long init(LittleEndianInputStream leis, long recordSize, long recordId) throws IOException {
// cx (4 bytes): A 32-bit unsigned integer that defines the x-coordinate of the point.
- width = (int)leis.readUInt();
+ int width = (int)leis.readUInt();
// cy (4 bytes): A 32-bit unsigned integer that defines the y-coordinate of the point.
- height = (int)leis.readUInt();
+ int height = (int)leis.readUInt();
+ size.setSize(width, height);
return 2*LittleEndianConsts.INT_SIZE;
}
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.util.Dimension2DDouble;
import org.apache.poi.util.Internal;
import org.apache.poi.util.LittleEndianInputStream;
private final LittleEndianInputStream stream;
private final List<HemfRecord> records = new ArrayList<>();
+ private boolean isParsed = false;
public HemfPicture(InputStream is) throws IOException {
this(new LittleEndianInputStream(is));
}
public List<HemfRecord> getRecords() {
- if (records.isEmpty()) {
+ if (!isParsed) {
+ // in case the (first) parsing throws an exception, we can provide the
+ // records up to that point
+ isParsed = true;
new HemfRecordIterator(stream).forEachRemaining(records::add);
}
return records;
*/
public Dimension2D getSize() {
HemfHeader header = (HemfHeader)getRecords().get(0);
+ final double coeff = (double) Units.EMU_PER_CENTIMETER / Units.EMU_PER_POINT / 10.;
Rectangle2D dim = header.getFrameRectangle();
+ double width = dim.getWidth(), height = dim.getHeight();
+ if (dim.isEmpty() || Math.rint(width*coeff) == 0 || Math.rint(height*coeff) == 0) {
+ for (HemfRecord r : getRecords()) {
+ if (r instanceof HemfWindowing.EmfSetWindowExtEx) {
+ Dimension2D d = ((HemfWindowing.EmfSetWindowExtEx)r).getSize();
+ width = d.getWidth();
+ height = d.getHeight();
+ // keep searching - sometimes there's another record
+ }
+ }
+ }
+
+ if (Math.rint(width*coeff) == 0 || Math.rint(height*coeff) == 0) {
+ width = 100;
+ height = 100;
+ }
- double coeff = (double)Units.EMU_PER_CENTIMETER/Units.EMU_PER_POINT/10.;
- return new Dimension2DDouble(dim.getWidth()*coeff, dim.getHeight()*coeff);
+ return new Dimension2DDouble(width*coeff, height*coeff);
}
public void draw(Graphics2D ctx, Rectangle2D graphicsBounds) {
import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode;
import org.apache.poi.hwmf.record.HwmfPalette.PaletteEntry;
import org.apache.poi.hwmf.record.HwmfPenStyle;
+import org.apache.poi.hwmf.record.HwmfTernaryRasterOp;
import org.apache.poi.hwmf.record.HwmfText.HwmfTextAlignment;
import org.apache.poi.hwmf.record.HwmfText.HwmfTextVerticalAlignment;
private HwmfTextVerticalAlignment textVAlignLatin;
private HwmfTextAlignment textAlignAsian;
private HwmfTextVerticalAlignment textVAlignAsian;
+ private HwmfTernaryRasterOp rasterOp;
public HwmfDrawProperties() {
window = new Rectangle2D.Double(0, 0, 1, 1);
textVAlignLatin = HwmfTextVerticalAlignment.TOP;
textAlignAsian = HwmfTextAlignment.RIGHT;
textVAlignAsian = HwmfTextVerticalAlignment.TOP;
+ rasterOp = HwmfTernaryRasterOp.PATCOPY;
}
public HwmfDrawProperties(HwmfDrawProperties other) {
this.textVAlignLatin = other.textVAlignLatin;
this.textAlignAsian = other.textAlignAsian;
this.textVAlignAsian = other.textVAlignAsian;
+ this.rasterOp = other.rasterOp;
}
public void setViewportExt(double width, double height) {
public int getWindingRule() {
return getPolyfillMode().awtFlag;
}
+
+ public HwmfTernaryRasterOp getRasterOp() {
+ return rasterOp;
+ }
+
+ public void setRasterOp(HwmfTernaryRasterOp rasterOp) {
+ this.rasterOp = rasterOp;
+ }
}
import java.awt.image.BufferedImage;
import java.nio.charset.Charset;
import java.text.AttributedString;
-import java.util.ArrayList;
+import java.util.BitSet;
import java.util.LinkedList;
import java.util.List;
-import java.util.ListIterator;
import java.util.NoSuchElementException;
+import java.util.TreeMap;
import org.apache.commons.codec.Charsets;
import org.apache.poi.common.usermodel.fonts.FontInfo;
protected final List<HwmfDrawProperties> propStack = new LinkedList<>();
protected HwmfDrawProperties prop;
protected final Graphics2D graphicsCtx;
- protected final List<HwmfObjectTableEntry> objectTable = new ArrayList<>();
+ protected final BitSet objectIndexes = new BitSet();
+ protected final TreeMap<Integer,HwmfObjectTableEntry> objectTable = new TreeMap<>();
private static final Charset DEFAULT_CHARSET = LocaleUtil.CHARSET_1252;
/** Bounding box from the placeable header */
}
public void draw(Shape shape) {
- HwmfLineDash lineDash = getProperties().getPenStyle().getLineDash();
+ HwmfPenStyle ps = getProperties().getPenStyle();
+ if (ps == null) {
+ return;
+ }
+ HwmfLineDash lineDash = ps.getLineDash();
if (lineDash == HwmfLineDash.NULL) {
// line is not drawn
return;
* @param entry
*/
public void addObjectTableEntry(HwmfObjectTableEntry entry) {
- ListIterator<HwmfObjectTableEntry> oIter = objectTable.listIterator();
- while (oIter.hasNext()) {
- HwmfObjectTableEntry tableEntry = oIter.next();
- if (tableEntry == null) {
- oIter.set(entry);
- return;
- }
- }
- objectTable.add(entry);
+ int objIdx = objectIndexes.nextClearBit(0);
+ objectIndexes.set(objIdx);
+ objectTable.put(objIdx, entry);
}
/**
* @throws IndexOutOfBoundsException if the index is out of range
*/
public void unsetObjectTableEntry(int index) {
- objectTable.set(index, null);
+ if (index < 0) {
+ throw new IndexOutOfBoundsException("Invalid index: "+index);
+ }
+ // sometime emfs remove object table entries before they set them
+ // so ignore requests, if the table entry doesn't exist
+ objectTable.remove(index);
+ objectIndexes.clear(index);
}
/**
}
AttributedString as = new AttributedString(textString);
addAttributes(as, font);
+
+ // disabled for the time being, as the results aren't promising
+ /*
if (dx != null && !dx.isEmpty()) {
//for multi-byte encodings (e.g. Shift_JIS), the byte length
//might not equal the string length().
final int cps = textString.codePointCount(0, textString.length());
final int unicodeSteps = Math.max(dx.size()/cps, 1);
- int dxPosition = 0;
+ int dxPosition = 0, lastDxPosition = 0;
int beginIndex = 0;
- int[] chars = {0};
while (beginIndex < textString.length() && dxPosition < dx.size()) {
int endIndex = textString.offsetByCodePoints(beginIndex, 1);
if (beginIndex > 0) {
// Tracking works as a prefix/advance space on characters whereas
// dx[...] is the complete width of the current char
// therefore we need to add the additional/suffix width to the next char
- as.addAttribute(TextAttribute.TRACKING, (float)((dx.get(dxPosition) - fontW) / fontH), beginIndex, endIndex);
+
+ as.addAttribute(TextAttribute.TRACKING, (float)((dx.get(lastDxPosition) - fontW) / fontH), beginIndex, endIndex);
}
+ lastDxPosition = dxPosition;
dxPosition += (isUnicode) ? unicodeSteps : (endIndex-beginIndex);
beginIndex = endIndex;
}
}
-
+ */
double angle = Math.toRadians(-font.getEscapement()/10.);
return fontHeight*3/4;
}
}
+
+ public void drawImage(BufferedImage img, Rectangle2D srcBounds, Rectangle2D dstBounds) {
+ // prop.getRasterOp();
+ graphicsCtx.drawImage(img,
+ (int)dstBounds.getX(),
+ (int)dstBounds.getY(),
+ (int)(dstBounds.getX()+dstBounds.getWidth()),
+ (int)(dstBounds.getY()+dstBounds.getHeight()),
+ (int)srcBounds.getX(),
+ (int)srcBounds.getY(),
+ (int)(srcBounds.getX()+srcBounds.getWidth()),
+ (int)(srcBounds.getY()+srcBounds.getHeight()),
+ null, // getProperties().getBackgroundColor().getColor(),
+ null
+ );
+ }
}
package org.apache.poi.hwmf.record;
-import javax.imageio.ImageIO;
+import java.awt.AlphaComposite;
+import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
+import java.awt.LinearGradientPaint;
+import java.awt.MultipleGradientPaint;
+import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
+import javax.imageio.ImageIO;
+
+import org.apache.poi.hwmf.usermodel.HwmfPicture;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianConsts;
*/
public class HwmfBitmapDib {
- private static final int MAX_RECORD_LENGTH = 10000000;
+ private static final int MAX_RECORD_LENGTH = HwmfPicture.MAX_RECORD_LENGTH;
public static enum BitCount {
/**
introSize += readColors(leis);
assert(introSize < 10000);
- int fileSize = (headerImageSize < headerSize) ? recordSize : (int)Math.min(introSize+headerImageSize,recordSize);
-
leis.reset();
- imageData = IOUtils.toByteArray(leis, fileSize);
-
+
assert( headerSize != 0x0C || ((((headerWidth * headerPlanes * headerBitCount.flag + 31) & ~31) / 8) * Math.abs(headerHeight)) == headerImageSize);
- return fileSize;
+ if (headerImageSize < headerSize) {
+ imageData = IOUtils.safelyAllocate(recordSize, MAX_RECORD_LENGTH);
+ leis.readFully(imageData);
+ return recordSize;
+ } else {
+ int fileSize = (int)Math.min(introSize+headerImageSize,recordSize);
+ imageData = IOUtils.safelyAllocate(fileSize, MAX_RECORD_LENGTH);
+ leis.readFully(imageData, 0, introSize);
+ leis.skipFully(recordSize-fileSize);
+ // emfs are sometimes truncated, read as much as possible
+ int readBytes = leis.read(imageData, introSize, fileSize-introSize);
+ return introSize+(recordSize-fileSize)+readBytes;
+ }
}
protected int readHeader(LittleEndianInputStream leis) throws IOException {
headerBitCount = BitCount.valueOf(leis.readUShort());
size += 4*LittleEndianConsts.SHORT_SIZE;
} else {
+ // fix header size, sometimes this is invalid
+ headerSize = 40;
+
// BitmapInfoHeader
// A 32-bit signed integer that defines the width of the DIB, in pixels.
// This value MUST be positive.
headerColorImportant = leis.readUInt();
size += 8*LittleEndianConsts.INT_SIZE+2*LittleEndianConsts.SHORT_SIZE;
}
- assert(size == headerSize);
return size;
}
return size;
}
+ public boolean isValid() {
+ return (imageData != null);
+ }
+
public InputStream getBMPStream() {
return new ByteArrayInputStream(getBMPData());
}
public BufferedImage getImage() {
try {
return ImageIO.read(getBMPStream());
- } catch (IOException e) {
- logger.log(POILogger.ERROR, "invalid bitmap data - returning black opaque image");
- BufferedImage bi = new BufferedImage(headerWidth, headerHeight, BufferedImage.TYPE_INT_ARGB);
- Graphics2D g = bi.createGraphics();
- g.setPaint(Color.black);
- g.fillRect(0, 0, headerWidth, headerHeight);
- g.dispose();
- return bi;
+ } catch (IOException|RuntimeException e) {
+ logger.log(POILogger.ERROR, "invalid bitmap data - returning placeholder image");
+ return getPlaceholder();
}
}
+
+ protected BufferedImage getPlaceholder() {
+ BufferedImage bi = new BufferedImage(headerWidth, headerHeight, BufferedImage.TYPE_INT_ARGB);
+ Graphics2D g = bi.createGraphics();
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+ g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
+ g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
+
+ g.setComposite(AlphaComposite.Clear);
+ g.fillRect(0, 0, headerWidth, headerHeight);
+
+ final int arcs = Math.min(headerWidth, headerHeight) / 7;
+
+ Color bg = Color.LIGHT_GRAY;
+ Color fg = Color.GRAY;
+ LinearGradientPaint lgp = new LinearGradientPaint(0f, 0f, 5, 5,
+ new float[] {0,.1f,.1001f}, new Color[] {fg,fg,bg}, MultipleGradientPaint.CycleMethod.REFLECT);
+ g.setComposite(AlphaComposite.SrcOver.derive(0.4f));
+ g.setPaint(lgp);
+ g.fillRoundRect(0, 0, headerWidth-1, headerHeight-1, arcs, arcs);
+
+ g.setColor(Color.DARK_GRAY);
+ g.setComposite(AlphaComposite.Src);
+ g.setStroke(new BasicStroke(2));
+ g.drawRoundRect(0, 0, headerWidth-1, headerHeight-1, arcs, arcs);
+ g.dispose();
+ return bi;
+ }
}
* The source of the color data is a DIB, and the destination of the transfer is
* the current output region in the playback device context.
*/
- public static class WmfStretchDib implements HwmfRecord, HwmfImageRecord, HwmfObjectTableEntry {
+ public static class WmfStretchDib implements HwmfRecord, HwmfImageRecord {
/**
* A 32-bit unsigned integer that defines how the source pixels, the current brush in
* the playback device context, and the destination pixels are to be combined to
@Override
public void draw(HwmfGraphics ctx) {
- ctx.addObjectTableEntry(this);
- }
-
- @Override
- public void applyObject(HwmfGraphics ctx) {
-
+ if (dib.isValid()) {
+ ctx.getProperties().setRasterOp(rasterOperation);
+ ctx.drawImage(getImage(), srcBounds, dstBounds);
+ }
}
@Override
@Override
public BufferedImage getImage() {
- return (target == null) ? null : target.getImage();
+ return (target != null && target.isValid()) ? target.getImage() : null;
}
}
public void draw(HwmfGraphics ctx) {
ctx.getProperties().setBkMode(bkMode);
}
+
+ @Override
+ public String toString() {
+ return "{ bkMode: '"+bkMode+"' }";
+ }
}
/**
@Override
public BufferedImage getImage() {
- if (patternDib != null) {
+ if (patternDib != null && patternDib.isValid()) {
return patternDib.getImage();
} else if (pattern16 != null) {
return pattern16.getImage();
props.setTextVAlignAsian(HwmfTextVerticalAlignment.TOP);
}
}
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{ align: '");
+
+ if (TA_CENTER.isSet(textAlignmentMode)) {
+ sb.append("center");
+ } else if (TA_RIGHT.isSet(textAlignmentMode)) {
+ sb.append("right");
+ } else {
+ sb.append("left");
+ }
+
+ sb.append("', align-asian: '");
+
+ if (VTA_CENTER.isSet(textAlignmentMode)) {
+ sb.append("center");
+ } else if (VTA_LEFT.isSet(textAlignmentMode)) {
+ sb.append("left");
+ } else {
+ sb.append("right");
+ }
+
+ sb.append("', valign: '");
+
+ if (TA_BASELINE.isSet(textAlignmentMode)) {
+ sb.append("baseline");
+ } else if (TA_BOTTOM.isSet(textAlignmentMode)) {
+ sb.append("bottom");
+ } else {
+ sb.append("top");
+ }
+
+ sb.append("', valign-asian: '");
+
+ if (VTA_BASELINE.isSet(textAlignmentMode)) {
+ sb.append("baseline");
+ } else if (VTA_BOTTOM.isSet(textAlignmentMode)) {
+ sb.append("bottom");
+ } else {
+ sb.append("top");
+ }
+
+ sb.append("' }");
+
+ return sb.toString();
+ }
}
public static class WmfCreateFontIndirect implements HwmfRecord, HwmfObjectTableEntry {
import java.awt.Shape;
import java.awt.geom.Area;
+import java.awt.geom.Dimension2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import org.apache.poi.hwmf.draw.HwmfGraphics;
+import org.apache.poi.util.Dimension2DDouble;
import org.apache.poi.util.LittleEndianConsts;
import org.apache.poi.util.LittleEndianInputStream;
*/
public static class WmfSetWindowExt implements HwmfRecord {
- /** A signed integer that defines the vertical extent of the window in logical units. */
- protected int height;
-
- /** A signed integer that defines the horizontal extent of the window in logical units. */
- protected int width;
+ protected final Dimension2D size = new Dimension2DDouble();
@Override
public HwmfRecordType getWmfRecordType() {
@Override
public int init(LittleEndianInputStream leis, long recordSize, int recordFunction) throws IOException {
- height = leis.readShort();
- width = leis.readShort();
+ // A signed integer that defines the vertical extent of the window in logical units.
+ int height = leis.readShort();
+ // A signed integer that defines the horizontal extent of the window in logical units.
+ int width = leis.readShort();
+ size.setSize(width, height);
return 2*LittleEndianConsts.SHORT_SIZE;
}
@Override
public void draw(HwmfGraphics ctx) {
- ctx.getProperties().setWindowExt(width, height);
+ ctx.getProperties().setWindowExt(size.getWidth(), size.getHeight());
ctx.updateWindowMapMode();
}
- public int getHeight() {
- return height;
- }
-
- public int getWidth() {
- return width;
+ public Dimension2D getSize() {
+ return size;
}
}
import org.apache.poi.util.Units;
public class HwmfPicture {
+ /** Max. record length - processing longer records will throw an exception */
+ public static final int MAX_RECORD_LENGTH = 50_000_000;
+
private static final POILogger logger = POILogFactory.getLogger(HwmfPicture.class);
final List<HwmfRecord> records = new ArrayList<>();
if (wOrg == null || wExt == null) {
throw new RuntimeException("invalid wmf file - window records are incomplete.");
}
- return new Rectangle2D.Double(wOrg.getX(), wOrg.getY(), wExt.getWidth(), wExt.getHeight());
+ return new Rectangle2D.Double(wOrg.getX(), wOrg.getY(), wExt.getSize().getWidth(), wExt.getSize().getHeight());
}
}
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
-import java.awt.Graphics2D;
-import java.awt.RenderingHints;
-import java.awt.geom.Dimension2D;
import java.awt.geom.Point2D;
-import java.awt.geom.Rectangle2D;
-import java.awt.image.BufferedImage;
+import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-
-import javax.imageio.ImageIO;
+import java.util.stream.Stream;
import org.apache.poi.POIDataSamples;
import org.apache.poi.hemf.record.emf.HemfComment;
import org.apache.poi.hemf.record.emf.HemfText;
import org.apache.poi.util.IOUtils;
import org.apache.poi.util.RecordFormatException;
-import org.apache.poi.util.Units;
-import org.junit.Ignore;
import org.junit.Test;
public class HemfPictureTest {
private static final POIDataSamples ss_samples = POIDataSamples.getSpreadSheetInstance();
private static final POIDataSamples sl_samples = POIDataSamples.getSlideShowInstance();
+ /*
@Test
- @Ignore("Only for manual tests")
+ @Ignore("Only for manual tests - need to add org.tukaani:xz:1.8 for this to work")
public void paint() throws IOException {
-// File f = new File("picture_14.emf"); // sl_samples.getFile("wrench.emf");
-// try (FileInputStream fis = new FileInputStream(f)) {
-
- try (ZipInputStream zis = new ZipInputStream(new FileInputStream("tmp/emf.zip"))) {
- for (;;) {
- ZipEntry ze = zis.getNextEntry();
- if (ze == null) {
- break;
- }
- final File pngName = new File("build/tmp",ze.getName().replaceFirst( ".*/","").replace(".emf", ".png"));
- if (pngName.exists()) {
- continue;
- }
+ byte buf[] = new byte[50_000_000];
+ final boolean writeLog = true;
+ final boolean savePng = true;
- // 263/263282_000.emf
-// if (!ze.getName().contains("298/298837_000.emf")) continue;
- HemfPicture emf = new HemfPicture(zis);
- System.out.println(ze.getName());
+ Set<String> passed = new HashSet<>();
- Dimension2D dim = emf.getSize();
- 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;
- }
+ 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"))) {
+ for (int idx=0;;idx++) {
+ SevenZArchiveEntry entry = sevenZFile.getNextEntry();
+ if (entry == null) break;
+ final String etName = entry.getName();
+
+ if (entry.isDirectory() || !etName.endsWith(".emf") || passed.contains(etName)) continue;
+
+ System.out.println(etName);
- BufferedImage bufImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
- Graphics2D g = bufImg.createGraphics();
- g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
- g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
- g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
- g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
-
- FileWriter fw = new FileWriter("record-list.txt");
- int i = 0;
- for (HemfRecord r : emf.getRecords()) {
- if (r.getEmfRecordType() != HemfRecordType.comment) {
- fw.write(i + " " + r.getEmfRecordType() + " " + r.toString() + "\n");
+ int size = sevenZFile.read(buf);
+
+ HemfPicture emf = null;
+ try {
+ emf = new HemfPicture(new ByteArrayInputStream(buf, 0, size));
+
+ // initialize parsing
+ emf.getRecords();
+ } catch (Exception|AssertionError e) {
+ if (writeLog) {
+ parseError.write(etName+" "+hashException(e)+"\n");
+ parseError.flush();
+ }
+ System.out.println("parse error");
+ // continue with the read records up to the error anyway
+ if (emf.getRecords().isEmpty()) {
+ continue;
}
- i++;
}
- fw.close();
- emf.draw(g, new Rectangle2D.Double(0, 0, width, height));
+ Graphics2D g = null;
+ try {
+ Dimension2D dim = emf.getSize();
+ double width = Units.pointsToPixel(dim.getWidth());
+ // keep aspect ratio for height
+ double height = Units.pointsToPixel(dim.getHeight());
+ double max = Math.max(width, height);
+ if (max > 1500.) {
+ width *= 1500. / max;
+ height *= 1500. / max;
+ }
+
+ BufferedImage bufImg = new BufferedImage((int)Math.ceil(width), (int)Math.ceil(height), BufferedImage.TYPE_INT_ARGB);
+ g = bufImg.createGraphics();
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+ g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
+ g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
+ g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
+
+ emf.draw(g, new Rectangle2D.Double(0, 0, width, height));
- g.dispose();
+ final File pngName = new File("build/tmp", etName.replaceFirst(".*"+"/", "").replace(".emf", ".png"));
+ if (savePng) {
+ ImageIO.write(bufImg, "PNG", pngName);
+ }
+ } catch (Exception|AssertionError e) {
+ System.out.println("render error");
+ if (writeLog) {
+ // dumpRecords(emf.getRecords());
+ renderError.write(etName+" "+hashException(e)+"\n");
+ renderError.flush();
+ }
+ continue;
+ } finally {
+ if (g != null) g.dispose();
+ }
- ImageIO.write(bufImg, "PNG", pngName);
+ if (writeLog) {
+ sucWrite.write(etName + "\n");
+ sucWrite.flush();
+ }
+ }
+ }
+ } */
-// break;
+ private static int hashException(Throwable e) {
+ StringBuilder sb = new StringBuilder();
+ for (StackTraceElement se : e.getStackTrace()) {
+ sb.append(se.getClassName()+":"+se.getLineNumber());
+ }
+ return sb.toString().hashCode();
+ }
+
+ private static void dumpRecords(HemfPicture emf) throws IOException {
+ FileWriter fw = new FileWriter("record-list.txt");
+ int i = 0;
+ for (HemfRecord r : emf.getRecords()) {
+ if (r.getEmfRecordType() != HemfRecordType.comment) {
+ fw.write(i + " " + r.getEmfRecordType() + " " + r.toString() + "\n");
}
+ i++;
}
+ fw.close();
}
+ private static BufferedWriter parseEmfLog(Set<String> passed, String logFile) throws IOException {
+ Path log = Paths.get(logFile);
+ StandardOpenOption soo;
+ if (Files.exists(log)) {
+ soo = StandardOpenOption.APPEND;
+ try (Stream<String> stream = Files.lines(log)) {
+ stream.forEach((s) -> passed.add(s.split("\\s")[0]));
+ }
+ } else {
+ soo = StandardOpenOption.CREATE;
+ }
+ return Files.newBufferedWriter(log, StandardCharsets.UTF_8, soo);
+ }
@Test
public void testBasicWindows() throws Exception {