882 lines
33 KiB
Java
882 lines
33 KiB
Java
/* ====================================================================
|
|
Licensed to the Apache Software Foundation (ASF) under one or more
|
|
contributor license agreements. See the NOTICE file distributed with
|
|
this work for additional information regarding copyright ownership.
|
|
The ASF licenses this file to You under the Apache License, Version 2.0
|
|
(the "License"); you may not use this file except in compliance with
|
|
the License. You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
==================================================================== */
|
|
|
|
package org.apache.poi.hslf.usermodel;
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.Closeable;
|
|
import java.io.File;
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.security.GeneralSecurityException;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.NavigableMap;
|
|
import java.util.TreeMap;
|
|
|
|
import org.apache.poi.POIDocument;
|
|
import org.apache.poi.hpsf.PropertySet;
|
|
import org.apache.poi.hslf.exceptions.CorruptPowerPointFileException;
|
|
import org.apache.poi.hslf.exceptions.HSLFException;
|
|
import org.apache.poi.hslf.record.CurrentUserAtom;
|
|
import org.apache.poi.hslf.record.DocumentEncryptionAtom;
|
|
import org.apache.poi.hslf.record.ExOleObjStg;
|
|
import org.apache.poi.hslf.record.PersistPtrHolder;
|
|
import org.apache.poi.hslf.record.PersistRecord;
|
|
import org.apache.poi.hslf.record.PositionDependentRecord;
|
|
import org.apache.poi.hslf.record.Record;
|
|
import org.apache.poi.hslf.record.RecordTypes;
|
|
import org.apache.poi.hslf.record.UserEditAtom;
|
|
import org.apache.poi.poifs.crypt.cryptoapi.CryptoAPIEncryptor;
|
|
import org.apache.poi.poifs.filesystem.DirectoryNode;
|
|
import org.apache.poi.poifs.filesystem.DocumentEntry;
|
|
import org.apache.poi.poifs.filesystem.DocumentInputStream;
|
|
import org.apache.poi.poifs.filesystem.EntryUtils;
|
|
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
|
|
import org.apache.poi.poifs.filesystem.POIFSFileSystem;
|
|
import org.apache.poi.sl.usermodel.PictureData.PictureType;
|
|
import org.apache.poi.util.IOUtils;
|
|
import org.apache.poi.util.LittleEndian;
|
|
import org.apache.poi.util.POILogFactory;
|
|
import org.apache.poi.util.POILogger;
|
|
|
|
/**
|
|
* This class contains the main functionality for the Powerpoint file
|
|
* "reader". It is only a very basic class for now
|
|
*/
|
|
public final class HSLFSlideShowImpl extends POIDocument implements Closeable {
|
|
public static final int UNSET_OFFSET = -1;
|
|
|
|
// For logging
|
|
private POILogger logger = POILogFactory.getLogger(this.getClass());
|
|
|
|
// Holds metadata on where things are in our document
|
|
private CurrentUserAtom currentUser;
|
|
|
|
// Low level contents of the file
|
|
private byte[] _docstream;
|
|
|
|
// Low level contents
|
|
private Record[] _records;
|
|
|
|
// Raw Pictures contained in the pictures stream
|
|
private List<HSLFPictureData> _pictures;
|
|
|
|
// Embedded objects stored in storage records in the document stream, lazily populated.
|
|
private HSLFObjectData[] _objects;
|
|
|
|
/**
|
|
* Returns the directory in the underlying POIFSFileSystem for the
|
|
* document that is open.
|
|
*/
|
|
protected DirectoryNode getPOIFSDirectory() {
|
|
return directory;
|
|
}
|
|
|
|
/**
|
|
* Constructs a Powerpoint document from fileName. Parses the document
|
|
* and places all the important stuff into data structures.
|
|
*
|
|
* @param fileName The name of the file to read.
|
|
* @throws IOException if there is a problem while parsing the document.
|
|
*/
|
|
@SuppressWarnings("resource")
|
|
public HSLFSlideShowImpl(String fileName) throws IOException {
|
|
this(new POIFSFileSystem(new File(fileName)));
|
|
}
|
|
|
|
/**
|
|
* Constructs a Powerpoint document from an input stream. Parses the
|
|
* document and places all the important stuff into data structures.
|
|
*
|
|
* @param inputStream the source of the data
|
|
* @throws IOException if there is a problem while parsing the document.
|
|
*/
|
|
@SuppressWarnings("resource")
|
|
public HSLFSlideShowImpl(InputStream inputStream) throws IOException {
|
|
//do Ole stuff
|
|
this(new POIFSFileSystem(inputStream));
|
|
}
|
|
|
|
/**
|
|
* Constructs a Powerpoint document from a POIFS Filesystem. Parses the
|
|
* document and places all the important stuff into data structures.
|
|
*
|
|
* @param filesystem the POIFS FileSystem to read from
|
|
* @throws IOException if there is a problem while parsing the document.
|
|
*/
|
|
public HSLFSlideShowImpl(POIFSFileSystem filesystem) throws IOException {
|
|
this(filesystem.getRoot());
|
|
}
|
|
|
|
/**
|
|
* Constructs a Powerpoint document from a POIFS Filesystem. Parses the
|
|
* document and places all the important stuff into data structures.
|
|
*
|
|
* @param filesystem the POIFS FileSystem to read from
|
|
* @throws IOException if there is a problem while parsing the document.
|
|
*/
|
|
public HSLFSlideShowImpl(NPOIFSFileSystem filesystem) throws IOException {
|
|
this(filesystem.getRoot());
|
|
}
|
|
|
|
/**
|
|
* Constructs a Powerpoint document from a specific point in a
|
|
* POIFS Filesystem. Parses the document and places all the
|
|
* important stuff into data structures.
|
|
*
|
|
* @param dir the POIFS directory to read from
|
|
* @throws IOException if there is a problem while parsing the document.
|
|
*/
|
|
public HSLFSlideShowImpl(DirectoryNode dir) throws IOException {
|
|
super(handleDualStorage(dir));
|
|
|
|
// First up, grab the "Current User" stream
|
|
// We need this before we can detect Encrypted Documents
|
|
readCurrentUserStream();
|
|
|
|
// Next up, grab the data that makes up the
|
|
// PowerPoint stream
|
|
readPowerPointStream();
|
|
|
|
// Now, build records based on the PowerPoint stream
|
|
buildRecords();
|
|
|
|
// Look for any other streams
|
|
readOtherStreams();
|
|
}
|
|
|
|
private static DirectoryNode handleDualStorage(DirectoryNode dir) throws IOException {
|
|
// when there's a dual storage entry, use it, as the outer document can't be read quite probably ...
|
|
String dualName = "PP97_DUALSTORAGE";
|
|
if (!dir.hasEntry(dualName)) return dir;
|
|
dir = (DirectoryNode)dir.getEntry(dualName);
|
|
return dir;
|
|
}
|
|
|
|
/**
|
|
* Constructs a new, empty, Powerpoint document.
|
|
*/
|
|
public static final HSLFSlideShowImpl create() {
|
|
InputStream is = HSLFSlideShowImpl.class.getResourceAsStream("/org/apache/poi/hslf/data/empty.ppt");
|
|
if (is == null) {
|
|
throw new HSLFException("Missing resource 'empty.ppt'");
|
|
}
|
|
try {
|
|
try {
|
|
return new HSLFSlideShowImpl(is);
|
|
} finally {
|
|
is.close();
|
|
}
|
|
} catch (IOException e) {
|
|
throw new HSLFException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts the main PowerPoint document stream from the
|
|
* POI file, ready to be passed
|
|
*
|
|
* @throws IOException
|
|
*/
|
|
private void readPowerPointStream() throws IOException
|
|
{
|
|
// Get the main document stream
|
|
DocumentEntry docProps =
|
|
(DocumentEntry)directory.getEntry("PowerPoint Document");
|
|
|
|
// Grab the document stream
|
|
int len = docProps.getSize();
|
|
InputStream is = directory.createDocumentInputStream("PowerPoint Document");
|
|
try {
|
|
_docstream = IOUtils.toByteArray(is, len);
|
|
} finally {
|
|
is.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds the list of records, based on the contents
|
|
* of the PowerPoint stream
|
|
*/
|
|
private void buildRecords()
|
|
{
|
|
// The format of records in a powerpoint file are:
|
|
// <little endian 2 byte "info">
|
|
// <little endian 2 byte "type">
|
|
// <little endian 4 byte "length">
|
|
// If it has a zero length, following it will be another record
|
|
// <xx xx yy yy 00 00 00 00> <xx xx yy yy zz zz zz zz>
|
|
// If it has a length, depending on its type it may have children or data
|
|
// If it has children, these will follow straight away
|
|
// <xx xx yy yy zz zz zz zz <xx xx yy yy zz zz zz zz>>
|
|
// If it has data, this will come straigh after, and run for the length
|
|
// <xx xx yy yy zz zz zz zz dd dd dd dd dd dd dd>
|
|
// All lengths given exclude the 8 byte record header
|
|
// (Data records are known as Atoms)
|
|
|
|
// Document should start with:
|
|
// 0F 00 E8 03 ## ## ## ##
|
|
// (type 1000 = document, info 00 0f is normal, rest is document length)
|
|
// 01 00 E9 03 28 00 00 00
|
|
// (type 1001 = document atom, info 00 01 normal, 28 bytes long)
|
|
// 80 16 00 00 E0 10 00 00 xx xx xx xx xx xx xx xx
|
|
// 05 00 00 00 0A 00 00 00 xx xx xx
|
|
// (the contents of the document atom, not sure what it means yet)
|
|
// (records then follow)
|
|
|
|
// When parsing a document, look to see if you know about that type
|
|
// of the current record. If you know it's a type that has children,
|
|
// process the record's data area looking for more records
|
|
// If you know about the type and it doesn't have children, either do
|
|
// something with the data (eg TextRun) or skip over it
|
|
// If you don't know about the type, play safe and skip over it (using
|
|
// its length to know where the next record will start)
|
|
//
|
|
|
|
_records = read(_docstream, (int)currentUser.getCurrentEditOffset());
|
|
}
|
|
|
|
private Record[] read(byte[] docstream, int usrOffset){
|
|
//sort found records by offset.
|
|
//(it is not necessary but SlideShow.findMostRecentCoreRecords() expects them sorted)
|
|
NavigableMap<Integer,Record> records = new TreeMap<Integer,Record>(); // offset -> record
|
|
Map<Integer,Integer> persistIds = new HashMap<Integer,Integer>(); // offset -> persistId
|
|
initRecordOffsets(docstream, usrOffset, records, persistIds);
|
|
HSLFSlideShowEncrypted decryptData = new HSLFSlideShowEncrypted(docstream, records);
|
|
|
|
for (Map.Entry<Integer,Record> entry : records.entrySet()) {
|
|
Integer offset = entry.getKey();
|
|
Record record = entry.getValue();
|
|
Integer persistId = persistIds.get(offset);
|
|
if (record == null) {
|
|
// all plain records have been already added,
|
|
// only new records need to be decrypted (tbd #35897)
|
|
decryptData.decryptRecord(docstream, persistId, offset);
|
|
record = Record.buildRecordAtOffset(docstream, offset);
|
|
entry.setValue(record);
|
|
}
|
|
|
|
if (record instanceof PersistRecord) {
|
|
((PersistRecord)record).setPersistId(persistId);
|
|
}
|
|
}
|
|
|
|
return records.values().toArray(new Record[records.size()]);
|
|
}
|
|
|
|
private void initRecordOffsets(byte[] docstream, int usrOffset, NavigableMap<Integer,Record> recordMap, Map<Integer,Integer> offset2id) {
|
|
while (usrOffset != 0){
|
|
UserEditAtom usr = (UserEditAtom) Record.buildRecordAtOffset(docstream, usrOffset);
|
|
recordMap.put(usrOffset, usr);
|
|
|
|
int psrOffset = usr.getPersistPointersOffset();
|
|
PersistPtrHolder ptr = (PersistPtrHolder)Record.buildRecordAtOffset(docstream, psrOffset);
|
|
recordMap.put(psrOffset, ptr);
|
|
|
|
for(Map.Entry<Integer,Integer> entry : ptr.getSlideLocationsLookup().entrySet()) {
|
|
Integer offset = entry.getValue();
|
|
Integer id = entry.getKey();
|
|
recordMap.put(offset, null); // reserve a slot for the record
|
|
offset2id.put(offset, id);
|
|
}
|
|
|
|
usrOffset = usr.getLastUserEditAtomOffset();
|
|
|
|
// check for corrupted user edit atom and try to repair it
|
|
// if the next user edit atom offset is already known, we would go into an endless loop
|
|
if (usrOffset > 0 && recordMap.containsKey(usrOffset)) {
|
|
// a user edit atom is usually located 36 byte before the smallest known record offset
|
|
usrOffset = recordMap.firstKey()-36;
|
|
// check that we really are located on a user edit atom
|
|
int ver_inst = LittleEndian.getUShort(docstream, usrOffset);
|
|
int type = LittleEndian.getUShort(docstream, usrOffset+2);
|
|
int len = LittleEndian.getInt(docstream, usrOffset+4);
|
|
if (ver_inst == 0 && type == 4085 && (len == 0x1C || len == 0x20)) {
|
|
logger.log(POILogger.WARN, "Repairing invalid user edit atom");
|
|
usr.setLastUserEditAtomOffset(usrOffset);
|
|
} else {
|
|
throw new CorruptPowerPointFileException("Powerpoint document contains invalid user edit atom");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public DocumentEncryptionAtom getDocumentEncryptionAtom() {
|
|
for (Record r : _records) {
|
|
if (r instanceof DocumentEncryptionAtom) {
|
|
return (DocumentEncryptionAtom)r;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Find the "Current User" stream, and load it
|
|
*/
|
|
private void readCurrentUserStream() {
|
|
try {
|
|
currentUser = new CurrentUserAtom(directory);
|
|
} catch(IOException ie) {
|
|
logger.log(POILogger.ERROR, "Error finding Current User Atom:\n" + ie);
|
|
currentUser = new CurrentUserAtom();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find any other streams from the filesystem, and load them
|
|
*/
|
|
private void readOtherStreams() {
|
|
// Currently, there aren't any
|
|
}
|
|
|
|
/**
|
|
* Find and read in pictures contained in this presentation.
|
|
* This is lazily called as and when we want to touch pictures.
|
|
*/
|
|
private void readPictures() throws IOException {
|
|
_pictures = new ArrayList<HSLFPictureData>();
|
|
|
|
// if the presentation doesn't contain pictures - will use a null set instead
|
|
if (!directory.hasEntry("Pictures")) return;
|
|
|
|
HSLFSlideShowEncrypted decryptData = new HSLFSlideShowEncrypted(getDocumentEncryptionAtom());
|
|
|
|
DocumentEntry entry = (DocumentEntry)directory.getEntry("Pictures");
|
|
DocumentInputStream is = directory.createDocumentInputStream(entry);
|
|
byte[] pictstream = IOUtils.toByteArray(is, entry.getSize());
|
|
is.close();
|
|
|
|
int pos = 0;
|
|
// An empty picture record (length 0) will take up 8 bytes
|
|
while (pos <= (pictstream.length-8)) {
|
|
int offset = pos;
|
|
|
|
decryptData.decryptPicture(pictstream, offset);
|
|
|
|
// Image signature
|
|
int signature = LittleEndian.getUShort(pictstream, pos);
|
|
pos += LittleEndian.SHORT_SIZE;
|
|
// Image type + 0xF018
|
|
int type = LittleEndian.getUShort(pictstream, pos);
|
|
pos += LittleEndian.SHORT_SIZE;
|
|
// Image size (excluding the 8 byte header)
|
|
int imgsize = LittleEndian.getInt(pictstream, pos);
|
|
pos += LittleEndian.INT_SIZE;
|
|
|
|
// When parsing the BStoreDelay stream, [MS-ODRAW] says that we
|
|
// should terminate if the type isn't 0xf007 or 0xf018->0xf117
|
|
if (!((type == 0xf007) || (type >= 0xf018 && type <= 0xf117)))
|
|
break;
|
|
|
|
// The image size must be 0 or greater
|
|
// (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");
|
|
}
|
|
|
|
// If they type (including the bonus 0xF018) is 0, skip it
|
|
PictureType pt = PictureType.forNativeID(type - 0xF018);
|
|
if (pt == null) {
|
|
logger.log(POILogger.ERROR, "Problem reading picture: Invalid image type 0, on picture with length " + imgsize + ".\nYou document will probably become corrupted if you save it!");
|
|
logger.log(POILogger.ERROR, "" + pos);
|
|
} else {
|
|
// 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 = new byte[imgsize];
|
|
System.arraycopy(pictstream, pos, imgdata, 0, imgdata.length);
|
|
pict.setRawData(imgdata);
|
|
|
|
pict.setOffset(offset);
|
|
pict.setIndex(_pictures.size());
|
|
_pictures.add(pict);
|
|
} catch(IllegalArgumentException e) {
|
|
logger.log(POILogger.ERROR, "Problem reading picture: " + e + "\nYou document will probably become corrupted if you save it!");
|
|
}
|
|
}
|
|
|
|
pos += imgsize;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* remove duplicated UserEditAtoms and merge PersistPtrHolder, i.e.
|
|
* remove document edit history
|
|
*/
|
|
public void normalizeRecords() {
|
|
try {
|
|
updateAndWriteDependantRecords(null, null);
|
|
} catch (IOException e) {
|
|
throw new CorruptPowerPointFileException(e);
|
|
}
|
|
_records = HSLFSlideShowEncrypted.normalizeRecords(_records);
|
|
}
|
|
|
|
|
|
/**
|
|
* This is a helper functions, which is needed for adding new position dependent records
|
|
* or finally write the slideshow to a file.
|
|
*
|
|
* @param os the stream to write to, if null only the references are updated
|
|
* @param interestingRecords a map of interesting records (PersistPtrHolder and UserEditAtom)
|
|
* referenced by their RecordType. Only the very last of each type will be saved to the map.
|
|
* May be null, if not needed.
|
|
* @throws IOException
|
|
*/
|
|
public void updateAndWriteDependantRecords(OutputStream os, Map<RecordTypes,PositionDependentRecord> interestingRecords)
|
|
throws IOException {
|
|
// For position dependent records, hold where they were and now are
|
|
// As we go along, update, and hand over, to any Position Dependent
|
|
// records we happen across
|
|
Map<Integer,Integer> oldToNewPositions = new HashMap<Integer,Integer>();
|
|
|
|
// First pass - figure out where all the position dependent
|
|
// records are going to end up, in the new scheme
|
|
// (Annoyingly, some powerpoint files have PersistPtrHolders
|
|
// that reference slides after the PersistPtrHolder)
|
|
UserEditAtom usr = null;
|
|
PersistPtrHolder ptr = null;
|
|
CountingOS cos = new CountingOS();
|
|
for (Record record : _records) {
|
|
// all top level records are position dependent
|
|
assert(record instanceof PositionDependentRecord);
|
|
PositionDependentRecord pdr = (PositionDependentRecord)record;
|
|
int oldPos = pdr.getLastOnDiskOffset();
|
|
int newPos = cos.size();
|
|
pdr.setLastOnDiskOffset(newPos);
|
|
if (oldPos != UNSET_OFFSET) {
|
|
// new records don't need a mapping, as they aren't in a relation yet
|
|
oldToNewPositions.put(oldPos,newPos);
|
|
}
|
|
|
|
// Grab interesting records as they come past
|
|
// this will only save the very last record of each type
|
|
RecordTypes saveme = null;
|
|
int recordType = (int)record.getRecordType();
|
|
if (recordType == RecordTypes.PersistPtrIncrementalBlock.typeID) {
|
|
saveme = RecordTypes.PersistPtrIncrementalBlock;
|
|
ptr = (PersistPtrHolder)pdr;
|
|
} else if (recordType == RecordTypes.UserEditAtom.typeID) {
|
|
saveme = RecordTypes.UserEditAtom;
|
|
usr = (UserEditAtom)pdr;
|
|
}
|
|
if (interestingRecords != null && saveme != null) {
|
|
interestingRecords.put(saveme,pdr);
|
|
}
|
|
|
|
// Dummy write out, so the position winds on properly
|
|
record.writeOut(cos);
|
|
}
|
|
cos.close();
|
|
|
|
if (usr == null || ptr == null) {
|
|
throw new HSLFException("UserEditAtom or PersistPtr can't be determined.");
|
|
}
|
|
|
|
Map<Integer,Integer> persistIds = new HashMap<Integer,Integer>();
|
|
for (Map.Entry<Integer,Integer> entry : ptr.getSlideLocationsLookup().entrySet()) {
|
|
persistIds.put(oldToNewPositions.get(entry.getValue()), entry.getKey());
|
|
}
|
|
|
|
HSLFSlideShowEncrypted encData = new HSLFSlideShowEncrypted(getDocumentEncryptionAtom());
|
|
|
|
for (Record record : _records) {
|
|
assert(record instanceof PositionDependentRecord);
|
|
// We've already figured out their new location, and
|
|
// told them that
|
|
// Tell them of the positions of the other records though
|
|
PositionDependentRecord pdr = (PositionDependentRecord)record;
|
|
Integer persistId = persistIds.get(pdr.getLastOnDiskOffset());
|
|
if (persistId == null) persistId = 0;
|
|
|
|
// For now, we're only handling PositionDependentRecord's that
|
|
// happen at the top level.
|
|
// In future, we'll need the handle them everywhere, but that's
|
|
// a bit trickier
|
|
pdr.updateOtherRecordReferences(oldToNewPositions);
|
|
|
|
// Whatever happens, write out that record tree
|
|
if (os != null) {
|
|
record.writeOut(encData.encryptRecord(os, persistId, record));
|
|
}
|
|
}
|
|
|
|
encData.close();
|
|
|
|
// Update and write out the Current User atom
|
|
int oldLastUserEditAtomPos = (int)currentUser.getCurrentEditOffset();
|
|
Integer newLastUserEditAtomPos = oldToNewPositions.get(oldLastUserEditAtomPos);
|
|
if (newLastUserEditAtomPos == null || usr.getLastOnDiskOffset() != newLastUserEditAtomPos) {
|
|
throw new HSLFException("Couldn't find the new location of the last UserEditAtom that used to be at " + oldLastUserEditAtomPos);
|
|
}
|
|
currentUser.setCurrentEditOffset(usr.getLastOnDiskOffset());
|
|
}
|
|
|
|
/**
|
|
* Writes out the slideshow to the currently open file.
|
|
*
|
|
* <p>This will fail (with an {@link IllegalStateException} if the
|
|
* slideshow was opened read-only, opened from an {@link InputStream}
|
|
* instead of a File, or if this is not the root document. For those cases,
|
|
* you must use {@link #write(OutputStream)} or {@link #write(File)} to
|
|
* write to a brand new document.
|
|
*
|
|
* @since POI 3.15 beta 3
|
|
*
|
|
* @throws IOException thrown on errors writing to the file
|
|
* @throws IllegalStateException if this isn't from a writable File
|
|
*/
|
|
@Override
|
|
public void write() throws IOException {
|
|
validateInPlaceWritePossible();
|
|
|
|
// Write the PowerPoint streams to the current FileSystem
|
|
// No need to do anything to other streams, already there!
|
|
write(directory.getFileSystem(), false);
|
|
|
|
// Sync with the File on disk
|
|
directory.getFileSystem().writeFilesystem();
|
|
}
|
|
|
|
/**
|
|
* Writes out the slideshow file the is represented by an instance
|
|
* of this class.
|
|
* <p>This will write out only the common OLE2 streams. If you require all
|
|
* streams to be written out, use {@link #write(File, boolean)}
|
|
* with <code>preserveNodes</code> set to <code>true</code>.
|
|
* @param newFile The File to write to.
|
|
* @throws IOException If there is an unexpected IOException from writing to the File
|
|
*/
|
|
@Override
|
|
public void write(File newFile) throws IOException {
|
|
// Write out, but only the common streams
|
|
write(newFile, false);
|
|
}
|
|
/**
|
|
* Writes out the slideshow file the is represented by an instance
|
|
* of this class.
|
|
* If you require all streams to be written out (eg Marcos, embeded
|
|
* documents), then set <code>preserveNodes</code> set to <code>true</code>
|
|
* @param newFile The File to write to.
|
|
* @param preserveNodes Should all OLE2 streams be written back out, or only the common ones?
|
|
* @throws IOException If there is an unexpected IOException from writing to the File
|
|
*/
|
|
public void write(File newFile, boolean preserveNodes) throws IOException {
|
|
// Get a new FileSystem to write into
|
|
POIFSFileSystem outFS = POIFSFileSystem.create(newFile);
|
|
|
|
try {
|
|
// Write into the new FileSystem
|
|
write(outFS, preserveNodes);
|
|
|
|
// Send the POIFSFileSystem object out to the underlying stream
|
|
outFS.writeFilesystem();
|
|
} finally {
|
|
outFS.close();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes out the slideshow file the is represented by an instance
|
|
* of this class.
|
|
* <p>This will write out only the common OLE2 streams. If you require all
|
|
* streams to be written out, use {@link #write(OutputStream, boolean)}
|
|
* with <code>preserveNodes</code> set to <code>true</code>.
|
|
* @param out The OutputStream to write to.
|
|
* @throws IOException If there is an unexpected IOException from
|
|
* the passed in OutputStream
|
|
*/
|
|
@Override
|
|
public void write(OutputStream out) throws IOException {
|
|
// Write out, but only the common streams
|
|
write(out, false);
|
|
}
|
|
/**
|
|
* Writes out the slideshow file the is represented by an instance
|
|
* of this class.
|
|
* If you require all streams to be written out (eg Marcos, embeded
|
|
* documents), then set <code>preserveNodes</code> set to <code>true</code>
|
|
* @param out The OutputStream to write to.
|
|
* @param preserveNodes Should all OLE2 streams be written back out, or only the common ones?
|
|
* @throws IOException If there is an unexpected IOException from
|
|
* the passed in OutputStream
|
|
*/
|
|
public void write(OutputStream out, boolean preserveNodes) throws IOException {
|
|
// Get a new FileSystem to write into
|
|
POIFSFileSystem outFS = new POIFSFileSystem();
|
|
|
|
try {
|
|
// Write into the new FileSystem
|
|
write(outFS, preserveNodes);
|
|
|
|
// Send the POIFSFileSystem object out to the underlying stream
|
|
outFS.writeFilesystem(out);
|
|
} finally {
|
|
outFS.close();
|
|
}
|
|
}
|
|
private void write(NPOIFSFileSystem outFS, boolean copyAllOtherNodes) throws IOException {
|
|
// read properties and pictures, with old encryption settings where appropriate
|
|
if (_pictures == null) {
|
|
readPictures();
|
|
}
|
|
getDocumentSummaryInformation();
|
|
|
|
// set new encryption settings
|
|
HSLFSlideShowEncrypted encryptedSS = new HSLFSlideShowEncrypted(getDocumentEncryptionAtom());
|
|
_records = encryptedSS.updateEncryptionRecord(_records);
|
|
|
|
// The list of entries we've written out
|
|
List<String> writtenEntries = new ArrayList<String>(1);
|
|
|
|
// Write out the Property Streams
|
|
writeProperties(outFS, writtenEntries);
|
|
|
|
BufAccessBAOS baos = new BufAccessBAOS();
|
|
|
|
// For position dependent records, hold where they were and now are
|
|
// As we go along, update, and hand over, to any Position Dependent
|
|
// records we happen across
|
|
updateAndWriteDependantRecords(baos, null);
|
|
|
|
// Update our cached copy of the bytes that make up the PPT stream
|
|
_docstream = new byte[baos.size()];
|
|
System.arraycopy(baos.getBuf(), 0, _docstream, 0, baos.size());
|
|
baos.close();
|
|
|
|
// Write the PPT stream into the POIFS layer
|
|
ByteArrayInputStream bais = new ByteArrayInputStream(_docstream);
|
|
outFS.createOrUpdateDocument(bais,"PowerPoint Document");
|
|
writtenEntries.add("PowerPoint Document");
|
|
|
|
currentUser.setEncrypted(encryptedSS.getDocumentEncryptionAtom() != null);
|
|
currentUser.writeToFS(outFS);
|
|
writtenEntries.add("Current User");
|
|
|
|
|
|
if (_pictures.size() > 0) {
|
|
BufAccessBAOS pict = new BufAccessBAOS();
|
|
for (HSLFPictureData p : _pictures) {
|
|
int offset = pict.size();
|
|
p.write(pict);
|
|
encryptedSS.encryptPicture(pict.getBuf(), offset);
|
|
}
|
|
outFS.createOrUpdateDocument(
|
|
new ByteArrayInputStream(pict.getBuf(), 0, pict.size()), "Pictures"
|
|
);
|
|
writtenEntries.add("Pictures");
|
|
pict.close();
|
|
}
|
|
|
|
// If requested, copy over any other streams we spot, eg Macros
|
|
if (copyAllOtherNodes) {
|
|
EntryUtils.copyNodes(directory.getFileSystem(), outFS, writtenEntries);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For a given named property entry, either return it or null if
|
|
* if it wasn't found
|
|
*
|
|
* @param setName The property to read
|
|
* @return The value of the given property or null if it wasn't found.
|
|
*/
|
|
protected PropertySet getPropertySet(String setName) {
|
|
DocumentEncryptionAtom dea = getDocumentEncryptionAtom();
|
|
return (dea == null)
|
|
? super.getPropertySet(setName)
|
|
: super.getPropertySet(setName, dea.getEncryptionInfo());
|
|
}
|
|
|
|
/**
|
|
* Writes out the standard Documment Information Properties (HPSF)
|
|
* @param outFS the POIFSFileSystem to write the properties into
|
|
* @param writtenEntries a list of POIFS entries to add the property names too
|
|
*
|
|
* @throws IOException if an error when writing to the
|
|
* {@link POIFSFileSystem} occurs
|
|
*/
|
|
protected void writeProperties(NPOIFSFileSystem outFS, List<String> writtenEntries) throws IOException {
|
|
super.writeProperties(outFS, writtenEntries);
|
|
DocumentEncryptionAtom dea = getDocumentEncryptionAtom();
|
|
if (dea != null) {
|
|
CryptoAPIEncryptor enc = (CryptoAPIEncryptor)dea.getEncryptionInfo().getEncryptor();
|
|
try {
|
|
enc.getSummaryEntries(outFS.getRoot()); // ignore OutputStream
|
|
} catch (IOException e) {
|
|
throw e;
|
|
} catch (GeneralSecurityException e) {
|
|
throw new IOException(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ******************* adding methods follow ********************* */
|
|
|
|
/**
|
|
* Adds a new root level record, at the end, but before the last
|
|
* PersistPtrIncrementalBlock.
|
|
*/
|
|
public synchronized int appendRootLevelRecord(Record newRecord) {
|
|
int addedAt = -1;
|
|
Record[] r = new Record[_records.length+1];
|
|
boolean added = false;
|
|
for(int i=(_records.length-1); i>=0; i--) {
|
|
if(added) {
|
|
// Just copy over
|
|
r[i] = _records[i];
|
|
} else {
|
|
r[(i+1)] = _records[i];
|
|
if(_records[i] instanceof PersistPtrHolder) {
|
|
r[i] = newRecord;
|
|
added = true;
|
|
addedAt = i;
|
|
}
|
|
}
|
|
}
|
|
_records = r;
|
|
return addedAt;
|
|
}
|
|
|
|
/**
|
|
* Add a new picture to this presentation.
|
|
*
|
|
* @return offset of this picture in the Pictures stream
|
|
*/
|
|
public int addPicture(HSLFPictureData img) {
|
|
// Process any existing pictures if we haven't yet
|
|
if(_pictures == null) {
|
|
try {
|
|
readPictures();
|
|
} catch(IOException e) {
|
|
throw new CorruptPowerPointFileException(e.getMessage());
|
|
}
|
|
}
|
|
|
|
// Add the new picture in
|
|
int offset = 0;
|
|
if(_pictures.size() > 0) {
|
|
HSLFPictureData prev = _pictures.get(_pictures.size() - 1);
|
|
offset = prev.getOffset() + prev.getRawData().length + 8;
|
|
}
|
|
img.setOffset(offset);
|
|
img.setIndex(_pictures.size()+1);
|
|
_pictures.add(img);
|
|
return offset;
|
|
}
|
|
|
|
/* ******************* fetching methods follow ********************* */
|
|
|
|
|
|
/**
|
|
* Returns an array of all the records found in the slideshow
|
|
*/
|
|
public Record[] getRecords() { return _records; }
|
|
|
|
/**
|
|
* Returns an array of the bytes of the file. Only correct after a
|
|
* call to open or write - at all other times might be wrong!
|
|
*/
|
|
public byte[] getUnderlyingBytes() { return _docstream; }
|
|
|
|
/**
|
|
* Fetch the Current User Atom of the document
|
|
*/
|
|
public CurrentUserAtom getCurrentUserAtom() { return currentUser; }
|
|
|
|
/**
|
|
* Return list of pictures contained in this presentation
|
|
*
|
|
* @return list with the read pictures or an empty list if the
|
|
* presentation doesn't contain pictures.
|
|
*/
|
|
public List<HSLFPictureData> getPictureData() {
|
|
if(_pictures == null) {
|
|
try {
|
|
readPictures();
|
|
} catch(IOException e) {
|
|
throw new CorruptPowerPointFileException(e.getMessage());
|
|
}
|
|
}
|
|
|
|
return Collections.unmodifiableList(_pictures);
|
|
}
|
|
|
|
/**
|
|
* Gets embedded object data from the slide show.
|
|
*
|
|
* @return the embedded objects.
|
|
*/
|
|
public HSLFObjectData[] getEmbeddedObjects() {
|
|
if (_objects == null) {
|
|
List<HSLFObjectData> objects = new ArrayList<HSLFObjectData>();
|
|
for (Record r : _records) {
|
|
if (r instanceof ExOleObjStg) {
|
|
objects.add(new HSLFObjectData((ExOleObjStg)r));
|
|
}
|
|
}
|
|
_objects = objects.toArray(new HSLFObjectData[objects.size()]);
|
|
}
|
|
return _objects;
|
|
}
|
|
|
|
@Override
|
|
public void close() throws IOException {
|
|
NPOIFSFileSystem fs = directory.getFileSystem();
|
|
if (fs != null) {
|
|
fs.close();
|
|
}
|
|
}
|
|
|
|
|
|
private static class BufAccessBAOS extends ByteArrayOutputStream {
|
|
public byte[] getBuf() {
|
|
return buf;
|
|
}
|
|
}
|
|
|
|
private static class CountingOS extends OutputStream {
|
|
int count = 0;
|
|
public void write(int b) throws IOException {
|
|
count++;
|
|
}
|
|
|
|
public void write(byte[] b) throws IOException {
|
|
count += b.length;
|
|
}
|
|
|
|
public void write(byte[] b, int off, int len) throws IOException {
|
|
count += len;
|
|
}
|
|
|
|
public int size() {
|
|
return count;
|
|
}
|
|
}
|
|
}
|