From 925f724d4cc4f604694c7f7ac9ac78a7792ba7f6 Mon Sep 17 00:00:00 2001 From: Nick Burch Date: Wed, 29 Nov 2006 14:49:40 +0000 Subject: [PATCH] Support compressed pictures properly, from bug #41032 git-svn-id: https://svn.apache.org/repos/asf/jakarta/poi/trunk@480585 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/hwpf/model/FIBFieldHandler.java | 19 +++- .../hwpf/model/UnhandledDataStructure.java | 6 + .../apache/poi/hwpf/usermodel/Picture.java | 106 +++++++++++++++--- .../org/apache/poi/hwpf/TestHWPFPictures.java | 36 ++++-- .../org/apache/poi/hwpf/data/vector_image.doc | Bin 0 -> 24064 bytes .../org/apache/poi/hwpf/data/vector_image.emf | Bin 0 -> 7348 bytes 6 files changed, 144 insertions(+), 23 deletions(-) create mode 100644 src/scratchpad/testcases/org/apache/poi/hwpf/data/vector_image.doc create mode 100644 src/scratchpad/testcases/org/apache/poi/hwpf/data/vector_image.emf diff --git a/src/scratchpad/src/org/apache/poi/hwpf/model/FIBFieldHandler.java b/src/scratchpad/src/org/apache/poi/hwpf/model/FIBFieldHandler.java index 160ddd1cb0..e95c27e64d 100644 --- a/src/scratchpad/src/org/apache/poi/hwpf/model/FIBFieldHandler.java +++ b/src/scratchpad/src/org/apache/poi/hwpf/model/FIBFieldHandler.java @@ -25,6 +25,8 @@ import java.io.IOException; import org.apache.poi.hwpf.model.io.HWPFOutputStream; import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; public class FIBFieldHandler { @@ -122,6 +124,8 @@ public class FIBFieldHandler public static final int STTBLISTNAMES = 91; public static final int STTBFUSSR = 92; + private static POILogger log = POILogFactory.getLogger(FIBFieldHandler.class); + private static final int FIELD_SIZE = LittleEndian.INT_SIZE * 2; private HashMap _unknownMap = new HashMap(); @@ -146,9 +150,18 @@ public class FIBFieldHandler { if (dsSize > 0) { - UnhandledDataStructure unhandled = new UnhandledDataStructure( - tableStream, dsOffset, dsSize); - _unknownMap.put(new Integer(x), unhandled); + if (dsOffset + dsSize > tableStream.length) + { + log.log(POILogger.WARN, "Unhandled data structure points to outside the buffer. " + + "offset = " + dsOffset + ", length = " + dsSize + + ", buffer length = " + tableStream.length); + } + else + { + UnhandledDataStructure unhandled = new UnhandledDataStructure( + tableStream, dsOffset, dsSize); + _unknownMap.put(new Integer(x), unhandled); + } } } _fields[x*2] = dsOffset; diff --git a/src/scratchpad/src/org/apache/poi/hwpf/model/UnhandledDataStructure.java b/src/scratchpad/src/org/apache/poi/hwpf/model/UnhandledDataStructure.java index 40a50e2f53..60edbe0633 100644 --- a/src/scratchpad/src/org/apache/poi/hwpf/model/UnhandledDataStructure.java +++ b/src/scratchpad/src/org/apache/poi/hwpf/model/UnhandledDataStructure.java @@ -23,7 +23,13 @@ public class UnhandledDataStructure public UnhandledDataStructure(byte[] buf, int offset, int length) { +// System.out.println("Yes, using my code"); _buf = new byte[length]; + if (offset + length > buf.length) + { + throw new IndexOutOfBoundsException("buffer length is " + buf.length + + "but code is trying to read " + length + " from offset " + offset); + } System.arraycopy(buf, offset, _buf, 0, length); } diff --git a/src/scratchpad/src/org/apache/poi/hwpf/usermodel/Picture.java b/src/scratchpad/src/org/apache/poi/hwpf/usermodel/Picture.java index 8adbf090f1..80e6f537d4 100644 --- a/src/scratchpad/src/org/apache/poi/hwpf/usermodel/Picture.java +++ b/src/scratchpad/src/org/apache/poi/hwpf/usermodel/Picture.java @@ -18,9 +18,14 @@ package org.apache.poi.hwpf.usermodel; import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.POILogger; +import org.apache.poi.util.POILogFactory; import java.io.OutputStream; import java.io.IOException; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.zip.InflaterInputStream; /** * Represents embedded picture extracted from Word Document @@ -28,8 +33,11 @@ import java.io.IOException; */ public class Picture { + private static final POILogger log = POILogFactory.getLogger(Picture.class); + // public static final int FILENAME_OFFSET = 0x7C; // public static final int FILENAME_SIZE_OFFSET = 0x6C; + static final int MFPMM_OFFSET = 0x6; static final int BLOCK_TYPE_OFFSET = 0xE; static final int PICT_HEADER_OFFSET = 0x4; static final int UNKNOWN_HEADER_SIZE = 0x49; @@ -41,13 +49,22 @@ public class Picture public static final byte[] TIFF = new byte[]{0x49, 0x49, 0x2A, 0x00}; public static final byte[] TIFF1 = new byte[]{0x4D, 0x4D, 0x00, 0x2A}; + public static final byte[] EMF = { 0x01, 0x00, 0x00, 0x00 }; + public static final byte[] WMF1 = { (byte)0xD7, (byte)0xCD, (byte)0xC6, (byte)0x9A, 0x00, 0x00 }; + public static final byte[] WMF2 = { 0x01, 0x00, 0x09, 0x00, 0x00, 0x03 }; // Windows 3.x + // TODO: DIB, PICT + public static final byte[] IHDR = new byte[]{'I', 'H', 'D', 'R'}; + public static final byte[] COMPRESSED1 = { (byte)0xFE, 0x78, (byte)0xDA }; + public static final byte[] COMPRESSED2 = { (byte)0xFE, 0x78, (byte)0x9C }; + private int dataBlockStartOfsset; private int pictureBytesStartOffset; private int dataBlockSize; private int size; // private String fileName; + private byte[] rawContent; private byte[] content; private byte[] _dataStream; private int aspectRatioX; @@ -77,9 +94,12 @@ public class Picture if (fillBytes) { - fillImageContent(_dataStream); + fillImageContent(); } + } + private void fillWidthHeight() + { String ext = suggestFileExtension(); // trying to extract width and height from pictures content: if ("jpg".equalsIgnoreCase(ext)) { @@ -121,8 +141,8 @@ public class Picture */ public void writeImageContent(OutputStream out) throws IOException { - if (content!=null && content.length>0) { - out.write(content, 0, size); + if (rawContent!=null && rawContent.length>0) { + out.write(rawContent, 0, size); } else { out.write(_dataStream, pictureBytesStartOffset, size); } @@ -135,11 +155,20 @@ public class Picture { if (content == null || content.length<=0) { - fillImageContent(this._dataStream); + fillImageContent(); } return content; } + public byte[] getRawContent() + { + if (rawContent == null || rawContent.length <= 0) + { + fillRawImageContent(); + } + return rawContent; + } + /** * * @return size in bytes of the picture @@ -171,10 +200,12 @@ public class Picture */ public String suggestFileExtension() { - if (content!=null && content.length>0) { - return suggestFileExtension(content, 0); + String extension = suggestFileExtension(_dataStream, pictureBytesStartOffset); + if ("".equals(extension)) { + // May be compressed. Get the uncompressed content and inspect that. + extension = suggestFileExtension(getContent(), 0); } - return suggestFileExtension(_dataStream, pictureBytesStartOffset); + return extension; } @@ -188,11 +219,16 @@ public class Picture return "gif"; } else if (matchSignature(_dataStream, BMP, pictureBytesStartOffset)) { return "bmp"; - } else if (matchSignature(_dataStream, TIFF, pictureBytesStartOffset)) { - return "tiff"; - } else if (matchSignature(_dataStream, TIFF1, pictureBytesStartOffset)) { + } else if (matchSignature(_dataStream, TIFF, pictureBytesStartOffset) || + matchSignature(_dataStream, TIFF1, pictureBytesStartOffset)) { return "tiff"; + } else if (matchSignature(content, WMF1, 0) || + matchSignature(content, WMF2, 0)) { + return "wmf"; + } else if (matchSignature(content, EMF, 0)) { + return "emf"; } + // TODO: DIB, PICT return ""; } @@ -233,10 +269,44 @@ public class Picture // return fileName.trim(); // } - private void fillImageContent(byte[] dataStream) + private void fillRawImageContent() { - this.content = new byte[size]; - System.arraycopy(dataStream, pictureBytesStartOffset, content, 0, size); + this.rawContent = new byte[size]; + System.arraycopy(_dataStream, pictureBytesStartOffset, rawContent, 0, size); + } + + private void fillImageContent() + { + byte[] rawContent = getRawContent(); + + // HACK: Detect compressed images. In reality there should be some way to determine + // this from the first 32 bytes, but I can't see any similarity between all the + // samples I have obtained, nor any similarity in the data block contents. + if (matchSignature(rawContent, COMPRESSED1, 32) || matchSignature(rawContent, COMPRESSED2, 32)) + { + try + { + InflaterInputStream in = new InflaterInputStream( + new ByteArrayInputStream(rawContent, 33, rawContent.length - 33)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int readBytes; + while ((readBytes = in.read(buf)) > 0) + { + out.write(buf, 0, readBytes); + } + content = out.toByteArray(); + } + catch (IOException e) + { + // Problems reading from the actual ByteArrayInputStream should never happen + // so this will only ever be a ZipException. + log.log(POILogger.INFO, "Possibly corrupt compression or non-compressed data", e); + } + } else { + // Raw data is not compressed. + content = rawContent; + } } private static int getPictureBytesStartOffset(int dataBlockStartOffset, byte[] _dataStream, int dataBlockSize) @@ -322,18 +392,28 @@ public class Picture this.height = getBigEndianInt(_dataStream, IHDR_CHUNK_WIDTH + 4); } } + /** * returns pixel width of the picture or -1 if dimensions determining was failed */ public int getWidth() { + if (width == -1) + { + fillWidthHeight(); + } return width; } + /** * returns pixel height of the picture or -1 if dimensions determining was failed */ public int getHeight() { + if (height == -1) + { + fillWidthHeight(); + } return height; } diff --git a/src/scratchpad/testcases/org/apache/poi/hwpf/TestHWPFPictures.java b/src/scratchpad/testcases/org/apache/poi/hwpf/TestHWPFPictures.java index e92e40c27e..080557a313 100644 --- a/src/scratchpad/testcases/org/apache/poi/hwpf/TestHWPFPictures.java +++ b/src/scratchpad/testcases/org/apache/poi/hwpf/TestHWPFPictures.java @@ -31,38 +31,40 @@ import junit.framework.TestCase; * @author nick */ public class TestHWPFPictures extends TestCase { - private HWPFDocument docA; - private HWPFDocument docB; private String docAFile; private String docBFile; + private String docCFile; private String imgAFile; private String imgBFile; + private String imgCFile; protected void setUp() throws Exception { String dirname = System.getProperty("HWPF.testdata.path"); docAFile = dirname + "/testPictures.doc"; docBFile = dirname + "/two_images.doc"; + docCFile = dirname + "/vector_image.doc"; imgAFile = dirname + "/simple_image.jpg"; imgBFile = dirname + "/simple_image.png"; + imgCFile = dirname + "/vector_image.emf"; } /** * Test just opening the files */ public void testOpen() throws Exception { - docA = new HWPFDocument(new FileInputStream(docAFile)); - docB = new HWPFDocument(new FileInputStream(docBFile)); + HWPFDocument docA = new HWPFDocument(new FileInputStream(docAFile)); + HWPFDocument docB = new HWPFDocument(new FileInputStream(docBFile)); } /** * Test that we have the right numbers of images in each file */ public void testImageCount() throws Exception { - docA = new HWPFDocument(new FileInputStream(docAFile)); - docB = new HWPFDocument(new FileInputStream(docBFile)); + HWPFDocument docA = new HWPFDocument(new FileInputStream(docAFile)); + HWPFDocument docB = new HWPFDocument(new FileInputStream(docBFile)); assertNotNull(docA.getPicturesTable()); assertNotNull(docB.getPicturesTable()); @@ -81,7 +83,7 @@ public class TestHWPFPictures extends TestCase { * Test that we have the right images in at least one file */ public void testImageData() throws Exception { - docB = new HWPFDocument(new FileInputStream(docBFile)); + HWPFDocument docB = new HWPFDocument(new FileInputStream(docBFile)); PicturesTable picB = docB.getPicturesTable(); List picturesB = picB.getAllPictures(); @@ -104,6 +106,26 @@ public class TestHWPFPictures extends TestCase { assertBytesSame(pic2B, pic2.getContent()); } + /** + * Test that compressed image data is correctly returned. + */ + public void testCompressedImageData() throws Exception { + HWPFDocument docC = new HWPFDocument(new FileInputStream(docCFile)); + PicturesTable picC = docC.getPicturesTable(); + List picturesC = picC.getAllPictures(); + + assertEquals(1, picturesC.size()); + + Picture pic = (Picture)picturesC.get(0); + assertNotNull(pic); + + // Check the same + byte[] picBytes = readFile(imgCFile); + + assertEquals(picBytes.length, pic.getContent().length); + assertBytesSame(picBytes, pic.getContent()); + } + private void assertBytesSame(byte[] a, byte[] b) { assertEquals(a.length, b.length); diff --git a/src/scratchpad/testcases/org/apache/poi/hwpf/data/vector_image.doc b/src/scratchpad/testcases/org/apache/poi/hwpf/data/vector_image.doc new file mode 100644 index 0000000000000000000000000000000000000000..89224718881995d82e533f62e77acf957c89c435 GIT binary patch literal 24064 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 literal 0 HcmV?d00001 -- 2.39.5