From a4aed9b002a2689122e2fe909067609a0a7b843f Mon Sep 17 00:00:00 2001 From: Andreas Beeker Date: Fri, 19 Oct 2018 22:53:33 +0000 Subject: [PATCH] #60656 - Support export file that contains emf and render it correctly git-svn-id: https://svn.apache.org/repos/asf/poi/branches/hemf@1844380 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/poi/hemf/draw/HemfGraphics.java | 22 +-- .../poi/hemf/record/emf/HemfComment.java | 24 ++- .../apache/poi/hemf/record/emf/HemfDraw.java | 13 +- .../apache/poi/hemf/record/emf/HemfFill.java | 28 +-- .../poi/hemf/record/emf/HemfHeader.java | 35 ++-- .../apache/poi/hemf/record/emf/HemfMisc.java | 50 ++++- .../hemf/record/emf/HemfRecordIterator.java | 11 +- .../poi/hemf/record/emf/HemfRecordType.java | 2 +- .../apache/poi/hemf/record/emf/HemfText.java | 77 +++++--- .../poi/hemf/record/emf/HemfWindowing.java | 5 +- .../poi/hemf/usermodel/HemfPicture.java | 27 ++- .../poi/hwmf/draw/HwmfDrawProperties.java | 12 ++ .../apache/poi/hwmf/draw/HwmfGraphics.java | 61 +++++-- .../apache/poi/hwmf/record/HwmfBitmapDib.java | 78 ++++++-- .../org/apache/poi/hwmf/record/HwmfFill.java | 14 +- .../org/apache/poi/hwmf/record/HwmfMisc.java | 7 +- .../org/apache/poi/hwmf/record/HwmfText.java | 48 +++++ .../apache/poi/hwmf/record/HwmfWindowing.java | 25 ++- .../poi/hwmf/usermodel/HwmfPicture.java | 5 +- .../poi/hemf/usermodel/HemfPictureTest.java | 171 ++++++++++++------ 20 files changed, 505 insertions(+), 210 deletions(-) diff --git a/src/scratchpad/src/org/apache/poi/hemf/draw/HemfGraphics.java b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfGraphics.java index 2cc1f0889d..8b18be82f9 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/draw/HemfGraphics.java +++ b/src/scratchpad/src/org/apache/poi/hemf/draw/HemfGraphics.java @@ -52,7 +52,7 @@ public class HemfGraphics extends HwmfGraphics { 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 @@ -105,6 +105,12 @@ public class HemfGraphics extends HwmfGraphics { 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(); @@ -135,19 +141,11 @@ public class HemfGraphics extends HwmfGraphics { */ 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 diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfComment.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfComment.java index c24562a047..6b8bd13df9 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfComment.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfComment.java @@ -28,6 +28,7 @@ import java.util.function.Supplier; 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; @@ -40,7 +41,7 @@ import org.apache.poi.util.RecordFormatException; */ @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), @@ -145,7 +146,12 @@ public class HemfComment { } 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. @@ -327,8 +333,12 @@ public class HemfComment { 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; @@ -405,8 +415,7 @@ public class HemfComment { } @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); @@ -431,7 +440,8 @@ public class HemfComment { 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; } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfDraw.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfDraw.java index 0cffdbc4b5..3596462364 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfDraw.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfDraw.java @@ -934,7 +934,7 @@ public class HemfDraw { 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()); } @@ -1041,15 +1041,16 @@ public class HemfDraw { } 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)); } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFill.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFill.java index ab632e28c8..2b814336f2 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFill.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfFill.java @@ -624,19 +624,19 @@ public class HemfFill { 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; @@ -646,8 +646,8 @@ public class HemfFill { static long readRgnData(final LittleEndianInputStream leis, final List 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); @@ -729,7 +729,7 @@ public class HemfFill { @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 diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfHeader.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfHeader.java index c1c0712c48..79e3b4943c 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfHeader.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfHeader.java @@ -17,17 +17,17 @@ 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; @@ -46,8 +46,7 @@ public class HemfHeader implements HemfRecord { 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; @@ -79,13 +78,7 @@ public class HemfHeader implements HemfRecord { return handles; } - public long getnDescription() { - return nDescription; - } - - public long getOffDescription() { - return offDescription; - } + public String getDescription() { return description; } public long getnPalEntries() { return nPalEntries; @@ -131,8 +124,7 @@ public class HemfHeader implements HemfRecord { ", bytes=" + bytes + ", records=" + records + ", handles=" + handles + - ", nDescription=" + nDescription + - ", offDescription=" + offDescription + + ", description=" + description + ", nPalEntries=" + nPalEntries + ", hasExtension1=" + hasExtension1 + ", cbPixelFormat=" + cbPixelFormat + @@ -156,6 +148,8 @@ public class HemfHeader implements HemfRecord { 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); @@ -174,8 +168,8 @@ public class HemfHeader implements HemfRecord { //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; @@ -183,6 +177,16 @@ public class HemfHeader implements HemfRecord { 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(); @@ -195,6 +199,7 @@ public class HemfHeader implements HemfRecord { hasExtension2 = true; size += readDimensionInt(leis, microDimension); } + return size; } } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfMisc.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfMisc.java index dd444f7c0e..ade921d200 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfMisc.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfMisc.java @@ -45,8 +45,6 @@ import org.apache.poi.util.LittleEndianConsts; 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 palette = new ArrayList<>(); @@ -535,6 +533,11 @@ public class HemfMisc { 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 { @@ -581,4 +584,47 @@ public class HemfMisc { ", 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; + } + } } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordIterator.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordIterator.java index 49452f2569..207c8ee830 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordIterator.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordIterator.java @@ -53,8 +53,15 @@ public class HemfRecordIterator implements Iterator { 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) { diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordType.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordType.java index 35d09b18af..7a3bf470e0 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordType.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfRecordType.java @@ -116,7 +116,7 @@ public enum HemfRecordType { 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), diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfText.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfText.java index 92528c8993..6b2f9a6d26 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfText.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfText.java @@ -113,36 +113,53 @@ public class HemfText { 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; + } + } } } diff --git a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfWindowing.java b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfWindowing.java index cbb6ada0d0..03c033b763 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfWindowing.java +++ b/src/scratchpad/src/org/apache/poi/hemf/record/emf/HemfWindowing.java @@ -38,9 +38,10 @@ public class HemfWindowing { @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; } diff --git a/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java b/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java index 77f34fd3bf..b8e863a85a 100644 --- a/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java +++ b/src/scratchpad/src/org/apache/poi/hemf/usermodel/HemfPicture.java @@ -34,6 +34,7 @@ import org.apache.poi.hemf.draw.HemfGraphics; 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; @@ -47,6 +48,7 @@ public class HemfPicture implements Iterable { private final LittleEndianInputStream stream; private final List records = new ArrayList<>(); + private boolean isParsed = false; public HemfPicture(InputStream is) throws IOException { this(new LittleEndianInputStream(is)); @@ -61,7 +63,10 @@ public class HemfPicture implements Iterable { } public List 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; @@ -89,10 +94,26 @@ public class HemfPicture implements Iterable { */ 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) { diff --git a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfDrawProperties.java b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfDrawProperties.java index 705f02705d..176c8e1bce 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfDrawProperties.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfDrawProperties.java @@ -37,6 +37,7 @@ import org.apache.poi.hwmf.record.HwmfMapMode; 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; @@ -65,6 +66,7 @@ public class HwmfDrawProperties { private HwmfTextVerticalAlignment textVAlignLatin; private HwmfTextAlignment textAlignAsian; private HwmfTextVerticalAlignment textVAlignAsian; + private HwmfTernaryRasterOp rasterOp; public HwmfDrawProperties() { window = new Rectangle2D.Double(0, 0, 1, 1); @@ -86,6 +88,7 @@ public class HwmfDrawProperties { textVAlignLatin = HwmfTextVerticalAlignment.TOP; textAlignAsian = HwmfTextAlignment.RIGHT; textVAlignAsian = HwmfTextVerticalAlignment.TOP; + rasterOp = HwmfTernaryRasterOp.PATCOPY; } public HwmfDrawProperties(HwmfDrawProperties other) { @@ -122,6 +125,7 @@ public class HwmfDrawProperties { this.textVAlignLatin = other.textVAlignLatin; this.textAlignAsian = other.textAlignAsian; this.textVAlignAsian = other.textVAlignAsian; + this.rasterOp = other.rasterOp; } public void setViewportExt(double width, double height) { @@ -375,4 +379,12 @@ public class HwmfDrawProperties { public int getWindingRule() { return getPolyfillMode().awtFlag; } + + public HwmfTernaryRasterOp getRasterOp() { + return rasterOp; + } + + public void setRasterOp(HwmfTernaryRasterOp rasterOp) { + this.rasterOp = rasterOp; + } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java index 7ac295e5ce..dff46ce863 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/draw/HwmfGraphics.java @@ -31,11 +31,11 @@ import java.awt.geom.Rectangle2D; 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; @@ -56,7 +56,8 @@ public class HwmfGraphics { protected final List propStack = new LinkedList<>(); protected HwmfDrawProperties prop; protected final Graphics2D graphicsCtx; - protected final List objectTable = new ArrayList<>(); + protected final BitSet objectIndexes = new BitSet(); + protected final TreeMap objectTable = new TreeMap<>(); private static final Charset DEFAULT_CHARSET = LocaleUtil.CHARSET_1252; /** Bounding box from the placeable header */ @@ -83,7 +84,11 @@ public class HwmfGraphics { } 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; @@ -201,15 +206,9 @@ public class HwmfGraphics { * @param entry */ public void addObjectTableEntry(HwmfObjectTableEntry entry) { - ListIterator 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); } /** @@ -242,7 +241,13 @@ public class HwmfGraphics { * @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); } /** @@ -353,6 +358,9 @@ public class HwmfGraphics { } 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(). @@ -371,22 +379,23 @@ public class HwmfGraphics { 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.); @@ -439,4 +448,20 @@ public class HwmfGraphics { 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 + ); + } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java index 82f593963e..a6f378512e 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfBitmapDib.java @@ -17,14 +17,21 @@ 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; @@ -38,7 +45,7 @@ import org.apache.poi.util.RecordFormatException; */ 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 { /** @@ -225,14 +232,23 @@ public class HwmfBitmapDib { 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 { @@ -262,6 +278,9 @@ public class HwmfBitmapDib { 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. @@ -306,7 +325,6 @@ public class HwmfBitmapDib { headerColorImportant = leis.readUInt(); size += 8*LittleEndianConsts.INT_SIZE+2*LittleEndianConsts.SHORT_SIZE; } - assert(size == headerSize); return size; } @@ -374,6 +392,10 @@ public class HwmfBitmapDib { return size; } + public boolean isValid() { + return (imageData != null); + } + public InputStream getBMPStream() { return new ByteArrayInputStream(getBMPData()); } @@ -407,14 +429,38 @@ public class HwmfBitmapDib { 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; + } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java index 0d6f85e4d8..4f15272e73 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfFill.java @@ -433,7 +433,7 @@ public class HwmfFill { * 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 @@ -487,12 +487,10 @@ public class HwmfFill { @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 @@ -720,7 +718,7 @@ public class HwmfFill { @Override public BufferedImage getImage() { - return (target == null) ? null : target.getImage(); + return (target != null && target.isValid()) ? target.getImage() : null; } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMisc.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMisc.java index 70e1d58aed..7bc46795bb 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMisc.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfMisc.java @@ -179,6 +179,11 @@ public class HwmfMisc { public void draw(HwmfGraphics ctx) { ctx.getProperties().setBkMode(bkMode); } + + @Override + public String toString() { + return "{ bkMode: '"+bkMode+"' }"; + } } /** @@ -436,7 +441,7 @@ public class HwmfMisc { @Override public BufferedImage getImage() { - if (patternDib != null) { + if (patternDib != null && patternDib.isValid()) { return patternDib.getImage(); } else if (pattern16 != null) { return pattern16.getImage(); diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java index 5228d11d0b..fb45170d22 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfText.java @@ -601,6 +601,54 @@ public class HwmfText { 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 { diff --git a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfWindowing.java b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfWindowing.java index e419157050..6e17b574fd 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfWindowing.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/record/HwmfWindowing.java @@ -21,10 +21,12 @@ import static org.apache.poi.hwmf.record.HwmfDraw.readBounds; 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; @@ -170,11 +172,7 @@ public class HwmfWindowing { */ 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() { @@ -183,23 +181,22 @@ public class HwmfWindowing { @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; } } diff --git a/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java b/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java index b380d46143..bf91dc864b 100644 --- a/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java +++ b/src/scratchpad/src/org/apache/poi/hwmf/usermodel/HwmfPicture.java @@ -43,6 +43,9 @@ import org.apache.poi.util.RecordFormatException; 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 records = new ArrayList<>(); @@ -156,7 +159,7 @@ public class HwmfPicture { 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()); } } diff --git a/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/HemfPictureTest.java b/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/HemfPictureTest.java index 3b078e1213..94be4712e5 100644 --- a/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/HemfPictureTest.java +++ b/src/scratchpad/testcases/org/apache/poi/hemf/usermodel/HemfPictureTest.java @@ -22,26 +22,22 @@ import static org.apache.poi.POITestCase.assertContains; 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; @@ -54,8 +50,6 @@ import org.apache.poi.hemf.record.emf.HemfRecordType; 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 { @@ -63,69 +57,130 @@ 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 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 passed, String logFile) throws IOException { + Path log = Paths.get(logFile); + StandardOpenOption soo; + if (Files.exists(log)) { + soo = StandardOpenOption.APPEND; + try (Stream 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 { -- 2.39.5