From f6cb7f14acf47450b536174202a05ce793a2cd52 Mon Sep 17 00:00:00 2001 From: Yegor Kozlov Date: Sun, 13 Oct 2013 07:39:40 +0000 Subject: [PATCH] Bugzilla 55578 - Support embedding OLE1.0 packages in HSSF git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1531623 13f79535-47bb-0310-9956-ffa450edef68 --- src/java/org/apache/poi/hpsf/ClassID.java | 21 +- .../record/EmbeddedObjectRefSubRecord.java | 14 +- .../apache/poi/hssf/record/FtCfSubRecord.java | 113 +++++++ .../poi/hssf/record/FtPioGrbitSubRecord.java | 167 +++++++++ .../org/apache/poi/hssf/record/SubRecord.java | 4 + .../poi/hssf/usermodel/HSSFPatriarch.java | 101 +++++- .../poi/hssf/usermodel/HSSFWorkbook.java | 85 ++++- .../poi/poifs/filesystem/EntryUtils.java | 4 +- .../poi/poifs/filesystem/Ole10Native.java | 317 +++++++++++------- .../poi/hssf/usermodel/TestHSSFPicture.java | 5 +- .../poi/hssf/usermodel/TestOLE2Embeding.java | 112 ++++++- 11 files changed, 800 insertions(+), 143 deletions(-) create mode 100644 src/java/org/apache/poi/hssf/record/FtCfSubRecord.java create mode 100644 src/java/org/apache/poi/hssf/record/FtPioGrbitSubRecord.java diff --git a/src/java/org/apache/poi/hpsf/ClassID.java b/src/java/org/apache/poi/hpsf/ClassID.java index dd623b8b77..9fab2227a8 100644 --- a/src/java/org/apache/poi/hpsf/ClassID.java +++ b/src/java/org/apache/poi/hpsf/ClassID.java @@ -30,7 +30,12 @@ import org.apache.poi.util.HexDump; */ public class ClassID { - + public static final ClassID OLE10_PACKAGE = new ClassID("{0003000C-0000-0000-C000-000000000046}"); + public static final ClassID PPT_SHOW = new ClassID("{64818D10-4F9B-11CF-86EA-00AA00B929E8}"); + public static final ClassID XLS_WORKBOOK = new ClassID("{00020841-0000-0000-C000-000000000046}"); + public static final ClassID TXT_ONLY = new ClassID("{5e941d80-bf96-11cd-b579-08002b30bfeb}"); // ??? + + /** *

The bytes making out the class ID in correct order, * i.e. big-endian.

@@ -64,6 +69,20 @@ public class ClassID } + /** + *

Creates a {@link ClassID} from a human-readable representation of the Class ID in standard + * format "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}".

+ * + * @param externalForm representation of the Class ID represented by this object. + */ + public ClassID(String externalForm) { + bytes = new byte[LENGTH]; + String clsStr = externalForm.replaceAll("[{}-]", ""); + for (int i=0; iThe number of bytes occupied by this object in the byte * stream.

*/ diff --git a/src/java/org/apache/poi/hssf/record/EmbeddedObjectRefSubRecord.java b/src/java/org/apache/poi/hssf/record/EmbeddedObjectRefSubRecord.java index 77e9fd86d6..88fb10005c 100644 --- a/src/java/org/apache/poi/hssf/record/EmbeddedObjectRefSubRecord.java +++ b/src/java/org/apache/poi/hssf/record/EmbeddedObjectRefSubRecord.java @@ -64,7 +64,7 @@ public final class EmbeddedObjectRefSubRecord extends SubRecord { // currently for testing only - needs review - EmbeddedObjectRefSubRecord() { + public EmbeddedObjectRefSubRecord() { field_2_unknownFormulaData = new byte[] { 0x02, 0x6C, 0x6A, 0x16, 0x01, }; // just some sample data. These values vary a lot field_6_unknown = EMPTY_BYTE_ARRAY; field_4_ole_classname = null; @@ -334,4 +334,16 @@ public final class EmbeddedObjectRefSubRecord extends SubRecord { sb.append("[/ftPictFmla]"); return sb.toString(); } + + public void setUnknownFormulaData(byte[] formularData) { + field_2_unknownFormulaData = formularData; + } + + public void setOleClassname(String oleClassname) { + field_4_ole_classname = oleClassname; + } + + public void setStorageId(int storageId) { + field_5_stream_id = storageId; + } } diff --git a/src/java/org/apache/poi/hssf/record/FtCfSubRecord.java b/src/java/org/apache/poi/hssf/record/FtCfSubRecord.java new file mode 100644 index 0000000000..95438334e1 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/FtCfSubRecord.java @@ -0,0 +1,113 @@ +/* ==================================================================== + 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.hssf.record; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndianInput; +import org.apache.poi.util.LittleEndianOutput; + + +/** + * The FtCf structure specifies the clipboard format of the picture-type Obj record containing this FtCf. + */ +public final class FtCfSubRecord extends SubRecord { + public final static short sid = 0x07; + public final static short length = 0x02; + + /** + * Specifies the format of the picture is an enhanced metafile. + */ + public static short METAFILE_BIT = (short)0x0002; + + /** + * Specifies the format of the picture is a bitmap. + */ + public static short BITMAP_BIT = (short)0x0009; + + /** + * Specifies the picture is in an unspecified format that is + * neither and enhanced metafile nor a bitmap. + */ + public static short UNSPECIFIED_BIT = (short)0xFFFF; + + private short flags = 0; + + /** + * Construct a new FtPioGrbitSubRecord and + * fill its data with the default values + */ + public FtCfSubRecord() { + } + + public FtCfSubRecord(LittleEndianInput in, int size) { + if (size != length) { + throw new RecordFormatException("Unexpected size (" + size + ")"); + } + flags = in.readShort(); + } + + /** + * Convert this record to string. + * Used by BiffViewer and other utilities. + */ + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append("[FtCf ]\n"); + buffer.append(" size = ").append(length).append("\n"); + buffer.append(" flags = ").append(HexDump.toHex(flags)).append("\n"); + buffer.append("[/FtCf ]\n"); + return buffer.toString(); + } + + /** + * Serialize the record data into the supplied array of bytes + * + * @param out the stream to serialize into + */ + public void serialize(LittleEndianOutput out) { + out.writeShort(sid); + out.writeShort(length); + out.writeShort(flags); + } + + protected int getDataSize() { + return length; + } + + /** + * @return id of this record. + */ + public short getSid() + { + return sid; + } + + public Object clone() { + FtCfSubRecord rec = new FtCfSubRecord(); + rec.flags = this.flags; + return rec; + } + + public short getFlags() { + return flags; + } + + public void setFlags(short flags) { + this.flags = flags; + } +} \ No newline at end of file diff --git a/src/java/org/apache/poi/hssf/record/FtPioGrbitSubRecord.java b/src/java/org/apache/poi/hssf/record/FtPioGrbitSubRecord.java new file mode 100644 index 0000000000..8562a05352 --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/FtPioGrbitSubRecord.java @@ -0,0 +1,167 @@ +/* ==================================================================== + 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.hssf.record; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndianInput; +import org.apache.poi.util.LittleEndianOutput; + + +/** + * This structure appears as part of an Obj record that represents image display properties. + */ +public final class FtPioGrbitSubRecord extends SubRecord { + public final static short sid = 0x08; + public final static short length = 0x02; + + /** + * A bit that specifies whether the picture's aspect ratio is preserved when rendered in + * different views (Normal view, Page Break Preview view, Page Layout view and printing). + */ + public static int AUTO_PICT_BIT = 1 << 0; + + /** + * A bit that specifies whether the pictFmla field of the Obj record that contains + * this FtPioGrbit specifies a DDE reference. + */ + public static int DDE_BIT = 1 << 1; + + /** + * A bit that specifies whether this object is expected to be updated on print to + * reflect the values in the cell associated with the object. + */ + public static int PRINT_CALC_BIT = 1 << 2; + + /** + * A bit that specifies whether the picture is displayed as an icon. + */ + public static int ICON_BIT = 1 << 3; + + /** + * A bit that specifies whether this object is an ActiveX control. + * It MUST NOT be the case that both fCtl and fDde are equal to 1. + */ + public static int CTL_BIT = 1 << 4; + + /** + * A bit that specifies whether the object data are stored in an + * embedding storage (= 0) or in the controls stream (ctls) (= 1). + */ + public static int PRSTM_BIT = 1 << 5; + + /** + * A bit that specifies whether this is a camera picture. + */ + public static int CAMERA_BIT = 1 << 7; + + /** + * A bit that specifies whether this picture's size has been explicitly set. + * 0 = picture size has been explicitly set, 1 = has not been set + */ + public static int DEFAULT_SIZE_BIT = 1 << 8; + + /** + * A bit that specifies whether the OLE server for the object is called + * to load the object's data automatically when the parent workbook is opened. + */ + public static int AUTO_LOAD_BIT = 1 << 9; + + + private short flags = 0; + + /** + * Construct a new FtPioGrbitSubRecord and + * fill its data with the default values + */ + public FtPioGrbitSubRecord() { + } + + public FtPioGrbitSubRecord(LittleEndianInput in, int size) { + if (size != length) { + throw new RecordFormatException("Unexpected size (" + size + ")"); + } + flags = in.readShort(); + } + + /** + * Use one of the bitmasks MANUAL_ADVANCE_BIT ... CURSOR_VISIBLE_BIT + * @param bitmask + * @param enabled + */ + public void setFlagByBit(int bitmask, boolean enabled) { + if (enabled) { + flags |= bitmask; + } else { + flags &= (0xFFFF ^ bitmask); + } + } + + public boolean getFlagByBit(int bitmask) { + return ((flags & bitmask) != 0); + } + + /** + * Convert this record to string. + * Used by BiffViewer and other utilities. + */ + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append("[FtPioGrbit ]\n"); + buffer.append(" size = ").append(length).append("\n"); + buffer.append(" flags = ").append(HexDump.toHex(flags)).append("\n"); + buffer.append("[/FtPioGrbit ]\n"); + return buffer.toString(); + } + + /** + * Serialize the record data into the supplied array of bytes + * + * @param out the stream to serialize into + */ + public void serialize(LittleEndianOutput out) { + out.writeShort(sid); + out.writeShort(length); + out.writeShort(flags); + } + + protected int getDataSize() { + return length; + } + + /** + * @return id of this record. + */ + public short getSid() + { + return sid; + } + + public Object clone() { + FtPioGrbitSubRecord rec = new FtPioGrbitSubRecord(); + rec.flags = this.flags; + return rec; + } + + public short getFlags() { + return flags; + } + + public void setFlags(short flags) { + this.flags = flags; + } +} \ No newline at end of file diff --git a/src/java/org/apache/poi/hssf/record/SubRecord.java b/src/java/org/apache/poi/hssf/record/SubRecord.java index fd54acc0a7..35b62c36c5 100644 --- a/src/java/org/apache/poi/hssf/record/SubRecord.java +++ b/src/java/org/apache/poi/hssf/record/SubRecord.java @@ -59,6 +59,10 @@ public abstract class SubRecord { return new LbsDataSubRecord(in, secondUShort, cmoOt); case FtCblsSubRecord.sid: return new FtCblsSubRecord(in, secondUShort); + case FtPioGrbitSubRecord.sid: + return new FtPioGrbitSubRecord(in, secondUShort); + case FtCfSubRecord.sid: + return new FtCfSubRecord(in, secondUShort); } return new UnknownSubRecord(in, sid, secondUShort); } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFPatriarch.java b/src/java/org/apache/poi/hssf/usermodel/HSSFPatriarch.java index c508c51ed0..6d61b22d56 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFPatriarch.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFPatriarch.java @@ -17,27 +17,35 @@ package org.apache.poi.hssf.usermodel; +import java.io.FileNotFoundException; import java.util.*; import org.apache.poi.ddf.*; import org.apache.poi.hssf.model.DrawingManager2; +import org.apache.poi.hssf.record.CommonObjectDataSubRecord; +import org.apache.poi.hssf.record.EmbeddedObjectRefSubRecord; +import org.apache.poi.hssf.record.EndSubRecord; import org.apache.poi.hssf.record.EscherAggregate; +import org.apache.poi.hssf.record.FtCfSubRecord; +import org.apache.poi.hssf.record.FtPioGrbitSubRecord; import org.apache.poi.hssf.record.NoteRecord; +import org.apache.poi.hssf.record.ObjRecord; import org.apache.poi.hssf.util.CellReference; +import org.apache.poi.poifs.filesystem.DirectoryEntry; +import org.apache.poi.poifs.filesystem.DirectoryNode; import org.apache.poi.ss.usermodel.Chart; -import org.apache.poi.util.POILogFactory; -import org.apache.poi.util.POILogger; -import org.apache.poi.util.StringUtil; -import org.apache.poi.util.Internal; -import org.apache.poi.ss.usermodel.Drawing; import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.Drawing; +import org.apache.poi.util.HexDump; +import org.apache.poi.util.Internal; +import org.apache.poi.util.StringUtil; /** * The patriarch is the toplevel container for shapes in a sheet. It does * little other than act as a container for other shapes and groups. */ public final class HSSFPatriarch implements HSSFShapeContainer, Drawing { - private static POILogger log = POILogFactory.getLogger(HSSFPatriarch.class); + // private static POILogger log = POILogFactory.getLogger(HSSFPatriarch.class); private final List _shapes = new ArrayList(); private final EscherSpgrRecord _spgrRecord; @@ -193,6 +201,87 @@ public final class HSSFPatriarch implements HSSFShapeContainer, Drawing { return createPicture((HSSFClientAnchor) anchor, pictureIndex); } + /** + * Adds a new OLE Package Shape + * + * @param anchor the client anchor describes how this picture is + * attached to the sheet. + * @param storageId the storageId returned by {@Link HSSFWorkbook.addOlePackage} + * @param pictureIndex the index of the picture (used as preview image) in the + * workbook collection of pictures. + * + * @return newly created shape + */ + public HSSFObjectData createObjectData(HSSFClientAnchor anchor, int storageId, int pictureIndex) { + ObjRecord obj = new ObjRecord(); + + CommonObjectDataSubRecord ftCmo = new CommonObjectDataSubRecord(); + ftCmo.setObjectType(CommonObjectDataSubRecord.OBJECT_TYPE_PICTURE); + // ftCmo.setObjectId(oleShape.getShapeId()); ... will be set by onCreate(...) + ftCmo.setLocked(true); + ftCmo.setPrintable(true); + ftCmo.setAutofill(true); + ftCmo.setAutoline(true); + ftCmo.setReserved1(0); + ftCmo.setReserved2(0); + ftCmo.setReserved3(0); + obj.addSubRecord(ftCmo); + + // FtCf (pictFormat) + FtCfSubRecord ftCf = new FtCfSubRecord(); + HSSFPictureData pictData = getSheet().getWorkbook().getAllPictures().get(pictureIndex-1); + switch (pictData.getFormat()) { + case HSSFWorkbook.PICTURE_TYPE_WMF: + case HSSFWorkbook.PICTURE_TYPE_EMF: + // this needs patch #49658 to be applied to actually work + ftCf.setFlags(FtCfSubRecord.METAFILE_BIT); + break; + case HSSFWorkbook.PICTURE_TYPE_DIB: + case HSSFWorkbook.PICTURE_TYPE_PNG: + case HSSFWorkbook.PICTURE_TYPE_JPEG: + case HSSFWorkbook.PICTURE_TYPE_PICT: + ftCf.setFlags(FtCfSubRecord.BITMAP_BIT); + break; + } + obj.addSubRecord(ftCf); + // FtPioGrbit (pictFlags) + FtPioGrbitSubRecord ftPioGrbit = new FtPioGrbitSubRecord(); + ftPioGrbit.setFlagByBit(FtPioGrbitSubRecord.AUTO_PICT_BIT, true); + obj.addSubRecord(ftPioGrbit); + + EmbeddedObjectRefSubRecord ftPictFmla = new EmbeddedObjectRefSubRecord(); + ftPictFmla.setUnknownFormulaData(new byte[]{2, 0, 0, 0, 0}); + ftPictFmla.setOleClassname("Paket"); + ftPictFmla.setStorageId(storageId); + + obj.addSubRecord(ftPictFmla); + obj.addSubRecord(new EndSubRecord()); + + String entryName = "MBD"+HexDump.toHex(storageId); + DirectoryEntry oleRoot; + try { + DirectoryNode dn = _sheet.getWorkbook().getRootDirectory(); + if (dn == null) throw new FileNotFoundException(); + oleRoot = (DirectoryEntry)dn.getEntry(entryName); + } catch (FileNotFoundException e) { + throw new IllegalStateException("trying to add ole shape without actually adding data first - use HSSFWorkbook.addOlePackage first", e); + } + + // create picture shape, which need to be minimal modified for oleshapes + HSSFPicture shape = new HSSFPicture(null, anchor); + shape.setPictureIndex(pictureIndex); + EscherContainerRecord spContainer = shape.getEscherContainer(); + EscherSpRecord spRecord = spContainer.getChildById(EscherSpRecord.RECORD_ID); + spRecord.setFlags(spRecord.getFlags() | EscherSpRecord.FLAG_OLESHAPE); + + HSSFObjectData oleShape = new HSSFObjectData(spContainer, obj, oleRoot); + addShape(oleShape); + onCreate(oleShape); + + + return oleShape; + } + /** * Creates a polygon * diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java b/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java index 9e42beab98..2ab0f6b2c5 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java @@ -18,15 +18,19 @@ package org.apache.poi.hssf.usermodel; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.regex.Pattern; import org.apache.commons.codec.digest.DigestUtils; @@ -36,6 +40,7 @@ import org.apache.poi.ddf.EscherBitmapBlip; import org.apache.poi.ddf.EscherBlipRecord; import org.apache.poi.ddf.EscherMetafileBlip; import org.apache.poi.ddf.EscherRecord; +import org.apache.poi.hpsf.ClassID; import org.apache.poi.hssf.OldExcelFormatException; import org.apache.poi.hssf.model.DrawingManager2; import org.apache.poi.hssf.model.HSSFFormulaParser; @@ -46,7 +51,11 @@ import org.apache.poi.hssf.record.*; import org.apache.poi.hssf.record.aggregates.RecordAggregate.RecordVisitor; import org.apache.poi.hssf.record.common.UnicodeString; import org.apache.poi.hssf.util.CellReference; +import org.apache.poi.poifs.filesystem.DirectoryEntry; import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.poifs.filesystem.EntryUtils; +import org.apache.poi.poifs.filesystem.FilteringDirectoryNode; +import org.apache.poi.poifs.filesystem.Ole10Native; import org.apache.poi.poifs.filesystem.POIFSFileSystem; import org.apache.poi.ss.formula.FormulaShifter; import org.apache.poi.ss.formula.FormulaType; @@ -57,10 +66,7 @@ import org.apache.poi.ss.formula.udf.UDFFinder; import org.apache.poi.ss.usermodel.Row.MissingCellPolicy; import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.ss.util.WorkbookUtil; -import org.apache.poi.util.Configurator; -import org.apache.poi.util.LittleEndian; -import org.apache.poi.util.POILogFactory; -import org.apache.poi.util.POILogger; +import org.apache.poi.util.*; /** @@ -1190,15 +1196,15 @@ public final class HSSFWorkbook extends POIDocument implements org.apache.poi.ss if (preserveNodes) { // Don't write out the old Workbook, we'll be doing our new one - excepts.add("Workbook"); // If the file had an "incorrect" name for the workbook stream, // don't write the old one as we'll use the correct name shortly - for (String wrongName : WORKBOOK_DIR_ENTRY_NAMES) { - excepts.add(wrongName); - } + excepts.addAll(Arrays.asList(WORKBOOK_DIR_ENTRY_NAMES)); // Copy over all the other nodes to our new poifs - copyNodes(this.directory, fs.getRoot(), excepts); + EntryUtils.copyNodes( + new FilteringDirectoryNode(this.directory, excepts) + , new FilteringDirectoryNode(fs.getRoot(), excepts) + ); // YK: preserve StorageClsid, it is important for embedded workbooks, // see Bugzilla 47920 @@ -1623,7 +1629,7 @@ public final class HSSFWorkbook extends POIDocument implements org.apache.poi.ss break; } - blipRecord.setRecordId( (short) ( EscherBitmapBlip.RECORD_ID_START + format ) ); + blipRecord.setRecordId((short) (EscherBitmapBlip.RECORD_ID_START + format)); switch (format) { case PICTURE_TYPE_EMF: @@ -1713,6 +1719,65 @@ public final class HSSFWorkbook extends POIDocument implements org.apache.poi.ss } + protected static Map getOleMap() { + Map olemap = new HashMap(); + olemap.put("PowerPoint Document", ClassID.PPT_SHOW); + for (String str : WORKBOOK_DIR_ENTRY_NAMES) { + olemap.put(str, ClassID.XLS_WORKBOOK); + } + // ... to be continued + return olemap; + } + + public int addOlePackage(POIFSFileSystem poiData, String label, String fileName, String command) + throws IOException { + DirectoryNode root = poiData.getRoot(); + Map olemap = getOleMap(); + for (Map.Entry entry : olemap.entrySet()) { + if (root.hasEntry(entry.getKey())) { + root.setStorageClsid(entry.getValue()); + break; + } + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + poiData.writeFilesystem(bos); + return addOlePackage(bos.toByteArray(), label, fileName, command); + } + + public int addOlePackage(byte[] oleData, String label, String fileName, String command) + throws IOException { + // check if we were created by POIFS otherwise create a new dummy POIFS for storing the package data + if (directory == null) { + directory = new POIFSFileSystem().getRoot(); + preserveNodes = true; + } + + // get free MBD-Node + int storageId = 0; + DirectoryEntry oleDir = null; + do { + String storageStr = "MBD"+ HexDump.toHex(++storageId); + if (!directory.hasEntry(storageStr)) { + oleDir = directory.createDirectory(storageStr); + oleDir.setStorageClsid(ClassID.OLE10_PACKAGE); + } + } while (oleDir == null); + + // the following data was taken from an example libre office document + // beside this "\u0001Ole" record there were several other records, e.g. CompObj, + // OlePresXXX, but it seems, that they aren't neccessary + byte oleBytes[] = { 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + oleDir.createDocument("\u0001Ole", new ByteArrayInputStream(oleBytes)); + + Ole10Native oleNative = new Ole10Native(label, fileName, command, oleData); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + oleNative.writeOut(bos); + oleDir.createDocument(Ole10Native.OLE10_NATIVE, new ByteArrayInputStream(bos.toByteArray())); + + return storageId; + } + /** * Is the workbook protected with a password (not encrypted)? */ diff --git a/src/java/org/apache/poi/poifs/filesystem/EntryUtils.java b/src/java/org/apache/poi/poifs/filesystem/EntryUtils.java index 60f2b8d38c..4bce0641ab 100644 --- a/src/java/org/apache/poi/poifs/filesystem/EntryUtils.java +++ b/src/java/org/apache/poi/poifs/filesystem/EntryUtils.java @@ -41,8 +41,10 @@ public class EntryUtils DirectoryEntry newTarget = null; if ( entry.isDirectoryEntry() ) { + DirectoryEntry dirEntry = (DirectoryEntry)entry; newTarget = target.createDirectory( entry.getName() ); - Iterator entries = ( (DirectoryEntry) entry ).getEntries(); + newTarget.setStorageClsid( dirEntry.getStorageClsid() ); + Iterator entries = dirEntry.getEntries(); while ( entries.hasNext() ) { diff --git a/src/java/org/apache/poi/poifs/filesystem/Ole10Native.java b/src/java/org/apache/poi/poifs/filesystem/Ole10Native.java index fbc58ab0c4..2c950da3ff 100644 --- a/src/java/org/apache/poi/poifs/filesystem/Ole10Native.java +++ b/src/java/org/apache/poi/poifs/filesystem/Ole10Native.java @@ -14,14 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. ==================================================================== */ - -package org.apache.poi.poifs.filesystem; - -import java.io.FileNotFoundException; -import java.io.IOException; - -import org.apache.poi.util.HexDump; -import org.apache.poi.util.LittleEndian; + +package org.apache.poi.poifs.filesystem; + +import java.io.ByteArrayOutputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; import org.apache.poi.util.LittleEndianConsts; import org.apache.poi.util.StringUtil; @@ -29,27 +31,27 @@ import org.apache.poi.util.StringUtil; * Represents an Ole10Native record which is wrapped around certain binary * files being embedded in OLE2 documents. * - * @author Rainer Schwarze - */ -public class Ole10Native { - // (the fields as they appear in the raw record:) - private final int totalSize; // 4 bytes, total size of record not including this field - private short flags1; // 2 bytes, unknown, mostly [02 00] - private final String label; // ASCIIZ, stored in this field without the terminating zero - private final String fileName; // ASCIIZ, stored in this field without the terminating zero - private short flags2; // 2 bytes, unknown, mostly [00 00] - // private byte unknown1Length; // 1 byte, specifying the length of the following byte array (unknown1) - private byte[] unknown1; // see below - private byte[] unknown2; // 3 bytes, unknown, mostly [00 00 00] - private final String command; // ASCIIZ, stored in this field without the terminating zero - private final int dataSize; // 4 bytes (if space), size of following buffer - private final byte[] dataBuffer; // varying size, the actual native data - private short flags3; // some final flags? or zero terminators?, sometimes not there - public static final String OLE10_NATIVE = "\u0001Ole10Native"; - - /** - * Creates an instance of this class from an embedded OLE Object. The OLE Object is expected - * to include a stream "{01}Ole10Native" which contains the actual + * @author Rainer Schwarze + */ +public class Ole10Native { + + public static final String OLE10_NATIVE = "\u0001Ole10Native"; + protected static final String ISO1 = "ISO-8859-1"; + + // (the fields as they appear in the raw record:) + private int totalSize; // 4 bytes, total size of record not including this field + private short flags1 = 2; // 2 bytes, unknown, mostly [02 00] + private String label; // ASCIIZ, stored in this field without the terminating zero + private String fileName; // ASCIIZ, stored in this field without the terminating zero + private short flags2 = 0; // 2 bytes, unknown, mostly [00 00] + private short unknown1 = 3; // see below + private String command; // ASCIIZ, stored in this field without the terminating zero + private byte[] dataBuffer; // varying size, the actual native data + private short flags3 = 0; // some final flags? or zero terminators?, sometimes not there + + /** + * Creates an instance of this class from an embedded OLE Object. The OLE Object is expected + * to include a stream "{01}Ole10Native" which contains the actual * data relevant for this class. * * @param poifs POI Filesystem object @@ -87,12 +89,22 @@ public class Ole10Native { directory.createDocumentInputStream(nativeEntry).read(data); return new Ole10Native(data, 0, plain); - } - - /** - * Creates an instance and fills the fields based on the data in the given buffer. - * - * @param data The buffer containing the Ole10Native record + } + + /** + * Creates an instance and fills the fields based on ... the fields + */ + public Ole10Native(String label, String filename, String command, byte[] data) { + setLabel(label); + setFileName(filename); + setCommand(command); + setDataBuffer(data); + } + + /** + * Creates an instance and fills the fields based on the data in the given buffer. + * + * @param data The buffer containing the Ole10Native record * @param offset The start offset of the record in the buffer * @throws Ole10NativeException on invalid or unexcepted data format */ @@ -117,61 +129,57 @@ public class Ole10Native { totalSize = LittleEndian.getInt(data, ofs); ofs += LittleEndianConsts.INT_SIZE; - if (plain) { - dataBuffer = new byte[totalSize-4]; - System.arraycopy(data, 4, dataBuffer, 0, dataBuffer.length); - dataSize = totalSize - 4; - - byte[] oleLabel = new byte[8]; - System.arraycopy(dataBuffer, 0, oleLabel, 0, Math.min(dataBuffer.length, 8)); + if (plain) { + dataBuffer = new byte[totalSize-4]; + System.arraycopy(data, 4, dataBuffer, 0, dataBuffer.length); + // int dataSize = totalSize - 4; + + byte[] oleLabel = new byte[8]; + System.arraycopy(dataBuffer, 0, oleLabel, 0, Math.min(dataBuffer.length, 8)); label = "ole-"+ HexDump.toHex(oleLabel); fileName = label; command = label; - } else { - flags1 = LittleEndian.getShort(data, ofs); - ofs += LittleEndianConsts.SHORT_SIZE; - int len = getStringLength(data, ofs); - label = StringUtil.getFromCompressedUnicode(data, ofs, len - 1); - ofs += len; - len = getStringLength(data, ofs); - fileName = StringUtil.getFromCompressedUnicode(data, ofs, len - 1); - ofs += len; - flags2 = LittleEndian.getShort(data, ofs); - ofs += LittleEndianConsts.SHORT_SIZE; - len = LittleEndian.getUByte(data, ofs); - unknown1 = new byte[len]; - ofs += len; - len = 3; - unknown2 = new byte[len]; - ofs += len; - len = getStringLength(data, ofs); - command = StringUtil.getFromCompressedUnicode(data, ofs, len - 1); - ofs += len; - - if (totalSize + LittleEndianConsts.INT_SIZE - ofs > LittleEndianConsts.INT_SIZE) { - dataSize = LittleEndian.getInt(data, ofs); - ofs += LittleEndianConsts.INT_SIZE; - - if (dataSize > totalSize || dataSize<0) { - throw new Ole10NativeException("Invalid Ole10Native"); - } - - dataBuffer = new byte[dataSize]; - System.arraycopy(data, ofs, dataBuffer, 0, dataSize); - ofs += dataSize; - - if (unknown1.length > 0) { - flags3 = LittleEndian.getShort(data, ofs); - ofs += LittleEndianConsts.SHORT_SIZE; - } else { - flags3 = 0; - } - } else { - throw new Ole10NativeException("Invalid Ole10Native"); - } - } - } - + } else { + flags1 = LittleEndian.getShort(data, ofs); + ofs += LittleEndianConsts.SHORT_SIZE; + + int len = getStringLength(data, ofs); + label = StringUtil.getFromCompressedUnicode(data, ofs, len - 1); + ofs += len; + + len = getStringLength(data, ofs); + fileName = StringUtil.getFromCompressedUnicode(data, ofs, len - 1); + ofs += len; + + flags2 = LittleEndian.getShort(data, ofs); + ofs += LittleEndianConsts.SHORT_SIZE; + + unknown1 = LittleEndian.getShort(data, ofs); + ofs += LittleEndianConsts.SHORT_SIZE; + + len = LittleEndian.getInt(data, ofs); + ofs += LittleEndianConsts.INT_SIZE; + + command = StringUtil.getFromCompressedUnicode(data, ofs, len - 1); + ofs += len; + + if (totalSize < ofs) { + throw new Ole10NativeException("Invalid Ole10Native"); + } + + int dataSize = LittleEndian.getInt(data, ofs); + ofs += LittleEndianConsts.INT_SIZE; + + if (dataSize < 0 || totalSize - (ofs - LittleEndianConsts.INT_SIZE) < dataSize) { + throw new Ole10NativeException("Invalid Ole10Native"); + } + + dataBuffer = new byte[dataSize]; + System.arraycopy(data, ofs, dataBuffer, 0, dataSize); + ofs += dataSize; + } + } + /* * Helper - determine length of zero terminated string (ASCIIZ). */ @@ -234,26 +242,17 @@ public class Ole10Native { /** * Returns unknown1 field - currently unknown. - * - * @return the unknown1 - */ - public byte[] getUnknown1() { - return unknown1; - } - - /** - * Returns the unknown2 field - currently being a byte[3] - mostly {0, 0, 0}. - * - * @return the unknown2 - */ - public byte[] getUnknown2() { - return unknown2; - } - - /** - * Returns the command field - usually the name of the file being embedded - * including the full path, may be a command specified during embedding the file. - * + * + * @return the unknown1 + */ + public short getUnknown1() { + return unknown1; + } + + /** + * Returns the command field - usually the name of the file being embedded + * including the full path, may be a command specified during embedding the file. + * * @return the command */ public String getCommand() { @@ -265,13 +264,13 @@ public class Ole10Native { * embedded. To be sure, that no data has been embedded, check whether * {@link #getDataBuffer()} returns null. * - * @return the dataSize - */ - public int getDataSize() { - return dataSize; - } - - /** + * @return the dataSize + */ + public int getDataSize() { + return dataBuffer.length; + } + + /** * Returns the buffer containing the embedded file's data, or null * if no data was embedded. Note that an embedding may provide information about * the data, but the actual data is not included. (So label, filename etc. are @@ -288,7 +287,89 @@ public class Ole10Native { * * @return the flags3 */ - public short getFlags3() { - return flags3; - } -} + public short getFlags3() { + return flags3; + } + + /** + * Have the contents printer out into an OutputStream, used when writing a + * file back out to disk (Normally, atom classes will keep their bytes + * around, but non atom classes will just request the bytes from their + * children, then chuck on their header and return) + */ + public void writeOut(OutputStream out) throws IOException { + byte intbuf[] = new byte[LittleEndianConsts.INT_SIZE]; + byte shortbuf[] = new byte[LittleEndianConsts.SHORT_SIZE]; + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + bos.write(intbuf); // total size, will be determined later .. + + LittleEndian.putShort(shortbuf, 0, getFlags1()); + bos.write(shortbuf); + + bos.write(getLabel().getBytes(ISO1)); + bos.write(0); + + bos.write(getFileName().getBytes(ISO1)); + bos.write(0); + + LittleEndian.putShort(shortbuf, 0, getFlags2()); + bos.write(shortbuf); + + LittleEndian.putShort(shortbuf, 0, getUnknown1()); + bos.write(shortbuf); + + LittleEndian.putInt(intbuf, 0, getCommand().length()+1); + bos.write(intbuf); + + bos.write(getCommand().getBytes(ISO1)); + bos.write(0); + + LittleEndian.putInt(intbuf, 0, getDataBuffer().length); + bos.write(intbuf); + + bos.write(getDataBuffer()); + + LittleEndian.putShort(shortbuf, 0, getFlags3()); + bos.write(shortbuf); + + // update total size - length of length-field (4 bytes) + byte data[] = bos.toByteArray(); + totalSize = data.length - LittleEndianConsts.INT_SIZE; + LittleEndian.putInt(data, 0, totalSize); + + out.write(data); + } + + public void setFlags1(short flags1) { + this.flags1 = flags1; + } + + public void setFlags2(short flags2) { + this.flags2 = flags2; + } + + public void setFlags3(short flags3) { + this.flags3 = flags3; + } + + public void setLabel(String label) { + this.label = label; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public void setCommand(String command) { + this.command = command; + } + + public void setUnknown1(short unknown1) { + this.unknown1 = unknown1; + } + + public void setDataBuffer(byte dataBuffer[]) { + this.dataBuffer = dataBuffer; + } +} diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFPicture.java b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFPicture.java index de8ffabf8b..de2b683a7e 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFPicture.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFPicture.java @@ -262,10 +262,7 @@ public final class TestHSSFPicture extends BaseTestPicture { System.arraycopy(pictureDataWmf, 22, wmfNoHeader, 0, pictureDataWmf.length-22); pictureDataOut = wb.getAllPictures().get(2).getData(); assertTrue(Arrays.equals(wmfNoHeader, pictureDataOut)); - - FileOutputStream fos = new FileOutputStream("vect.xls"); - wb.write(fos); - fos.close(); + } } diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestOLE2Embeding.java b/src/testcases/org/apache/poi/hssf/usermodel/TestOLE2Embeding.java index 661b1d51f1..22700b28cc 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestOLE2Embeding.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestOLE2Embeding.java @@ -17,11 +17,23 @@ package org.apache.poi.hssf.usermodel; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; import java.util.List; import junit.framework.TestCase; +import org.apache.poi.POIDataSamples; import org.apache.poi.hssf.HSSFTestDataSamples; +import org.apache.poi.poifs.filesystem.DirectoryNode; +import org.apache.poi.poifs.filesystem.Ole10Native; +import org.apache.poi.poifs.filesystem.POIFSFileSystem; +import org.apache.poi.ss.usermodel.ClientAnchor; +import org.apache.poi.ss.usermodel.CreationHelper; /** * @@ -39,7 +51,7 @@ public final class TestOLE2Embeding extends TestCase { public void testEmbeddedObjects() throws Exception { HSSFWorkbook workbook = HSSFTestDataSamples.openSampleWorkbook("ole2-embedding.xls"); - List objects = workbook.getAllEmbeddedObjects(); + List objects = workbook.getAllEmbeddedObjects(); assertEquals("Wrong number of objects", 2, objects.size()); assertEquals("Wrong name for first object", "MBD06CAB431", ((HSSFObjectData) @@ -48,5 +60,101 @@ public final class TestOLE2Embeding extends TestCase { ((HSSFObjectData) objects.get(1)).getDirectory().getName()); } -} + + public void testReallyEmbedSomething() throws Exception { + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet(); + HSSFPatriarch patriarch = sheet.createDrawingPatriarch(); + byte[] pictureData = HSSFTestDataSamples.getTestDataFileContent("logoKarmokar4.png"); + byte[] picturePPT = POIDataSamples.getSlideShowInstance().readFile("clock.jpg"); + int imgIdx = wb.addPicture(pictureData, HSSFWorkbook.PICTURE_TYPE_PNG); + POIFSFileSystem pptPoifs = getSamplePPT(); + int pptIdx = wb.addOlePackage(pptPoifs, "Sample-PPT", "sample.ppt", "sample.ppt"); + POIFSFileSystem xlsPoifs = getSampleXLS(); + int imgPPT = wb.addPicture(picturePPT, HSSFWorkbook.PICTURE_TYPE_JPEG); + int xlsIdx = wb.addOlePackage(xlsPoifs, "Sample-XLS", "sample.xls", "sample.xls"); + int txtIdx = wb.addOlePackage(getSampleTXT(), "Sample-TXT", "sample.txt", "sample.txt"); + + int rowoffset = 5; + int coloffset = 5; + + CreationHelper ch = wb.getCreationHelper(); + HSSFClientAnchor anchor = (HSSFClientAnchor)ch.createClientAnchor(); + anchor.setAnchor((short)(2+coloffset), 1+rowoffset, 0, 0, (short)(3+coloffset), 5+rowoffset, 0, 0); + anchor.setAnchorType(ClientAnchor.DONT_MOVE_AND_RESIZE); + + patriarch.createObjectData(anchor, pptIdx, imgPPT); + + anchor = (HSSFClientAnchor)ch.createClientAnchor(); + anchor.setAnchor((short)(5+coloffset), 1+rowoffset, 0, 0, (short)(6+coloffset), 5+rowoffset, 0, 0); + anchor.setAnchorType(ClientAnchor.DONT_MOVE_AND_RESIZE); + + patriarch.createObjectData(anchor, xlsIdx, imgIdx); + + anchor = (HSSFClientAnchor)ch.createClientAnchor(); + anchor.setAnchor((short)(3+coloffset), 10+rowoffset, 0, 0, (short)(5+coloffset), 11+rowoffset, 0, 0); + anchor.setAnchorType(ClientAnchor.DONT_MOVE_AND_RESIZE); + + patriarch.createObjectData(anchor, txtIdx, imgIdx); + + anchor = (HSSFClientAnchor)ch.createClientAnchor(); + anchor.setAnchor((short)(1+coloffset), -2+rowoffset, 0, 0, (short)(7+coloffset), 14+rowoffset, 0, 0); + anchor.setAnchorType(ClientAnchor.DONT_MOVE_AND_RESIZE); + + HSSFSimpleShape circle = patriarch.createSimpleShape(anchor); + circle.setShapeType(HSSFSimpleShape.OBJECT_TYPE_OVAL); + circle.setNoFill(true); + + if (false) { + FileOutputStream fos = new FileOutputStream("embed.xls"); + wb.write(fos); + fos.close(); + } + + wb = HSSFTestDataSamples.writeOutAndReadBack(wb); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + HSSFObjectData od = wb.getAllEmbeddedObjects().get(0); + Ole10Native ole10 = Ole10Native.createFromEmbeddedOleObject((DirectoryNode)od.getDirectory()); + bos.reset(); + pptPoifs.writeFilesystem(bos); + assertTrue(Arrays.equals(ole10.getDataBuffer(), bos.toByteArray())); + + od = wb.getAllEmbeddedObjects().get(1); + ole10 = Ole10Native.createFromEmbeddedOleObject((DirectoryNode)od.getDirectory()); + bos.reset(); + xlsPoifs.writeFilesystem(bos); + assertTrue(Arrays.equals(ole10.getDataBuffer(), bos.toByteArray())); + + od = wb.getAllEmbeddedObjects().get(2); + ole10 = Ole10Native.createFromEmbeddedOleObject((DirectoryNode)od.getDirectory()); + assertTrue(Arrays.equals(ole10.getDataBuffer(), getSampleTXT())); + + } + + static POIFSFileSystem getSamplePPT() throws IOException { + // scratchpad classes are not available, so we use something pre-cooked + InputStream is = POIDataSamples.getSlideShowInstance().openResourceAsStream("with_textbox.ppt"); + POIFSFileSystem poifs = new POIFSFileSystem(is); + is.close(); + + return poifs; + } + + static POIFSFileSystem getSampleXLS() throws IOException { + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet(); + sheet.createRow(5).createCell(2).setCellValue("yo dawg i herd you like embeddet objekts, so we put a ole in your ole so you can save a file while you save a file"); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + wb.write(bos); + POIFSFileSystem poifs = new POIFSFileSystem(new ByteArrayInputStream(bos.toByteArray())); + + return poifs; + } + + static byte[] getSampleTXT() { + return "All your base are belong to us".getBytes(); + } +} \ No newline at end of file -- 2.39.5