From d1c9a07860e365db4e335d24ad67e87544bdcceb Mon Sep 17 00:00:00 2001 From: Marius Volkhart Date: Sun, 28 Feb 2021 23:16:14 +0000 Subject: [PATCH] Add the ability to edit HSLFPictureData contents Pictures can now be edited by calling HSLFPictureData#setData(byte[]). The byte[] should contain the image data as an image viewer might read it. To enable this functionality, a tighter coupling between the EscherBSERecords of the slideshow and the HSLFPictureData was required. This ensures that changes in image data size are accurately recorded in the records. In the course of coupling the records and the HSLFPictureData, various scenarios arose where a mapping of records to pictures was non-trivial. Accordingly, the HSLFSlideShowImpl#matchPicturesAndRecords(...) function was added to perform a more sophisticated matching pass. This function is heavily exercised by org.apache.poi.hslf.usermodel.TestBugs.testFile[5] and PPTX2PNG.render[2], as well as the new TestPictures#testSlideshowWithIncorrectOffsets(). Closes #225 git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1887017 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/poi/sl/usermodel/PictureData.java | 4 + .../src/org/apache/poi/hslf/blip/Bitmap.java | 52 +++- .../src/org/apache/poi/hslf/blip/DIB.java | 34 +- .../src/org/apache/poi/hslf/blip/EMF.java | 55 +++- .../src/org/apache/poi/hslf/blip/JPEG.java | 29 ++ .../org/apache/poi/hslf/blip/Metafile.java | 80 ++++- .../src/org/apache/poi/hslf/blip/PICT.java | 52 +++- .../src/org/apache/poi/hslf/blip/PNG.java | 28 ++ .../src/org/apache/poi/hslf/blip/WMF.java | 53 +++- .../org/apache/poi/hslf/dev/PPTXMLDump.java | 3 +- .../apache/poi/hslf/usermodel/HSLFFill.java | 4 +- .../poi/hslf/usermodel/HSLFPictureData.java | 291 ++++++++++++++++-- .../poi/hslf/usermodel/HSLFPictureShape.java | 4 +- .../poi/hslf/usermodel/HSLFSlideShow.java | 56 ++-- .../poi/hslf/usermodel/HSLFSlideShowImpl.java | 237 ++++++++++++-- .../poi/hslf/dev/BaseTestPPTIterating.java | 1 + .../poi/hslf/usermodel/TestPicture.java | 12 +- .../poi/hslf/usermodel/TestPictures.java | 179 ++++++++++- .../slideshow/ppt_with_png_encrypted.ppt | Bin 0 -> 26112 bytes test-data/spreadsheet/stress.xls | Bin 37888 -> 51712 bytes 20 files changed, 1038 insertions(+), 136 deletions(-) create mode 100644 test-data/slideshow/ppt_with_png_encrypted.ppt diff --git a/src/java/org/apache/poi/sl/usermodel/PictureData.java b/src/java/org/apache/poi/sl/usermodel/PictureData.java index 83b54d75c7..df2ca79b01 100644 --- a/src/java/org/apache/poi/sl/usermodel/PictureData.java +++ b/src/java/org/apache/poi/sl/usermodel/PictureData.java @@ -105,6 +105,10 @@ public interface PictureData { /** * Sets the binary picture data + *

+ * The format of the data must match the format of {@link #getType()}. Failure to match the picture data may result + * in data loss. + * * @param data picture data */ void setData(byte[] data) throws IOException; diff --git a/src/scratchpad/src/org/apache/poi/hslf/blip/Bitmap.java b/src/scratchpad/src/org/apache/poi/hslf/blip/Bitmap.java index afc8cd5481..bb5904da55 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/blip/Bitmap.java +++ b/src/scratchpad/src/org/apache/poi/hslf/blip/Bitmap.java @@ -20,13 +20,17 @@ package org.apache.poi.hslf.blip; import java.awt.Dimension; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; import javax.imageio.ImageIO; +import org.apache.poi.ddf.EscherBSERecord; +import org.apache.poi.ddf.EscherContainerRecord; import org.apache.poi.hslf.usermodel.HSLFPictureData; +import org.apache.poi.hslf.usermodel.HSLFSlideShow; import org.apache.poi.util.IOUtils; +import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; import org.apache.poi.util.Units; /** @@ -35,6 +39,29 @@ import org.apache.poi.util.Units; */ public abstract class Bitmap extends HSLFPictureData { + /** + * @deprecated Use {@link HSLFSlideShow#addPicture(byte[], PictureType)} or one of it's overloads to create new + * {@link Bitmap}. This API led to detached {@link Bitmap} instances (See Bugzilla + * 46122) and prevented adding additional functionality. + */ + @Deprecated + @Removal(version = "5.3") + public Bitmap() { + this(new EscherContainerRecord(), new EscherBSERecord()); + } + + /** + * Creates a new instance. + * + * @param recordContainer Record tracking all pictures. Should be attached to the slideshow that this picture is + * linked to. + * @param bse Record referencing this picture. Should be attached to the slideshow that this picture is linked to. + */ + @Internal + protected Bitmap(EscherContainerRecord recordContainer, EscherBSERecord bse) { + super(recordContainer, bse); + } + @Override public byte[] getData(){ byte[] rawdata = getRawData(); @@ -43,17 +70,22 @@ public abstract class Bitmap extends HSLFPictureData { } @Override - public void setData(byte[] data) throws IOException { + protected byte[] formatImageForSlideshow(byte[] data) { byte[] checksum = getChecksum(data); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write(checksum); - if (getUIDInstanceCount() == 2) { - out.write(checksum); - } - out.write(0); - out.write(data); + byte[] rawData = new byte[checksum.length * getUIDInstanceCount() + 1 + data.length]; + int offset = 0; - setRawData(out.toByteArray()); + System.arraycopy(checksum, 0, rawData, offset, checksum.length); + offset += checksum.length; + + if (getUIDInstanceCount() == 2) { + System.arraycopy(checksum, 0, rawData, offset, checksum.length); + offset += checksum.length; + } + + offset++; + System.arraycopy(data, 0, rawData, offset, data.length); + return rawData; } @Override diff --git a/src/scratchpad/src/org/apache/poi/hslf/blip/DIB.java b/src/scratchpad/src/org/apache/poi/hslf/blip/DIB.java index 8a091bd7ab..1840d55409 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/blip/DIB.java +++ b/src/scratchpad/src/org/apache/poi/hslf/blip/DIB.java @@ -17,10 +17,13 @@ package org.apache.poi.hslf.blip; -import java.io.IOException; - +import org.apache.poi.ddf.EscherBSERecord; +import org.apache.poi.ddf.EscherContainerRecord; +import org.apache.poi.hslf.usermodel.HSLFSlideShow; import org.apache.poi.util.IOUtils; +import org.apache.poi.util.Internal; import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.Removal; /** * Represents a DIB picture data in a PPT file @@ -35,6 +38,29 @@ public final class DIB extends Bitmap { */ private static final int HEADER_SIZE = 14; + /** + * @deprecated Use {@link HSLFSlideShow#addPicture(byte[], PictureType)} or one of it's overloads to create new + * {@link DIB}. This API led to detached {@link DIB} instances (See Bugzilla + * 46122) and prevented adding additional functionality. + */ + @Deprecated + @Removal(version = "5.3") + public DIB() { + this(new EscherContainerRecord(), new EscherBSERecord()); + } + + /** + * Creates a new instance. + * + * @param recordContainer Record tracking all pictures. Should be attached to the slideshow that this picture is + * linked to. + * @param bse Record referencing this picture. Should be attached to the slideshow that this picture is linked to. + */ + @Internal + public DIB(EscherContainerRecord recordContainer, EscherBSERecord bse) { + super(recordContainer, bse); + } + @Override public PictureType getType(){ return PictureType.DIB; @@ -100,9 +126,9 @@ public final class DIB extends Bitmap { } @Override - public void setData(byte[] data) throws IOException { + protected byte[] formatImageForSlideshow(byte[] data) { //cut off the bitmap file-header byte[] dib = IOUtils.safelyClone(data, HEADER_SIZE, data.length-HEADER_SIZE, data.length); - super.setData(dib); + return super.formatImageForSlideshow(dib); } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/blip/EMF.java b/src/scratchpad/src/org/apache/poi/hslf/blip/EMF.java index ba90372f85..e6bf60da16 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/blip/EMF.java +++ b/src/scratchpad/src/org/apache/poi/hslf/blip/EMF.java @@ -24,9 +24,14 @@ import java.io.IOException; import java.io.InputStream; import java.util.zip.InflaterInputStream; +import org.apache.poi.ddf.EscherBSERecord; +import org.apache.poi.ddf.EscherContainerRecord; import org.apache.poi.hslf.exceptions.HSLFException; +import org.apache.poi.hslf.usermodel.HSLFSlideShow; import org.apache.poi.sl.image.ImageHeaderEMF; import org.apache.poi.util.IOUtils; +import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; import org.apache.poi.util.Units; /** @@ -34,6 +39,29 @@ import org.apache.poi.util.Units; */ public final class EMF extends Metafile { + /** + * @deprecated Use {@link HSLFSlideShow#addPicture(byte[], PictureType)} or one of it's overloads to create new + * {@link EMF}. This API led to detached {@link EMF} instances (See Bugzilla + * 46122) and prevented adding additional functionality. + */ + @Deprecated + @Removal(version = "5.3") + public EMF() { + this(new EscherContainerRecord(), new EscherBSERecord()); + } + + /** + * Creates a new instance. + * + * @param recordContainer Record tracking all pictures. Should be attached to the slideshow that this picture is + * linked to. + * @param bse Record referencing this picture. Should be attached to the slideshow that this picture is linked to. + */ + @Internal + public EMF(EscherContainerRecord recordContainer, EscherBSERecord bse) { + super(recordContainer, bse); + } + @Override public byte[] getData(){ try { @@ -60,11 +88,11 @@ public final class EMF extends Metafile { } @Override - public void setData(byte[] data) throws IOException { + protected byte[] formatImageForSlideshow(byte[] data) { byte[] compressed = compress(data, 0, data.length); ImageHeaderEMF nHeader = new ImageHeaderEMF(data, 0); - + Header header = new Header(); header.setWmfSize(data.length); header.setBounds(nHeader.getBounds()); @@ -73,15 +101,22 @@ public final class EMF extends Metafile { header.setZipSize(compressed.length); byte[] checksum = getChecksum(data); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write(checksum); - if (getUIDInstanceCount() == 2) { - out.write(checksum); - } - header.write(out); - out.write(compressed); + byte[] rawData = new byte[checksum.length * getUIDInstanceCount() + header.getSize() + compressed.length]; + int offset = 0; - setRawData(out.toByteArray()); + System.arraycopy(checksum, 0, rawData, offset, checksum.length); + offset += checksum.length; + + if (getUIDInstanceCount() == 2) { + System.arraycopy(checksum, 0, rawData, offset, checksum.length); + offset += checksum.length; + } + + header.write(rawData, offset); + offset += header.getSize(); + System.arraycopy(compressed, 0, rawData, offset, compressed.length); + + return rawData; } @Override diff --git a/src/scratchpad/src/org/apache/poi/hslf/blip/JPEG.java b/src/scratchpad/src/org/apache/poi/hslf/blip/JPEG.java index 08ba6b73e7..cf90746d6b 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/blip/JPEG.java +++ b/src/scratchpad/src/org/apache/poi/hslf/blip/JPEG.java @@ -18,6 +18,12 @@ package org.apache.poi.hslf.blip; +import org.apache.poi.ddf.EscherBSERecord; +import org.apache.poi.ddf.EscherContainerRecord; +import org.apache.poi.hslf.usermodel.HSLFSlideShow; +import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; + /** * Represents a JPEG picture data in a PPT file */ @@ -26,6 +32,29 @@ public final class JPEG extends Bitmap { public enum ColorSpace { rgb, cymk } private ColorSpace colorSpace = ColorSpace.rgb; + + /** + * @deprecated Use {@link HSLFSlideShow#addPicture(byte[], PictureType)} or one of it's overloads to create new + * {@link JPEG}. This API led to detached {@link JPEG} instances (See Bugzilla + * 46122) and prevented adding additional functionality. + */ + @Deprecated + @Removal(version = "5.3") + public JPEG() { + this(new EscherContainerRecord(), new EscherBSERecord()); + } + + /** + * Creates a new instance. + * + * @param recordContainer Record tracking all pictures. Should be attached to the slideshow that this picture is + * linked to. + * @param bse Record referencing this picture. Should be attached to the slideshow that this picture is linked to. + */ + @Internal + public JPEG(EscherContainerRecord recordContainer, EscherBSERecord bse) { + super(recordContainer, bse); + } @Override public PictureType getType(){ diff --git a/src/scratchpad/src/org/apache/poi/hslf/blip/Metafile.java b/src/scratchpad/src/org/apache/poi/hslf/blip/Metafile.java index 6ebd4a0a61..ea855d460e 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/blip/Metafile.java +++ b/src/scratchpad/src/org/apache/poi/hslf/blip/Metafile.java @@ -25,9 +25,15 @@ import java.io.IOException; import java.io.OutputStream; import java.util.zip.DeflaterOutputStream; +import org.apache.poi.ddf.EscherBSERecord; +import org.apache.poi.ddf.EscherContainerRecord; import org.apache.poi.hslf.usermodel.HSLFPictureData; +import org.apache.poi.hslf.usermodel.HSLFSlideShow; +import org.apache.poi.util.Internal; +import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndianInputStream; import org.apache.poi.util.LittleEndianOutputStream; +import org.apache.poi.util.Removal; import org.apache.poi.util.Units; /** @@ -36,6 +42,29 @@ import org.apache.poi.util.Units; */ public abstract class Metafile extends HSLFPictureData { + /** + * @deprecated Use {@link HSLFSlideShow#addPicture(byte[], PictureType)} or one of it's overloads to create new + * {@link Metafile}. This API led to detached {@link Metafile} instances (See Bugzilla + * 46122) and prevented adding additional functionality. + */ + @Deprecated + @Removal(version = "5.3") + public Metafile() { + this(new EscherContainerRecord(), new EscherBSERecord()); + } + + /** + * Creates a new instance. + * + * @param recordContainer Record tracking all pictures. Should be attached to the slideshow that this picture is + * linked to. + * @param bse Record referencing this picture. Should be attached to the slideshow that this picture is linked to. + */ + @Internal + protected Metafile(EscherContainerRecord recordContainer, EscherBSERecord bse) { + super(recordContainer, bse); + } + /** * A structure which represents a 34-byte header preceding the compressed metafile data */ @@ -117,6 +146,44 @@ public abstract class Metafile extends HSLFPictureData { leos.writeByte(filter); } + void write(byte[] destination, int offset) { + //hmf + LittleEndian.putInt(destination, offset, wmfsize); + offset += 4; + + //left + LittleEndian.putInt(destination, offset, bounds.x); + offset += 4; + + //top + LittleEndian.putInt(destination, offset, bounds.y); + offset += 4; + + //right + LittleEndian.putInt(destination, offset, bounds.x + bounds.width); + offset += 4; + + //bottom + LittleEndian.putInt(destination, offset, bounds.y + bounds.height); + offset += 4; + + //inch + LittleEndian.putInt(destination, offset, size.width); + offset += 4; + + //inch + LittleEndian.putInt(destination, offset, size.height); + offset += 4; + + LittleEndian.putInt(destination, offset, zipsize); + offset += 4; + + destination[offset] = (byte) compression; + offset++; + + destination[offset] = (byte) filter; + } + public int getSize(){ return 34; } @@ -146,11 +213,16 @@ public abstract class Metafile extends HSLFPictureData { } } - protected static byte[] compress(byte[] bytes, int offset, int length) throws IOException { + protected static byte[] compress(byte[] bytes, int offset, int length) { ByteArrayOutputStream out = new ByteArrayOutputStream(); - DeflaterOutputStream deflater = new DeflaterOutputStream( out ); - deflater.write(bytes, offset, length); - deflater.close(); + try (DeflaterOutputStream deflater = new DeflaterOutputStream(out)) { + deflater.write(bytes, offset, length); + } catch (IOException e) { + // IOException won't get thrown by the DeflaterOutputStream in this configuration because: + // 1. ByteArrayOutputStream doesn't throw an IOException during writes. + // 2. The DeflaterOutputStream is not finished until we're done writing. + throw new AssertionError("Won't happen", e); + } return out.toByteArray(); } diff --git a/src/scratchpad/src/org/apache/poi/hslf/blip/PICT.java b/src/scratchpad/src/org/apache/poi/hslf/blip/PICT.java index 33dcde3b0e..a6c2765ec7 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/blip/PICT.java +++ b/src/scratchpad/src/org/apache/poi/hslf/blip/PICT.java @@ -26,9 +26,14 @@ import java.util.zip.InflaterInputStream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.poi.ddf.EscherBSERecord; +import org.apache.poi.ddf.EscherContainerRecord; import org.apache.poi.hslf.exceptions.HSLFException; +import org.apache.poi.hslf.usermodel.HSLFSlideShow; import org.apache.poi.sl.image.ImageHeaderPICT; import org.apache.poi.util.IOUtils; +import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; import org.apache.poi.util.Units; import static org.apache.logging.log4j.util.Unbox.box; @@ -39,6 +44,28 @@ import static org.apache.logging.log4j.util.Unbox.box; public final class PICT extends Metafile { private static final Logger LOG = LogManager.getLogger(PICT.class); + /** + * @deprecated Use {@link HSLFSlideShow#addPicture(byte[], PictureType)} or one of it's overloads to create new + * {@link PICT}. This API led to detached {@link PICT} instances (See Bugzilla + * 46122) and prevented adding additional functionality. + */ + @Deprecated + @Removal(version = "5.3") + public PICT() { + this(new EscherContainerRecord(), new EscherBSERecord()); + } + + /** + * Creates a new instance. + * + * @param recordContainer Record tracking all pictures. Should be attached to the slideshow that this picture is + * linked to. + * @param bse Record referencing this picture. Should be attached to the slideshow that this picture is linked to. + */ + @Internal + public PICT(EscherContainerRecord recordContainer, EscherBSERecord bse) { + super(recordContainer, bse); + } @Override public byte[] getData(){ @@ -93,7 +120,7 @@ public final class PICT extends Metafile { } @Override - public void setData(byte[] data) throws IOException { + protected byte[] formatImageForSlideshow(byte[] data) { // skip the first 512 bytes - they are MAC specific crap final int nOffset = ImageHeaderPICT.PICT_HEADER_OFFSET; ImageHeaderPICT nHeader = new ImageHeaderPICT(data, nOffset); @@ -108,15 +135,22 @@ public final class PICT extends Metafile { header.setDimension(new Dimension(Units.toEMU(nDim.getWidth()), Units.toEMU(nDim.getHeight()))); byte[] checksum = getChecksum(data); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write(checksum); - if (getUIDInstanceCount() == 2) { - out.write(checksum); - } - header.write(out); - out.write(compressed); + byte[] rawData = new byte[checksum.length * getUIDInstanceCount() + header.getSize() + compressed.length]; + int offset = 0; - setRawData(out.toByteArray()); + System.arraycopy(checksum, 0, rawData, offset, checksum.length); + offset += checksum.length; + + if (getUIDInstanceCount() == 2) { + System.arraycopy(checksum, 0, rawData, offset, checksum.length); + offset += checksum.length; + } + + header.write(rawData, offset); + offset += header.getSize(); + System.arraycopy(compressed, 0, rawData, offset, compressed.length); + + return rawData; } @Override diff --git a/src/scratchpad/src/org/apache/poi/hslf/blip/PNG.java b/src/scratchpad/src/org/apache/poi/hslf/blip/PNG.java index 8e05aa7611..197cb3d828 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/blip/PNG.java +++ b/src/scratchpad/src/org/apache/poi/hslf/blip/PNG.java @@ -17,13 +17,41 @@ package org.apache.poi.hslf.blip; +import org.apache.poi.ddf.EscherBSERecord; +import org.apache.poi.ddf.EscherContainerRecord; +import org.apache.poi.hslf.usermodel.HSLFSlideShow; import org.apache.poi.sl.image.ImageHeaderPNG; +import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; /** * Represents a PNG picture data in a PPT file */ public final class PNG extends Bitmap { + /** + * @deprecated Use {@link HSLFSlideShow#addPicture(byte[], PictureType)} or one of it's overloads to create new + * {@link PNG}. This API led to detached {@link PNG} instances (See Bugzilla + * 46122) and prevented adding additional functionality. + */ + @Deprecated + @Removal(version = "5.3") + public PNG() { + this(new EscherContainerRecord(), new EscherBSERecord()); + } + + /** + * Creates a new instance. + * + * @param recordContainer Record tracking all pictures. Should be attached to the slideshow that this picture is + * linked to. + * @param bse Record referencing this picture. Should be attached to the slideshow that this picture is linked to. + */ + @Internal + public PNG(EscherContainerRecord recordContainer, EscherBSERecord bse) { + super(recordContainer, bse); + } + @Override public byte[] getData() { return new ImageHeaderPNG(super.getData()).extractPNG(); diff --git a/src/scratchpad/src/org/apache/poi/hslf/blip/WMF.java b/src/scratchpad/src/org/apache/poi/hslf/blip/WMF.java index 1d00322911..55b74bdb6f 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/blip/WMF.java +++ b/src/scratchpad/src/org/apache/poi/hslf/blip/WMF.java @@ -24,9 +24,14 @@ import java.io.IOException; import java.io.InputStream; import java.util.zip.InflaterInputStream; +import org.apache.poi.ddf.EscherBSERecord; +import org.apache.poi.ddf.EscherContainerRecord; import org.apache.poi.hslf.exceptions.HSLFException; +import org.apache.poi.hslf.usermodel.HSLFSlideShow; import org.apache.poi.sl.image.ImageHeaderWMF; import org.apache.poi.util.IOUtils; +import org.apache.poi.util.Internal; +import org.apache.poi.util.Removal; import org.apache.poi.util.Units; /** @@ -34,6 +39,29 @@ import org.apache.poi.util.Units; */ public final class WMF extends Metafile { + /** + * @deprecated Use {@link HSLFSlideShow#addPicture(byte[], PictureType)} or one of it's overloads to create new + * {@link WMF}. This API led to detached {@link WMF} instances (See Bugzilla + * 46122) and prevented adding additional functionality. + */ + @Deprecated + @Removal(version = "5.3") + public WMF() { + this(new EscherContainerRecord(), new EscherBSERecord()); + } + + /** + * Creates a new instance. + * + * @param recordContainer Record tracking all pictures. Should be attached to the slideshow that this picture is + * linked to. + * @param bse Record referencing this picture. Should be attached to the slideshow that this picture is linked to. + */ + @Internal + public WMF(EscherContainerRecord recordContainer, EscherBSERecord bse) { + super(recordContainer, bse); + } + @Override public byte[] getData(){ try { @@ -64,7 +92,7 @@ public final class WMF extends Metafile { } @Override - public void setData(byte[] data) throws IOException { + protected byte[] formatImageForSlideshow(byte[] data) { int pos = 0; ImageHeaderWMF nHeader = new ImageHeaderWMF(data, pos); pos += nHeader.getLength(); @@ -79,15 +107,22 @@ public final class WMF extends Metafile { header.setZipSize(compressed.length); byte[] checksum = getChecksum(data); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.write(checksum); - if (getUIDInstanceCount() == 2) { - out.write(checksum); - } - header.write(out); - out.write(compressed); + byte[] rawData = new byte[checksum.length * getUIDInstanceCount() + header.getSize() + compressed.length]; + int offset = 0; - setRawData(out.toByteArray()); + System.arraycopy(checksum, 0, rawData, offset, checksum.length); + offset += checksum.length; + + if (getUIDInstanceCount() == 2) { + System.arraycopy(checksum, 0, rawData, offset, checksum.length); + offset += checksum.length; + } + + header.write(rawData, offset); + offset += header.getSize(); + System.arraycopy(compressed, 0, rawData, offset, compressed.length); + + return rawData; } @Override diff --git a/src/scratchpad/src/org/apache/poi/hslf/dev/PPTXMLDump.java b/src/scratchpad/src/org/apache/poi/hslf/dev/PPTXMLDump.java index 6b13a07920..4e431976a3 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/dev/PPTXMLDump.java +++ b/src/scratchpad/src/org/apache/poi/hslf/dev/PPTXMLDump.java @@ -173,7 +173,6 @@ public final class PPTXMLDump { return; } - byte[] pictdata = IOUtils.safelyClone(data, pos + PICT_HEADER_SIZE, size, MAX_RECORD_LENGTH); pos += PICT_HEADER_SIZE + size; padding++; @@ -183,7 +182,7 @@ public final class PPTXMLDump { dump(out, header, 0, header.length, padding, true); write(out, "" + CR, padding); write(out, "" + CR, padding); - dump(out, pictdata, 0, Math.min(pictdata.length, 100), padding, true); + dump(out, data, 0, Math.min(size, 100), padding, true); write(out, "" + CR, padding); padding--; write(out, "" + CR, padding); diff --git a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFFill.java b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFFill.java index 7fa9323209..5275669b76 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFFill.java +++ b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFFill.java @@ -570,7 +570,9 @@ public final class HSLFFill { } else { EscherBSERecord bse = (EscherBSERecord)lst.get(idx - 1); for (HSLFPictureData pd : pict) { - if (pd.getOffset() == bse.getOffset()){ + + // Reference equals is safe because these BSE belong to the same slideshow + if (pd.bse == bse) { return pd; } } diff --git a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFPictureData.java b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFPictureData.java index 2f3371d714..5b418f98c3 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFPictureData.java +++ b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFPictureData.java @@ -23,11 +23,18 @@ import java.io.OutputStream; import java.security.MessageDigest; import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Supplier; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.poi.common.usermodel.GenericRecord; +import org.apache.poi.ddf.EscherBSERecord; +import org.apache.poi.ddf.EscherContainerRecord; import org.apache.poi.ddf.EscherRecordTypes; import org.apache.poi.hslf.blip.DIB; import org.apache.poi.hslf.blip.EMF; @@ -38,8 +45,10 @@ import org.apache.poi.hslf.blip.WMF; import org.apache.poi.poifs.crypt.CryptoFunctions; import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.sl.usermodel.PictureData; +import org.apache.poi.util.Internal; import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndianConsts; +import org.apache.poi.util.Removal; import org.apache.poi.util.Units; /** @@ -47,19 +56,37 @@ import org.apache.poi.util.Units; */ public abstract class HSLFPictureData implements PictureData, GenericRecord { + private static final Logger LOGGER = LogManager.getLogger(HSLFPictureData.class); + /** * Size of the image checksum calculated using MD5 algorithm. */ protected static final int CHECKSUM_SIZE = 16; /** - * Binary data of the picture - */ - private byte[] rawdata; - /** - * The offset to the picture in the stream + * Size of the image preamble in bytes. + *

+ * The preamble describes how the image should be decoded. All image types have the same preamble format. The + * preamble has little endian encoding. Below is a diagram of the preamble contents. + * + *

+     *  0               1               2               3
+     *  0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |           Signature           |          Picture Type         |
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * |                       Formatted Length                        |
+     * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+     * 
*/ - private int offset; + static final int PREAMBLE_SIZE = 8; + + /** + * Binary data of the picture, formatted as it will be stored in the {@link HSLFSlideShow}. + *

+ * This does not include the {@link #PREAMBLE_SIZE preamble}. + */ + private byte[] formattedData; /** * The instance type/signatures defines if one or two UID instances will be included @@ -71,6 +98,43 @@ public abstract class HSLFPictureData implements PictureData, GenericRecord { */ private int index = -1; + /** + * {@link EscherRecordTypes#BSTORE_CONTAINER BStore} record tracking all pictures. Should be attached to the + * slideshow that this picture is linked to. + */ + final EscherContainerRecord bStore; + + /** + * Record referencing this picture. Should be attached to the slideshow that this picture is linked to. + */ + final EscherBSERecord bse; + + /** + * @deprecated Use {@link HSLFSlideShow#addPicture(byte[], PictureType)} or one of it's overloads to create new + * {@link HSLFPictureData}. This API led to detached {@link HSLFPictureData} instances (See Bugzilla + * 46122) and prevented adding additional functionality. + */ + @Deprecated + @Removal(version = "5.3") + public HSLFPictureData() { + this(new EscherContainerRecord(), new EscherBSERecord()); + LOGGER.atWarn().log("The no-arg constructor is deprecated. Some functionality such as updating pictures won't " + + "work."); + } + + /** + * Creates a new instance. + * + * @param bStore {@link EscherRecordTypes#BSTORE_CONTAINER BStore} record tracking all pictures. Should be attached + * to the slideshow that this picture is linked to. + * @param bse Record referencing this picture. Should be attached to the slideshow that this picture is linked to. + */ + @Internal + protected HSLFPictureData(EscherContainerRecord bStore, EscherBSERecord bse) { + this.bStore = Objects.requireNonNull(bStore); + this.bse = Objects.requireNonNull(bse); + } + /** * Blip signature. */ @@ -95,17 +159,34 @@ public abstract class HSLFPictureData implements PictureData, GenericRecord { } /** - * Returns the raw binary data of this Picture excluding the first 8 bytes - * which hold image signature and size of the image data. + * Returns the formatted, binary data of this picture excluding the {@link #PREAMBLE_SIZE preamble} bytes. + *

+ * Primarily intended for internal POI use. Use {@link #getData()} to retrieve the picture represented by this + * object. * - * @return picture data + * @return Picture data formatted for the HSLF format. + * @see #getData() + * @see #formatImageForSlideshow(byte[]) */ public byte[] getRawData(){ - return rawdata; + return formattedData; } + /** + * Sets the formatted data for this picture. + *

+ * Primarily intended for internal POI use. Use {@link #setData(byte[])} to change the picture represented by this + * object. + * + * @param data Picture data formatted for the HSLF format. Excludes the {@link #PREAMBLE_SIZE preamble}. + * @see #setData(byte[]) + * @see #formatImageForSlideshow(byte[]) + * @deprecated Set image data using {@link #setData(byte[])}. + */ + @Deprecated + @Removal(version = "5.3") public void setRawData(byte[] data){ - rawdata = (data == null) ? null : data.clone(); + formattedData = (data == null) ? null : data.clone(); } /** @@ -114,7 +195,7 @@ public abstract class HSLFPictureData implements PictureData, GenericRecord { * @return offset in the 'Pictures' stream */ public int getOffset(){ - return offset; + return bse.getOffset(); } /** @@ -122,21 +203,25 @@ public abstract class HSLFPictureData implements PictureData, GenericRecord { * We need to set it when a new picture is created. * * @param offset in the 'Pictures' stream + * @deprecated This function was only intended for POI internal use. If you have a use case you're concerned about, + * please open an issue in the POI issue tracker. */ + @Deprecated + @Removal(version = "5.3") public void setOffset(int offset){ - this.offset = offset; + LOGGER.atWarn().log("HSLFPictureData#setOffset is deprecated."); } /** * Returns 16-byte checksum of this picture */ public byte[] getUID(){ - return Arrays.copyOf(rawdata, 16); + return Arrays.copyOf(formattedData, CHECKSUM_SIZE); } @Override public byte[] getChecksum() { - return getChecksum(getData()); + return getUID(); } /** @@ -173,25 +258,105 @@ public abstract class HSLFPictureData implements PictureData, GenericRecord { } /** - * Create an instance of PictureData by type. + * Create an instance of {@link HSLFPictureData} by type. * - * @param type type of the picture data. - * Must be one of the static constants defined in the Picture class. - * @return concrete instance of PictureData + * @param type type of picture. + * @return concrete instance of {@link HSLFPictureData}. + * @deprecated Use {@link HSLFSlideShow#addPicture(byte[], PictureType)} or one of it's overloads to create new + * {@link HSLFPictureData}. This API led to detached {@link HSLFPictureData} instances (See Bugzilla + * 46122) and prevented adding additional functionality. */ + @Deprecated + @Removal(version = "5.3") public static HSLFPictureData create(PictureType type){ - HSLFPictureData pict; - switch (type){ - case EMF: pict = new EMF(); break; - case WMF: pict = new WMF(); break; - case PICT: pict = new PICT(); break; - case JPEG: pict = new JPEG(); break; - case PNG: pict = new PNG(); break; - case DIB: pict = new DIB(); break; + LOGGER.atWarn().log("HSLFPictureData#create(PictureType) is deprecated. Some functionality such " + + "as updating pictures won't work."); + + // This record code is a stub. It exists only for API compatibility. + EscherContainerRecord record = new EscherContainerRecord(); + EscherBSERecord bse = new EscherBSERecord(); + return new HSLFSlideShowImpl.PictureFactory(record, type, new byte[0], 0, 0) + .setRecord(bse) + .build(); + } + + /** + * Creates a new instance of the given image type using data already formatted for storage inside the slideshow. + *

+ * This function is most handy when parsing an existing slideshow, as the picture data are already formatted. + * @param type Image type. + * @param recordContainer Record tracking all pictures. Should be attached to the slideshow that this picture is + * linked to. + * @param bse Record referencing this picture. Should be attached to the slideshow that this picture is linked to. + * @param data Image data formatted for storage in the slideshow. This does not include the + * {@link #PREAMBLE_SIZE preamble}. + * @param signature Image format-specific signature. See subclasses for signature details. + * @return New instance. + * + * @see #createFromImageData(PictureType, EscherContainerRecord, EscherBSERecord, byte[]) + */ + static HSLFPictureData createFromSlideshowData( + PictureType type, + EscherContainerRecord recordContainer, + EscherBSERecord bse, + byte[] data, + int signature + ) { + HSLFPictureData instance = newInstance(type, recordContainer, bse); + instance.setSignature(signature); + instance.formattedData = data; + return instance; + } + + /** + * Creates a new instance of the given image type using data already formatted for storage inside the slideshow. + *

+ * This function is most handy when adding new pictures to a slideshow, as the image data provided by users is not + * yet formatted. + * + * @param type Image type. + * @param recordContainer Record tracking all pictures. Should be attached to the slideshow that this picture is + * linked to. + * @param bse Record referencing this picture. Should be attached to the slideshow that this picture is linked to. + * @param data Original image data. If these bytes were written to a disk, a common image viewer would be able to + * render the image. + * @return New instance. + * + * @see #createFromSlideshowData(PictureType, EscherContainerRecord, EscherBSERecord, byte[], int) + * @see #setData(byte[]) + */ + static HSLFPictureData createFromImageData( + PictureType type, + EscherContainerRecord recordContainer, + EscherBSERecord bse, + byte[] data + ) { + HSLFPictureData instance = newInstance(type, recordContainer, bse); + instance.formattedData = instance.formatImageForSlideshow(data); + return instance; + } + + private static HSLFPictureData newInstance( + PictureType type, + EscherContainerRecord recordContainer, + EscherBSERecord bse + ) { + switch (type) { + case EMF: + return new EMF(recordContainer, bse); + case WMF: + return new WMF(recordContainer, bse); + case PICT: + return new PICT(recordContainer, bse); + case JPEG: + return new JPEG(recordContainer, bse); + case PNG: + return new PNG(recordContainer, bse); + case DIB: + return new DIB(recordContainer, bse); default: throw new IllegalArgumentException("Unsupported picture type: " + type); } - return pict; } /** @@ -204,14 +369,15 @@ public abstract class HSLFPictureData implements PictureData, GenericRecord { * @return the 24 byte header which preceeds the actual picture data. */ public byte[] getHeader() { - byte[] header = new byte[16 + 8]; + byte[] header = new byte[CHECKSUM_SIZE + PREAMBLE_SIZE]; LittleEndian.putInt(header, 0, getSignature()); LittleEndian.putInt(header, 4, getRawData().length); - System.arraycopy(rawdata, 0, header, 8, 16); + System.arraycopy(formattedData, 0, header, PREAMBLE_SIZE, CHECKSUM_SIZE); return header; } /** + * Returns the 1-based index of this picture. * @return the 1-based index of this pictures within the pictures stream */ public int getIndex() { @@ -225,6 +391,71 @@ public abstract class HSLFPictureData implements PictureData, GenericRecord { this.index = index; } + /** + * Formats the picture data for storage in the slideshow. + *

+ * Images stored in {@link HSLFSlideShow}s are represented differently than when they are standalone files. The + * exact formatting differs for each image type. + * + * @param data Original image data. If these bytes were written to a disk, a common image viewer would be able to + * render the image. + * @return Formatted image representation. + */ + protected abstract byte[] formatImageForSlideshow(byte[] data); + + /** + * @return Size of this picture when stored in the image stream inside the {@link HSLFSlideShow}. + */ + int getBseSize() { + return formattedData.length + PREAMBLE_SIZE; + } + + @Override + public final void setData(byte[] data) throws IOException { + /* + * When working with slideshow pictures, we need to be aware of 2 container units. The first is a list of + * HSLFPictureData that are the programmatic reference for working with the pictures. The second is the + * Blip Store. For the purposes of this function, you can think of the Blip Store as containing a list of + * pointers (with a small summary) to the picture in the slideshow. + * + * When updating a picture, we need to update the in-memory data structure (this instance), but we also need to + * update the stored pointer. When modifying the pointer, we also need to modify all subsequent pointers, since + * they might shift based on a change in the byte count of the underlying image. + */ + int oldSize = getBseSize(); + formattedData = formatImageForSlideshow(data); + int newSize = getBseSize(); + int changeInSize = newSize - oldSize; + byte[] newUid = getUID(); + + boolean foundBseForOldImage = false; + + // Get the BSE records & sort the list by offset, so we can proceed to shift offsets + @SuppressWarnings("unchecked") // The BStore only contains BSE records + List bseRecords = (List) (Object) bStore.getChildRecords(); + bseRecords.sort(Comparator.comparingInt(EscherBSERecord::getOffset)); + + for (EscherBSERecord bse : bseRecords) { + + if (foundBseForOldImage) { + + // The BSE for this picture was modified in a previous iteration, and we are now adjusting + // subsequent offsets. + bse.setOffset(bse.getOffset() + changeInSize); + + } else if (bse == this.bse) { // Reference equals is safe because these BSE belong to the same slideshow + + // This BSE matches the current image. Update the size and UID. + foundBseForOldImage = true; + + bse.setUid(newUid); + + // Image byte count may have changed, so update the pointer. + bse.setSize(newSize); + } + } + } + @Override public final String getContentType() { return getType().contentType; diff --git a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFPictureShape.java b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFPictureShape.java index 49921dc81b..548fdff3d1 100644 --- a/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFPictureShape.java +++ b/src/scratchpad/src/org/apache/poi/hslf/usermodel/HSLFPictureShape.java @@ -125,7 +125,9 @@ public class HSLFPictureShape extends HSLFSimpleShape implements PictureShape(); - // if the presentation doesn't contain pictures - will use a null set instead + // if the presentation doesn't contain pictures, will use an empty collection instead if (!getDirectory().hasEntry("Pictures")) { + _pictures = new ArrayList<>(); return; } DocumentEntry entry = (DocumentEntry) getDirectory().getEntry("Pictures"); - DocumentInputStream is = getDirectory().createDocumentInputStream(entry); - byte[] pictstream = IOUtils.toByteArray(is, entry.getSize()); - is.close(); + EscherContainerRecord blipStore = getBlipStore(); + byte[] pictstream; + try (DocumentInputStream is = getDirectory().createDocumentInputStream(entry)) { + pictstream = IOUtils.toByteArray(is, entry.getSize()); + } + List factories = new ArrayList<>(); try (HSLFSlideShowEncrypted decryptData = new HSLFSlideShowEncrypted(getDocumentEncryptionAtom())) { int pos = 0; // An empty picture record (length 0) will take up 8 bytes - while (pos <= (pictstream.length - 8)) { + while (pos <= (pictstream.length - HSLFPictureData.PREAMBLE_SIZE)) { int offset = pos; decryptData.decryptPicture(pictstream, offset); @@ -388,7 +404,7 @@ public final class HSLFSlideShowImpl extends POIDocument implements Closeable { // (0 is allowed, but odd, since we do wind on by the header each // time, so we won't get stuck) if (imgsize < 0) { - throw new CorruptPowerPointFileException("The file contains a picture, at position " + _pictures.size() + ", which has a negatively sized data length, so we can't trust any of the picture data"); + throw new CorruptPowerPointFileException("The file contains a picture, at position " + factories.size() + ", which has a negatively sized data length, so we can't trust any of the picture data"); } // If the type (including the bonus 0xF018) is 0, skip it @@ -404,26 +420,127 @@ public final class HSLFSlideShowImpl extends POIDocument implements Closeable { "in others, this could indicate a corrupt file"); break; } - // Build the PictureData object from the data - try { - HSLFPictureData pict = HSLFPictureData.create(pt); - pict.setSignature(signature); - // Copy the data, ready to pass to PictureData - byte[] imgdata = IOUtils.safelyClone(pictstream, pos, imgsize, MAX_RECORD_LENGTH); - pict.setRawData(imgdata); + // Copy the data, ready to pass to PictureData + byte[] imgdata = IOUtils.safelyClone(pictstream, pos, imgsize, MAX_RECORD_LENGTH); - pict.setOffset(offset); - pict.setIndex(_pictures.size() + 1); // index is 1-based - _pictures.add(pict); - } catch (IllegalArgumentException e) { - LOG.atError().withThrowable(e).log("Problem reading picture. Your document will probably become corrupted if you save it!"); - } + factories.add(new PictureFactory(blipStore, pt, imgdata, offset, signature)); } pos += imgsize; } } + + matchPicturesAndRecords(factories, blipStore); + + List pictures = new ArrayList<>(); + for (PictureFactory it : factories) { + try { + HSLFPictureData pict = it.build(); + + pict.setIndex(pictures.size() + 1); // index is 1-based + pictures.add(pict); + } catch (IllegalArgumentException e) { + LOG.atError().withThrowable(e).log("Problem reading picture. Your document will probably become corrupted if you save it!"); + } + } + + _pictures = pictures; + } + + /** + * Matches all of the {@link PictureFactory PictureFactories} for a slideshow with {@link EscherBSERecord}s in the + * Blip Store for the slideshow. + *

+ * When reading a slideshow into memory, we have to match the records in the Blip Store with the factories + * representing picture in the pictures stream. This can be difficult, as presentations might have incorrectly + * formatted data. This function attempts to perform matching using multiple heuristics to increase the likelihood + * of finding all pairs, while aiming to reduce the likelihood of associating incorrect pairs. + * + * @param factories Factories for creating {@link HSLFPictureData} out of the pictures stream. + * @param blipStore Blip Store of the presentation being loaded. + */ + private static void matchPicturesAndRecords(List factories, EscherContainerRecord blipStore) { + // LinkedList because we're sorting and removing. + LinkedList unmatchedFactories = new LinkedList<>(factories); + unmatchedFactories.sort(Comparator.comparingInt(PictureFactory::getOffset)); + + // Arrange records by offset. In the common case of a well-formed slideshow, where every factory has a + // matching record, this is somewhat wasteful, but is necessary to handle the uncommon case where multiple + // records share an offset. + Map> unmatchedRecords = new HashMap<>(); + for (EscherRecord child : blipStore) { + EscherBSERecord record = (EscherBSERecord) child; + unmatchedRecords.computeIfAbsent(record.getOffset(), k -> new ArrayList<>()).add(record); + } + + // The first pass through the factories only pairs a factory with a record if we're very confident that they + // are a match. Confidence comes from a perfect match on the offset, and if necessary, the UID. Matched + // factories and records are removed from the unmatched collections. + for (Iterator iterator = unmatchedFactories.iterator(); iterator.hasNext(); ) { + PictureFactory factory = iterator.next(); + int physicalOffset = factory.getOffset(); + List recordsAtOffset = unmatchedRecords.get(physicalOffset); + + if (recordsAtOffset == null || recordsAtOffset.isEmpty()) { + // There are no records that have an offset matching the physical offset in the stream. We'll do + // more complicated and less reliable matching for this factory after all "well known" + // image <-> record pairs have been found. + LOG.atDebug().log("No records with offset {}", box(physicalOffset)); + } else if (recordsAtOffset.size() == 1) { + // Only 1 record has the same offset as the target image. Assume these are a pair. + factory.setRecord(recordsAtOffset.get(0)); + unmatchedRecords.remove(physicalOffset); + iterator.remove(); + } else { + + // Multiple records share an offset. Perform additional matching based on UID. + for (int i = 0; i < recordsAtOffset.size(); i++) { + EscherBSERecord record = recordsAtOffset.get(i); + byte[] recordUid = record.getUid(); + byte[] imageHeader = Arrays.copyOf(factory.imageData, HSLFPictureData.CHECKSUM_SIZE); + if (Arrays.equals(recordUid, imageHeader)) { + factory.setRecord(record); + recordsAtOffset.remove(i); + iterator.remove(); + break; + } + } + } + } + + // At this point, any factories remaining didn't have a record with a matching offset. The second pass + // through the factories pairs based on the UID. Factories for which a record with a matching UID cannot be + // found will get a new record. + List remainingRecords = unmatchedRecords.values() + .stream() + .flatMap(Collection::stream) + .collect(Collectors.toList()); + + for (PictureFactory factory : unmatchedFactories) { + + boolean matched = false; + for (int i = remainingRecords.size() - 1; i >= 0; i--) { + EscherBSERecord record = remainingRecords.get(i); + byte[] recordUid = record.getUid(); + byte[] imageHeader = Arrays.copyOf(factory.imageData, HSLFPictureData.CHECKSUM_SIZE); + if (Arrays.equals(recordUid, imageHeader)) { + remainingRecords.remove(i); + factory.setRecord(record); + record.setOffset(factory.getOffset()); + matched = true; + } + } + + if (!matched) { + // Synthesize a new record + LOG.atDebug().log("No record found for picture at offset {}", box(factory.offset)); + EscherBSERecord record = HSLFSlideShow.addNewEscherBseRecord(blipStore, factory.type, factory.imageData, factory.offset); + factory.setRecord(record); + } + } + + LOG.atDebug().log("Found {} unmatched records.", box(remainingRecords.size())); } /** @@ -756,9 +873,8 @@ public final class HSLFSlideShowImpl extends POIDocument implements Closeable { int offset = 0; if (_pictures.size() > 0) { HSLFPictureData prev = _pictures.get(_pictures.size() - 1); - offset = prev.getOffset() + prev.getRawData().length + 8; + offset = prev.getOffset() + prev.getBseSize(); } - img.setOffset(offset); img.setIndex(_pictures.size() + 1); // index is 1-based _pictures.add(img); return offset; @@ -825,6 +941,32 @@ public final class HSLFSlideShowImpl extends POIDocument implements Closeable { return _objects; } + private EscherContainerRecord getBlipStore() { + Document documentRecord = null; + for (Record record : _records) { + if (record.getRecordType() == RecordTypes.Document.typeID) { + documentRecord = (Document) record; + break; + } + } + + if (documentRecord == null) { + throw new CorruptPowerPointFileException("Document record is missing"); + } + + EscherContainerRecord blipStore; + + EscherContainerRecord dggContainer = documentRecord.getPPDrawingGroup().getDggContainer(); + blipStore = HSLFShape.getEscherChild(dggContainer, EscherContainerRecord.BSTORE_CONTAINER); + if (blipStore == null) { + blipStore = new EscherContainerRecord(); + blipStore.setRecordId(EscherContainerRecord.BSTORE_CONTAINER); + + dggContainer.addChildBefore(blipStore, EscherOptRecord.RECORD_ID); + } + return blipStore; + } + @Override public void close() throws IOException { // only close the filesystem, if we are based on the root node. @@ -903,4 +1045,55 @@ public final class HSLFSlideShowImpl extends POIDocument implements Closeable { return count; } } + + /** + * Assists in creating {@link HSLFPictureData} when parsing a slideshow. + * + * This class is relied upon heavily by {@link #matchPicturesAndRecords(List, EscherContainerRecord)}. + */ + static final class PictureFactory { + final byte[] imageData; + + private final EscherContainerRecord recordContainer; + private final PictureData.PictureType type; + private final int offset; + private final int signature; + private EscherBSERecord record; + + PictureFactory( + EscherContainerRecord recordContainer, + PictureData.PictureType type, + byte[] imageData, + int offset, + int signature + ) { + this.recordContainer = Objects.requireNonNull(recordContainer); + this.type = Objects.requireNonNull(type); + this.imageData = Objects.requireNonNull(imageData); + this.offset = offset; + this.signature = signature; + } + + int getOffset() { + return offset; + } + + /** + * Constructs a new {@link HSLFPictureData}. + *

+ * The {@link EscherBSERecord} must have been set via {@link #setRecord(EscherBSERecord)} prior to invocation. + */ + HSLFPictureData build() { + Objects.requireNonNull(record, "Can't build an instance until the record has been assigned."); + return HSLFPictureData.createFromSlideshowData(type, recordContainer, record, imageData, signature); + } + + /** + * Sets the {@link EscherBSERecord} with which this factory should create a {@link HSLFPictureData}. + */ + PictureFactory setRecord(EscherBSERecord bse) { + record = bse; + return this; + } + } } diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/dev/BaseTestPPTIterating.java b/src/scratchpad/testcases/org/apache/poi/hslf/dev/BaseTestPPTIterating.java index 869b96e4b8..6722f00f41 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/dev/BaseTestPPTIterating.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/dev/BaseTestPPTIterating.java @@ -55,6 +55,7 @@ public abstract class BaseTestPPTIterating { ENCRYPTED_FILES.add("Password_Protected-np-hello.ppt"); ENCRYPTED_FILES.add("Password_Protected-56-hello.ppt"); ENCRYPTED_FILES.add("Password_Protected-hello.ppt"); + ENCRYPTED_FILES.add("ppt_with_png_encrypted.ppt"); } protected static final Map> EXCLUDED = diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestPicture.java b/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestPicture.java index e514f78835..55a50be321 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestPicture.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestPicture.java @@ -35,6 +35,7 @@ import javax.imageio.ImageIO; import org.apache.poi.POIDataSamples; import org.apache.poi.ddf.EscherBSERecord; +import org.apache.poi.ddf.EscherContainerRecord; import org.apache.poi.sl.usermodel.PictureData.PictureType; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -87,16 +88,19 @@ public final class TestPicture { } /** - * Picture#getEscherBSERecord threw NullPointerException if EscherContainerRecord.BSTORE_CONTAINER - * was not found. The correct behaviour is to return null. + * {@link HSLFPictureShape#getEscherBSERecord()} threw {@link NullPointerException} if + * {@link EscherContainerRecord#BSTORE_CONTAINER} was not found. The correct behaviour is to return null. */ @Test void bug46122() throws IOException { + HSLFPictureData detachedData; + try (HSLFSlideShow ppt = new HSLFSlideShow()) { + detachedData = ppt.addPicture(new byte[0], PictureType.PNG); + } try (HSLFSlideShow ppt = new HSLFSlideShow()) { HSLFSlide slide = ppt.createSlide(); - HSLFPictureData pd = HSLFPictureData.create(PictureType.PNG); - HSLFPictureShape pict = new HSLFPictureShape(pd); //index to non-existing picture data + HSLFPictureShape pict = new HSLFPictureShape(detachedData); //index to non-existing picture data pict.setAnchor(new Rectangle2D.Double(50, 50, 100, 100)); pict.setSheet(slide); HSLFPictureData data = pict.getPictureData(); diff --git a/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestPictures.java b/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestPictures.java index fce2e1804d..40076c758f 100644 --- a/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestPictures.java +++ b/src/scratchpad/testcases/org/apache/poi/hslf/usermodel/TestPictures.java @@ -27,9 +27,14 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URL; import java.util.Arrays; +import java.util.Collections; import java.util.List; +import java.util.Random; import org.apache.poi.POIDataSamples; +import org.apache.poi.ddf.EscherBSERecord; +import org.apache.poi.ddf.EscherContainerRecord; +import org.apache.poi.ddf.EscherRecord; import org.apache.poi.hslf.HSLFTestDataSamples; import org.apache.poi.hslf.blip.DIB; import org.apache.poi.hslf.blip.EMF; @@ -37,6 +42,7 @@ import org.apache.poi.hslf.blip.JPEG; import org.apache.poi.hslf.blip.PICT; import org.apache.poi.hslf.blip.PNG; import org.apache.poi.hslf.blip.WMF; +import org.apache.poi.hssf.record.crypto.Biff8EncryptionKey; import org.apache.poi.sl.image.ImageHeaderEMF; import org.apache.poi.sl.image.ImageHeaderPICT; import org.apache.poi.sl.image.ImageHeaderWMF; @@ -512,9 +518,8 @@ public final class TestPictures { int streamSize = out.size(); - HSLFPictureData data = HSLFPictureData.create(PictureType.JPEG); - data.setData(new byte[100]); - int offset = hslf.addPicture(data); + HSLFPictureData data = ppt.addPicture(new byte[100], PictureType.JPEG); + int offset = data.getOffset(); assertEquals(streamSize, offset); assertEquals(3, ppt.getPictureData().size()); @@ -560,4 +565,172 @@ public final class TestPictures { assertEquals(1, picture.getIndex()); } } + + /** + * Verify that it is possible for a user to change the contents of a {@link HSLFPictureData} using + * {@link HSLFPictureData#setData(byte[])}, and that the changes are saved to the slideshow. + */ + @Test + void testEditPictureData() throws IOException { + byte[] newImage = slTests.readFile("tomcat.png"); + ByteArrayOutputStream modifiedSlideShow = new ByteArrayOutputStream(); + + // Load an existing slideshow and modify the image + try (HSLFSlideShow ppt = HSLFTestDataSamples.getSlideShow("ppt_with_png.ppt")) { + HSLFPictureData picture = ppt.getPictureData().get(0); + picture.setData(newImage); + ppt.write(modifiedSlideShow); + } + + // Load the modified slideshow and verify the image content + try (HSLFSlideShow ppt = new HSLFSlideShow(new ByteArrayInputStream(modifiedSlideShow.toByteArray()))) { + HSLFPictureData picture = ppt.getPictureData().get(0); + byte[] modifiedImageData = picture.getData(); + assertArrayEquals(newImage, modifiedImageData); + } + } + + /** + * Verify that it is possible for a user to change the contents of an encrypted {@link HSLFPictureData} using + * {@link HSLFPictureData#setData(byte[])}, and that the changes are saved to the slideshow. + */ + @Test + void testEditPictureDataEncrypted() throws IOException { + byte[] newImage = slTests.readFile("tomcat.png"); + ByteArrayOutputStream modifiedSlideShow = new ByteArrayOutputStream(); + + Biff8EncryptionKey.setCurrentUserPassword("password"); + try { + // Load an existing slideshow and modify the image + try (HSLFSlideShow ppt = HSLFTestDataSamples.getSlideShow("ppt_with_png_encrypted.ppt")) { + HSLFPictureData picture = ppt.getPictureData().get(0); + picture.setData(newImage); + ppt.write(modifiedSlideShow); + } + + // Load the modified slideshow and verify the image content + try (HSLFSlideShow ppt = new HSLFSlideShow(new ByteArrayInputStream(modifiedSlideShow.toByteArray()))) { + HSLFPictureData picture = ppt.getPictureData().get(0); + byte[] modifiedImageData = picture.getData(); + assertArrayEquals(newImage, modifiedImageData); + } + } finally { + Biff8EncryptionKey.setCurrentUserPassword(null); + } + } + + /** + * Verify that the {@link EscherBSERecord#getOffset()} values are modified for all images after the image being + * changed. + */ + @Test + void testEditPictureDataRecordOffsetsAreShifted() throws IOException { + int[] originalOffsets = {0, 12013, 15081, 34162, 59563}; + int[] modifiedOffsets = {0, 35, 3103, 22184, 47585}; + + ByteArrayOutputStream inMemory = new ByteArrayOutputStream(); + try (HSLFSlideShow ppt = HSLFTestDataSamples.getSlideShow("pictures.ppt")) { + int[] offsets = ppt.getPictureData().stream().mapToInt(HSLFPictureData::getOffset).toArray(); + assertArrayEquals(originalOffsets, offsets); + + HSLFPictureData imageBeingChanged = ppt.getPictureData().get(0); + // It doesn't matter that this isn't a valid image. We are just testing offsets here. + imageBeingChanged.setData(new byte[10]); + + // Verify that the in-memory representations have all been updated + offsets = ppt.getPictureData().stream().mapToInt(HSLFPictureData::getOffset).toArray(); + assertArrayEquals(modifiedOffsets, offsets); + + ppt.write(inMemory); + } + + try (HSLFSlideShow ppt = new HSLFSlideShow(new ByteArrayInputStream(inMemory.toByteArray()))) { + + // Verify that the persisted representations have all been updated + int[] offsets = ppt.getPictureData().stream().mapToInt(HSLFPictureData::getOffset).toArray(); + assertArrayEquals(modifiedOffsets, offsets); + } + } + + /** + * Verify that the {@link EscherBSERecord#getOffset()} values are modified for all images after the image being + * changed, but assuming that the records are not stored in a sorted-by-offset fashion. + * + * We have not encountered a file that has meaningful data that is not sorted. However, we have encountered files + * that have records with an offset of 0 interspersed between meaningful records. See {@code 53446.ppt} and + * {@code alterman_security.ppt} for examples. + */ + @Test + void testEditPictureDataOutOfOrderRecords() throws IOException { + int[] modifiedOffsets = {0, 35, 3103, 22184, 47585}; + + ByteArrayOutputStream inMemory = new ByteArrayOutputStream(); + try (HSLFSlideShow ppt = HSLFTestDataSamples.getSlideShow("pictures.ppt")) { + + // For this test we're going to intentionally manipulate the records into a shuffled order. + EscherContainerRecord container = ppt.getPictureData().get(0).bStore; + List children = container.getChildRecords(); + for (EscherRecord child : children) { + container.removeChildRecord(child); + } + Collections.shuffle(children); + for (EscherRecord child : children) { + container.addChildRecord(child); + } + + HSLFPictureData imageBeingChanged = ppt.getPictureData().get(0); + // It doesn't matter that this isn't a valid image. We are just testing offsets here. + imageBeingChanged.setData(new byte[10]); + + // Verify that the in-memory representations have all been updated + int[] offsets = ppt.getPictureData().stream().mapToInt(HSLFPictureData::getOffset).toArray(); + Arrays.sort(offsets); + assertArrayEquals(modifiedOffsets, offsets); + + ppt.write(inMemory); + } + + try (HSLFSlideShow ppt = new HSLFSlideShow(new ByteArrayInputStream(inMemory.toByteArray()))) { + + // Verify that the persisted representations have all been updated + int[] offsets = ppt.getPictureData().stream().mapToInt(HSLFPictureData::getOffset).toArray(); + Arrays.sort(offsets); + assertArrayEquals(modifiedOffsets, offsets); + } + } + + /** + * Verify that a slideshow with records that have offsets not matching those of the pictures in the stream still + * correctly pairs the records and pictures. + */ + @Test + void testSlideshowWithIncorrectOffsets() throws IOException { + int[] originalOffsets; + int originalNumberOfRecords; + + // Create a presentation that has records with unmatched offsets, but with matched UIDs. + ByteArrayOutputStream inMemory = new ByteArrayOutputStream(); + try (HSLFSlideShow ppt = HSLFTestDataSamples.getSlideShow("pictures.ppt")) { + originalOffsets = ppt.getPictureData().stream().mapToInt(HSLFPictureData::getOffset).toArray(); + originalNumberOfRecords = ppt.getPictureData().get(0).bStore.getChildRecords().size(); + + Random random = new Random(); + for (HSLFPictureData picture : ppt.getPictureData()) { + // Bound is arbitrary and irrelevant to the test. + picture.bse.setOffset(random.nextInt(500_000)); + } + ppt.write(inMemory); + } + + try (HSLFSlideShow ppt = new HSLFSlideShow(new ByteArrayInputStream(inMemory.toByteArray()))) { + + // Verify that the offsets all got fixed. + int[] offsets = ppt.getPictureData().stream().mapToInt(HSLFPictureData::getOffset).toArray(); + assertArrayEquals(originalOffsets, offsets); + + // Verify that there are the same number of records as in the original slideshow. + int numberOfRecords = ppt.getPictureData().get(0).bStore.getChildRecords().size(); + assertEquals(originalNumberOfRecords, numberOfRecords); + } + } } diff --git a/test-data/slideshow/ppt_with_png_encrypted.ppt b/test-data/slideshow/ppt_with_png_encrypted.ppt new file mode 100644 index 0000000000000000000000000000000000000000..4965d8623e9e8bec1d11fd2513023656076ef2b5 GIT binary patch literal 26112 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%0Dszh~|KUui$z>5s25WW9)i&@EEl$A?EBs%~U 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 SOW$YRe;OeE=kwp#0{;(sUC~?s delta 12017 zcmbVSYiw25m0ssQ>FePUltaPGVg~&&MiaxZp4Ou-otWMeUz%w7|@` z8jx|PE%c)~TPDa=6K^0A1o1QZA&u}boozQ%%-uk|7vV4xS_00X^< z)k~pIibiWvR0K_gQFd2=$tJF&j$l*-dFDufQ7ee71bQ(iz?kTb(RvuXl9cUn%n^)P zK_phmjgLOYodDw^SfmBx5+H8zv?Y3s;Z3)YL8V(59>v)qO5q|B9E?kjBbwug=17oO z1~4r6OAiZ1SkZVsLoe6S%XRc}9lhL?2$pHVTq}qnP*ko36OLfQ5llFO3DGmXPKciA zHD;Qk30iZ6X!_lcl{J>oL<=HmlemTcVgE??4|hjE#k^MjE02|DFflX%6FE&YF@+UP z*7|wEye-V6#lW1xE%?l{Oe|9Y6N`iLTONl`!yF|(4Re%b;ucUemCqQBd|^H6%$+nYfb__9f}(7-4?w_#7k5nVLDqGO>>;s?p4`!YofA+F2Guw6isHtYzY6 zQ#40g6$$gOFq0Mob1t{g&mzmjjTtbDh54?-ES6{uv)D2*ofXw;eXP;S&l^Ovbryrq zdEA0etiS2mhp8Vhl`wY*GpU(MeAa8GvP>)v0kcGyH-$N0GfRZoq?sj_iPb^T0&P_) z%&&ymY%xT;P%}#{6YGbf7Hu_Nn5)l~Hj{fGAp|nPp&dP4KUOJFdA* zmWRdm3Ou)Q3wbKDd@M8pzg#@uK8Fk&X1RDa%yP@ba-wL7)}J8EmM1cq6NI@``cQ8Ta=jVSXmeH22+Frvh&Uz{7>l6`O!xu4rQ>-A?x2dG5JMUWRyYAxI005TdR)*mkM&a0;|eQ?byU%M9bu)D!b&H= zN=L9#1UKjaE3F>Z+CZ<$39yRwQUzqjxXKZ%as;caAeL)I8+CxwodBmhJ)SOlX6jBC zJu`Ku8@-gQ^MT+DC&C$y;0zHoEBOo&yifOdh7lx8F-4nnfYnZb)lLShSuYjbBGn?e zSqoNML2NUD;7lh#JP-sKoatn6rlU90>S23Qv?U#2igznn)zVFFLD1YLHI8772ySId z+$A+u5E-$8ywhdg#kbp%_T9=D1h9#$}CTAd8Girz`B z*J|}pb_M}1CJ+6`tpkb0PK1j^&{XA%MbK2`i>+Xr2%gdkZ4%&V+sa&7+eFW7$8Dmw zf;fEKCT${^h!j%0|IU*|L+7769w|^{blaip=lPjWQthH(_TP3J5o%^dXLJlpgt5>*2(#tI%xG5#(>yV*u*{VXbEPn!7N+^`V5Kl0XFH6_O3UOToqc{o z`|J|tXO7P<$7h#i;sGIGt`cU=ONiF^T;=#&Wtk{R74A*x=W1b|6{eX(s~w-KEwkHU zb_?@k$7i?Wv)eN9^r2`_`&=W;@|QDnyT(+#Ml-hxv*p{F%&o%wYt7thnJ8%$J*%1b3-f7Vni+Y& zFrU-R`z;e&xuWMabDJls&?HMO*B#%sl~fEKBZ@4Gh#ha5_|Q`4g`- zhlZI))b14!wlC}r*%;R7Yz*spjz8r}(On8yU9+vRDrRF?5p(9f)F3-e9>r>&%_-%|U9dP7tD?a23ADynlu-{mt>en2|XdbOH6a?@hCA-}IR4p=5C zc|||a_(ALQp!ocu#Z9;eEfclAqPMigA4g!yC5JSHCMu9bAAyMN5up`=&z6K!|g zdN?i~{@&tdY>tEJKXa`zuS9zCptRq+R;}A7r2WOUCjKhaY<4GQ0lTHOPKrh6^?J6b zVhpqClvsSAEl!EWi(+94w9{f?%E{AW@wuaM#?d$<7LBjh7X<$}c?cH%L$5bwU=IuS zo`XFiSoDodyGI4Gk=bSHFAj z9-{ZuhpzA4yNB34dHCZ0;URuc9xmOy_dev@lZPeWzjqIqWq#+xR+;u2AdE<#?wf4fLM65vB?@6`k!~6ilwmFCqt~J zgkVz{GvS#?riEY?Ay{RI9&eqgyTY3uVmKoNs}8|thF~=z*er|jD4U+lwqStG3Bl$v zmYs##5WBh%Y+g2&;`G;NgN!wVV2vRdYK!avMum}$EeOGy8M|whSr}s25`rxX!CFJG z#UWT*fDN_%WRrJ>V7o%F-67bX02_M${clEBVkPVeK%4&F5NvMt zn8yH@8LbokTw&o~{pGKxz!E;R7*Xio-u<60n1OV5cAC1H1cqKCq)-