From d0ed7b34286ed637c5a5cc448ee997b035d18e16 Mon Sep 17 00:00:00 2001 From: Glen Stampoultzis Date: Tue, 10 Feb 2004 22:03:32 +0000 Subject: [PATCH] The escher records themselves git-svn-id: https://svn.apache.org/repos/asf/jakarta/poi/branches/REL_2_BRANCH@353499 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/ddf/DefaultEscherRecordFactory.java | 114 ++ .../apache/poi/ddf/EscherArrayProperty.java | 162 +++ .../org/apache/poi/ddf/EscherBSERecord.java | 383 +++++++ .../org/apache/poi/ddf/EscherBlipRecord.java | 417 ++++++++ .../apache/poi/ddf/EscherBoolProperty.java | 50 + .../poi/ddf/EscherChildAnchorRecord.java | 176 ++++ .../poi/ddf/EscherClientAnchorRecord.java | 317 ++++++ .../poi/ddf/EscherClientDataRecord.java | 130 +++ .../apache/poi/ddf/EscherComplexProperty.java | 152 +++ .../apache/poi/ddf/EscherContainerRecord.java | 168 +++ .../org/apache/poi/ddf/EscherDgRecord.java | 163 +++ .../org/apache/poi/ddf/EscherDggRecord.java | 241 +++++ src/java/org/apache/poi/ddf/EscherDump.java | 990 ++++++++++++++++++ .../org/apache/poi/ddf/EscherOptRecord.java | 174 +++ .../org/apache/poi/ddf/EscherProperties.java | 607 +++++++++++ .../org/apache/poi/ddf/EscherProperty.java | 78 ++ .../apache/poi/ddf/EscherPropertyFactory.java | 91 ++ .../poi/ddf/EscherPropertyMetaData.java | 51 + .../org/apache/poi/ddf/EscherRGBProperty.java | 37 + src/java/org/apache/poi/ddf/EscherRecord.java | 273 +++++ .../apache/poi/ddf/EscherRecordFactory.java | 16 + .../poi/ddf/EscherSerializationListener.java | 27 + .../poi/ddf/EscherShapePathProperty.java | 25 + .../apache/poi/ddf/EscherSimpleProperty.java | 103 ++ .../org/apache/poi/ddf/EscherSpRecord.java | 201 ++++ .../org/apache/poi/ddf/EscherSpgrRecord.java | 192 ++++ .../poi/ddf/EscherSplitMenuColorsRecord.java | 171 +++ .../apache/poi/ddf/EscherTextboxRecord.java | 215 ++++ .../ddf/NullEscherSerializationListener.java | 20 + .../apache/poi/ddf/UnknownEscherRecord.java | 234 +++++ src/java/org/apache/poi/ddf/package.html | 11 + 31 files changed, 5989 insertions(+) create mode 100644 src/java/org/apache/poi/ddf/DefaultEscherRecordFactory.java create mode 100644 src/java/org/apache/poi/ddf/EscherArrayProperty.java create mode 100644 src/java/org/apache/poi/ddf/EscherBSERecord.java create mode 100644 src/java/org/apache/poi/ddf/EscherBlipRecord.java create mode 100644 src/java/org/apache/poi/ddf/EscherBoolProperty.java create mode 100644 src/java/org/apache/poi/ddf/EscherChildAnchorRecord.java create mode 100644 src/java/org/apache/poi/ddf/EscherClientAnchorRecord.java create mode 100644 src/java/org/apache/poi/ddf/EscherClientDataRecord.java create mode 100644 src/java/org/apache/poi/ddf/EscherComplexProperty.java create mode 100644 src/java/org/apache/poi/ddf/EscherContainerRecord.java create mode 100644 src/java/org/apache/poi/ddf/EscherDgRecord.java create mode 100644 src/java/org/apache/poi/ddf/EscherDggRecord.java create mode 100644 src/java/org/apache/poi/ddf/EscherDump.java create mode 100644 src/java/org/apache/poi/ddf/EscherOptRecord.java create mode 100644 src/java/org/apache/poi/ddf/EscherProperties.java create mode 100644 src/java/org/apache/poi/ddf/EscherProperty.java create mode 100644 src/java/org/apache/poi/ddf/EscherPropertyFactory.java create mode 100644 src/java/org/apache/poi/ddf/EscherPropertyMetaData.java create mode 100644 src/java/org/apache/poi/ddf/EscherRGBProperty.java create mode 100644 src/java/org/apache/poi/ddf/EscherRecord.java create mode 100644 src/java/org/apache/poi/ddf/EscherRecordFactory.java create mode 100644 src/java/org/apache/poi/ddf/EscherSerializationListener.java create mode 100644 src/java/org/apache/poi/ddf/EscherShapePathProperty.java create mode 100644 src/java/org/apache/poi/ddf/EscherSimpleProperty.java create mode 100644 src/java/org/apache/poi/ddf/EscherSpRecord.java create mode 100644 src/java/org/apache/poi/ddf/EscherSpgrRecord.java create mode 100644 src/java/org/apache/poi/ddf/EscherSplitMenuColorsRecord.java create mode 100644 src/java/org/apache/poi/ddf/EscherTextboxRecord.java create mode 100644 src/java/org/apache/poi/ddf/NullEscherSerializationListener.java create mode 100644 src/java/org/apache/poi/ddf/UnknownEscherRecord.java create mode 100644 src/java/org/apache/poi/ddf/package.html diff --git a/src/java/org/apache/poi/ddf/DefaultEscherRecordFactory.java b/src/java/org/apache/poi/ddf/DefaultEscherRecordFactory.java new file mode 100644 index 0000000000..09d8c5f816 --- /dev/null +++ b/src/java/org/apache/poi/ddf/DefaultEscherRecordFactory.java @@ -0,0 +1,114 @@ +package org.apache.poi.ddf; + +import org.apache.poi.hssf.record.RecordFormatException; + +import java.lang.reflect.Constructor; +import java.util.HashMap; +import java.util.Map; + +/** + * Generates escher records when provided the byte array containing those records. + * + * @author Glen Stampoultzis + * @see EscherRecordFactory + */ +public class DefaultEscherRecordFactory + implements EscherRecordFactory +{ + private static Class[] escherRecordClasses = { + EscherBSERecord.class, EscherOptRecord.class, EscherClientAnchorRecord.class, EscherDgRecord.class, + EscherSpgrRecord.class, EscherSpRecord.class, EscherClientDataRecord.class, EscherDggRecord.class, + EscherSplitMenuColorsRecord.class, EscherChildAnchorRecord.class, EscherTextboxRecord.class + }; + private static Map recordsMap = recordsToMap( escherRecordClasses ); + + /** + * Creates an instance of the escher record factory + */ + public DefaultEscherRecordFactory() + { + } + + /** + * Generates an escher record including the any children contained under that record. + * An exception is thrown if the record could not be generated. + * + * @param data The byte array containing the records + * @param offset The starting offset into the byte array + * @return The generated escher record + */ + public EscherRecord createRecord( byte[] data, int offset ) + { + EscherRecord.EscherRecordHeader header = EscherRecord.EscherRecordHeader.readHeader( data, offset ); + if ( ( header.getOptions() & (short) 0x000F ) == (short) 0x000F ) + { + EscherContainerRecord r = new EscherContainerRecord(); + r.setRecordId( header.getRecordId() ); + r.setOptions( header.getOptions() ); + return r; + } + else if ( header.getRecordId() >= EscherBlipRecord.RECORD_ID_START && header.getRecordId() <= EscherBlipRecord.RECORD_ID_END ) + { + EscherBlipRecord r = new EscherBlipRecord(); + r.setRecordId( header.getRecordId() ); + r.setOptions( header.getOptions() ); + return r; + } + else + { + Constructor recordConstructor = (Constructor) recordsMap.get( new Short( header.getRecordId() ) ); + EscherRecord escherRecord = null; + if ( recordConstructor != null ) + { + try + { + escherRecord = (EscherRecord) recordConstructor.newInstance( new Object[]{} ); + escherRecord.setRecordId( header.getRecordId() ); + escherRecord.setOptions( header.getOptions() ); + } + catch ( Exception e ) + { + escherRecord = null; + } + } + return escherRecord == null ? new UnknownEscherRecord() : escherRecord; + } + } + + /** + * Converts from a list of classes into a map that contains the record id as the key and + * the Constructor in the value part of the map. It does this by using reflection to look up + * the RECORD_ID field then using reflection again to find a reference to the constructor. + * + * @param records The records to convert + * @return The map containing the id/constructor pairs. + */ + private static Map recordsToMap( Class[] records ) + { + Map result = new HashMap(); + Constructor constructor; + + for ( int i = 0; i < records.length; i++ ) + { + Class record = null; + short sid = 0; + + record = records[i]; + try + { + sid = record.getField( "RECORD_ID" ).getShort( null ); + constructor = record.getConstructor( new Class[] + { + } ); + } + catch ( Exception illegalArgumentException ) + { + throw new RecordFormatException( + "Unable to determine record types" ); + } + result.put( new Short( sid ), constructor ); + } + return result; + } + +} diff --git a/src/java/org/apache/poi/ddf/EscherArrayProperty.java b/src/java/org/apache/poi/ddf/EscherArrayProperty.java new file mode 100644 index 0000000000..8979b30a29 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherArrayProperty.java @@ -0,0 +1,162 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.HexDump; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * Escher array properties are the most wierd construction ever invented + * with all sorts of special cases. I'm hopeful I've got them all. + * + * @author Glen Stampoultzis (glens at superlinksoftware.com) + */ +public class EscherArrayProperty + extends EscherComplexProperty +{ + private static final int FIXED_SIZE = 3 * 2; + + public EscherArrayProperty( short id, byte[] complexData ) + { + super( id, checkComplexData(complexData) ); + } + + public EscherArrayProperty( short propertyNumber, boolean isBlipId, byte[] complexData ) + { + super( propertyNumber, isBlipId, checkComplexData(complexData) ); + } + + private static byte[] checkComplexData( byte[] complexData ) + { + if (complexData == null || complexData.length == 0) + complexData = new byte[6]; + + return complexData; + } + + public int getNumberOfElementsInArray() + { + return LittleEndian.getUShort( complexData, 0 ); + } + + public void setNumberOfElementsInArray( int numberOfElements ) + { + int expectedArraySize = numberOfElements * getActualSizeOfElements(getSizeOfElements()) + FIXED_SIZE; + if ( expectedArraySize != complexData.length ) + { + byte[] newArray = new byte[expectedArraySize]; + System.arraycopy( complexData, 0, newArray, 0, complexData.length ); + complexData = newArray; + } + LittleEndian.putShort( complexData, 0, (short) numberOfElements ); + } + + public int getNumberOfElementsInMemory() + { + return LittleEndian.getUShort( complexData, 2 ); + } + + public void setNumberOfElementsInMemory( int numberOfElements ) + { + int expectedArraySize = numberOfElements * getActualSizeOfElements(getSizeOfElements()) + FIXED_SIZE; + if ( expectedArraySize != complexData.length ) + { + byte[] newArray = new byte[expectedArraySize]; + System.arraycopy( complexData, 0, newArray, 0, expectedArraySize ); + complexData = newArray; + } + LittleEndian.putShort( complexData, 2, (short) numberOfElements ); + } + + public short getSizeOfElements() + { + return LittleEndian.getShort( complexData, 4 ); + } + + public void setSizeOfElements( int sizeOfElements ) + { + LittleEndian.putShort( complexData, 4, (short) sizeOfElements ); + + int expectedArraySize = getNumberOfElementsInArray() * getActualSizeOfElements(getSizeOfElements()) + FIXED_SIZE; + if ( expectedArraySize != complexData.length ) + { + // Keep just the first 6 bytes. The rest is no good to us anyway. + byte[] newArray = new byte[expectedArraySize]; + System.arraycopy( complexData, 0, newArray, 0, 6 ); + complexData = newArray; + } + } + + public byte[] getElement( int index ) + { + int actualSize = getActualSizeOfElements(getSizeOfElements()); + byte[] result = new byte[actualSize]; + System.arraycopy(complexData, FIXED_SIZE + index * actualSize, result, 0, result.length ); + return result; + } + + public void setElement( int index, byte[] element ) + { + int actualSize = getActualSizeOfElements(getSizeOfElements()); + System.arraycopy( element, 0, complexData, FIXED_SIZE + index * actualSize, actualSize); + } + + public String toString() + { + String nl = System.getProperty("line.separator"); + + StringBuffer results = new StringBuffer(); + results.append(" {EscherArrayProperty:" + nl); + results.append(" Num Elements: " + getNumberOfElementsInArray() + nl); + results.append(" Num Elements In Memory: " + getNumberOfElementsInMemory() + nl); + results.append(" Size of elements: " + getSizeOfElements() + nl); + for (int i = 0; i < getNumberOfElementsInArray(); i++) + { + results.append(" Element " + i + ": " + HexDump.toHex(getElement(i)) + nl); + } + results.append("}" + nl); + + return "propNum: " + getPropertyNumber() + + ", propName: " + EscherProperties.getPropertyName( getPropertyNumber() ) + + ", complex: " + isComplex() + + ", blipId: " + isBlipId() + + ", data: " + nl + results.toString(); + } + + /** + * We have this method because the way in which arrays in escher works + * is screwed for seemly arbitary reasons. While most properties are + * fairly consistent and have a predictable array size, escher arrays + * have special cases. + * + * @param data The data array containing the escher array information + * @param offset The offset into the array to start reading from. + * @return the number of bytes used by this complex property. + */ + public int setArrayData( byte[] data, int offset ) + { + short numElements = LittleEndian.getShort(data, offset); + short numReserved = LittleEndian.getShort(data, offset + 2); + short sizeOfElements = LittleEndian.getShort(data, offset + 4); + + int arraySize = getActualSizeOfElements(sizeOfElements) * numElements; + if (arraySize == complexData.length) + complexData = new byte[arraySize + 6]; // Calculation missing the header for some reason + System.arraycopy(data, offset, complexData, 0, complexData.length ); + return complexData.length; + } + + /** + * Sometimes the element size is stored as a negative number. We + * negate it and shift it to get the real value. + */ + public static int getActualSizeOfElements(short sizeOfElements) + { + if (sizeOfElements < 0) + return (short) ( ( -sizeOfElements ) >> 2 ); + else + return sizeOfElements; + } + +} diff --git a/src/java/org/apache/poi/ddf/EscherBSERecord.java b/src/java/org/apache/poi/ddf/EscherBSERecord.java new file mode 100644 index 0000000000..2d170ab070 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherBSERecord.java @@ -0,0 +1,383 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.HexDump; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; + +/** + * The BSE record is related closely to the EscherBlipRecord and stores + * extra information about the blip. + * + * @author Glen Stampoultzis + * @see EscherBlipRecord + */ +public class EscherBSERecord + extends EscherRecord +{ + public static final short RECORD_ID = (short) 0xF007; + public static final String RECORD_DESCRIPTION = "MsofbtBSE"; + + public static final byte BT_ERROR = 0; + public static final byte BT_UNKNOWN = 1; + public static final byte BT_EMF = 2; + public static final byte BT_WMF = 3; + public static final byte BT_PICT = 4; + public static final byte BT_JPEG = 5; + public static final byte BT_PNG = 6; + public static final byte BT_DIB = 7; + + private byte field_1_blipTypeWin32; + private byte field_2_blipTypeMacOS; + private byte[] field_3_uid; // 16 bytes + private short field_4_tag; + private int field_5_size; + private int field_6_ref; + private int field_7_offset; + private byte field_8_usage; + private byte field_9_name; + private byte field_10_unused2; + private byte field_11_unused3; + + private byte[] remainingData; + + /** + * This method deserializes the record from a byte array. + * + * @param data The byte array containing the escher record information + * @param offset The starting offset into data. + * @param recordFactory May be null since this is not a container record. + * @return The number of bytes read from the byte array. + */ + public int fillFields( byte[] data, int offset, + EscherRecordFactory recordFactory + ) + { + int bytesRemaining = readHeader( data, offset ); + int pos = offset + 8; + field_1_blipTypeWin32 = data[pos]; + field_2_blipTypeMacOS = data[pos + 1]; + System.arraycopy( data, pos + 2, field_3_uid = new byte[16], 0, 16 ); + field_4_tag = LittleEndian.getShort( data, pos + 18 ); + field_5_size = LittleEndian.getInt( data, pos + 20 ); + field_6_ref = LittleEndian.getInt( data, pos + 24 ); + field_7_offset = LittleEndian.getInt( data, pos + 28 ); + field_8_usage = data[pos + 32]; + field_9_name = data[pos + 33]; + field_10_unused2 = data[pos + 34]; + field_11_unused3 = data[pos + 35]; + bytesRemaining -= 36; + remainingData = new byte[bytesRemaining]; + System.arraycopy( data, pos + 36, remainingData, 0, bytesRemaining ); + return bytesRemaining + 8 + 36; + + } + + /** + * This method serializes this escher record into a byte array. + * + * @param offset The offset into data to start writing the record data to. + * @param data The byte array to serialize to. + * @param listener A listener to retrieve start and end callbacks. Use a NullEscherSerailizationListener to ignore these events. + * @return The number of bytes written. + * @see NullEscherSerializationListener + */ + public int serialize( int offset, byte[] data, EscherSerializationListener listener ) + { + listener.beforeRecordSerialize( offset, getRecordId(), this ); + + LittleEndian.putShort( data, offset, getOptions() ); + LittleEndian.putShort( data, offset + 2, getRecordId() ); + int remainingBytes = remainingData.length + 36; + LittleEndian.putInt( data, offset + 4, remainingBytes ); + + data[offset + 8] = field_1_blipTypeWin32; + data[offset + 9] = field_2_blipTypeMacOS; + for ( int i = 0; i < 16; i++ ) + data[offset + 10 + i] = field_3_uid[i]; + LittleEndian.putShort( data, offset + 26, field_4_tag ); + LittleEndian.putInt( data, offset + 28, field_5_size ); + LittleEndian.putInt( data, offset + 32, field_6_ref ); + LittleEndian.putInt( data, offset + 36, field_7_offset ); + data[offset + 40] = field_8_usage; + data[offset + 41] = field_9_name; + data[offset + 42] = field_10_unused2; + data[offset + 43] = field_11_unused3; + System.arraycopy( remainingData, 0, data, offset + 44, remainingData.length ); + int pos = offset + 8 + 36 + remainingData.length; + + listener.afterRecordSerialize(pos, getRecordId(), pos - offset, this); + return pos - offset; + } + + /** + * Returns the number of bytes that are required to serialize this record. + * + * @return Number of bytes + */ + public int getRecordSize() + { + return 8 + 1 + 1 + 16 + 2 + 4 + 4 + 4 + 1 + 1 + 1 + 1 + remainingData.length; + } + + /** + * The short name for this record + */ + public String getRecordName() + { + return "BSE"; + } + + /** + * The expected blip type under windows (failure to match this blip type will result in + * Excel converting to this format). + */ + public byte getBlipTypeWin32() + { + return field_1_blipTypeWin32; + } + + /** + * Set the expected win32 blip type + */ + public void setBlipTypeWin32( byte blipTypeWin32 ) + { + this.field_1_blipTypeWin32 = blipTypeWin32; + } + + /** + * The expected blip type under MacOS (failure to match this blip type will result in + * Excel converting to this format). + */ + public byte getBlipTypeMacOS() + { + return field_2_blipTypeMacOS; + } + + /** + * Set the expected MacOS blip type + */ + public void setBlipTypeMacOS( byte blipTypeMacOS ) + { + this.field_2_blipTypeMacOS = blipTypeMacOS; + } + + /** + * 16 byte MD4 checksum. + */ + public byte[] getUid() + { + return field_3_uid; + } + + /** + * 16 byte MD4 checksum. + */ + public void setUid( byte[] uid ) + { + this.field_3_uid = uid; + } + + /** + * unused + */ + public short getTag() + { + return field_4_tag; + } + + /** + * unused + */ + public void setTag( short tag ) + { + this.field_4_tag = tag; + } + + /** + * Blip size in stream. + */ + public int getSize() + { + return field_5_size; + } + + /** + * Blip size in stream. + */ + public void setSize( int size ) + { + this.field_5_size = size; + } + + /** + * The reference count of this blip. + */ + public int getRef() + { + return field_6_ref; + } + + /** + * The reference count of this blip. + */ + public void setRef( int ref ) + { + this.field_6_ref = ref; + } + + /** + * File offset in the delay stream. + */ + public int getOffset() + { + return field_7_offset; + } + + /** + * File offset in the delay stream. + */ + public void setOffset( int offset ) + { + this.field_7_offset = offset; + } + + /** + * Defines the way this blip is used. + */ + public byte getUsage() + { + return field_8_usage; + } + + /** + * Defines the way this blip is used. + */ + public void setUsage( byte usage ) + { + this.field_8_usage = usage; + } + + /** + * The length in characters of the blip name. + */ + public byte getName() + { + return field_9_name; + } + + /** + * The length in characters of the blip name. + */ + public void setName( byte name ) + { + this.field_9_name = name; + } + + public byte getUnused2() + { + return field_10_unused2; + } + + public void setUnused2( byte unused2 ) + { + this.field_10_unused2 = unused2; + } + + public byte getUnused3() + { + return field_11_unused3; + } + + public void setUnused3( byte unused3 ) + { + this.field_11_unused3 = unused3; + } + + /** + * Any remaining data in this record. + */ + public byte[] getRemainingData() + { + return remainingData; + } + + /** + * Any remaining data in this record. + */ + public void setRemainingData( byte[] remainingData ) + { + this.remainingData = remainingData; + } + + /** + * Calculate the string representation of this object + */ + public String toString() + { + String nl = System.getProperty( "line.separator" ); + + String extraData; + ByteArrayOutputStream b = new ByteArrayOutputStream(); + try + { + HexDump.dump( this.remainingData, 0, b, 0 ); + extraData = b.toString(); + } + catch ( Exception e ) + { + extraData = e.toString(); + } + return getClass().getName() + ":" + nl + + " RecordId: 0x" + HexDump.toHex( RECORD_ID ) + nl + + " Options: 0x" + HexDump.toHex( getOptions() ) + nl + + " BlipTypeWin32: " + field_1_blipTypeWin32 + nl + + " BlipTypeMacOS: " + field_2_blipTypeMacOS + nl + + " SUID: " + HexDump.toHex(field_3_uid) + nl + + " Tag: " + field_4_tag + nl + + " Size: " + field_5_size + nl + + " Ref: " + field_6_ref + nl + + " Offset: " + field_7_offset + nl + + " Usage: " + field_8_usage + nl + + " Name: " + field_9_name + nl + + " Unused2: " + field_10_unused2 + nl + + " Unused3: " + field_11_unused3 + nl + + " Extra Data:" + nl + extraData; + + + } + + /** + * Retrieve the string representation given a blip id. + */ + public String getBlipType( byte b ) + { + switch ( b ) + { + case BT_ERROR: + return " ERROR"; + case BT_UNKNOWN: + return " UNKNOWN"; + case BT_EMF: + return " EMF"; + case BT_WMF: + return " WMF"; + case BT_PICT: + return " PICT"; + case BT_JPEG: + return " JPEG"; + case BT_PNG: + return " PNG"; + case BT_DIB: + return " DIB"; + default: + if ( b < 32 ) + return " NotKnown"; + else + return " Client"; + } + } + + +} diff --git a/src/java/org/apache/poi/ddf/EscherBlipRecord.java b/src/java/org/apache/poi/ddf/EscherBlipRecord.java new file mode 100644 index 0000000000..ca8be718b4 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherBlipRecord.java @@ -0,0 +1,417 @@ +package org.apache.poi.ddf; + +import org.apache.poi.hssf.record.RecordFormatException; +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.IOException; +import java.util.zip.InflaterInputStream; +import java.util.zip.DeflaterOutputStream; + +/** + * The blip record is used to hold details about large binary objects that occur in escher such + * as JPEG, GIF, PICT and WMF files. The contents of the stream is usually compressed. Inflate + * can be used to decompress the data. + * + * @author Glen Stampoultzis + * @see java.util.zip.Inflater + */ +public class EscherBlipRecord + extends EscherRecord +{ + public static final short RECORD_ID_START = (short) 0xF018; + public static final short RECORD_ID_END = (short) 0xF117; + public static final String RECORD_DESCRIPTION = "msofbtBlip"; + private static final int HEADER_SIZE = 8; + + private byte[] field_1_secondaryUID; + private int field_2_cacheOfSize; + private int field_3_boundaryTop; + private int field_4_boundaryLeft; + private int field_5_boundaryWidth; + private int field_6_boundaryHeight; + private int field_7_width; + private int field_8_height; + private int field_9_cacheOfSavedSize; + private byte field_10_compressionFlag; + private byte field_11_filter; + private byte[] field_12_data; + + + /** + * This method deserializes the record from a byte array. + * + * @param data The byte array containing the escher record information + * @param offset The starting offset into data. + * @param recordFactory May be null since this is not a container record. + * @return The number of bytes read from the byte array. + */ + public int fillFields( byte[] data, int offset, + EscherRecordFactory recordFactory + ) + { + int bytesAfterHeader = readHeader( data, offset ); + int pos = offset + HEADER_SIZE; + + int size = 0; + field_1_secondaryUID = new byte[16]; + System.arraycopy( data, pos + size, field_1_secondaryUID, 0, 16 ); size += 16; + field_2_cacheOfSize = LittleEndian.getInt( data, pos + size );size+=4; + field_3_boundaryTop = LittleEndian.getInt( data, pos + size );size+=4; + field_4_boundaryLeft = LittleEndian.getInt( data, pos + size );size+=4; + field_5_boundaryWidth = LittleEndian.getInt( data, pos + size );size+=4; + field_6_boundaryHeight = LittleEndian.getInt( data, pos + size );size+=4; + field_7_width = LittleEndian.getInt( data, pos + size );size+=4; + field_8_height = LittleEndian.getInt( data, pos + size );size+=4; + field_9_cacheOfSavedSize = LittleEndian.getInt( data, pos + size );size+=4; + field_10_compressionFlag = data[pos + size]; size++; + field_11_filter = data[pos + size]; size++; + + int bytesRemaining = bytesAfterHeader - size; + field_12_data = new byte[bytesRemaining]; + System.arraycopy(data, pos + size, field_12_data, 0, bytesRemaining); + + return bytesRemaining + HEADER_SIZE + bytesAfterHeader; + } + + + /** + * This method serializes this escher record into a byte array. + * + * @param offset The offset into data to start writing the record data to. + * @param data The byte array to serialize to. + * @param listener A listener to retrieve start and end callbacks. Use a NullEscherSerailizationListener to ignore these events. + * @return The number of bytes written. + * + * @see NullEscherSerializationListener + */ + public int serialize( int offset, byte[] data, EscherSerializationListener listener ) + { + listener.beforeRecordSerialize(offset, getRecordId(), this); + + LittleEndian.putShort( data, offset, getOptions() ); + LittleEndian.putShort( data, offset + 2, getRecordId() ); + int remainingBytes = field_12_data.length + 36; + LittleEndian.putInt( data, offset + 4, remainingBytes ); + + int pos = offset + HEADER_SIZE; + System.arraycopy(field_1_secondaryUID, 0, data, pos, 16 ); pos += 16; + LittleEndian.putInt( data, pos, field_2_cacheOfSize); pos += 4; + LittleEndian.putInt( data, pos, field_3_boundaryTop); pos += 4; + LittleEndian.putInt( data, pos, field_4_boundaryLeft); pos += 4; + LittleEndian.putInt( data, pos, field_5_boundaryWidth); pos += 4; + LittleEndian.putInt( data, pos, field_6_boundaryHeight); pos += 4; + LittleEndian.putInt( data, pos, field_7_width); pos += 4; + LittleEndian.putInt( data, pos, field_8_height); pos += 4; + LittleEndian.putInt( data, pos, field_9_cacheOfSavedSize); pos += 4; + data[pos++] = field_10_compressionFlag; + data[pos++] = field_11_filter; + System.arraycopy(field_12_data, 0, data, pos, field_12_data.length); pos += field_12_data.length; + + listener.afterRecordSerialize(pos, getRecordId(), pos - offset, this); + return pos - offset; + } + + /** + * Returns the number of bytes that are required to serialize this record. + * + * @return Number of bytes + */ + public int getRecordSize() + { + return 58 + field_12_data.length; + } + + /** + * The short name for this record + */ + public String getRecordName() + { + return "Blip"; + } + + /** + * Retrieve the secondary UID + */ + public byte[] getSecondaryUID() + { + return field_1_secondaryUID; + } + + /** + * Set the secondary UID + */ + public void setSecondaryUID( byte[] field_1_secondaryUID ) + { + this.field_1_secondaryUID = field_1_secondaryUID; + } + + /** + * Retrieve the cache of the metafile size + */ + public int getCacheOfSize() + { + return field_2_cacheOfSize; + } + + /** + * Set the cache of the metafile size + */ + public void setCacheOfSize( int field_2_cacheOfSize ) + { + this.field_2_cacheOfSize = field_2_cacheOfSize; + } + + /** + * Retrieve the top boundary of the metafile drawing commands + */ + public int getBoundaryTop() + { + return field_3_boundaryTop; + } + + /** + * Set the top boundary of the metafile drawing commands + */ + public void setBoundaryTop( int field_3_boundaryTop ) + { + this.field_3_boundaryTop = field_3_boundaryTop; + } + + /** + * Retrieve the left boundary of the metafile drawing commands + */ + public int getBoundaryLeft() + { + return field_4_boundaryLeft; + } + + /** + * Set the left boundary of the metafile drawing commands + */ + public void setBoundaryLeft( int field_4_boundaryLeft ) + { + this.field_4_boundaryLeft = field_4_boundaryLeft; + } + + /** + * Retrieve the boundary width of the metafile drawing commands + */ + public int getBoundaryWidth() + { + return field_5_boundaryWidth; + } + + /** + * Set the boundary width of the metafile drawing commands + */ + public void setBoundaryWidth( int field_5_boundaryWidth ) + { + this.field_5_boundaryWidth = field_5_boundaryWidth; + } + + /** + * Retrieve the boundary height of the metafile drawing commands + */ + public int getBoundaryHeight() + { + return field_6_boundaryHeight; + } + + /** + * Set the boundary height of the metafile drawing commands + */ + public void setBoundaryHeight( int field_6_boundaryHeight ) + { + this.field_6_boundaryHeight = field_6_boundaryHeight; + } + + /** + * Retrieve the width of the metafile in EMU's (English Metric Units). + */ + public int getWidth() + { + return field_7_width; + } + + /** + * Set the width of the metafile in EMU's (English Metric Units). + */ + public void setWidth( int width ) + { + this.field_7_width = width; + } + + /** + * Retrieve the height of the metafile in EMU's (English Metric Units). + */ + public int getHeight() + { + return field_8_height; + } + + /** + * Set the height of the metafile in EMU's (English Metric Units). + */ + public void setHeight( int height ) + { + this.field_8_height = height; + } + + /** + * Retrieve the cache of the saved size + */ + public int getCacheOfSavedSize() + { + return field_9_cacheOfSavedSize; + } + + /** + * Set the cache of the saved size + */ + public void setCacheOfSavedSize( int field_9_cacheOfSavedSize ) + { + this.field_9_cacheOfSavedSize = field_9_cacheOfSavedSize; + } + + /** + * Is the contents of the blip compressed? + */ + public byte getCompressionFlag() + { + return field_10_compressionFlag; + } + + /** + * Set whether the contents of the blip is compressed + */ + public void setCompressionFlag( byte field_10_compressionFlag ) + { + this.field_10_compressionFlag = field_10_compressionFlag; + } + + /** + * Filter should always be 0 + */ + public byte getFilter() + { + return field_11_filter; + } + + /** + * Filter should always be 0 + */ + public void setFilter( byte field_11_filter ) + { + this.field_11_filter = field_11_filter; + } + + /** + * The BLIP data + */ + public byte[] getData() + { + return field_12_data; + } + + /** + * The BLIP data + */ + public void setData( byte[] field_12_data ) + { + this.field_12_data = field_12_data; + } + + /** + * The string representation of this record. + * + * @return A string + */ + public String toString() + { + String nl = System.getProperty( "line.separator" ); + + String extraData; + ByteArrayOutputStream b = new ByteArrayOutputStream(); + try + { + HexDump.dump( this.field_12_data, 0, b, 0 ); + extraData = b.toString(); + } + catch ( Exception e ) + { + extraData = e.toString(); + } + return getClass().getName() + ":" + nl + + " RecordId: 0x" + HexDump.toHex( getRecordId() ) + nl + + " Options: 0x" + HexDump.toHex( getOptions() ) + nl + + " Secondary UID: " + HexDump.toHex( field_1_secondaryUID ) + nl + + " CacheOfSize: " + field_2_cacheOfSize + nl + + " BoundaryTop: " + field_3_boundaryTop + nl + + " BoundaryLeft: " + field_4_boundaryLeft + nl + + " BoundaryWidth: " + field_5_boundaryWidth + nl + + " BoundaryHeight: " + field_6_boundaryHeight + nl + + " X: " + field_7_width + nl + + " Y: " + field_8_height + nl + + " CacheOfSavedSize: " + field_9_cacheOfSavedSize + nl + + " CompressionFlag: " + field_10_compressionFlag + nl + + " Filter: " + field_11_filter + nl + + " Data:" + nl + extraData; + } + + /** + * Compress the contents of the provided array + * + * @param data An uncompressed byte array + * @see DeflaterOutputStream#write(int b) + */ + public static byte[] compress( byte[] data ) + { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream( out ); + try + { + for ( int i = 0; i < data.length; i++ ) + deflaterOutputStream.write( data[i] ); + } + catch ( IOException e ) + { + throw new RecordFormatException( e.toString() ); + } + + return out.toByteArray(); + } + + /** + * Decompresses a byte array. + * + * @param data The compressed byte array + * @param pos The starting position into the byte array + * @param length The number of compressed bytes to decompress + * @return An uncompressed byte array + * @see InflaterInputStream#read + */ + public static byte[] decompress( byte[] data, int pos, int length ) + { + byte[] compressedData = new byte[length]; + System.arraycopy( data, pos + 50, compressedData, 0, length ); + InputStream compressedInputStream = new ByteArrayInputStream( compressedData ); + InflaterInputStream inflaterInputStream = new InflaterInputStream( compressedInputStream ); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int c; + try + { + while ( ( c = inflaterInputStream.read() ) != -1 ) + out.write( c ); + } + catch ( IOException e ) + { + throw new RecordFormatException( e.toString() ); + } + return out.toByteArray(); + } + +} diff --git a/src/java/org/apache/poi/ddf/EscherBoolProperty.java b/src/java/org/apache/poi/ddf/EscherBoolProperty.java new file mode 100644 index 0000000000..86a7a0a5c5 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherBoolProperty.java @@ -0,0 +1,50 @@ +package org.apache.poi.ddf; + +/** + * Represents a boolean property. The actual utility of this property is in doubt because many + * of the properties marked as boolean seem to actually contain special values. In other words + * they're not true booleans. + * + * @author Glen Stampoultzis + * @see EscherSimpleProperty + * @see EscherProperty + */ +public class EscherBoolProperty + extends EscherSimpleProperty +{ + /** + * Create an instance of an escher boolean property. + * + * @param propertyNumber The property number + * @param value The 32 bit value of this bool property + */ + public EscherBoolProperty( short propertyNumber, int value ) + { + super( propertyNumber, false, false, value ); + } + + /** + * Whether this boolean property is true + */ + public boolean isTrue() + { + return propertyValue != 0; + } + + /** + * Whether this boolean property is false + */ + public boolean isFalse() + { + return propertyValue == 0; + } + +// public String toString() +// { +// return "propNum: " + getPropertyNumber() +// + ", complex: " + isComplex() +// + ", blipId: " + isBlipId() +// + ", value: " + (getValue() != 0); +// } + +} diff --git a/src/java/org/apache/poi/ddf/EscherChildAnchorRecord.java b/src/java/org/apache/poi/ddf/EscherChildAnchorRecord.java new file mode 100644 index 0000000000..f53ba7d726 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherChildAnchorRecord.java @@ -0,0 +1,176 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; + +/** + * The escher child achor record is used to specify the position of a shape under an + * existing group. The first level of shape records use a EscherClientAnchor record instead. + * + * @author Glen Stampoultzis + * @see EscherChildAnchorRecord + */ +public class EscherChildAnchorRecord + extends EscherRecord +{ + public static final short RECORD_ID = (short) 0xF00F; + public static final String RECORD_DESCRIPTION = "MsofbtChildAnchor"; + + private int field_1_dx1; + private int field_2_dy1; + private int field_3_dx2; + private int field_4_dy2; + + /** + * This method deserializes the record from a byte array. + * + * @param data The byte array containing the escher record information + * @param offset The starting offset into data. + * @param recordFactory May be null since this is not a container record. + * @return The number of bytes read from the byte array. + */ + public int fillFields( byte[] data, int offset, EscherRecordFactory recordFactory ) + { + int bytesRemaining = readHeader( data, offset ); + int pos = offset + 8; + int size = 0; + field_1_dx1 = LittleEndian.getInt( data, pos + size );size+=4; + field_2_dy1 = LittleEndian.getInt( data, pos + size );size+=4; + field_3_dx2 = LittleEndian.getInt( data, pos + size );size+=4; + field_4_dy2 = LittleEndian.getInt( data, pos + size );size+=4; + return 8 + size; + } + + /** + * This method serializes this escher record into a byte array. + * + * @param offset The offset into data to start writing the record data to. + * @param data The byte array to serialize to. + * @param listener A listener to retrieve start and end callbacks. Use a NullEscherSerailizationListener to ignore these events. + * @return The number of bytes written. + * @see NullEscherSerializationListener + */ + public int serialize( int offset, byte[] data, EscherSerializationListener listener ) + { + listener.beforeRecordSerialize( offset, getRecordId(), this ); + int pos = offset; + LittleEndian.putShort( data, pos, getOptions() ); pos += 2; + LittleEndian.putShort( data, pos, getRecordId() ); pos += 2; + LittleEndian.putInt( data, pos, getRecordSize()-8 ); pos += 4; + LittleEndian.putInt( data, pos, field_1_dx1 ); pos += 4; + LittleEndian.putInt( data, pos, field_2_dy1 ); pos += 4; + LittleEndian.putInt( data, pos, field_3_dx2 ); pos += 4; + LittleEndian.putInt( data, pos, field_4_dy2 ); pos += 4; + + listener.afterRecordSerialize( pos, getRecordId(), pos - offset, this ); + return pos - offset; + } + + /** + * Returns the number of bytes that are required to serialize this record. + * + * @return Number of bytes + */ + public int getRecordSize() + { + return 8 + 4 * 4; + } + + /** + * The record id for the EscherChildAnchorRecord. + */ + public short getRecordId() + { + return RECORD_ID; + } + + /** + * The short name for this record + */ + public String getRecordName() + { + return "ChildAnchor"; + } + + /** + * The string representation of this record + */ + public String toString() + { + String nl = System.getProperty("line.separator"); + + return getClass().getName() + ":" + nl + + " RecordId: 0x" + HexDump.toHex(RECORD_ID) + nl + + " Options: 0x" + HexDump.toHex(getOptions()) + nl + + " X1: " + field_1_dx1 + nl + + " Y1: " + field_2_dy1 + nl + + " X2: " + field_3_dx2 + nl + + " Y2: " + field_4_dy2 + nl ; + + } + + /** + * Retrieves offset within the parent coordinate space for the top left point. + */ + public int getDx1() + { + return field_1_dx1; + } + + /** + * Sets offset within the parent coordinate space for the top left point. + */ + public void setDx1( int field_1_dx1 ) + { + this.field_1_dx1 = field_1_dx1; + } + + /** + * Gets offset within the parent coordinate space for the top left point. + */ + public int getDy1() + { + return field_2_dy1; + } + + /** + * Sets offset within the parent coordinate space for the top left point. + */ + public void setDy1( int field_2_dy1 ) + { + this.field_2_dy1 = field_2_dy1; + } + + /** + * Retrieves offset within the parent coordinate space for the bottom right point. + */ + public int getDx2() + { + return field_3_dx2; + } + + /** + * Sets offset within the parent coordinate space for the bottom right point. + */ + public void setDx2( int field_3_dx2 ) + { + this.field_3_dx2 = field_3_dx2; + } + + /** + * Gets the offset within the parent coordinate space for the bottom right point. + */ + public int getDy2() + { + return field_4_dy2; + } + + /** + * Sets the offset within the parent coordinate space for the bottom right point. + */ + public void setDy2( int field_4_dy2 ) + { + this.field_4_dy2 = field_4_dy2; + } + +} diff --git a/src/java/org/apache/poi/ddf/EscherClientAnchorRecord.java b/src/java/org/apache/poi/ddf/EscherClientAnchorRecord.java new file mode 100644 index 0000000000..d61d77b7a7 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherClientAnchorRecord.java @@ -0,0 +1,317 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; + +import java.io.ByteArrayOutputStream; + +/** + * The escher client anchor specifies which rows and cells the shape is bound to as well as + * the offsets within those cells. Each cell is 1024 units wide by 256 units long regardless + * of the actual size of the cell. The EscherClientAnchorRecord only applies to the top-most + * shapes. Shapes contained in groups are bound using the EscherChildAnchorRecords. + * + * @author Glen Stampoultzis + * @see EscherChildAnchorRecord + */ +public class EscherClientAnchorRecord + extends EscherRecord +{ + public static final short RECORD_ID = (short) 0xF010; + public static final String RECORD_DESCRIPTION = "MsofbtClientAnchor"; + + private short field_1_flag; + private short field_2_col1; + private short field_3_dx1; + private short field_4_row1; + private short field_5_dy1; + private short field_6_col2; + private short field_7_dx2; + private short field_8_row2; + private short field_9_dy2; + private byte[] remainingData; + + /** + * This method deserializes the record from a byte array. + * + * @param data The byte array containing the escher record information + * @param offset The starting offset into data. + * @param recordFactory May be null since this is not a container record. + * @return The number of bytes read from the byte array. + */ + public int fillFields( byte[] data, int offset, EscherRecordFactory recordFactory ) + { + int bytesRemaining = readHeader( data, offset ); + int pos = offset + 8; + int size = 0; + field_1_flag = LittleEndian.getShort( data, pos + size ); size += 2; + field_2_col1 = LittleEndian.getShort( data, pos + size ); size += 2; + field_3_dx1 = LittleEndian.getShort( data, pos + size ); size += 2; + field_4_row1 = LittleEndian.getShort( data, pos + size ); size += 2; + field_5_dy1 = LittleEndian.getShort( data, pos + size ); size += 2; + field_6_col2 = LittleEndian.getShort( data, pos + size ); size += 2; + field_7_dx2 = LittleEndian.getShort( data, pos + size ); size += 2; + field_8_row2 = LittleEndian.getShort( data, pos + size ); size += 2; + field_9_dy2 = LittleEndian.getShort( data, pos + size ); size += 2; + bytesRemaining -= size; + remainingData = new byte[bytesRemaining]; + System.arraycopy( data, pos + size, remainingData, 0, bytesRemaining ); + return 8 + size + bytesRemaining; + } + + /** + * This method serializes this escher record into a byte array. + * + * @param offset The offset into data to start writing the record data to. + * @param data The byte array to serialize to. + * @param listener A listener to retrieve start and end callbacks. Use a NullEscherSerailizationListener to ignore these events. + * @return The number of bytes written. + * @see NullEscherSerializationListener + */ + public int serialize( int offset, byte[] data, EscherSerializationListener listener ) + { + listener.beforeRecordSerialize( offset, getRecordId(), this ); + + if (remainingData == null) remainingData = new byte[0]; + LittleEndian.putShort( data, offset, getOptions() ); + LittleEndian.putShort( data, offset + 2, getRecordId() ); + int remainingBytes = remainingData.length + 18; + LittleEndian.putInt( data, offset + 4, remainingBytes ); + LittleEndian.putShort( data, offset + 8, field_1_flag ); + LittleEndian.putShort( data, offset + 10, field_2_col1 ); + LittleEndian.putShort( data, offset + 12, field_3_dx1 ); + LittleEndian.putShort( data, offset + 14, field_4_row1 ); + LittleEndian.putShort( data, offset + 16, field_5_dy1 ); + LittleEndian.putShort( data, offset + 18, field_6_col2 ); + LittleEndian.putShort( data, offset + 20, field_7_dx2 ); + LittleEndian.putShort( data, offset + 22, field_8_row2 ); + LittleEndian.putShort( data, offset + 24, field_9_dy2 ); + System.arraycopy( remainingData, 0, data, offset + 26, remainingData.length ); + int pos = offset + 8 + 18 + remainingData.length; + + listener.afterRecordSerialize( pos, getRecordId(), pos - offset, this ); + return pos - offset; + } + + /** + * Returns the number of bytes that are required to serialize this record. + * + * @return Number of bytes + */ + public int getRecordSize() + { + return 8 + 18 + (remainingData == null ? 0 : remainingData.length); + } + + /** + * The record id for this record. + */ + public short getRecordId() + { + return RECORD_ID; + } + + /** + * The short name for this record + */ + public String getRecordName() + { + return "ClientAnchor"; + } + + /** + * Returns the string representation for this record. + * + * @return A string + */ + public String toString() + { + String nl = System.getProperty("line.separator"); + + String extraData; + ByteArrayOutputStream b = new ByteArrayOutputStream(); + try + { + HexDump.dump(this.remainingData, 0, b, 0); + extraData = b.toString(); + } + catch ( Exception e ) + { + extraData = "error"; + } + return getClass().getName() + ":" + nl + + " RecordId: 0x" + HexDump.toHex(RECORD_ID) + nl + + " Options: 0x" + HexDump.toHex(getOptions()) + nl + + " Flag: " + field_1_flag + nl + + " Col1: " + field_2_col1 + nl + + " DX1: " + field_3_dx1 + nl + + " Row1: " + field_4_row1 + nl + + " DY1: " + field_5_dy1 + nl + + " Col2: " + field_6_col2 + nl + + " DX2: " + field_7_dx2 + nl + + " Row2: " + field_8_row2 + nl + + " DY2: " + field_9_dy2 + nl + + " Extra Data:" + nl + extraData; + + } + + /** + * 0 = Move and size with Cells, 2 = Move but don't size with cells, 3 = Don't move or size with cells. + */ + public short getFlag() + { + return field_1_flag; + } + + /** + * 0 = Move and size with Cells, 2 = Move but don't size with cells, 3 = Don't move or size with cells. + */ + public void setFlag( short field_1_flag ) + { + this.field_1_flag = field_1_flag; + } + + /** + * The column number for the top-left position. 0 based. + */ + public short getCol1() + { + return field_2_col1; + } + + /** + * The column number for the top-left position. 0 based. + */ + public void setCol1( short field_2_col1 ) + { + this.field_2_col1 = field_2_col1; + } + + /** + * The x offset within the top-left cell. Range is from 0 to 1023. + */ + public short getDx1() + { + return field_3_dx1; + } + + /** + * The x offset within the top-left cell. Range is from 0 to 1023. + */ + public void setDx1( short field_3_dx1 ) + { + this.field_3_dx1 = field_3_dx1; + } + + /** + * The row number for the top-left corner of the shape. + */ + public short getRow1() + { + return field_4_row1; + } + + /** + * The row number for the top-left corner of the shape. + */ + public void setRow1( short field_4_row1 ) + { + this.field_4_row1 = field_4_row1; + } + + /** + * The y offset within the top-left corner of the current shape. + */ + public short getDy1() + { + return field_5_dy1; + } + + /** + * The y offset within the top-left corner of the current shape. + */ + public void setDy1( short field_5_dy1 ) + { + this.field_5_dy1 = field_5_dy1; + } + + /** + * The column of the bottom right corner of this shape. + */ + public short getCol2() + { + return field_6_col2; + } + + /** + * The column of the bottom right corner of this shape. + */ + public void setCol2( short field_6_col2 ) + { + this.field_6_col2 = field_6_col2; + } + + /** + * The x offset withing the cell for the bottom-right corner of this shape. + */ + public short getDx2() + { + return field_7_dx2; + } + + /** + * The x offset withing the cell for the bottom-right corner of this shape. + */ + public void setDx2( short field_7_dx2 ) + { + this.field_7_dx2 = field_7_dx2; + } + + /** + * The row number for the bottom-right corner of the current shape. + */ + public short getRow2() + { + return field_8_row2; + } + + /** + * The row number for the bottom-right corner of the current shape. + */ + public void setRow2( short field_8_row2 ) + { + this.field_8_row2 = field_8_row2; + } + + /** + * The y offset withing the cell for the bottom-right corner of this shape. + */ + public short getDy2() + { + return field_9_dy2; + } + + /** + * The y offset withing the cell for the bottom-right corner of this shape. + */ + public void setDy2( short field_9_dy2 ) + { + this.field_9_dy2 = field_9_dy2; + } + + /** + * Any remaining data in the record + */ + public byte[] getRemainingData() + { + return remainingData; + } + + /** + * Any remaining data in the record + */ + public void setRemainingData( byte[] remainingData ) + { + this.remainingData = remainingData; + } +} diff --git a/src/java/org/apache/poi/ddf/EscherClientDataRecord.java b/src/java/org/apache/poi/ddf/EscherClientDataRecord.java new file mode 100644 index 0000000000..d568996f47 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherClientDataRecord.java @@ -0,0 +1,130 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; + +import java.io.ByteArrayOutputStream; + +/** + * The EscherClientDataRecord is used to store client specific data about the position of a + * shape within a container. + * + * @author Glen Stampoultzis + */ +public class EscherClientDataRecord + extends EscherRecord +{ + public static final short RECORD_ID = (short) 0xF011; + public static final String RECORD_DESCRIPTION = "MsofbtClientData"; + + private byte[] remainingData; + + /** + * This method deserializes the record from a byte array. + * + * @param data The byte array containing the escher record information + * @param offset The starting offset into data. + * @param recordFactory May be null since this is not a container record. + * @return The number of bytes read from the byte array. + */ + public int fillFields( byte[] data, int offset, EscherRecordFactory recordFactory ) + { + int bytesRemaining = readHeader( data, offset ); + int pos = offset + 8; + remainingData = new byte[bytesRemaining]; + System.arraycopy( data, pos, remainingData, 0, bytesRemaining ); + return 8 + bytesRemaining; + } + + /** + * This method serializes this escher record into a byte array. + * + * @param offset The offset into data to start writing the record data to. + * @param data The byte array to serialize to. + * @param listener A listener to retrieve start and end callbacks. Use a NullEscherSerailizationListener to ignore these events. + * @return The number of bytes written. + * @see NullEscherSerializationListener + */ + public int serialize( int offset, byte[] data, EscherSerializationListener listener ) + { + listener.beforeRecordSerialize( offset, getRecordId(), this ); + + if (remainingData == null) remainingData = new byte[0]; + LittleEndian.putShort( data, offset, getOptions() ); + LittleEndian.putShort( data, offset + 2, getRecordId() ); + LittleEndian.putInt( data, offset + 4, remainingData.length ); + System.arraycopy( remainingData, 0, data, offset + 8, remainingData.length ); + int pos = offset + 8 + remainingData.length; + + listener.afterRecordSerialize( pos, getRecordId(), pos - offset, this ); + return pos - offset; + } + + /** + * Returns the number of bytes that are required to serialize this record. + * + * @return Number of bytes + */ + public int getRecordSize() + { + return 8 + (remainingData == null ? 0 : remainingData.length); + } + + /** + * Returns the identifier of this record. + */ + public short getRecordId() + { + return RECORD_ID; + } + + /** + * The short name for this record + */ + public String getRecordName() + { + return "ClientData"; + } + + /** + * Returns the string representation of this record. + */ + public String toString() + { + String nl = System.getProperty("line.separator"); + + String extraData; + ByteArrayOutputStream b = new ByteArrayOutputStream(); + try + { + HexDump.dump(this.remainingData, 0, b, 0); + extraData = b.toString(); + } + catch ( Exception e ) + { + extraData = "error"; + } + return getClass().getName() + ":" + nl + + " RecordId: 0x" + HexDump.toHex(RECORD_ID) + nl + + " Options: 0x" + HexDump.toHex(getOptions()) + nl + + " Extra Data:" + nl + + extraData; + + } + + /** + * Any data recording this record. + */ + public byte[] getRemainingData() + { + return remainingData; + } + + /** + * Any data recording this record. + */ + public void setRemainingData( byte[] remainingData ) + { + this.remainingData = remainingData; + } +} diff --git a/src/java/org/apache/poi/ddf/EscherComplexProperty.java b/src/java/org/apache/poi/ddf/EscherComplexProperty.java new file mode 100644 index 0000000000..4788a66c71 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherComplexProperty.java @@ -0,0 +1,152 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.HexDump; + +import java.util.Arrays; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * A complex property differs from a simple property in that the data can not fit inside a 32 bit + * integer. See the specification for more detailed information regarding exactly what is + * stored here. + * + * @author Glen Stampoultzis + */ +public class EscherComplexProperty + extends EscherProperty +{ + byte[] complexData = new byte[0]; + + /** + * Create a complex property using the property id and a byte array containing the complex + * data value. + * + * @param id The id consists of the property number, a flag indicating whether this is a blip id and a flag + * indicating that this is a complex property. + * @param complexData The value of this property. + */ + public EscherComplexProperty( short id, byte[] complexData ) + { + super( id ); + this.complexData = complexData; + } + + /** + * Create a complex property using the property number, a flag to indicate whether this is a + * blip reference and the complex property data. + * + * @param propertyNumber The property number + * @param isBlipId Whether this is a blip id. Should be false. + * @param complexData The value of this complex property. + */ + public EscherComplexProperty( short propertyNumber, boolean isBlipId, byte[] complexData ) + { + super( propertyNumber, true, isBlipId ); + this.complexData = complexData; + } + + /** + * Serializes the simple part of this property. ie the first 6 bytes. + */ + public int serializeSimplePart( byte[] data, int pos ) + { + LittleEndian.putShort(data, pos, getId()); + LittleEndian.putInt(data, pos + 2, complexData.length); + return 6; + } + + /** + * Serializes the complex part of this property + * + * @param data The data array to serialize to + * @param pos The offset within data to start serializing to. + * @return The number of bytes serialized. + */ + public int serializeComplexPart( byte[] data, int pos ) + { + System.arraycopy(complexData, 0, data, pos, complexData.length); + return complexData.length; + } + + /** + * Get the complex data value. + */ + public byte[] getComplexData() + { + return complexData; + } + + /** + * Determine whether this property is equal to another property. + * + * @param o The object to compare to. + * @return True if the objects are equal. + */ + public boolean equals( Object o ) + { + if ( this == o ) return true; + if ( !( o instanceof EscherComplexProperty ) ) return false; + + final EscherComplexProperty escherComplexProperty = (EscherComplexProperty) o; + + if ( !Arrays.equals( complexData, escherComplexProperty.complexData ) ) return false; + + return true; + } + + /** + * Caclulates the number of bytes required to serialize this property. + * + * @return Number of bytes + */ + public int getPropertySize() + { + return 6 + complexData.length; + } + + /** + * Calculates a hashcode for this property. + */ + public int hashCode() + { + return getId() * 11; + } + + /** + * Retrieves the string representation for this property. + */ + public String toString() + { + String dataStr; + ByteArrayOutputStream b = new ByteArrayOutputStream(); + try + { + HexDump.dump( this.complexData, 0, b, 0 ); + dataStr = b.toString(); + } + catch ( Exception e ) + { + dataStr = e.toString(); + } + finally + { + try + { + b.close(); + } + catch ( IOException e ) + { + e.printStackTrace(); + } + } + + return "propNum: " + getPropertyNumber() + + ", propName: " + EscherProperties.getPropertyName( getPropertyNumber() ) + + ", complex: " + isComplex() + + ", blipId: " + isBlipId() + + ", data: " + System.getProperty("line.separator") + dataStr; + } + +} diff --git a/src/java/org/apache/poi/ddf/EscherContainerRecord.java b/src/java/org/apache/poi/ddf/EscherContainerRecord.java new file mode 100644 index 0000000000..41c7a7ec9e --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherContainerRecord.java @@ -0,0 +1,168 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.io.PrintWriter; + +/** + * Escher container records store other escher records as children. + * The container records themselves never store any information beyond + * the standard header used by all escher records. This one record is + * used to represent many different types of records. + * + * @author Glen Stampoultzis + */ +public class EscherContainerRecord extends EscherRecord +{ + public static final short DGG_CONTAINER = (short)0xF000; + public static final short BSTORE_CONTAINER = (short)0xF001; + public static final short DG_CONTAINER = (short)0xF002; + public static final short SPGR_CONTAINER = (short)0xF003; + public static final short SP_CONTAINER = (short)0xF004; + public static final short SOLVER_CONTAINER = (short)0xF005; + + private List childRecords = new ArrayList(); + + public int fillFields( byte[] data, int offset, EscherRecordFactory recordFactory ) + { + int bytesRemaining = readHeader( data, offset ); + int bytesWritten = 8; + offset += 8; + while ( bytesRemaining > 0 && offset < data.length ) + { + EscherRecord child = recordFactory.createRecord(data, offset); + int childBytesWritten = child.fillFields( data, offset, recordFactory ); + bytesWritten += childBytesWritten; + offset += childBytesWritten; + bytesRemaining -= childBytesWritten; + getChildRecords().add( child ); + if (offset >= data.length && bytesRemaining > 0) + { + System.out.println("WARNING: " + bytesRemaining + " bytes remaining but no space left"); + } + } + return bytesWritten; + } + + public int serialize( int offset, byte[] data, EscherSerializationListener listener ) + { + listener.beforeRecordSerialize( offset, getRecordId(), this ); + + LittleEndian.putShort(data, offset, getOptions()); + LittleEndian.putShort(data, offset+2, getRecordId()); + int remainingBytes = 0; + for ( Iterator iterator = getChildRecords().iterator(); iterator.hasNext(); ) + { + EscherRecord r = (EscherRecord) iterator.next(); + remainingBytes += r.getRecordSize(); + } + LittleEndian.putInt(data, offset+4, remainingBytes); + int pos = offset+8; + for ( Iterator iterator = getChildRecords().iterator(); iterator.hasNext(); ) + { + EscherRecord r = (EscherRecord) iterator.next(); + pos += r.serialize(pos, data, listener ); + } + + listener.afterRecordSerialize( pos, getRecordId(), pos - offset, this ); + return pos - offset; + } + + public int getRecordSize() + { + int childRecordsSize = 0; + for ( Iterator iterator = getChildRecords().iterator(); iterator.hasNext(); ) + { + EscherRecord r = (EscherRecord) iterator.next(); + childRecordsSize += r.getRecordSize(); + } + return 8 + childRecordsSize; + } + + public List getChildRecords() + { + return childRecords; + } + + public void setChildRecords( List childRecords ) + { + this.childRecords = childRecords; + } + + public String getRecordName() + { + switch ((short)getRecordId()) + { + case DGG_CONTAINER: + return "DggContainer"; + case BSTORE_CONTAINER: + return "BStoreContainer"; + case DG_CONTAINER: + return "DgContainer"; + case SPGR_CONTAINER: + return "SpgrContainer"; + case SP_CONTAINER: + return "SpContainer"; + case SOLVER_CONTAINER: + return "SolverContainer"; + default: + return "Container 0x" + HexDump.toHex(getRecordId()); + } + } + + public void display( PrintWriter w, int indent ) + { + super.display( w, indent ); + for ( Iterator iterator = childRecords.iterator(); iterator.hasNext(); ) + { + EscherRecord escherRecord = (EscherRecord) iterator.next(); + escherRecord.display( w, indent + 1 ); + } + } + + public void addChildRecord( EscherRecord record ) + { + this.childRecords.add( record ); + } + + public String toString() + { + String nl = System.getProperty( "line.separator" ); + + StringBuffer children = new StringBuffer(); + if ( getChildRecords().size() > 0 ) + { + children.append( " children: " + nl ); + for ( Iterator iterator = getChildRecords().iterator(); iterator.hasNext(); ) + { + EscherRecord record = (EscherRecord) iterator.next(); + children.append( record.toString() ); +// children.append( nl ); + } + } + + return getClass().getName() + " (" + getRecordName() + "):" + nl + + " isContainer: " + isContainerRecord() + nl + + " options: 0x" + HexDump.toHex( getOptions() ) + nl + + " recordId: 0x" + HexDump.toHex( getRecordId() ) + nl + + " numchildren: " + getChildRecords().size() + nl + + children.toString(); + + } + + public EscherSpRecord getChildById( short recordId ) + { + for ( Iterator iterator = childRecords.iterator(); iterator.hasNext(); ) + { + EscherRecord escherRecord = (EscherRecord) iterator.next(); + if (escherRecord.getRecordId() == recordId) + return (EscherSpRecord) escherRecord; + } + return null; + } + +} diff --git a/src/java/org/apache/poi/ddf/EscherDgRecord.java b/src/java/org/apache/poi/ddf/EscherDgRecord.java new file mode 100644 index 0000000000..049bf383a6 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherDgRecord.java @@ -0,0 +1,163 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; + +/** + * This record simply holds the number of shapes in the drawing group and the + * last shape id used for this drawing group. + * + * @author Glen Stampoultzis + */ +public class EscherDgRecord + extends EscherRecord +{ + public static final short RECORD_ID = (short) 0xF008; + public static final String RECORD_DESCRIPTION = "MsofbtDg"; + + private int field_1_numShapes; + private int field_2_lastMSOSPID; + + /** + * This method deserializes the record from a byte array. + * + * @param data The byte array containing the escher record information + * @param offset The starting offset into data. + * @param recordFactory May be null since this is not a container record. + * @return The number of bytes read from the byte array. + */ + public int fillFields( byte[] data, int offset, EscherRecordFactory recordFactory ) + { + int bytesRemaining = readHeader( data, offset ); + int pos = offset + 8; + int size = 0; + field_1_numShapes = LittleEndian.getInt( data, pos + size ); size += 4; + field_2_lastMSOSPID = LittleEndian.getInt( data, pos + size ); size += 4; +// bytesRemaining -= size; +// remainingData = new byte[bytesRemaining]; +// System.arraycopy( data, pos + size, remainingData, 0, bytesRemaining ); + return getRecordSize(); + } + + /** + * This method serializes this escher record into a byte array. + * + * @param offset The offset into data to start writing the record data to. + * @param data The byte array to serialize to. + * @param listener A listener to retrieve start and end callbacks. Use a NullEscherSerailizationListener to ignore these events. + * @return The number of bytes written. + * @see NullEscherSerializationListener + */ + public int serialize( int offset, byte[] data, EscherSerializationListener listener ) + { + listener.beforeRecordSerialize( offset, getRecordId(), this ); + + LittleEndian.putShort( data, offset, getOptions() ); + LittleEndian.putShort( data, offset + 2, getRecordId() ); + LittleEndian.putInt( data, offset + 4, 8 ); + LittleEndian.putInt( data, offset + 8, field_1_numShapes ); + LittleEndian.putInt( data, offset + 12, field_2_lastMSOSPID ); +// System.arraycopy( remainingData, 0, data, offset + 26, remainingData.length ); +// int pos = offset + 8 + 18 + remainingData.length; + + listener.afterRecordSerialize( offset + 16, getRecordId(), getRecordSize(), this ); + return getRecordSize(); + } + + /** + * Returns the number of bytes that are required to serialize this record. + * + * @return Number of bytes + */ + public int getRecordSize() + { + return 8 + 8; + } + + public short getRecordId() + { + return RECORD_ID; + } + + /** + * The short name for this record + */ + public String getRecordName() + { + return "Dg"; + } + + /** + * Returns the string representation of this record. + */ + public String toString() + { + String nl = System.getProperty("line.separator"); + +// String extraData; +// ByteArrayOutputStream b = new ByteArrayOutputStream(); +// try +// { +// HexDump.dump(this.remainingData, 0, b, 0); +// extraData = b.toString(); +// } +// catch ( Exception e ) +// { +// extraData = "error"; +// } + return getClass().getName() + ":" + nl + + " RecordId: 0x" + HexDump.toHex(RECORD_ID) + nl + + " Options: 0x" + HexDump.toHex(getOptions()) + nl + + " NumShapes: " + field_1_numShapes + nl + + " LastMSOSPID: " + field_2_lastMSOSPID + nl; + + } + + /** + * The number of shapes in this drawing group. + */ + public int getNumShapes() + { + return field_1_numShapes; + } + + /** + * The number of shapes in this drawing group. + */ + public void setNumShapes( int field_1_numShapes ) + { + this.field_1_numShapes = field_1_numShapes; + } + + /** + * The last shape id used in this drawing group. + */ + public int getLastMSOSPID() + { + return field_2_lastMSOSPID; + } + + /** + * The last shape id used in this drawing group. + */ + public void setLastMSOSPID( int field_2_lastMSOSPID ) + { + this.field_2_lastMSOSPID = field_2_lastMSOSPID; + } + + /** + * Gets the drawing group id for this record. This is encoded in the + * instance part of the option record. + * + * @return a drawing group id. + */ + public short getDrawingGroupId() + { + return (short) ( getOptions() >> 4 ); + } + + public void incrementShapeCount() + { + this.field_1_numShapes++; + } +} diff --git a/src/java/org/apache/poi/ddf/EscherDggRecord.java b/src/java/org/apache/poi/ddf/EscherDggRecord.java new file mode 100644 index 0000000000..1adcf82fc4 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherDggRecord.java @@ -0,0 +1,241 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.hssf.record.RecordFormatException; + +import java.lang.reflect.Array; +import java.util.*; + +/** + * This record defines the drawing groups used for a particular sheet. + */ +public class EscherDggRecord + extends EscherRecord +{ + public static final short RECORD_ID = (short) 0xF006; + public static final String RECORD_DESCRIPTION = "MsofbtDgg"; + + private int field_1_shapeIdMax; +// private int field_2_numIdClusters; // for some reason the number of clusters is actually the real number + 1 + private int field_3_numShapesSaved; + private int field_4_drawingsSaved; + private FileIdCluster[] field_5_fileIdClusters; + + public static class FileIdCluster + { + public FileIdCluster( int drawingGroupId, int numShapeIdsUsed ) + { + this.field_1_drawingGroupId = drawingGroupId; + this.field_2_numShapeIdsUsed = numShapeIdsUsed; + } + + private int field_1_drawingGroupId; + private int field_2_numShapeIdsUsed; + + public int getDrawingGroupId() + { + return field_1_drawingGroupId; + } + + public int getNumShapeIdsUsed() + { + return field_2_numShapeIdsUsed; + } + + public void incrementShapeId( ) + { + this.field_2_numShapeIdsUsed++; + } + } + + /** + * This method deserializes the record from a byte array. + * + * @param data The byte array containing the escher record information + * @param offset The starting offset into data. + * @param recordFactory May be null since this is not a container record. + * @return The number of bytes read from the byte array. + */ + public int fillFields( byte[] data, int offset, EscherRecordFactory recordFactory ) + { + int bytesRemaining = readHeader( data, offset ); + int pos = offset + 8; + int size = 0; + field_1_shapeIdMax = LittleEndian.getInt( data, pos + size );size+=4; + int field_2_numIdClusters = LittleEndian.getInt( data, pos + size );size+=4; + field_3_numShapesSaved = LittleEndian.getInt( data, pos + size );size+=4; + field_4_drawingsSaved = LittleEndian.getInt( data, pos + size );size+=4; + field_5_fileIdClusters = new FileIdCluster[field_2_numIdClusters-1]; + for (int i = 0; i < field_2_numIdClusters-1; i++) + { + field_5_fileIdClusters[i] = new FileIdCluster(LittleEndian.getInt( data, pos + size ), LittleEndian.getInt( data, pos + size + 4 )); + size += 8; + } + bytesRemaining -= size; + if (bytesRemaining != 0) + throw new RecordFormatException("Expecting no remaining data but got " + bytesRemaining + " byte(s)."); + return 8 + size + bytesRemaining; + } + + /** + * This method serializes this escher record into a byte array. + * + * @param offset The offset into data to start writing the record data to. + * @param data The byte array to serialize to. + * @param listener A listener to retrieve start and end callbacks. Use a NullEscherSerailizationListener to ignore these events. + * @return The number of bytes written. + * + * @see NullEscherSerializationListener + */ + public int serialize( int offset, byte[] data, EscherSerializationListener listener ) + { + listener.beforeRecordSerialize( offset, getRecordId(), this ); + + int pos = offset; + LittleEndian.putShort( data, pos, getOptions() ); pos += 2; + LittleEndian.putShort( data, pos, getRecordId() ); pos += 2; + int remainingBytes = getRecordSize() - 8; + LittleEndian.putInt( data, pos, remainingBytes ); pos += 4; + LittleEndian.putInt( data, pos, field_1_shapeIdMax ); pos += 4; + LittleEndian.putInt( data, pos, getNumIdClusters() ); pos += 4; + LittleEndian.putInt( data, pos, field_3_numShapesSaved ); pos += 4; + LittleEndian.putInt( data, pos, field_4_drawingsSaved ); pos += 4; + for ( int i = 0; i < field_5_fileIdClusters.length; i++ ) + { + LittleEndian.putInt( data, pos, field_5_fileIdClusters[i].field_1_drawingGroupId ); pos += 4; + LittleEndian.putInt( data, pos, field_5_fileIdClusters[i].field_2_numShapeIdsUsed ); pos += 4; + } + + listener.afterRecordSerialize( pos, getRecordId(), getRecordSize(), this ); + return getRecordSize(); + } + + /** + * Returns the number of bytes that are required to serialize this record. + * + * @return Number of bytes + */ + public int getRecordSize() + { + return 8 + 16 + (8 * field_5_fileIdClusters.length); + } + + public short getRecordId() + { + return RECORD_ID; + } + + /** + * The short name for this record + */ + public String getRecordName() + { + return "Dgg"; + } + + public String toString() + { + String nl = System.getProperty("line.separator"); + +// String extraData; +// ByteArrayOutputStream b = new ByteArrayOutputStream(); +// try +// { +// HexDump.dump(this.remainingData, 0, b, 0); +// extraData = b.toString(); +// } +// catch ( Exception e ) +// { +// extraData = "error"; +// } + StringBuffer field_5_string = new StringBuffer(); + for ( int i = 0; i < field_5_fileIdClusters.length; i++ ) + { + field_5_string.append(" DrawingGroupId").append(i+1).append(": "); + field_5_string.append(field_5_fileIdClusters[i].field_1_drawingGroupId); + field_5_string.append(nl); + field_5_string.append(" NumShapeIdsUsed").append(i+1).append(": "); + field_5_string.append(field_5_fileIdClusters[i].field_2_numShapeIdsUsed); + field_5_string.append(nl); + } + return getClass().getName() + ":" + nl + + " RecordId: 0x" + HexDump.toHex(RECORD_ID) + nl + + " Options: 0x" + HexDump.toHex(getOptions()) + nl + + " ShapeIdMax: " + field_1_shapeIdMax + nl + + " NumIdClusters: " + getNumIdClusters() + nl + + " NumShapesSaved: " + field_3_numShapesSaved + nl + + " DrawingsSaved: " + field_4_drawingsSaved + nl + + "" + field_5_string.toString(); + + } + + public int getShapeIdMax() + { + return field_1_shapeIdMax; + } + + /** + * The maximum is actually the next available. shape id. + */ + public void setShapeIdMax( int field_1_shapeIdMax ) + { + this.field_1_shapeIdMax = field_1_shapeIdMax; + } + + public int getNumIdClusters() + { + return field_5_fileIdClusters.length + 1; + } + + public int getNumShapesSaved() + { + return field_3_numShapesSaved; + } + + public void setNumShapesSaved( int field_3_numShapesSaved ) + { + this.field_3_numShapesSaved = field_3_numShapesSaved; + } + + public int getDrawingsSaved() + { + return field_4_drawingsSaved; + } + + public void setDrawingsSaved( int field_4_drawingsSaved ) + { + this.field_4_drawingsSaved = field_4_drawingsSaved; + } + + public FileIdCluster[] getFileIdClusters() + { + return field_5_fileIdClusters; + } + + public void setFileIdClusters( FileIdCluster[] field_5_fileIdClusters ) + { + this.field_5_fileIdClusters = field_5_fileIdClusters; + } + + public void addCluster( int dgId, int numShapedUsed ) + { + List clusters = new ArrayList(Arrays.asList(field_5_fileIdClusters)); + clusters.add(new FileIdCluster(dgId, numShapedUsed)); + Collections.sort(clusters, new Comparator() + { + public int compare( Object o1, Object o2 ) + { + FileIdCluster f1 = (FileIdCluster) o1; + FileIdCluster f2 = (FileIdCluster) o2; + if (f1.getDrawingGroupId() == f2.getDrawingGroupId()) + return 0; + if (f1.getDrawingGroupId() < f2.getDrawingGroupId()) + return -1; + else + return +1; + } + } ); + field_5_fileIdClusters = (FileIdCluster[]) clusters.toArray( new FileIdCluster[clusters.size()] ); + } +} diff --git a/src/java/org/apache/poi/ddf/EscherDump.java b/src/java/org/apache/poi/ddf/EscherDump.java new file mode 100644 index 0000000000..7930486964 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherDump.java @@ -0,0 +1,990 @@ +/* ==================================================================== + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2003 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Apache" and "Apache Software Foundation" and + * "Apache POI" must not be used to endorse or promote products + * derived from this software without prior written permission. For + * written permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache", + * "Apache POI", nor may "Apache" appear in their name, without + * prior written permission of the Apache Software Foundation. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ + +package org.apache.poi.ddf; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.HexRead; +import org.apache.poi.util.LittleEndian; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.util.zip.InflaterInputStream; + +/** + * Used to dump the contents of escher records to a PrintStream. + * + * @author Glen Stampoultzis (glens at apache.org) + */ +public class EscherDump +{ + + public EscherDump() + { + } + + /** + * Decodes the escher stream from a byte array and dumps the results to + * a print stream. + * + * @param data The data array containing the escher records. + * @param offset The starting offset within the data array. + * @param size The number of bytes to read. + * @param out The output stream to write the results to. + * + */ + public void dump( byte[] data, int offset, int size, PrintStream out ) throws IOException, LittleEndian.BufferUnderrunException + { + EscherRecordFactory recordFactory = new DefaultEscherRecordFactory(); + int pos = offset; + while ( pos < offset + size ) + { + EscherRecord r = recordFactory.createRecord(data, pos); + int bytesRead = r.fillFields(data, pos, recordFactory ); + System.out.println( r.toString() ); + pos += bytesRead; + } + } + + /** + * This version of dump is a translation from the open office escher dump routine. + * + * @param maxLength The number of bytes to read + * @param in An input stream to read from. + * @param out An output stream to write to. + */ + public void dumpOld( long maxLength, InputStream in, PrintStream out ) throws IOException, LittleEndian.BufferUnderrunException + { + long remainingBytes = maxLength; + short options; // 4 bits for the version and 12 bits for the instance + short recordId; + int recordBytesRemaining; // including enclosing records + StringBuffer stringBuf = new StringBuffer(); + short nDumpSize; + String recordName; + + boolean atEOF = false; + + while ( !atEOF && ( remainingBytes > 0 ) ) + { + stringBuf = new StringBuffer(); + options = LittleEndian.readShort( in ); + recordId = LittleEndian.readShort( in ); + recordBytesRemaining = LittleEndian.readInt( in ); + + remainingBytes -= 2 + 2 + 4; + + switch ( recordId ) + { + case (short) 0xF000: + recordName = "MsofbtDggContainer"; + break; + case (short) 0xF006: + recordName = "MsofbtDgg"; + break; + case (short) 0xF016: + recordName = "MsofbtCLSID"; + break; + case (short) 0xF00B: + recordName = "MsofbtOPT"; + break; + case (short) 0xF11A: + recordName = "MsofbtColorMRU"; + break; + case (short) 0xF11E: + recordName = "MsofbtSplitMenuColors"; + break; + case (short) 0xF001: + recordName = "MsofbtBstoreContainer"; + break; + case (short) 0xF007: + recordName = "MsofbtBSE"; + break; + case (short) 0xF002: + recordName = "MsofbtDgContainer"; + break; + case (short) 0xF008: + recordName = "MsofbtDg"; + break; + case (short) 0xF118: + recordName = "MsofbtRegroupItem"; + break; + case (short) 0xF120: + recordName = "MsofbtColorScheme"; + break; + case (short) 0xF003: + recordName = "MsofbtSpgrContainer"; + break; + case (short) 0xF004: + recordName = "MsofbtSpContainer"; + break; + case (short) 0xF009: + recordName = "MsofbtSpgr"; + break; + case (short) 0xF00A: + recordName = "MsofbtSp"; + break; + case (short) 0xF00C: + recordName = "MsofbtTextbox"; + break; + case (short) 0xF00D: + recordName = "MsofbtClientTextbox"; + break; + case (short) 0xF00E: + recordName = "MsofbtAnchor"; + break; + case (short) 0xF00F: + recordName = "MsofbtChildAnchor"; + break; + case (short) 0xF010: + recordName = "MsofbtClientAnchor"; + break; + case (short) 0xF011: + recordName = "MsofbtClientData"; + break; + case (short) 0xF11F: + recordName = "MsofbtOleObject"; + break; + case (short) 0xF11D: + recordName = "MsofbtDeletedPspl"; + break; + case (short) 0xF005: + recordName = "MsofbtSolverContainer"; + break; + case (short) 0xF012: + recordName = "MsofbtConnectorRule"; + break; + case (short) 0xF013: + recordName = "MsofbtAlignRule"; + break; + case (short) 0xF014: + recordName = "MsofbtArcRule"; + break; + case (short) 0xF015: + recordName = "MsofbtClientRule"; + break; + case (short) 0xF017: + recordName = "MsofbtCalloutRule"; + break; + case (short) 0xF119: + recordName = "MsofbtSelection"; + break; + case (short) 0xF122: + recordName = "MsofbtUDefProp"; + break; + default: + if ( recordId >= (short) 0xF018 && recordId <= (short) 0xF117 ) + recordName = "MsofbtBLIP"; + else if ( ( options & (short) 0x000F ) == (short) 0x000F ) + recordName = "UNKNOWN container"; + else + recordName = "UNKNOWN ID"; + } + + stringBuf.append( " " ); + stringBuf.append( HexDump.toHex( recordId ) ); + stringBuf.append( " " ).append( recordName ).append( " [" ); + stringBuf.append( HexDump.toHex( options ) ); + stringBuf.append( ',' ); + stringBuf.append( HexDump.toHex( recordBytesRemaining ) ); + stringBuf.append( "] instance: " ); + stringBuf.append( HexDump.toHex( ( (short) ( options >> 4 ) ) ) ); + out.println( stringBuf.toString() ); + + + if ( recordId == (short) 0xF007 && 36 <= remainingBytes && 36 <= recordBytesRemaining ) + { // BSE, FBSE + // ULONG nP = pIn->GetRecPos(); + + byte n8; + // short n16; + // int n32; + + stringBuf = new StringBuffer( " btWin32: " ); + n8 = (byte) in.read(); + stringBuf.append( HexDump.toHex( n8 ) ); + stringBuf.append( getBlipType( n8 ) ); + stringBuf.append( " btMacOS: " ); + n8 = (byte) in.read(); + stringBuf.append( HexDump.toHex( n8 ) ); + stringBuf.append( getBlipType( n8 ) ); + out.println( stringBuf.toString() ); + + out.println( " rgbUid:" ); + HexDump.dump( in, out, 0, 16 ); + + out.print( " tag: " ); + outHex( 2, in, out ); + out.println(); + out.print( " size: " ); + outHex( 4, in, out ); + out.println(); + out.print( " cRef: " ); + outHex( 4, in, out ); + out.println(); + out.print( " offs: " ); + outHex( 4, in, out ); + out.println(); + out.print( " usage: " ); + outHex( 1, in, out ); + out.println(); + out.print( " cbName: " ); + outHex( 1, in, out ); + out.println(); + out.print( " unused2: " ); + outHex( 1, in, out ); + out.println(); + out.print( " unused3: " ); + outHex( 1, in, out ); + out.println(); + + // subtract the number of bytes we've read + remainingBytes -= 36; + //n -= pIn->GetRecPos() - nP; + recordBytesRemaining = 0; // loop to MsofbtBLIP + } + else if ( recordId == (short) 0xF010 && 0x12 <= remainingBytes && 0x12 <= recordBytesRemaining ) + { // ClientAnchor + //ULONG nP = pIn->GetRecPos(); + // short n16; + + out.print( " Flag: " ); + outHex( 2, in, out ); + out.println(); + out.print( " Col1: " ); + outHex( 2, in, out ); + out.print( " dX1: " ); + outHex( 2, in, out ); + out.print( " Row1: " ); + outHex( 2, in, out ); + out.print( " dY1: " ); + outHex( 2, in, out ); + out.println(); + out.print( " Col2: " ); + outHex( 2, in, out ); + out.print( " dX2: " ); + outHex( 2, in, out ); + out.print( " Row2: " ); + outHex( 2, in, out ); + out.print( " dY2: " ); + outHex( 2, in, out ); + out.println(); + + remainingBytes -= 18; + recordBytesRemaining -= 18; + + } + else if ( recordId == (short) 0xF00B || recordId == (short) 0xF122 ) + { // OPT + int nComplex = 0; + out.println( " PROPID VALUE" ); + while ( recordBytesRemaining >= 6 + nComplex && remainingBytes >= 6 + nComplex ) + { + short n16; + int n32; + n16 = LittleEndian.readShort( in ); + n32 = LittleEndian.readInt( in ); + + recordBytesRemaining -= 6; + remainingBytes -= 6; + out.print( " " ); + out.print( HexDump.toHex( n16 ) ); + out.print( " (" ); + int propertyId = n16 & (short) 0x3FFF; + out.print( " " + propertyId ); + if ( ( n16 & (short) 0x8000 ) == 0 ) + { + if ( ( n16 & (short) 0x4000 ) != 0 ) + out.print( ", fBlipID" ); + out.print( ") " ); + + out.print( HexDump.toHex( n32 ) ); + + if ( ( n16 & (short) 0x4000 ) == 0 ) + { + out.print( " (" ); + out.print( dec1616( n32 ) ); + out.print( ')' ); + out.print( " {" + propName( (short)propertyId ) + "}" ); + } + out.println(); + } + else + { + out.print( ", fComplex) " ); + out.print( HexDump.toHex( n32 ) ); + out.print( " - Complex prop len" ); + out.println( " {" + propName( (short)propertyId ) + "}" ); + + nComplex += n32; + } + + } + // complex property data + while ( ( nComplex & remainingBytes ) > 0 ) + { + nDumpSize = ( nComplex > (int) remainingBytes ) ? (short) remainingBytes : (short) nComplex; + HexDump.dump( in, out, 0, nDumpSize ); + nComplex -= nDumpSize; + recordBytesRemaining -= nDumpSize; + remainingBytes -= nDumpSize; + } + } + else if ( recordId == (short) 0xF012 ) + { + out.print( " Connector rule: " ); + out.print( LittleEndian.readInt( in ) ); + out.print( " ShapeID A: " ); + out.print( LittleEndian.readInt( in ) ); + out.print( " ShapeID B: " ); + out.print( LittleEndian.readInt( in ) ); + out.print( " ShapeID connector: " ); + out.print( LittleEndian.readInt( in ) ); + out.print( " Connect pt A: " ); + out.print( LittleEndian.readInt( in ) ); + out.print( " Connect pt B: " ); + out.println( LittleEndian.readInt( in ) ); + + recordBytesRemaining -= 24; + remainingBytes -= 24; + } + else if ( recordId >= (short) 0xF018 && recordId < (short) 0xF117 ) + { + out.println( " Secondary UID: " ); + HexDump.dump( in, out, 0, 16 ); + out.println( " Cache of size: " + HexDump.toHex( LittleEndian.readInt( in ) ) ); + out.println( " Boundary top: " + HexDump.toHex( LittleEndian.readInt( in ) ) ); + out.println( " Boundary left: " + HexDump.toHex( LittleEndian.readInt( in ) ) ); + out.println( " Boundary width: " + HexDump.toHex( LittleEndian.readInt( in ) ) ); + out.println( " Boundary height: " + HexDump.toHex( LittleEndian.readInt( in ) ) ); + out.println( " X: " + HexDump.toHex( LittleEndian.readInt( in ) ) ); + out.println( " Y: " + HexDump.toHex( LittleEndian.readInt( in ) ) ); + out.println( " Cache of saved size: " + HexDump.toHex( LittleEndian.readInt( in ) ) ); + out.println( " Compression Flag: " + HexDump.toHex( (byte) in.read() ) ); + out.println( " Filter: " + HexDump.toHex( (byte) in.read() ) ); + out.println( " Data (after decompression): " ); + + recordBytesRemaining -= 34 + 16; + remainingBytes -= 34 + 16; + + nDumpSize = ( recordBytesRemaining > (int) remainingBytes ) ? (short) remainingBytes : (short) recordBytesRemaining; + + + byte[] buf = new byte[nDumpSize]; + int read = in.read( buf ); + while ( read != -1 && read < nDumpSize ) + read += in.read( buf, read, buf.length ); + ByteArrayInputStream bin = new ByteArrayInputStream( buf ); + + InputStream in1 = new InflaterInputStream( bin ); + int bytesToDump = -1; + HexDump.dump( in1, out, 0, bytesToDump ); + + recordBytesRemaining -= nDumpSize; + remainingBytes -= nDumpSize; + + } + + boolean isContainer = ( options & (short) 0x000F ) == (short) 0x000F; + if ( isContainer && remainingBytes >= 0 ) + { // Container + if ( recordBytesRemaining <= (int) remainingBytes ) + out.println( " completed within" ); + else + out.println( " continued elsewhere" ); + } + else if ( remainingBytes >= 0 ) + // -> 0x0000 ... 0x0FFF + { + nDumpSize = ( recordBytesRemaining > (int) remainingBytes ) ? (short) remainingBytes : (short) recordBytesRemaining; + + if ( nDumpSize != 0 ) + { + HexDump.dump( in, out, 0, nDumpSize ); + remainingBytes -= nDumpSize; + } + } + else + out.println( " >> OVERRUN <<" ); + } + + } + + /** + * Returns a property name given a property id. This is used only by the + * old escher dump routine. + * + * @param propertyId The property number for the name + * @return A descriptive name. + */ + private String propName( short propertyId ) + { + class PropName { + public PropName( int id, String name ) + { + this.id = id; + this.name = name; + } + + int id; + String name; + } + + final PropName[] props = new PropName[] { + new PropName(4, "transform.rotation"), + new PropName(119, "protection.lockrotation"), + new PropName(120, "protection.lockaspectratio"), + new PropName(121, "protection.lockposition"), + new PropName(122, "protection.lockagainstselect"), + new PropName(123, "protection.lockcropping"), + new PropName(124, "protection.lockvertices"), + new PropName(125, "protection.locktext"), + new PropName(126, "protection.lockadjusthandles"), + new PropName(127, "protection.lockagainstgrouping"), + new PropName(128, "text.textid"), + new PropName(129, "text.textleft"), + new PropName(130, "text.texttop"), + new PropName(131, "text.textright"), + new PropName(132, "text.textbottom"), + new PropName(133, "text.wraptext"), + new PropName(134, "text.scaletext"), + new PropName(135, "text.anchortext"), + new PropName(136, "text.textflow"), + new PropName(137, "text.fontrotation"), + new PropName(138, "text.idofnextshape"), + new PropName(139, "text.bidir"), + new PropName(187, "text.singleclickselects"), + new PropName(188, "text.usehostmargins"), + new PropName(189, "text.rotatetextwithshape"), + new PropName(190, "text.sizeshapetofittext"), + new PropName(191, "text.sizetexttofitshape"), + new PropName(192, "geotext.unicode"), + new PropName(193, "geotext.rtftext"), + new PropName(194, "geotext.alignmentoncurve"), + new PropName(195, "geotext.defaultpointsize"), + new PropName(196, "geotext.textspacing"), + new PropName(197, "geotext.fontfamilyname"), + new PropName(240, "geotext.reverseroworder"), + new PropName(241, "geotext.hastexteffect"), + new PropName(242, "geotext.rotatecharacters"), + new PropName(243, "geotext.kerncharacters"), + new PropName(244, "geotext.tightortrack"), + new PropName(245, "geotext.stretchtofitshape"), + new PropName(246, "geotext.charboundingbox"), + new PropName(247, "geotext.scaletextonpath"), + new PropName(248, "geotext.stretchcharheight"), + new PropName(249, "geotext.nomeasurealongpath"), + new PropName(250, "geotext.boldfont"), + new PropName(251, "geotext.italicfont"), + new PropName(252, "geotext.underlinefont"), + new PropName(253, "geotext.shadowfont"), + new PropName(254, "geotext.smallcapsfont"), + new PropName(255, "geotext.strikethroughfont"), + new PropName(256, "blip.cropfromtop"), + new PropName(257, "blip.cropfrombottom"), + new PropName(258, "blip.cropfromleft"), + new PropName(259, "blip.cropfromright"), + new PropName(260, "blip.bliptodisplay"), + new PropName(261, "blip.blipfilename"), + new PropName(262, "blip.blipflags"), + new PropName(263, "blip.transparentcolor"), + new PropName(264, "blip.contrastsetting"), + new PropName(265, "blip.brightnesssetting"), + new PropName(266, "blip.gamma"), + new PropName(267, "blip.pictureid"), + new PropName(268, "blip.doublemod"), + new PropName(269, "blip.picturefillmod"), + new PropName(270, "blip.pictureline"), + new PropName(271, "blip.printblip"), + new PropName(272, "blip.printblipfilename"), + new PropName(273, "blip.printflags"), + new PropName(316, "blip.nohittestpicture"), + new PropName(317, "blip.picturegray"), + new PropName(318, "blip.picturebilevel"), + new PropName(319, "blip.pictureactive"), + new PropName(320, "geometry.left"), + new PropName(321, "geometry.top"), + new PropName(322, "geometry.right"), + new PropName(323, "geometry.bottom"), + new PropName(324, "geometry.shapepath"), + new PropName(325, "geometry.vertices"), + new PropName(326, "geometry.segmentinfo"), + new PropName(327, "geometry.adjustvalue"), + new PropName(328, "geometry.adjust2value"), + new PropName(329, "geometry.adjust3value"), + new PropName(330, "geometry.adjust4value"), + new PropName(331, "geometry.adjust5value"), + new PropName(332, "geometry.adjust6value"), + new PropName(333, "geometry.adjust7value"), + new PropName(334, "geometry.adjust8value"), + new PropName(335, "geometry.adjust9value"), + new PropName(336, "geometry.adjust10value"), + new PropName(378, "geometry.shadowOK"), + new PropName(379, "geometry.3dok"), + new PropName(380, "geometry.lineok"), + new PropName(381, "geometry.geotextok"), + new PropName(382, "geometry.fillshadeshapeok"), + new PropName(383, "geometry.fillok"), + new PropName(384, "fill.filltype"), + new PropName(385, "fill.fillcolor"), + new PropName(386, "fill.fillopacity"), + new PropName(387, "fill.fillbackcolor"), + new PropName(388, "fill.backopacity"), + new PropName(389, "fill.crmod"), + new PropName(390, "fill.patterntexture"), + new PropName(391, "fill.blipfilename"), + new PropName(392, "fill.blipflags"), + new PropName(393, "fill.width"), + new PropName(394, "fill.height"), + new PropName(395, "fill.angle"), + new PropName(396, "fill.focus"), + new PropName(397, "fill.toleft"), + new PropName(398, "fill.totop"), + new PropName(399, "fill.toright"), + new PropName(400, "fill.tobottom"), + new PropName(401, "fill.rectleft"), + new PropName(402, "fill.recttop"), + new PropName(403, "fill.rectright"), + new PropName(404, "fill.rectbottom"), + new PropName(405, "fill.dztype"), + new PropName(406, "fill.shadepreset"), + new PropName(407, "fill.shadecolors"), + new PropName(408, "fill.originx"), + new PropName(409, "fill.originy"), + new PropName(410, "fill.shapeoriginx"), + new PropName(411, "fill.shapeoriginy"), + new PropName(412, "fill.shadetype"), + new PropName(443, "fill.filled"), + new PropName(444, "fill.hittestfill"), + new PropName(445, "fill.shape"), + new PropName(446, "fill.userect"), + new PropName(447, "fill.nofillhittest"), + new PropName(448, "linestyle.color"), + new PropName(449, "linestyle.opacity"), + new PropName(450, "linestyle.backcolor"), + new PropName(451, "linestyle.crmod"), + new PropName(452, "linestyle.linetype"), + new PropName(453, "linestyle.fillblip"), + new PropName(454, "linestyle.fillblipname"), + new PropName(455, "linestyle.fillblipflags"), + new PropName(456, "linestyle.fillwidth"), + new PropName(457, "linestyle.fillheight"), + new PropName(458, "linestyle.filldztype"), + new PropName(459, "linestyle.linewidth"), + new PropName(460, "linestyle.linemiterlimit"), + new PropName(461, "linestyle.linestyle"), + new PropName(462, "linestyle.linedashing"), + new PropName(463, "linestyle.linedashstyle"), + new PropName(464, "linestyle.linestartarrowhead"), + new PropName(465, "linestyle.lineendarrowhead"), + new PropName(466, "linestyle.linestartarrowwidth"), + new PropName(467, "linestyle.lineestartarrowlength"), + new PropName(468, "linestyle.lineendarrowwidth"), + new PropName(469, "linestyle.lineendarrowlength"), + new PropName(470, "linestyle.linejoinstyle"), + new PropName(471, "linestyle.lineendcapstyle"), + new PropName(507, "linestyle.arrowheadsok"), + new PropName(508, "linestyle.anyline"), + new PropName(509, "linestyle.hitlinetest"), + new PropName(510, "linestyle.linefillshape"), + new PropName(511, "linestyle.nolinedrawdash"), + new PropName(512, "shadowstyle.type"), + new PropName(513, "shadowstyle.color"), + new PropName(514, "shadowstyle.highlight"), + new PropName(515, "shadowstyle.crmod"), + new PropName(516, "shadowstyle.opacity"), + new PropName(517, "shadowstyle.offsetx"), + new PropName(518, "shadowstyle.offsety"), + new PropName(519, "shadowstyle.secondoffsetx"), + new PropName(520, "shadowstyle.secondoffsety"), + new PropName(521, "shadowstyle.scalextox"), + new PropName(522, "shadowstyle.scaleytox"), + new PropName(523, "shadowstyle.scalextoy"), + new PropName(524, "shadowstyle.scaleytoy"), + new PropName(525, "shadowstyle.perspectivex"), + new PropName(526, "shadowstyle.perspectivey"), + new PropName(527, "shadowstyle.weight"), + new PropName(528, "shadowstyle.originx"), + new PropName(529, "shadowstyle.originy"), + new PropName(574, "shadowstyle.shadow"), + new PropName(575, "shadowstyle.shadowobsured"), + new PropName(576, "perspective.type"), + new PropName(577, "perspective.offsetx"), + new PropName(578, "perspective.offsety"), + new PropName(579, "perspective.scalextox"), + new PropName(580, "perspective.scaleytox"), + new PropName(581, "perspective.scalextoy"), + new PropName(582, "perspective.scaleytox"), + new PropName(583, "perspective.perspectivex"), + new PropName(584, "perspective.perspectivey"), + new PropName(585, "perspective.weight"), + new PropName(586, "perspective.originx"), + new PropName(587, "perspective.originy"), + new PropName(639, "perspective.perspectiveon"), + new PropName(640, "3d.specularamount"), + new PropName(661, "3d.diffuseamount"), + new PropName(662, "3d.shininess"), + new PropName(663, "3d.edgethickness"), + new PropName(664, "3d.extrudeforward"), + new PropName(665, "3d.extrudebackward"), + new PropName(666, "3d.extrudeplane"), + new PropName(667, "3d.extrusioncolor"), + new PropName(648, "3d.crmod"), + new PropName(700, "3d.3deffect"), + new PropName(701, "3d.metallic"), + new PropName(702, "3d.useextrusioncolor"), + new PropName(703, "3d.lightface"), + new PropName(704, "3dstyle.yrotationangle"), + new PropName(705, "3dstyle.xrotationangle"), + new PropName(706, "3dstyle.rotationaxisx"), + new PropName(707, "3dstyle.rotationaxisy"), + new PropName(708, "3dstyle.rotationaxisz"), + new PropName(709, "3dstyle.rotationangle"), + new PropName(710, "3dstyle.rotationcenterx"), + new PropName(711, "3dstyle.rotationcentery"), + new PropName(712, "3dstyle.rotationcenterz"), + new PropName(713, "3dstyle.rendermode"), + new PropName(714, "3dstyle.tolerance"), + new PropName(715, "3dstyle.xviewpoint"), + new PropName(716, "3dstyle.yviewpoint"), + new PropName(717, "3dstyle.zviewpoint"), + new PropName(718, "3dstyle.originx"), + new PropName(719, "3dstyle.originy"), + new PropName(720, "3dstyle.skewangle"), + new PropName(721, "3dstyle.skewamount"), + new PropName(722, "3dstyle.ambientintensity"), + new PropName(723, "3dstyle.keyx"), + new PropName(724, "3dstyle.keyy"), + new PropName(725, "3dstyle.keyz"), + new PropName(726, "3dstyle.keyintensity"), + new PropName(727, "3dstyle.fillx"), + new PropName(728, "3dstyle.filly"), + new PropName(729, "3dstyle.fillz"), + new PropName(730, "3dstyle.fillintensity"), + new PropName(763, "3dstyle.constrainrotation"), + new PropName(764, "3dstyle.rotationcenterauto"), + new PropName(765, "3dstyle.parallel"), + new PropName(766, "3dstyle.keyharsh"), + new PropName(767, "3dstyle.fillharsh"), + new PropName(769, "shape.master"), + new PropName(771, "shape.connectorstyle"), + new PropName(772, "shape.blackandwhitesettings"), + new PropName(773, "shape.wmodepurebw"), + new PropName(774, "shape.wmodebw"), + new PropName(826, "shape.oleicon"), + new PropName(827, "shape.preferrelativeresize"), + new PropName(828, "shape.lockshapetype"), + new PropName(830, "shape.deleteattachedobject"), + new PropName(831, "shape.backgroundshape"), + new PropName(832, "callout.callouttype"), + new PropName(833, "callout.xycalloutgap"), + new PropName(834, "callout.calloutangle"), + new PropName(835, "callout.calloutdroptype"), + new PropName(836, "callout.calloutdropspecified"), + new PropName(837, "callout.calloutlengthspecified"), + new PropName(889, "callout.iscallout"), + new PropName(890, "callout.calloutaccentbar"), + new PropName(891, "callout.callouttextborder"), + new PropName(892, "callout.calloutminusx"), + new PropName(893, "callout.calloutminusy"), + new PropName(894, "callout.dropauto"), + new PropName(895, "callout.lengthspecified"), + new PropName(896, "groupshape.shapename"), + new PropName(897, "groupshape.description"), + new PropName(898, "groupshape.hyperlink"), + new PropName(899, "groupshape.wrappolygonvertices"), + new PropName(900, "groupshape.wrapdistleft"), + new PropName(901, "groupshape.wrapdisttop"), + new PropName(902, "groupshape.wrapdistright"), + new PropName(903, "groupshape.wrapdistbottom"), + new PropName(904, "groupshape.regroupid"), + new PropName(953, "groupshape.editedwrap"), + new PropName(954, "groupshape.behinddocument"), + new PropName(955, "groupshape.ondblclicknotify"), + new PropName(956, "groupshape.isbutton"), + new PropName(957, "groupshape.1dadjustment"), + new PropName(958, "groupshape.hidden"), + new PropName(959, "groupshape.print"), + }; + + for ( int i = 0; i < props.length; i++ ) + { + if (props[i].id == propertyId) + { + return props[i].name; + } + } + + return "unknown property"; + } + + /** + * Returns the blip description given a blip id. + * + * @param b blip id + * @return A description. + */ + private String getBlipType( byte b ) + { + switch ( b ) + { + case 0: + return " ERROR"; + case 1: + return " UNKNOWN"; + case 2: + return " EMF"; + case 3: + return " WMF"; + case 4: + return " PICT"; + case 5: + return " JPEG"; + case 6: + return " PNG"; + case 7: + return " DIB"; + default: + if ( b < 32 ) + return " NotKnown"; + else + return " Client"; + } + } + + /** + * Straight conversion from OO. Converts a type of float. + */ + private String dec1616( int n32 ) + { + String result = ""; + result += (short) ( n32 >> 16 ); + result += '.'; + result += (short) ( n32 & (short) 0xFFFF ); + return result; + } + + /** + * Dumps out a hex value by reading from a input stream. + * + * @param bytes How many bytes this hex value consists of. + * @param in The stream to read the hex value from. + * @param out The stream to write the nicely formatted hex value to. + */ + private void outHex( int bytes, InputStream in, PrintStream out ) throws IOException, LittleEndian.BufferUnderrunException + { + switch ( bytes ) + { + case 1: + out.print( HexDump.toHex( (byte) in.read() ) ); + break; + case 2: + out.print( HexDump.toHex( LittleEndian.readShort( in ) ) ); + break; + case 4: + out.print( HexDump.toHex( LittleEndian.readInt( in ) ) ); + break; + default: + throw new IOException( "Unable to output variable of that width" ); + } + } + + /** + * A simple test stub. + */ + public static void main( String[] args ) throws IOException + { + String dump = + "0F 00 00 F0 89 07 00 00 00 00 06 F0 18 00 00 00 " + + "05 04 00 00 02 00 00 00 05 00 00 00 01 00 00 00 " + + "01 00 00 00 05 00 00 00 4F 00 01 F0 2F 07 00 00 " + + "42 00 07 F0 B7 01 00 00 03 04 3F 14 AE 6B 0F 65 " + + "B0 48 BF 5E 94 63 80 E8 91 73 FF 00 93 01 00 00 " + + "01 00 00 00 00 00 00 00 00 00 FF FF 20 54 1C F0 " + + "8B 01 00 00 3F 14 AE 6B 0F 65 B0 48 BF 5E 94 63 " + + "80 E8 91 73 92 0E 00 00 00 00 00 00 00 00 00 00 " + + "D1 07 00 00 DD 05 00 00 4A AD 6F 00 8A C5 53 00 " + + "59 01 00 00 00 FE 78 9C E3 9B C4 00 04 AC 77 D9 " + + "2F 32 08 32 FD E7 61 F8 FF 0F C8 FD 05 C5 30 19 " + + "10 90 63 90 FA 0F 06 0C 8C 0C 5C 70 19 43 30 EB " + + "0E FB 05 86 85 0C DB 18 58 80 72 8C 70 16 0B 83 " + + "05 56 51 29 88 C9 60 D9 69 0C 6C 20 26 23 03 C8 " + + "74 B0 A8 0E 03 07 FB 45 56 C7 A2 CC C4 1C 06 66 " + + "A0 0D 2C 40 39 5E 86 4C 06 3D A0 4E 10 D0 60 D9 " + + "C8 58 CC E8 CF B0 80 61 3A 8A 7E 0D C6 23 AC 4F " + + "E0 E2 98 B6 12 2B 06 73 9D 12 E3 52 56 59 F6 08 " + + "8A CC 52 66 A3 50 FF 96 2B 94 E9 DF 4C A1 FE 2D " + + "3A 03 AB 9F 81 C2 F0 A3 54 BF 0F 85 EE A7 54 FF " + + "40 FB 7F A0 E3 9F D2 F4 4F 71 FE 19 58 FF 2B 31 " + + "7F 67 36 3B 25 4F 99 1B 4E 53 A6 5F 89 25 95 E9 " + + "C4 00 C7 83 12 F3 1F 26 35 4A D3 D2 47 0E 0A C3 " + + "41 8E C9 8A 52 37 DC 15 A1 D0 0D BC 4C 06 0C 2B " + + "28 2C 13 28 D4 EF 43 61 5A A0 58 3F 85 71 E0 4B " + + "69 9E 64 65 FE 39 C0 E5 22 30 1D 30 27 0E 74 3A " + + "18 60 FD 4A CC B1 2C 13 7D 07 36 2D 2A 31 85 B2 " + + "6A 0D 74 1D 1D 22 4D 99 FE 60 0A F5 9B EC 1C 58 " + + "FD 67 06 56 3F 38 0D 84 3C A5 30 0E 28 D3 AF C4 " + + "A4 CA FA 44 7A 0D 65 6E 60 7F 4D A1 1B 24 58 F7 " + + "49 AF A5 CC 0D CC DF 19 FE 03 00 F0 B1 25 4D 42 " + + "00 07 F0 E1 01 00 00 03 04 39 50 BE 98 B0 6F 57 " + + "24 31 70 5D 23 2F 9F 10 66 FF 00 BD 01 00 00 01 " + + "00 00 00 00 00 00 00 00 00 FF FF 20 54 1C F0 B5 " + + "01 00 00 39 50 BE 98 B0 6F 57 24 31 70 5D 23 2F " + + "9F 10 66 DA 03 00 00 00 00 00 00 00 00 00 00 D1 " + + "07 00 00 DD 05 00 00 4A AD 6F 00 8A C5 53 00 83 " + + "01 00 00 00 FE 78 9C A5 52 BF 4B 42 51 14 3E F7 " + + "DC 77 7A 16 45 48 8B 3C 48 A8 16 15 0D 6C 88 D0 " + + "04 C3 40 A3 32 1C 84 96 08 21 04 A1 C5 5C A2 35 " + + "82 C0 35 6A AB 1C 6A 6B A8 24 5A 83 68 08 84 84 " + + "96 A2 86 A0 7F C2 86 5E E7 5E F5 41 E4 10 BC 03 " + + "1F E7 FB F1 CE B9 F7 F1 9E 7C 05 2E 7A 37 9B E0 " + + "45 7B 10 EC 6F 96 5F 1D 74 13 55 7E B0 6C 5D 20 " + + "60 C0 49 A2 9A BD 99 4F 50 83 1B 30 38 13 0E 33 " + + "60 A6 A7 6B B5 37 EB F4 10 FA 14 15 A0 B6 6B 37 " + + "0C 1E B3 49 73 5B A5 C2 26 48 3E C1 E0 6C 08 4A " + + "30 C9 93 AA 02 B8 20 13 62 05 4E E1 E8 D7 7C C0 " + + "B8 14 95 5E BE B8 A7 CF 1E BE 55 2C 56 B9 78 DF " + + "08 7E 88 4C 27 FF 7B DB FF 7A DD B7 1A 17 67 34 " + + "6A AE BA DA 35 D1 E7 72 BE FE EC 6E FE DA E5 7C " + + "3D EC 7A DE 03 FD 50 06 0B 23 F2 0E F3 B2 A5 11 " + + "91 0D 4C B5 B5 F3 BF 94 C1 8F 24 F7 D9 6F 60 94 " + + "3B C9 9A F3 1C 6B E7 BB F0 2E 49 B2 25 2B C6 B1 " + + "EE 69 EE 15 63 4F 71 7D CE 85 CC C8 35 B9 C3 28 " + + "28 CE D0 5C 67 79 F2 4A A2 14 23 A4 38 43 73 9D " + + "2D 69 2F C1 08 31 9F C5 5C 9B EB 7B C5 69 19 B3 " + + "B4 81 F3 DC E3 B4 8E 8B CC B3 94 53 5A E7 41 2A " + + "63 9A AA 38 C5 3D 48 BB EC 57 59 6F 2B AD 73 1F " + + "1D 60 92 AE 70 8C BB 8F CE 31 C1 3C 49 27 4A EB " + + "DC A4 5B 8C D1 0B 0E 73 37 E9 11 A7 99 C7 E8 41 " + + "69 B0 7F 00 96 F2 A7 E8 42 00 07 F0 B4 01 00 00 " + + "03 04 1A BA F9 D6 A9 B9 3A 03 08 61 E9 90 FF 7B " + + "9E E6 FF 00 90 01 00 00 01 00 00 00 00 00 00 00 " + + "00 00 FF FF 20 54 1C F0 88 01 00 00 1A BA F9 D6 " + + "A9 B9 3A 03 08 61 E9 90 FF 7B 9E E6 12 0E 00 00 " + + "00 00 00 00 00 00 00 00 D1 07 00 00 DD 05 00 00 " + + "4A AD 6F 00 8A C5 53 00 56 01 00 00 00 FE 78 9C " + + "E3 13 62 00 02 D6 BB EC 17 19 04 99 FE F3 30 FC " + + "FF 07 E4 FE 82 62 98 0C 08 C8 31 48 FD 07 03 06 " + + "46 06 2E B8 8C 21 98 75 87 FD 02 C3 42 86 6D 0C " + + "2C 40 39 46 38 8B 85 C1 02 AB A8 14 C4 64 B0 EC " + + "34 06 36 10 93 91 01 64 3A 58 54 87 81 83 FD 22 " + + "AB 63 51 66 62 0E 03 33 D0 06 16 A0 1C 2F 43 26 " + + "83 1E 50 27 08 68 B0 6C 64 2C 66 F4 67 58 C0 30 " + + "1D 45 BF 06 E3 11 D6 27 70 71 4C 5B 89 15 83 B9 " + + "4E 89 71 29 AB 2C 7B 04 45 66 29 B3 51 A8 7F CB " + + "15 CA F4 6F A6 50 FF 16 9D 81 D5 CF 40 61 F8 51 " + + "AA DF 87 42 F7 53 AA 7F A0 FD 3F D0 F1 4F 69 FA " + + "A7 38 FF 0C AC FF 95 98 BF 33 9B 9D 92 A7 CC 0D " + + "A7 29 D3 AF C4 92 CA 74 62 80 E3 41 89 F9 0F 93 " + + "1A A5 69 E9 23 07 85 E1 20 C7 64 45 A9 1B EE 8A " + + "50 E8 06 5E 26 03 86 15 14 96 09 14 EA F7 A1 30 " + + "2D 50 AC 9F C2 38 F0 A5 34 4F B2 32 FF 1C E0 72 " + + "11 98 0E 98 13 07 38 1D 28 31 C7 B2 4C F4 1D D8 " + + "B4 A0 C4 14 CA AA 35 D0 75 64 88 34 65 FA 83 29 " + + "D4 6F B2 73 60 F5 9F A1 54 FF 0E CA D3 40 C8 53 " + + "0A E3 E0 09 85 6E 50 65 7D 22 BD 86 32 37 B0 BF " + + "A6 D0 0D 12 AC FB A4 D7 52 E6 06 E6 EF 0C FF 01 " + + "97 1D 12 C7 42 00 07 F0 C3 01 00 00 03 04 BA 4C " + + "B6 23 BA 8B 27 BE C8 55 59 86 24 9F 89 D4 FF 00 " + + "9F 01 00 00 01 00 00 00 00 00 00 00 00 00 FF FF " + + "20 54 1C F0 97 01 00 00 BA 4C B6 23 BA 8B 27 BE " + + "C8 55 59 86 24 9F 89 D4 AE 0E 00 00 00 00 00 00 " + + "00 00 00 00 D1 07 00 00 DD 05 00 00 4A AD 6F 00 " + + "8A C5 53 00 65 01 00 00 00 FE 78 9C E3 5B C7 00 " + + "04 AC 77 D9 2F 32 08 32 FD E7 61 F8 FF 0F C8 FD " + + "05 C5 30 19 10 90 63 90 FA 0F 06 0C 8C 0C 5C 70 " + + "19 43 30 EB 0E FB 05 86 85 0C DB 18 58 80 72 8C " + + "70 16 0B 83 05 56 51 29 88 C9 60 D9 69 0C 6C 20 " + + "26 23 03 C8 74 B0 A8 0E 03 07 FB 45 56 C7 A2 CC " + + "C4 1C 06 66 A0 0D 2C 40 39 5E 86 4C 06 3D A0 4E " + + "10 D0 60 99 C6 B8 98 D1 9F 61 01 C3 74 14 FD 1A " + + "8C 2B D8 84 B1 88 4B A5 A5 75 03 01 50 DF 59 46 " + + "77 46 0F A8 3C A6 AB 88 15 83 B9 5E 89 B1 8B D5 " + + "97 2D 82 22 B3 94 29 D5 BF E5 CA C0 EA DF AC 43 " + + "A1 FD 14 EA 67 A0 30 FC 28 D5 EF 43 A1 FB 7D 87 " + + "B8 FF 07 3A FE 07 3A FD 53 EA 7E 0A C3 4F 89 F9 " + + "0E 73 EA 69 79 CA DC 70 8A 32 FD 4A 2C 5E 4C DF " + + "87 7A 3C BC E0 A5 30 1E 3E 31 C5 33 AC A0 30 2F " + + "52 A8 DF 87 C2 30 A4 54 3F A5 65 19 85 65 A9 12 " + + "D3 2B 16 0D 8A CB 13 4A F3 E3 27 E6 09 03 9D 0E " + + "06 58 BF 12 B3 13 CB C1 01 4E 8B 4A 4C 56 AC 91 " + + "03 5D 37 86 48 53 A6 3F 98 42 FD 26 3B 07 56 FF " + + "99 1D 14 EA A7 CC 7E 70 1A 08 79 42 61 1C 3C A5 " + + "D0 0D 9C 6C C2 32 6B 29 73 03 DB 6B CA DC C0 F8 " + + "97 F5 AD CC 1A CA DC C0 F4 83 32 37 B0 A4 30 CE " + + "FC C7 48 99 1B FE 33 32 FC 07 00 6C CC 2E 23 33 " + + "00 0B F0 12 00 00 00 BF 00 08 00 08 00 81 01 09 " + + "00 00 08 C0 01 40 00 00 08 40 00 1E F1 10 00 00 " + + "00 0D 00 00 08 0C 00 00 08 17 00 00 08 F7 00 00 " + + "10 "; + + // Decode the stream to bytes + byte[] bytes = HexRead.readData( new ByteArrayInputStream( dump.getBytes() ), -1 ); + // Create a new instance of the escher dumper + EscherDump dumper = new EscherDump(); + // Dump the contents of scher to screen. +// dumper.dumpOld( bytes.length, new ByteArrayInputStream( bytes ), System.out ); + dumper.dump(bytes, 0, bytes.length, System.out); + + } + + public void dump( int recordSize, byte[] data, PrintStream out ) throws IOException, LittleEndian.BufferUnderrunException + { +// ByteArrayInputStream is = new ByteArrayInputStream( data ); +// dump( recordSize, is, out ); + dump( data, 0, recordSize, System.out ); + } +} diff --git a/src/java/org/apache/poi/ddf/EscherOptRecord.java b/src/java/org/apache/poi/ddf/EscherOptRecord.java new file mode 100644 index 0000000000..4efab8ba3d --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherOptRecord.java @@ -0,0 +1,174 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.HexDump; + +import java.util.*; +import java.io.IOException; + +/** + * The opt record is used to store property values for a shape. It is the key to determining + * the attributes of a shape. Properties can be of two types: simple or complex. Simple types + * are fixed length. Complex properties are variable length. + * + * @author Glen Stampoultzis + */ +public class EscherOptRecord + extends EscherRecord +{ + public static final short RECORD_ID = (short) 0xF00B; + public static final String RECORD_DESCRIPTION = "msofbtOPT"; + + private List properties = new ArrayList(); + + /** + * This method deserializes the record from a byte array. + * + * @param data The byte array containing the escher record information + * @param offset The starting offset into data. + * @param recordFactory May be null since this is not a container record. + * @return The number of bytes read from the byte array. + */ + public int fillFields( byte[] data, int offset, EscherRecordFactory recordFactory ) + { + int bytesRemaining = readHeader( data, offset ); + int pos = offset + 8; + + EscherPropertyFactory f = new EscherPropertyFactory(); + properties = f.createProperties( data, pos, getInstance() ); + return bytesRemaining + 8; + } + + /** + * This method serializes this escher record into a byte array. + * + * @param offset The offset into data to start writing the record data to. + * @param data The byte array to serialize to. + * @param listener A listener to retrieve start and end callbacks. Use a NullEscherSerailizationListener to ignore these events. + * + * @return The number of bytes written. + * @see NullEscherSerializationListener + */ + public int serialize( int offset, byte[] data, EscherSerializationListener listener ) + { + listener.beforeRecordSerialize( offset, getRecordId(), this ); + + LittleEndian.putShort( data, offset, getOptions() ); + LittleEndian.putShort( data, offset + 2, getRecordId() ); + LittleEndian.putInt( data, offset + 4, getPropertiesSize() ); + int pos = offset + 8; + for ( Iterator iterator = properties.iterator(); iterator.hasNext(); ) + { + EscherProperty escherProperty = (EscherProperty) iterator.next(); + pos += escherProperty.serializeSimplePart( data, pos ); + } + for ( Iterator iterator = properties.iterator(); iterator.hasNext(); ) + { + EscherProperty escherProperty = (EscherProperty) iterator.next(); + pos += escherProperty.serializeComplexPart( data, pos ); + } + listener.afterRecordSerialize( pos, getRecordId(), pos - offset, this ); + return pos - offset; + } + + /** + * Returns the number of bytes that are required to serialize this record. + * + * @return Number of bytes + */ + public int getRecordSize() + { + return 8 + getPropertiesSize(); + } + + /** + * Automatically recalculate the correct option + */ + public short getOptions() + { + setOptions( (short) ( ( properties.size() << 4 ) | 0x3 ) ); + return super.getOptions(); + } + + /** + * The short name for this record + */ + public String getRecordName() + { + return "Opt"; + } + + private int getPropertiesSize() + { + int totalSize = 0; + for ( Iterator iterator = properties.iterator(); iterator.hasNext(); ) + { + EscherProperty escherProperty = (EscherProperty) iterator.next(); + totalSize += escherProperty.getPropertySize(); + } + return totalSize; + } + + /** + * Retrieve the string representation of this record. + */ + public String toString() + { + String nl = System.getProperty( "line.separator" ); + StringBuffer propertiesBuf = new StringBuffer(); + for ( Iterator iterator = properties.iterator(); iterator.hasNext(); ) + propertiesBuf.append( " " + + iterator.next().toString() + + nl ); + + return "org.apache.poi.ddf.EscherOptRecord:" + nl + + " isContainer: " + isContainerRecord() + nl + + " options: 0x" + HexDump.toHex( getOptions() ) + nl + + " recordId: 0x" + HexDump.toHex( getRecordId() ) + nl + + " numchildren: " + getChildRecords().size() + nl + + " properties:" + nl + + propertiesBuf.toString(); + } + + /** + * The list of properties stored by this record. + */ + public List getEscherProperties() + { + return properties; + } + + /** + * The list of properties stored by this record. + */ + public EscherProperty getEscherProperty( int index ) + { + return (EscherProperty) properties.get( index ); + } + + /** + * Add a property to this record. + */ + public void addEscherProperty( EscherProperty prop ) + { + properties.add( prop ); + } + + /** + * Records should be sorted by property number before being stored. + */ + public void sortProperties() + { + Collections.sort( properties, new Comparator() + { + public int compare( Object o1, Object o2 ) + { + EscherProperty p1 = (EscherProperty) o1; + EscherProperty p2 = (EscherProperty) o2; + return new Short( p1.getPropertyNumber() ).compareTo( new Short( p2.getPropertyNumber() ) ); + } + } ); + } + + +} diff --git a/src/java/org/apache/poi/ddf/EscherProperties.java b/src/java/org/apache/poi/ddf/EscherProperties.java new file mode 100644 index 0000000000..ea1c8745c5 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherProperties.java @@ -0,0 +1,607 @@ +package org.apache.poi.ddf; + +import java.util.HashMap; +import java.util.Map; + +/** + * Provides a list of all known escher properties including the description and + * type. + * + * @author Glen Stampoultzis (glens at apache.org) + */ +public class EscherProperties +{ + + // Property constants + public static final short TRANSFORM__ROTATION = 4; + public static final short PROTECTION__LOCKROTATION = 119; + public static final short PROTECTION__LOCKASPECTRATIO = 120; + public static final short PROTECTION__LOCKPOSITION = 121; + public static final short PROTECTION__LOCKAGAINSTSELECT = 122; + public static final short PROTECTION__LOCKCROPPING = 123; + public static final short PROTECTION__LOCKVERTICES = 124; + public static final short PROTECTION__LOCKTEXT = 125; + public static final short PROTECTION__LOCKADJUSTHANDLES = 126; + public static final short PROTECTION__LOCKAGAINSTGROUPING = 127; + public static final short TEXT__TEXTID = 128; + public static final short TEXT__TEXTLEFT = 129; + public static final short TEXT__TEXTTOP = 130; + public static final short TEXT__TEXTRIGHT = 131; + public static final short TEXT__TEXTBOTTOM = 132; + public static final short TEXT__WRAPTEXT = 133; + public static final short TEXT__SCALETEXT = 134; + public static final short TEXT__ANCHORTEXT = 135; + public static final short TEXT__TEXTFLOW = 136; + public static final short TEXT__FONTROTATION = 137; + public static final short TEXT__IDOFNEXTSHAPE = 138; + public static final short TEXT__BIDIR = 139; + public static final short TEXT__SINGLECLICKSELECTS = 187; + public static final short TEXT__USEHOSTMARGINS = 188; + public static final short TEXT__ROTATETEXTWITHSHAPE = 189; + public static final short TEXT__SIZESHAPETOFITTEXT = 190; + public static final short TEXT__SIZE_TEXT_TO_FIT_SHAPE = 191 ; + public static final short GEOTEXT__UNICODE = 192; + public static final short GEOTEXT__RTFTEXT = 193; + public static final short GEOTEXT__ALIGNMENTONCURVE = 194; + public static final short GEOTEXT__DEFAULTPOINTSIZE = 195; + public static final short GEOTEXT__TEXTSPACING = 196; + public static final short GEOTEXT__FONTFAMILYNAME = 197; + public static final short GEOTEXT__REVERSEROWORDER = 240; + public static final short GEOTEXT__HASTEXTEFFECT = 241; + public static final short GEOTEXT__ROTATECHARACTERS = 242; + public static final short GEOTEXT__KERNCHARACTERS = 243; + public static final short GEOTEXT__TIGHTORTRACK = 244; + public static final short GEOTEXT__STRETCHTOFITSHAPE = 245; + public static final short GEOTEXT__CHARBOUNDINGBOX = 246; + public static final short GEOTEXT__SCALETEXTONPATH = 247; + public static final short GEOTEXT__STRETCHCHARHEIGHT = 248; + public static final short GEOTEXT__NOMEASUREALONGPATH = 249; + public static final short GEOTEXT__BOLDFONT = 250; + public static final short GEOTEXT__ITALICFONT = 251; + public static final short GEOTEXT__UNDERLINEFONT = 252; + public static final short GEOTEXT__SHADOWFONT = 253; + public static final short GEOTEXT__SMALLCAPSFONT = 254; + public static final short GEOTEXT__STRIKETHROUGHFONT = 255; + public static final short BLIP__CROPFROMTOP = 256; + public static final short BLIP__CROPFROMBOTTOM = 257; + public static final short BLIP__CROPFROMLEFT = 258; + public static final short BLIP__CROPFROMRIGHT = 259; + public static final short BLIP__BLIPTODISPLAY = 260; + public static final short BLIP__BLIPFILENAME = 261; + public static final short BLIP__BLIPFLAGS = 262; + public static final short BLIP__TRANSPARENTCOLOR = 263; + public static final short BLIP__CONTRASTSETTING = 264; + public static final short BLIP__BRIGHTNESSSETTING = 265; + public static final short BLIP__GAMMA = 266; + public static final short BLIP__PICTUREID = 267; + public static final short BLIP__DOUBLEMOD = 268; + public static final short BLIP__PICTUREFILLMOD = 269; + public static final short BLIP__PICTURELINE = 270; + public static final short BLIP__PRINTBLIP = 271; + public static final short BLIP__PRINTBLIPFILENAME = 272; + public static final short BLIP__PRINTFLAGS = 273; + public static final short BLIP__NOHITTESTPICTURE = 316; + public static final short BLIP__PICTUREGRAY = 317; + public static final short BLIP__PICTUREBILEVEL = 318; + public static final short BLIP__PICTUREACTIVE = 319; + public static final short GEOMETRY__LEFT = 320; + public static final short GEOMETRY__TOP = 321; + public static final short GEOMETRY__RIGHT = 322; + public static final short GEOMETRY__BOTTOM = 323; + public static final short GEOMETRY__SHAPEPATH = 324; + public static final short GEOMETRY__VERTICES = 325; + public static final short GEOMETRY__SEGMENTINFO = 326; + public static final short GEOMETRY__ADJUSTVALUE = 327; + public static final short GEOMETRY__ADJUST2VALUE = 328; + public static final short GEOMETRY__ADJUST3VALUE = 329; + public static final short GEOMETRY__ADJUST4VALUE = 330; + public static final short GEOMETRY__ADJUST5VALUE = 331; + public static final short GEOMETRY__ADJUST6VALUE = 332; + public static final short GEOMETRY__ADJUST7VALUE = 333; + public static final short GEOMETRY__ADJUST8VALUE = 334; + public static final short GEOMETRY__ADJUST9VALUE = 335; + public static final short GEOMETRY__ADJUST10VALUE = 336; + public static final short GEOMETRY__SHADOWok = 378; + public static final short GEOMETRY__3DOK = 379; + public static final short GEOMETRY__LINEOK = 380; + public static final short GEOMETRY__GEOTEXTOK = 381; + public static final short GEOMETRY__FILLSHADESHAPEOK = 382; + public static final short GEOMETRY__FILLOK = 383; + public static final short FILL__FILLTYPE = 384; + public static final short FILL__FILLCOLOR = 385 ; + public static final short FILL__FILLOPACITY = 386; + public static final short FILL__FILLBACKCOLOR = 387; + public static final short FILL__BACKOPACITY = 388; + public static final short FILL__CRMOD = 389; + public static final short FILL__PATTERNTEXTURE = 390; + public static final short FILL__BLIPFILENAME = 391; + public static final short FILL__BLIPFLAGS = 392; + public static final short FILL__WIDTH = 393; + public static final short FILL__HEIGHT = 394; + public static final short FILL__ANGLE = 395; + public static final short FILL__FOCUS = 396; + public static final short FILL__TOLEFT = 397; + public static final short FILL__TOTOP = 398; + public static final short FILL__TORIGHT = 399; + public static final short FILL__TOBOTTOM = 400; + public static final short FILL__RECTLEFT = 401; + public static final short FILL__RECTTOP = 402; + public static final short FILL__RECTRIGHT = 403; + public static final short FILL__RECTBOTTOM = 404; + public static final short FILL__DZTYPE = 405; + public static final short FILL__SHADEPRESET = 406; + public static final short FILL__SHADECOLORS = 407; + public static final short FILL__ORIGINX = 408; + public static final short FILL__ORIGINY = 409; + public static final short FILL__SHAPEORIGINX = 410; + public static final short FILL__SHAPEORIGINY = 411; + public static final short FILL__SHADETYPE = 412; + public static final short FILL__FILLED = 443; + public static final short FILL__HITTESTFILL = 444; + public static final short FILL__SHAPE = 445; + public static final short FILL__USERECT = 446; + public static final short FILL__NOFILLHITTEST = 447; + public static final short LINESTYLE__COLOR = 448 ; + public static final short LINESTYLE__OPACITY = 449; + public static final short LINESTYLE__BACKCOLOR = 450; + public static final short LINESTYLE__CRMOD = 451; + public static final short LINESTYLE__LINETYPE = 452; + public static final short LINESTYLE__FILLBLIP = 453; + public static final short LINESTYLE__FILLBLIPNAME = 454; + public static final short LINESTYLE__FILLBLIPFLAGS = 455; + public static final short LINESTYLE__FILLWIDTH = 456; + public static final short LINESTYLE__FILLHEIGHT = 457; + public static final short LINESTYLE__FILLDZTYPE = 458; + public static final short LINESTYLE__LINEWIDTH = 459; + public static final short LINESTYLE__LINEMITERLIMIT = 460; + public static final short LINESTYLE__LINESTYLE = 461; + public static final short LINESTYLE__LINEDASHING = 462; + public static final short LINESTYLE__LINEDASHSTYLE = 463; + public static final short LINESTYLE__LINESTARTARROWHEAD = 464; + public static final short LINESTYLE__LINEENDARROWHEAD = 465; + public static final short LINESTYLE__LINESTARTARROWWIDTH = 466; + public static final short LINESTYLE__LINEESTARTARROWLENGTH = 467; + public static final short LINESTYLE__LINEENDARROWWIDTH = 468; + public static final short LINESTYLE__LINEENDARROWLENGTH = 469; + public static final short LINESTYLE__LINEJOINSTYLE = 470; + public static final short LINESTYLE__LINEENDCAPSTYLE = 471; + public static final short LINESTYLE__ARROWHEADSOK = 507; + public static final short LINESTYLE__ANYLINE = 508; + public static final short LINESTYLE__HITLINETEST = 509; + public static final short LINESTYLE__LINEFILLSHAPE = 510; + public static final short LINESTYLE__NOLINEDRAWDASH = 511; + public static final short SHADOWSTYLE__TYPE = 512; + public static final short SHADOWSTYLE__COLOR = 513; + public static final short SHADOWSTYLE__HIGHLIGHT = 514; + public static final short SHADOWSTYLE__CRMOD = 515; + public static final short SHADOWSTYLE__OPACITY = 516; + public static final short SHADOWSTYLE__OFFSETX = 517; + public static final short SHADOWSTYLE__OFFSETY = 518; + public static final short SHADOWSTYLE__SECONDOFFSETX = 519; + public static final short SHADOWSTYLE__SECONDOFFSETY = 520; + public static final short SHADOWSTYLE__SCALEXTOX = 521; + public static final short SHADOWSTYLE__SCALEYTOX = 522; + public static final short SHADOWSTYLE__SCALEXTOY = 523; + public static final short SHADOWSTYLE__SCALEYTOY = 524; + public static final short SHADOWSTYLE__PERSPECTIVEX = 525; + public static final short SHADOWSTYLE__PERSPECTIVEY = 526; + public static final short SHADOWSTYLE__WEIGHT = 527; + public static final short SHADOWSTYLE__ORIGINX = 528; + public static final short SHADOWSTYLE__ORIGINY = 529; + public static final short SHADOWSTYLE__SHADOW = 574; + public static final short SHADOWSTYLE__SHADOWOBSURED = 575; + public static final short PERSPECTIVE__TYPE = 576; + public static final short PERSPECTIVE__OFFSETX = 577; + public static final short PERSPECTIVE__OFFSETY = 578; + public static final short PERSPECTIVE__SCALEXTOX = 579; + public static final short PERSPECTIVE__SCALEYTOX = 580; + public static final short PERSPECTIVE__SCALEXTOY = 581; + public static final short PERSPECTIVE__SCALEYTOY = 582; + public static final short PERSPECTIVE__PERSPECTIVEX = 583; + public static final short PERSPECTIVE__PERSPECTIVEY = 584; + public static final short PERSPECTIVE__WEIGHT = 585; + public static final short PERSPECTIVE__ORIGINX = 586; + public static final short PERSPECTIVE__ORIGINY = 587; + public static final short PERSPECTIVE__PERSPECTIVEON = 639; + public static final short THREED__SPECULARAMOUNT = 640; + public static final short THREED__DIFFUSEAMOUNT = 661; + public static final short THREED__SHININESS = 662; + public static final short THREED__EDGETHICKNESS = 663; + public static final short THREED__EXTRUDEFORWARD = 664; + public static final short THREED__EXTRUDEBACKWARD = 665; + public static final short THREED__EXTRUDEPLANE = 666; + public static final short THREED__EXTRUSIONCOLOR = 667; + public static final short THREED__CRMOD = 648; + public static final short THREED__3DEFFECT = 700; + public static final short THREED__METALLIC = 701; + public static final short THREED__USEEXTRUSIONCOLOR = 702; + public static final short THREED__LIGHTFACE = 703; + public static final short THREEDSTYLE__YROTATIONANGLE = 704; + public static final short THREEDSTYLE__XROTATIONANGLE = 705; + public static final short THREEDSTYLE__ROTATIONAXISX = 706; + public static final short THREEDSTYLE__ROTATIONAXISY = 707; + public static final short THREEDSTYLE__ROTATIONAXISZ = 708; + public static final short THREEDSTYLE__ROTATIONANGLE = 709; + public static final short THREEDSTYLE__ROTATIONCENTERX = 710; + public static final short THREEDSTYLE__ROTATIONCENTERY = 711; + public static final short THREEDSTYLE__ROTATIONCENTERZ = 712; + public static final short THREEDSTYLE__RENDERMODE = 713; + public static final short THREEDSTYLE__TOLERANCE = 714; + public static final short THREEDSTYLE__XVIEWPOINT = 715; + public static final short THREEDSTYLE__YVIEWPOINT = 716; + public static final short THREEDSTYLE__ZVIEWPOINT = 717; + public static final short THREEDSTYLE__ORIGINX = 718; + public static final short THREEDSTYLE__ORIGINY = 719; + public static final short THREEDSTYLE__SKEWANGLE = 720; + public static final short THREEDSTYLE__SKEWAMOUNT = 721; + public static final short THREEDSTYLE__AMBIENTINTENSITY = 722; + public static final short THREEDSTYLE__KEYX = 723; + public static final short THREEDSTYLE__KEYY = 724; + public static final short THREEDSTYLE__KEYZ = 725; + public static final short THREEDSTYLE__KEYINTENSITY = 726; + public static final short THREEDSTYLE__FILLX = 727; + public static final short THREEDSTYLE__FILLY = 728; + public static final short THREEDSTYLE__FILLZ = 729; + public static final short THREEDSTYLE__FILLINTENSITY = 730; + public static final short THREEDSTYLE__CONSTRAINROTATION = 763; + public static final short THREEDSTYLE__ROTATIONCENTERAUTO = 764; + public static final short THREEDSTYLE__PARALLEL = 765; + public static final short THREEDSTYLE__KEYHARSH = 766; + public static final short THREEDSTYLE__FILLHARSH = 767; + public static final short SHAPE__MASTER = 769; + public static final short SHAPE__CONNECTORSTYLE = 771; + public static final short SHAPE__BLACKANDWHITESETTINGS = 772; + public static final short SHAPE__WMODEPUREBW = 773; + public static final short SHAPE__WMODEBW = 774; + public static final short SHAPE__OLEICON = 826; + public static final short SHAPE__PREFERRELATIVERESIZE = 827; + public static final short SHAPE__LOCKSHAPETYPE = 828; + public static final short SHAPE__DELETEATTACHEDOBJECT = 830; + public static final short SHAPE__BACKGROUNDSHAPE = 831; + public static final short CALLOUT__CALLOUTTYPE = 832; + public static final short CALLOUT__XYCALLOUTGAP = 833; + public static final short CALLOUT__CALLOUTANGLE = 834; + public static final short CALLOUT__CALLOUTDROPTYPE = 835; + public static final short CALLOUT__CALLOUTDROPSPECIFIED = 836; + public static final short CALLOUT__CALLOUTLENGTHSPECIFIED = 837; + public static final short CALLOUT__ISCALLOUT = 889; + public static final short CALLOUT__CALLOUTACCENTBAR = 890; + public static final short CALLOUT__CALLOUTTEXTBORDER = 891; + public static final short CALLOUT__CALLOUTMINUSX = 892; + public static final short CALLOUT__CALLOUTMINUSY = 893; + public static final short CALLOUT__DROPAUTO = 894; + public static final short CALLOUT__LENGTHSPECIFIED = 895; + public static final short GROUPSHAPE__SHAPENAME = 896; + public static final short GROUPSHAPE__DESCRIPTION = 897; + public static final short GROUPSHAPE__HYPERLINK = 898; + public static final short GROUPSHAPE__WRAPPOLYGONVERTICES = 899; + public static final short GROUPSHAPE__WRAPDISTLEFT = 900; + public static final short GROUPSHAPE__WRAPDISTTOP = 901; + public static final short GROUPSHAPE__WRAPDISTRIGHT = 902; + public static final short GROUPSHAPE__WRAPDISTBOTTOM = 903; + public static final short GROUPSHAPE__REGROUPID = 904; + public static final short GROUPSHAPE__EDITEDWRAP = 953; + public static final short GROUPSHAPE__BEHINDDOCUMENT = 954; + public static final short GROUPSHAPE__ONDBLCLICKNOTIFY = 955; + public static final short GROUPSHAPE__ISBUTTON = 956; + public static final short GROUPSHAPE__1DADJUSTMENT = 957; + public static final short GROUPSHAPE__HIDDEN = 958; + public static final short GROUPSHAPE__PRINT = 959; + + + private static Map properties; + + private static void initProps() + { + if ( properties == null ) + { + properties = new HashMap(); + addProp( TRANSFORM__ROTATION, data( "transform.rotation" ) ); + addProp( PROTECTION__LOCKROTATION , data( "protection.lockrotation" ) ); + addProp( PROTECTION__LOCKASPECTRATIO , data( "protection.lockaspectratio" ) ); + addProp( PROTECTION__LOCKPOSITION , data( "protection.lockposition" ) ); + addProp( PROTECTION__LOCKAGAINSTSELECT , data( "protection.lockagainstselect" ) ); + addProp( PROTECTION__LOCKCROPPING , data( "protection.lockcropping" ) ); + addProp( PROTECTION__LOCKVERTICES , data( "protection.lockvertices" ) ); + addProp( PROTECTION__LOCKTEXT , data( "protection.locktext" ) ); + addProp( PROTECTION__LOCKADJUSTHANDLES , data( "protection.lockadjusthandles" ) ); + addProp( PROTECTION__LOCKAGAINSTGROUPING , data( "protection.lockagainstgrouping", EscherPropertyMetaData.TYPE_BOOLEAN ) ); + addProp( TEXT__TEXTID , data( "text.textid" ) ); + addProp( TEXT__TEXTLEFT , data( "text.textleft" ) ); + addProp( TEXT__TEXTTOP , data( "text.texttop" ) ); + addProp( TEXT__TEXTRIGHT , data( "text.textright" ) ); + addProp( TEXT__TEXTBOTTOM , data( "text.textbottom" ) ); + addProp( TEXT__WRAPTEXT , data( "text.wraptext" ) ); + addProp( TEXT__SCALETEXT , data( "text.scaletext" ) ); + addProp( TEXT__ANCHORTEXT , data( "text.anchortext" ) ); + addProp( TEXT__TEXTFLOW , data( "text.textflow" ) ); + addProp( TEXT__FONTROTATION , data( "text.fontrotation" ) ); + addProp( TEXT__IDOFNEXTSHAPE , data( "text.idofnextshape" ) ); + addProp( TEXT__BIDIR , data( "text.bidir" ) ); + addProp( TEXT__SINGLECLICKSELECTS , data( "text.singleclickselects" ) ); + addProp( TEXT__USEHOSTMARGINS , data( "text.usehostmargins" ) ); + addProp( TEXT__ROTATETEXTWITHSHAPE , data( "text.rotatetextwithshape" ) ); + addProp( TEXT__SIZESHAPETOFITTEXT , data( "text.sizeshapetofittext" ) ); + addProp( TEXT__SIZE_TEXT_TO_FIT_SHAPE, data( "text.sizetexttofitshape", EscherPropertyMetaData.TYPE_BOOLEAN ) ); + addProp( GEOTEXT__UNICODE , data( "geotext.unicode" ) ); + addProp( GEOTEXT__RTFTEXT , data( "geotext.rtftext" ) ); + addProp( GEOTEXT__ALIGNMENTONCURVE , data( "geotext.alignmentoncurve" ) ); + addProp( GEOTEXT__DEFAULTPOINTSIZE , data( "geotext.defaultpointsize" ) ); + addProp( GEOTEXT__TEXTSPACING , data( "geotext.textspacing" ) ); + addProp( GEOTEXT__FONTFAMILYNAME , data( "geotext.fontfamilyname" ) ); + addProp( GEOTEXT__REVERSEROWORDER , data( "geotext.reverseroworder" ) ); + addProp( GEOTEXT__HASTEXTEFFECT , data( "geotext.hastexteffect" ) ); + addProp( GEOTEXT__ROTATECHARACTERS , data( "geotext.rotatecharacters" ) ); + addProp( GEOTEXT__KERNCHARACTERS , data( "geotext.kerncharacters" ) ); + addProp( GEOTEXT__TIGHTORTRACK , data( "geotext.tightortrack" ) ); + addProp( GEOTEXT__STRETCHTOFITSHAPE , data( "geotext.stretchtofitshape" ) ); + addProp( GEOTEXT__CHARBOUNDINGBOX , data( "geotext.charboundingbox" ) ); + addProp( GEOTEXT__SCALETEXTONPATH , data( "geotext.scaletextonpath" ) ); + addProp( GEOTEXT__STRETCHCHARHEIGHT , data( "geotext.stretchcharheight" ) ); + addProp( GEOTEXT__NOMEASUREALONGPATH , data( "geotext.nomeasurealongpath" ) ); + addProp( GEOTEXT__BOLDFONT , data( "geotext.boldfont" ) ); + addProp( GEOTEXT__ITALICFONT , data( "geotext.italicfont" ) ); + addProp( GEOTEXT__UNDERLINEFONT , data( "geotext.underlinefont" ) ); + addProp( GEOTEXT__SHADOWFONT , data( "geotext.shadowfont" ) ); + addProp( GEOTEXT__SMALLCAPSFONT , data( "geotext.smallcapsfont" ) ); + addProp( GEOTEXT__STRIKETHROUGHFONT , data( "geotext.strikethroughfont" ) ); + addProp( BLIP__CROPFROMTOP , data( "blip.cropfromtop" ) ); + addProp( BLIP__CROPFROMBOTTOM , data( "blip.cropfrombottom" ) ); + addProp( BLIP__CROPFROMLEFT , data( "blip.cropfromleft" ) ); + addProp( BLIP__CROPFROMRIGHT , data( "blip.cropfromright" ) ); + addProp( BLIP__BLIPTODISPLAY , data( "blip.bliptodisplay" ) ); + addProp( BLIP__BLIPFILENAME , data( "blip.blipfilename" ) ); + addProp( BLIP__BLIPFLAGS , data( "blip.blipflags" ) ); + addProp( BLIP__TRANSPARENTCOLOR , data( "blip.transparentcolor" ) ); + addProp( BLIP__CONTRASTSETTING , data( "blip.contrastsetting" ) ); + addProp( BLIP__BRIGHTNESSSETTING , data( "blip.brightnesssetting" ) ); + addProp( BLIP__GAMMA , data( "blip.gamma" ) ); + addProp( BLIP__PICTUREID , data( "blip.pictureid" ) ); + addProp( BLIP__DOUBLEMOD , data( "blip.doublemod" ) ); + addProp( BLIP__PICTUREFILLMOD , data( "blip.picturefillmod" ) ); + addProp( BLIP__PICTURELINE , data( "blip.pictureline" ) ); + addProp( BLIP__PRINTBLIP , data( "blip.printblip" ) ); + addProp( BLIP__PRINTBLIPFILENAME , data( "blip.printblipfilename" ) ); + addProp( BLIP__PRINTFLAGS , data( "blip.printflags" ) ); + addProp( BLIP__NOHITTESTPICTURE , data( "blip.nohittestpicture" ) ); + addProp( BLIP__PICTUREGRAY , data( "blip.picturegray" ) ); + addProp( BLIP__PICTUREBILEVEL , data( "blip.picturebilevel" ) ); + addProp( BLIP__PICTUREACTIVE , data( "blip.pictureactive" ) ); + addProp( GEOMETRY__LEFT , data( "geometry.left" ) ); + addProp( GEOMETRY__TOP , data( "geometry.top" ) ); + addProp( GEOMETRY__RIGHT , data( "geometry.right" ) ); + addProp( GEOMETRY__BOTTOM , data( "geometry.bottom" ) ); + addProp( GEOMETRY__SHAPEPATH , data( "geometry.shapepath", EscherPropertyMetaData.TYPE_SHAPEPATH ) ); + addProp( GEOMETRY__VERTICES , data( "geometry.vertices" , EscherPropertyMetaData.TYPE_ARRAY ) ); + addProp( GEOMETRY__SEGMENTINFO , data( "geometry.segmentinfo", EscherPropertyMetaData.TYPE_ARRAY ) ); + addProp( GEOMETRY__ADJUSTVALUE , data( "geometry.adjustvalue" ) ); + addProp( GEOMETRY__ADJUST2VALUE , data( "geometry.adjust2value" ) ); + addProp( GEOMETRY__ADJUST3VALUE , data( "geometry.adjust3value" ) ); + addProp( GEOMETRY__ADJUST4VALUE , data( "geometry.adjust4value" ) ); + addProp( GEOMETRY__ADJUST5VALUE , data( "geometry.adjust5value" ) ); + addProp( GEOMETRY__ADJUST6VALUE , data( "geometry.adjust6value" ) ); + addProp( GEOMETRY__ADJUST7VALUE , data( "geometry.adjust7value" ) ); + addProp( GEOMETRY__ADJUST8VALUE , data( "geometry.adjust8value" ) ); + addProp( GEOMETRY__ADJUST9VALUE , data( "geometry.adjust9value" ) ); + addProp( GEOMETRY__ADJUST10VALUE , data( "geometry.adjust10value" ) ); + addProp( GEOMETRY__SHADOWok , data( "geometry.shadowOK" ) ); + addProp( GEOMETRY__3DOK , data( "geometry.3dok" ) ); + addProp( GEOMETRY__LINEOK , data( "geometry.lineok" ) ); + addProp( GEOMETRY__GEOTEXTOK , data( "geometry.geotextok" ) ); + addProp( GEOMETRY__FILLSHADESHAPEOK , data( "geometry.fillshadeshapeok" ) ); + addProp( GEOMETRY__FILLOK , data( "geometry.fillok", EscherPropertyMetaData.TYPE_BOOLEAN ) ); + addProp( FILL__FILLTYPE , data( "fill.filltype" ) ); + addProp( FILL__FILLCOLOR, data( "fill.fillcolor", EscherPropertyMetaData.TYPE_RGB ) ); + addProp( FILL__FILLOPACITY , data( "fill.fillopacity" ) ); + addProp( FILL__FILLBACKCOLOR , data( "fill.fillbackcolor", EscherPropertyMetaData.TYPE_RGB ) ); + addProp( FILL__BACKOPACITY , data( "fill.backopacity" ) ); + addProp( FILL__CRMOD , data( "fill.crmod" ) ); + addProp( FILL__PATTERNTEXTURE , data( "fill.patterntexture" ) ); + addProp( FILL__BLIPFILENAME , data( "fill.blipfilename" ) ); + addProp( FILL__BLIPFLAGS, data( "fill.blipflags" ) ); + addProp( FILL__WIDTH , data( "fill.width" ) ); + addProp( FILL__HEIGHT , data( "fill.height" ) ); + addProp( FILL__ANGLE , data( "fill.angle" ) ); + addProp( FILL__FOCUS , data( "fill.focus" ) ); + addProp( FILL__TOLEFT , data( "fill.toleft" ) ); + addProp( FILL__TOTOP , data( "fill.totop" ) ); + addProp( FILL__TORIGHT , data( "fill.toright" ) ); + addProp( FILL__TOBOTTOM , data( "fill.tobottom" ) ); + addProp( FILL__RECTLEFT , data( "fill.rectleft" ) ); + addProp( FILL__RECTTOP , data( "fill.recttop" ) ); + addProp( FILL__RECTRIGHT , data( "fill.rectright" ) ); + addProp( FILL__RECTBOTTOM , data( "fill.rectbottom" ) ); + addProp( FILL__DZTYPE , data( "fill.dztype" ) ); + addProp( FILL__SHADEPRESET , data( "fill.shadepreset" ) ); + addProp( FILL__SHADECOLORS , data( "fill.shadecolors", EscherPropertyMetaData.TYPE_ARRAY ) ); + addProp( FILL__ORIGINX , data( "fill.originx" ) ); + addProp( FILL__ORIGINY , data( "fill.originy" ) ); + addProp( FILL__SHAPEORIGINX , data( "fill.shapeoriginx" ) ); + addProp( FILL__SHAPEORIGINY , data( "fill.shapeoriginy" ) ); + addProp( FILL__SHADETYPE , data( "fill.shadetype" ) ); + addProp( FILL__FILLED , data( "fill.filled" ) ); + addProp( FILL__HITTESTFILL , data( "fill.hittestfill" ) ); + addProp( FILL__SHAPE , data( "fill.shape" ) ); + addProp( FILL__USERECT , data( "fill.userect" ) ); + addProp( FILL__NOFILLHITTEST , data( "fill.nofillhittest", EscherPropertyMetaData.TYPE_BOOLEAN ) ); + addProp( LINESTYLE__COLOR, data( "linestyle.color", EscherPropertyMetaData.TYPE_RGB ) ); + addProp( LINESTYLE__OPACITY , data( "linestyle.opacity" ) ); + addProp( LINESTYLE__BACKCOLOR , data( "linestyle.backcolor", EscherPropertyMetaData.TYPE_RGB ) ); + addProp( LINESTYLE__CRMOD , data( "linestyle.crmod" ) ); + addProp( LINESTYLE__LINETYPE , data( "linestyle.linetype" ) ); + addProp( LINESTYLE__FILLBLIP , data( "linestyle.fillblip" ) ); + addProp( LINESTYLE__FILLBLIPNAME , data( "linestyle.fillblipname" ) ); + addProp( LINESTYLE__FILLBLIPFLAGS , data( "linestyle.fillblipflags" ) ); + addProp( LINESTYLE__FILLWIDTH , data( "linestyle.fillwidth" ) ); + addProp( LINESTYLE__FILLHEIGHT , data( "linestyle.fillheight" ) ); + addProp( LINESTYLE__FILLDZTYPE , data( "linestyle.filldztype" ) ); + addProp( LINESTYLE__LINEWIDTH , data( "linestyle.linewidth" ) ); + addProp( LINESTYLE__LINEMITERLIMIT , data( "linestyle.linemiterlimit" ) ); + addProp( LINESTYLE__LINESTYLE , data( "linestyle.linestyle" ) ); + addProp( LINESTYLE__LINEDASHING , data( "linestyle.linedashing" ) ); + addProp( LINESTYLE__LINEDASHSTYLE , data( "linestyle.linedashstyle", EscherPropertyMetaData.TYPE_ARRAY ) ); + addProp( LINESTYLE__LINESTARTARROWHEAD , data( "linestyle.linestartarrowhead" ) ); + addProp( LINESTYLE__LINEENDARROWHEAD , data( "linestyle.lineendarrowhead" ) ); + addProp( LINESTYLE__LINESTARTARROWWIDTH , data( "linestyle.linestartarrowwidth" ) ); + addProp( LINESTYLE__LINEESTARTARROWLENGTH , data( "linestyle.lineestartarrowlength" ) ); + addProp( LINESTYLE__LINEENDARROWWIDTH , data( "linestyle.lineendarrowwidth" ) ); + addProp( LINESTYLE__LINEENDARROWLENGTH , data( "linestyle.lineendarrowlength" ) ); + addProp( LINESTYLE__LINEJOINSTYLE , data( "linestyle.linejoinstyle" ) ); + addProp( LINESTYLE__LINEENDCAPSTYLE , data( "linestyle.lineendcapstyle" ) ); + addProp( LINESTYLE__ARROWHEADSOK , data( "linestyle.arrowheadsok" ) ); + addProp( LINESTYLE__ANYLINE , data( "linestyle.anyline" ) ); + addProp( LINESTYLE__HITLINETEST , data( "linestyle.hitlinetest" ) ); + addProp( LINESTYLE__LINEFILLSHAPE , data( "linestyle.linefillshape" ) ); + addProp( LINESTYLE__NOLINEDRAWDASH , data( "linestyle.nolinedrawdash", EscherPropertyMetaData.TYPE_BOOLEAN ) ); + addProp( SHADOWSTYLE__TYPE , data( "shadowstyle.type" ) ); + addProp( SHADOWSTYLE__COLOR , data( "shadowstyle.color", EscherPropertyMetaData.TYPE_RGB ) ); + addProp( SHADOWSTYLE__HIGHLIGHT , data( "shadowstyle.highlight" ) ); + addProp( SHADOWSTYLE__CRMOD , data( "shadowstyle.crmod" ) ); + addProp( SHADOWSTYLE__OPACITY , data( "shadowstyle.opacity" ) ); + addProp( SHADOWSTYLE__OFFSETX , data( "shadowstyle.offsetx" ) ); + addProp( SHADOWSTYLE__OFFSETY , data( "shadowstyle.offsety" ) ); + addProp( SHADOWSTYLE__SECONDOFFSETX , data( "shadowstyle.secondoffsetx" ) ); + addProp( SHADOWSTYLE__SECONDOFFSETY , data( "shadowstyle.secondoffsety" ) ); + addProp( SHADOWSTYLE__SCALEXTOX , data( "shadowstyle.scalextox" ) ); + addProp( SHADOWSTYLE__SCALEYTOX , data( "shadowstyle.scaleytox" ) ); + addProp( SHADOWSTYLE__SCALEXTOY , data( "shadowstyle.scalextoy" ) ); + addProp( SHADOWSTYLE__SCALEYTOY , data( "shadowstyle.scaleytoy" ) ); + addProp( SHADOWSTYLE__PERSPECTIVEX , data( "shadowstyle.perspectivex" ) ); + addProp( SHADOWSTYLE__PERSPECTIVEY , data( "shadowstyle.perspectivey" ) ); + addProp( SHADOWSTYLE__WEIGHT , data( "shadowstyle.weight" ) ); + addProp( SHADOWSTYLE__ORIGINX , data( "shadowstyle.originx" ) ); + addProp( SHADOWSTYLE__ORIGINY , data( "shadowstyle.originy" ) ); + addProp( SHADOWSTYLE__SHADOW , data( "shadowstyle.shadow" ) ); + addProp( SHADOWSTYLE__SHADOWOBSURED , data( "shadowstyle.shadowobsured" ) ); + addProp( PERSPECTIVE__TYPE , data( "perspective.type" ) ); + addProp( PERSPECTIVE__OFFSETX , data( "perspective.offsetx" ) ); + addProp( PERSPECTIVE__OFFSETY , data( "perspective.offsety" ) ); + addProp( PERSPECTIVE__SCALEXTOX , data( "perspective.scalextox" ) ); + addProp( PERSPECTIVE__SCALEYTOX , data( "perspective.scaleytox" ) ); + addProp( PERSPECTIVE__SCALEXTOY , data( "perspective.scalextoy" ) ); + addProp( PERSPECTIVE__SCALEYTOY , data( "perspective.scaleytoy" ) ); + addProp( PERSPECTIVE__PERSPECTIVEX , data( "perspective.perspectivex" ) ); + addProp( PERSPECTIVE__PERSPECTIVEY , data( "perspective.perspectivey" ) ); + addProp( PERSPECTIVE__WEIGHT , data( "perspective.weight" ) ); + addProp( PERSPECTIVE__ORIGINX , data( "perspective.originx" ) ); + addProp( PERSPECTIVE__ORIGINY , data( "perspective.originy" ) ); + addProp( PERSPECTIVE__PERSPECTIVEON , data( "perspective.perspectiveon" ) ); + addProp( THREED__SPECULARAMOUNT , data( "3d.specularamount" ) ); + addProp( THREED__DIFFUSEAMOUNT , data( "3d.diffuseamount" ) ); + addProp( THREED__SHININESS , data( "3d.shininess" ) ); + addProp( THREED__EDGETHICKNESS , data( "3d.edgethickness" ) ); + addProp( THREED__EXTRUDEFORWARD , data( "3d.extrudeforward" ) ); + addProp( THREED__EXTRUDEBACKWARD , data( "3d.extrudebackward" ) ); + addProp( THREED__EXTRUDEPLANE , data( "3d.extrudeplane" ) ); + addProp( THREED__EXTRUSIONCOLOR , data( "3d.extrusioncolor", EscherPropertyMetaData.TYPE_RGB ) ); + addProp( THREED__CRMOD , data( "3d.crmod" ) ); + addProp( THREED__3DEFFECT , data( "3d.3deffect" ) ); + addProp( THREED__METALLIC , data( "3d.metallic" ) ); + addProp( THREED__USEEXTRUSIONCOLOR , data( "3d.useextrusioncolor", EscherPropertyMetaData.TYPE_RGB ) ); + addProp( THREED__LIGHTFACE , data( "3d.lightface" ) ); + addProp( THREEDSTYLE__YROTATIONANGLE , data( "3dstyle.yrotationangle" ) ); + addProp( THREEDSTYLE__XROTATIONANGLE , data( "3dstyle.xrotationangle" ) ); + addProp( THREEDSTYLE__ROTATIONAXISX , data( "3dstyle.rotationaxisx" ) ); + addProp( THREEDSTYLE__ROTATIONAXISY , data( "3dstyle.rotationaxisy" ) ); + addProp( THREEDSTYLE__ROTATIONAXISZ , data( "3dstyle.rotationaxisz" ) ); + addProp( THREEDSTYLE__ROTATIONANGLE , data( "3dstyle.rotationangle" ) ); + addProp( THREEDSTYLE__ROTATIONCENTERX , data( "3dstyle.rotationcenterx" ) ); + addProp( THREEDSTYLE__ROTATIONCENTERY , data( "3dstyle.rotationcentery" ) ); + addProp( THREEDSTYLE__ROTATIONCENTERZ , data( "3dstyle.rotationcenterz" ) ); + addProp( THREEDSTYLE__RENDERMODE , data( "3dstyle.rendermode" ) ); + addProp( THREEDSTYLE__TOLERANCE , data( "3dstyle.tolerance" ) ); + addProp( THREEDSTYLE__XVIEWPOINT , data( "3dstyle.xviewpoint" ) ); + addProp( THREEDSTYLE__YVIEWPOINT , data( "3dstyle.yviewpoint" ) ); + addProp( THREEDSTYLE__ZVIEWPOINT , data( "3dstyle.zviewpoint" ) ); + addProp( THREEDSTYLE__ORIGINX , data( "3dstyle.originx" ) ); + addProp( THREEDSTYLE__ORIGINY , data( "3dstyle.originy" ) ); + addProp( THREEDSTYLE__SKEWANGLE , data( "3dstyle.skewangle" ) ); + addProp( THREEDSTYLE__SKEWAMOUNT , data( "3dstyle.skewamount" ) ); + addProp( THREEDSTYLE__AMBIENTINTENSITY , data( "3dstyle.ambientintensity" ) ); + addProp( THREEDSTYLE__KEYX , data( "3dstyle.keyx" ) ); + addProp( THREEDSTYLE__KEYY , data( "3dstyle.keyy" ) ); + addProp( THREEDSTYLE__KEYZ , data( "3dstyle.keyz" ) ); + addProp( THREEDSTYLE__KEYINTENSITY , data( "3dstyle.keyintensity" ) ); + addProp( THREEDSTYLE__FILLX , data( "3dstyle.fillx" ) ); + addProp( THREEDSTYLE__FILLY , data( "3dstyle.filly" ) ); + addProp( THREEDSTYLE__FILLZ , data( "3dstyle.fillz" ) ); + addProp( THREEDSTYLE__FILLINTENSITY , data( "3dstyle.fillintensity" ) ); + addProp( THREEDSTYLE__CONSTRAINROTATION , data( "3dstyle.constrainrotation" ) ); + addProp( THREEDSTYLE__ROTATIONCENTERAUTO , data( "3dstyle.rotationcenterauto" ) ); + addProp( THREEDSTYLE__PARALLEL , data( "3dstyle.parallel" ) ); + addProp( THREEDSTYLE__KEYHARSH , data( "3dstyle.keyharsh" ) ); + addProp( THREEDSTYLE__FILLHARSH , data( "3dstyle.fillharsh" ) ); + addProp( SHAPE__MASTER , data( "shape.master" ) ); + addProp( SHAPE__CONNECTORSTYLE , data( "shape.connectorstyle" ) ); + addProp( SHAPE__BLACKANDWHITESETTINGS , data( "shape.blackandwhitesettings" ) ); + addProp( SHAPE__WMODEPUREBW , data( "shape.wmodepurebw" ) ); + addProp( SHAPE__WMODEBW , data( "shape.wmodebw" ) ); + addProp( SHAPE__OLEICON , data( "shape.oleicon" ) ); + addProp( SHAPE__PREFERRELATIVERESIZE , data( "shape.preferrelativeresize" ) ); + addProp( SHAPE__LOCKSHAPETYPE , data( "shape.lockshapetype" ) ); + addProp( SHAPE__DELETEATTACHEDOBJECT , data( "shape.deleteattachedobject" ) ); + addProp( SHAPE__BACKGROUNDSHAPE , data( "shape.backgroundshape" ) ); + addProp( CALLOUT__CALLOUTTYPE , data( "callout.callouttype" ) ); + addProp( CALLOUT__XYCALLOUTGAP , data( "callout.xycalloutgap" ) ); + addProp( CALLOUT__CALLOUTANGLE , data( "callout.calloutangle" ) ); + addProp( CALLOUT__CALLOUTDROPTYPE , data( "callout.calloutdroptype" ) ); + addProp( CALLOUT__CALLOUTDROPSPECIFIED , data( "callout.calloutdropspecified" ) ); + addProp( CALLOUT__CALLOUTLENGTHSPECIFIED , data( "callout.calloutlengthspecified" ) ); + addProp( CALLOUT__ISCALLOUT , data( "callout.iscallout" ) ); + addProp( CALLOUT__CALLOUTACCENTBAR , data( "callout.calloutaccentbar" ) ); + addProp( CALLOUT__CALLOUTTEXTBORDER , data( "callout.callouttextborder" ) ); + addProp( CALLOUT__CALLOUTMINUSX , data( "callout.calloutminusx" ) ); + addProp( CALLOUT__CALLOUTMINUSY , data( "callout.calloutminusy" ) ); + addProp( CALLOUT__DROPAUTO , data( "callout.dropauto" ) ); + addProp( CALLOUT__LENGTHSPECIFIED , data( "callout.lengthspecified" ) ); + addProp( GROUPSHAPE__SHAPENAME , data( "groupshape.shapename" ) ); + addProp( GROUPSHAPE__DESCRIPTION , data( "groupshape.description" ) ); + addProp( GROUPSHAPE__HYPERLINK , data( "groupshape.hyperlink" ) ); + addProp( GROUPSHAPE__WRAPPOLYGONVERTICES , data( "groupshape.wrappolygonvertices", EscherPropertyMetaData.TYPE_ARRAY ) ); + addProp( GROUPSHAPE__WRAPDISTLEFT , data( "groupshape.wrapdistleft" ) ); + addProp( GROUPSHAPE__WRAPDISTTOP , data( "groupshape.wrapdisttop" ) ); + addProp( GROUPSHAPE__WRAPDISTRIGHT , data( "groupshape.wrapdistright" ) ); + addProp( GROUPSHAPE__WRAPDISTBOTTOM , data( "groupshape.wrapdistbottom" ) ); + addProp( GROUPSHAPE__REGROUPID , data( "groupshape.regroupid" ) ); + addProp( GROUPSHAPE__EDITEDWRAP , data( "groupshape.editedwrap" ) ); + addProp( GROUPSHAPE__BEHINDDOCUMENT , data( "groupshape.behinddocument" ) ); + addProp( GROUPSHAPE__ONDBLCLICKNOTIFY , data( "groupshape.ondblclicknotify" ) ); + addProp( GROUPSHAPE__ISBUTTON , data( "groupshape.isbutton" ) ); + addProp( GROUPSHAPE__1DADJUSTMENT , data( "groupshape.1dadjustment" ) ); + addProp( GROUPSHAPE__HIDDEN , data( "groupshape.hidden" ) ); + addProp( GROUPSHAPE__PRINT , data( "groupshape.print", EscherPropertyMetaData.TYPE_BOOLEAN ) ); + } + } + + private static void addProp( int s, EscherPropertyMetaData data ) + { + properties.put( new Short( (short) s ), data ); + } + + private static EscherPropertyMetaData data( String propName, byte type ) + { + return new EscherPropertyMetaData( propName, type ); + } + + private static EscherPropertyMetaData data( String propName ) + { + return new EscherPropertyMetaData( propName ); + } + + public static String getPropertyName( short propertyId ) + { + initProps(); + EscherPropertyMetaData o = (EscherPropertyMetaData) properties.get( new Short( propertyId ) ); + return o == null ? "unknown" : o.getDescription(); + } + + public static byte getPropertyType( short propertyId ) + { + initProps(); + EscherPropertyMetaData escherPropertyMetaData = (EscherPropertyMetaData) properties.get( new Short( propertyId ) ); + return escherPropertyMetaData == null ? 0 : escherPropertyMetaData.getType(); + } +} + + + diff --git a/src/java/org/apache/poi/ddf/EscherProperty.java b/src/java/org/apache/poi/ddf/EscherProperty.java new file mode 100644 index 0000000000..787ccf2f8d --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherProperty.java @@ -0,0 +1,78 @@ +package org.apache.poi.ddf; + +/** + * This is the abstract base class for all escher properties. + * + * @see EscherOptRecord + * + * @author Glen Stampoultzis (glens at apache.org) + */ +abstract public class EscherProperty +{ + private short id; + + /** + * The id is distinct from the actual property number. The id includes the property number the blip id + * flag and an indicator whether the property is complex or not. + */ + public EscherProperty( short id ) + { + this.id = id; + } + + /** + * Constructs a new escher property. The three parameters are combined to form a property + * id. + */ + public EscherProperty( short propertyNumber, boolean isComplex, boolean isBlipId ) + { + this.id = (short)(propertyNumber + + (isComplex ? 0x8000 : 0x0) + + (isBlipId ? 0x4000 : 0x0)); + } + + public short getId() + { + return id; + } + + public short getPropertyNumber() + { + return (short) ( id & (short) 0x3FFF ); + } + + public boolean isComplex() + { + return ( id & (short) 0x8000 ) != 0; + } + + public boolean isBlipId() + { + return ( id & (short) 0x4000 ) != 0; + } + + public String getName() + { + return EscherProperties.getPropertyName(id); + } + + /** + * Most properties are just 6 bytes in length. Override this if we're + * dealing with complex properties. + */ + public int getPropertySize() + { + return 6; + } + + /** + * Escher properties consist of a simple fixed length part and a complex variable length part. + * The fixed length part is serialized first. + */ + abstract public int serializeSimplePart( byte[] data, int pos ); + /** + * Escher properties consist of a simple fixed length part and a complex variable length part. + * The fixed length part is serialized first. + */ + abstract public int serializeComplexPart( byte[] data, int pos ); +} diff --git a/src/java/org/apache/poi/ddf/EscherPropertyFactory.java b/src/java/org/apache/poi/ddf/EscherPropertyFactory.java new file mode 100644 index 0000000000..cc6b7ba7a8 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherPropertyFactory.java @@ -0,0 +1,91 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.LittleEndian; +import org.apache.poi.hssf.record.RecordFormatException; + +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; + +/** + * Generates a property given a reference into the byte array storing that property. + * + * @author Glen Stampoultzis + */ +public class EscherPropertyFactory +{ + /** + * Create new properties from a byte array. + * + * @param data The byte array containing the property + * @param offset The starting offset into the byte array + * @return The new properties + */ + public List createProperties( byte[] data, int offset, short numProperties ) + { + List results = new ArrayList(); + + int pos = offset; + int complexBytes = 0; +// while ( bytesRemaining >= 6 ) + for (int i = 0; i < numProperties; i++) + { + short propId; + int propData; + propId = LittleEndian.getShort( data, pos ); + propData = LittleEndian.getInt( data, pos + 2 ); + short propNumber = (short) ( propId & (short) 0x3FFF ); + boolean isComplex = ( propId & (short) 0x8000 ) != 0; + boolean isBlipId = ( propId & (short) 0x4000 ) != 0; + if ( isComplex ) + complexBytes = propData; + else + complexBytes = 0; + byte propertyType = EscherProperties.getPropertyType( (short) propNumber ); + if ( propertyType == EscherPropertyMetaData.TYPE_BOOLEAN ) + results.add( new EscherBoolProperty( propNumber, propData ) ); + else if ( propertyType == EscherPropertyMetaData.TYPE_RGB ) + results.add( new EscherRGBProperty( propNumber, propData ) ); + else if ( propertyType == EscherPropertyMetaData.TYPE_SHAPEPATH ) + results.add( new EscherShapePathProperty( propNumber, propData ) ); + else + { + if ( !isComplex ) + results.add( new EscherSimpleProperty( propNumber, propData ) ); + else + { + if ( propertyType == EscherPropertyMetaData.TYPE_ARRAY) + results.add( new EscherArrayProperty( propId, new byte[propData]) ); + else + results.add( new EscherComplexProperty( propId, new byte[propData]) ); + + } + } + pos += 6; +// bytesRemaining -= 6 + complexBytes; + } + + // Get complex data + for ( Iterator iterator = results.iterator(); iterator.hasNext(); ) + { + EscherProperty p = (EscherProperty) iterator.next(); + if (p instanceof EscherComplexProperty) + { + if (p instanceof EscherArrayProperty) + { + pos += ((EscherArrayProperty)p).setArrayData(data, pos); + } + else + { + byte[] complexData = ((EscherComplexProperty)p).getComplexData(); + System.arraycopy(data, pos, complexData, 0, complexData.length); + pos += complexData.length; + } + } + } + + return results; + } + + +} diff --git a/src/java/org/apache/poi/ddf/EscherPropertyMetaData.java b/src/java/org/apache/poi/ddf/EscherPropertyMetaData.java new file mode 100644 index 0000000000..97576064e7 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherPropertyMetaData.java @@ -0,0 +1,51 @@ +package org.apache.poi.ddf; + +/** + * This class stores the type and description of an escher property. + * + * @author Glen Stampoultzis (glens at apache.org) + */ +public class EscherPropertyMetaData +{ + // Escher property types. + public final static byte TYPE_UNKNOWN = (byte) 0; + public final static byte TYPE_BOOLEAN = (byte) 1; + public final static byte TYPE_RGB = (byte) 2; + public final static byte TYPE_SHAPEPATH = (byte) 3; + public final static byte TYPE_SIMPLE = (byte)4; + public final static byte TYPE_ARRAY = (byte)5;; + + private String description; + private byte type; + + + /** + * @param description The description of the escher property. + */ + public EscherPropertyMetaData( String description ) + { + this.description = description; + } + + /** + * + * @param description The description of the escher property. + * @param type The type of the property. + */ + public EscherPropertyMetaData( String description, byte type ) + { + this.description = description; + this.type = type; + } + + public String getDescription() + { + return description; + } + + public byte getType() + { + return type; + } + +} diff --git a/src/java/org/apache/poi/ddf/EscherRGBProperty.java b/src/java/org/apache/poi/ddf/EscherRGBProperty.java new file mode 100644 index 0000000000..1b8c9d2952 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherRGBProperty.java @@ -0,0 +1,37 @@ +package org.apache.poi.ddf; + +/** + * A color property. + * + * @author Glen Stampoultzis (glens at apache.org) + */ +public class EscherRGBProperty + extends EscherSimpleProperty +{ + + public EscherRGBProperty( short propertyNumber, int rgbColor ) + { + super( propertyNumber, false, false, rgbColor ); + } + + public int getRgbColor() + { + return propertyValue; + } + + public byte getRed() + { + return (byte) ( propertyValue & 0xFF ); + } + + public byte getGreen() + { + return (byte) ( (propertyValue >> 8) & 0xFF ); + } + + public byte getBlue() + { + return (byte) ( (propertyValue >> 16) & 0xFF ); + } + +} diff --git a/src/java/org/apache/poi/ddf/EscherRecord.java b/src/java/org/apache/poi/ddf/EscherRecord.java new file mode 100644 index 0000000000..0001bbfec0 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherRecord.java @@ -0,0 +1,273 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.LittleEndian; + +import java.io.PrintWriter; +import java.util.Collections; +import java.util.List; + +/** + * The base abstract record from which all escher records are defined. Subclasses will need + * to define methods for serialization/deserialization and for determining the record size. + * + * @author Glen Stampoultzis + */ +abstract public class EscherRecord +{ + private short options; + private short recordId; + + /** + * Create a new instance + */ + public EscherRecord() + { + } + + /** + * Delegates to fillFields(byte[], int, EscherRecordFactory) + * + * @see #fillFields(byte[], int, org.apache.poi.ddf.EscherRecordFactory) + */ + protected int fillFields( byte[] data, EscherRecordFactory f ) + { + return fillFields( data, 0, f ); + } + + /** + * The contract of this method is to deserialize an escher record including + * it's children. + * + * @param data The byte array containing the serialized escher + * records. + * @param offset The offset into the byte array. + * @param recordFactory A factory for creating new escher records. + * @return The number of bytes written. + */ + public abstract int fillFields( byte[] data, int offset, EscherRecordFactory recordFactory ); + + /** + * Reads the 8 byte header information and populates the options + * and recordId records. + * + * @param data the byte array to read from + * @param offset the offset to start reading from + * @return the number of bytes remaining in this record. This + * may include the children if this is a container. + */ + protected int readHeader( byte[] data, int offset ) + { + EscherRecordHeader header = EscherRecordHeader.readHeader(data, offset); + options = header.getOptions(); + recordId = header.getRecordId(); + return header.getRemainingBytes(); + } + + /** + * Determine whether this is a container record by inspecting the option + * field. + * @return true is this is a container field. + */ + public boolean isContainerRecord() + { + return (options & (short)0x000f) == (short)0x000f; + } + + /** + * @return The options field for this record. All records have one. + */ + public short getOptions() + { + return options; + } + + /** + * Set the options this this record. Container records should have the + * last nibble set to 0xF. + */ + public void setOptions( short options ) + { + this.options = options; + } + + /** + * Serializes to a new byte array. This is done by delegating to + * serialize(int, byte[]); + * + * @return the serialized record. + * @see #serialize(int, byte[]) + */ + public byte[] serialize() + { + byte[] retval = new byte[getRecordSize()]; + + serialize( 0, retval ); + return retval; + } + + /** + * Serializes to an existing byte array without serialization listener. + * This is done by delegating to serialize(int, byte[], EscherSerializationListener). + * + * @param offset the offset within the data byte array. + * @param data the data array to serialize to. + * @return The number of bytes written. + * + * @see #serialize(int, byte[], org.apache.poi.ddf.EscherSerializationListener) + */ + public int serialize( int offset, byte[] data) + { + return serialize( offset, data, new NullEscherSerializationListener() ); + } + + /** + * Serializes the record to an existing byte array. + * + * @param offset the offset within the byte array + * @param data the data array to serialize to + * @param listener a listener for begin and end serialization events. This + * is useful because the serialization is + * hierarchical/recursive and sometimes you need to be able + * break into that. + * @return the number of bytes written. + */ + public abstract int serialize( int offset, byte[] data, EscherSerializationListener listener ); + + /** + * Subclasses should effeciently return the number of bytes required to + * serialize the record. + * + * @return number of bytes + */ + abstract public int getRecordSize(); + + /** + * Return the current record id. + * + * @return The 16 bit record id. + */ + public short getRecordId() + { + return recordId; + } + + /** + * Sets the record id for this record. + */ + public void setRecordId( short recordId ) + { + this.recordId = recordId; + } + + /** + * @return Returns the children of this record. By default this will + * be an empty list. EscherCotainerRecord is the only record + * that may contain children. + * + * @see EscherContainerRecord + */ + public List getChildRecords() { return Collections.EMPTY_LIST; } + + /** + * Sets the child records for this record. By default this will throw + * an exception as only EscherContainerRecords may have children. + * + * @param childRecords Not used in base implementation. + */ + public void setChildRecords( List childRecords ) { throw new IllegalArgumentException("This record does not support child records."); } + + /** + * Escher records may need to be clonable in the future. + */ + public Object clone() + { + throw new RuntimeException( "The class " + getClass().getName() + " needs to define a clone method" ); + } + + /** + * Returns the indexed child record. + */ + public EscherRecord getChild( int index ) + { + return (EscherRecord) getChildRecords().get(index); + } + + /** + * The display methods allows escher variables to print the record names + * according to their hierarchy. + * + * @param w The print writer to output to. + * @param indent The current indent level. + */ + public void display(PrintWriter w, int indent) + { + for (int i = 0; i < indent * 4; i++) w.print(' '); + w.println(getRecordName()); + } + + /** + * Subclasses should return the short name for this escher record. + */ + public abstract String getRecordName(); + + /** + * Returns the instance part of the option record. + * + * @return The instance part of the record + */ + public short getInstance() + { + return (short) ( options >> 4 ); + } + + /** + * This class reads the standard escher header. + */ + static class EscherRecordHeader + { + private short options; + private short recordId; + private int remainingBytes; + + private EscherRecordHeader() + { + } + + public static EscherRecordHeader readHeader( byte[] data, int offset ) + { + EscherRecordHeader header = new EscherRecordHeader(); + header.options = LittleEndian.getShort(data, offset); + header.recordId = LittleEndian.getShort(data, offset + 2); + header.remainingBytes = LittleEndian.getInt( data, offset + 4 ); + return header; + } + + + public short getOptions() + { + return options; + } + + public short getRecordId() + { + return recordId; + } + + public int getRemainingBytes() + { + return remainingBytes; + } + + public String toString() + { + return "EscherRecordHeader{" + + "options=" + options + + ", recordId=" + recordId + + ", remainingBytes=" + remainingBytes + + "}"; + } + + + } + +} diff --git a/src/java/org/apache/poi/ddf/EscherRecordFactory.java b/src/java/org/apache/poi/ddf/EscherRecordFactory.java new file mode 100644 index 0000000000..4c6e22c8a6 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherRecordFactory.java @@ -0,0 +1,16 @@ +package org.apache.poi.ddf; + +/** + * The escher record factory interface allows for the creation of escher + * records from a pointer into a data array. + * + * @author Glen Stampoultzis (glens at apache.org) + */ +public interface EscherRecordFactory +{ + /** + * Create a new escher record from the data provided. Does not attempt + * to fill the contents of the record however. + */ + EscherRecord createRecord( byte[] data, int offset ); +} diff --git a/src/java/org/apache/poi/ddf/EscherSerializationListener.java b/src/java/org/apache/poi/ddf/EscherSerializationListener.java new file mode 100644 index 0000000000..ddea267c7e --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherSerializationListener.java @@ -0,0 +1,27 @@ +package org.apache.poi.ddf; + +/** + * Interface for listening to escher serialization events. + * + * @author Glen Stampoultzis (glens at apache.org) + */ +public interface EscherSerializationListener +{ + /** + * Fired before a given escher record is serialized. + * + * @param offset The position in the data array at which the record will be serialized. + * @param recordId The id of the record about to be serialized. + */ + void beforeRecordSerialize(int offset, short recordId, EscherRecord record); + + /** + * Fired after a record has been serialized. + * + * @param offset The position of the end of the serialized record + 1 + * @param recordId The id of the record about to be serialized + * @param size The number of bytes written for this record. If it is a container + * record then this will include the size of any included records. + */ + void afterRecordSerialize(int offset, short recordId, int size, EscherRecord record); +} diff --git a/src/java/org/apache/poi/ddf/EscherShapePathProperty.java b/src/java/org/apache/poi/ddf/EscherShapePathProperty.java new file mode 100644 index 0000000000..df70e9c83f --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherShapePathProperty.java @@ -0,0 +1,25 @@ +package org.apache.poi.ddf; + +/** + * Defines the constants for the various possible shape paths. + * + * @author Glen Stampoultzis (glens at apache.org) + */ +public class EscherShapePathProperty + extends EscherSimpleProperty +{ + + public static final int LINE_OF_STRAIGHT_SEGMENTS = 0; + public static final int CLOSED_POLYGON = 1; + public static final int CURVES = 2; + public static final int CLOSED_CURVES = 3; + public static final int COMPLEX = 4; + + public EscherShapePathProperty( short propertyNumber, int shapePath ) + { + super( propertyNumber, false, false, shapePath ); + } + + + +} diff --git a/src/java/org/apache/poi/ddf/EscherSimpleProperty.java b/src/java/org/apache/poi/ddf/EscherSimpleProperty.java new file mode 100644 index 0000000000..b61474efdd --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherSimpleProperty.java @@ -0,0 +1,103 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.HexDump; + +/** + * A simple property is of fixed length and as a property number in addition + * to a 32-bit value. Properties that can't be stored in only 32-bits are + * stored as EscherComplexProperty objects. + * + * @author Glen Stampoultzis (glens at apache.org) + */ +public class EscherSimpleProperty extends EscherProperty +{ + protected int propertyValue; + + /** + * The id is distinct from the actual property number. The id includes the property number the blip id + * flag and an indicator whether the property is complex or not. + */ + public EscherSimpleProperty( short id, int propertyValue ) + { + super( id ); + this.propertyValue = propertyValue; + } + + /** + * Constructs a new escher property. The three parameters are combined to form a property + * id. + */ + public EscherSimpleProperty( short propertyNumber, boolean isComplex, boolean isBlipId, int propertyValue ) + { + super( propertyNumber, isComplex, isBlipId ); + this.propertyValue = propertyValue; + } + + /** + * Serialize the simple part of the escher record. + * + * @return the number of bytes serialized. + */ + public int serializeSimplePart( byte[] data, int offset ) + { + LittleEndian.putShort(data, offset, getId()); + LittleEndian.putInt(data, offset + 2, propertyValue); + return 6; + } + + /** + * Escher properties consist of a simple fixed length part and a complex variable length part. + * The fixed length part is serialized first. + */ + public int serializeComplexPart( byte[] data, int pos ) + { + return 0; + } + + /** + * @return Return the 32 bit value of this property. + */ + public int getPropertyValue() + { + return propertyValue; + } + + /** + * Returns true if one escher property is equal to another. + */ + public boolean equals( Object o ) + { + if ( this == o ) return true; + if ( !( o instanceof EscherSimpleProperty ) ) return false; + + final EscherSimpleProperty escherSimpleProperty = (EscherSimpleProperty) o; + + if ( propertyValue != escherSimpleProperty.propertyValue ) return false; + if ( getId() != escherSimpleProperty.getId() ) return false; + + return true; + } + + /** + * Returns a hashcode so that this object can be stored in collections that + * require the use of such things. + */ + public int hashCode() + { + return propertyValue; + } + + /** + * @return the string representation of this property. + */ + public String toString() + { + return "propNum: " + getPropertyNumber() + + ", propName: " + EscherProperties.getPropertyName( getPropertyNumber() ) + + ", complex: " + isComplex() + + ", blipId: " + isBlipId() + + ", value: " + propertyValue + " (0x" + HexDump.toHex(propertyValue) + ")"; + } + +} diff --git a/src/java/org/apache/poi/ddf/EscherSpRecord.java b/src/java/org/apache/poi/ddf/EscherSpRecord.java new file mode 100644 index 0000000000..a5e2be9ff9 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherSpRecord.java @@ -0,0 +1,201 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; + +/** + * Together the the EscherOptRecord this record defines some of the basic + * properties of a shape. + * + * @author Glen Stampoultzis (glens at apache.org) + */ +public class EscherSpRecord + extends EscherRecord +{ + public static final short RECORD_ID = (short) 0xF00A; + public static final String RECORD_DESCRIPTION = "MsofbtSp"; + + public static final int FLAG_GROUP = 0x0001; + public static final int FLAG_CHILD = 0x0002; + public static final int FLAG_PATRIARCH = 0x0004; + public static final int FLAG_DELETED = 0x0008; + public static final int FLAG_OLESHAPE = 0x0010; + public static final int FLAG_HAVEMASTER = 0x0020; + public static final int FLAG_FLIPHORIZ = 0x0040; + public static final int FLAG_FLIPVERT = 0x0080; + public static final int FLAG_CONNECTOR = 0x0100; + public static final int FLAG_HAVEANCHOR = 0x0200; + public static final int FLAG_BACKGROUND = 0x0400; + public static final int FLAG_HASSHAPETYPE = 0x0800; + + private int field_1_shapeId; + private int field_2_flags; + + /** + * This method deserializes the record from a byte array. + * + * @param data The byte array containing the escher record information + * @param offset The starting offset into data. + * @param recordFactory May be null since this is not a container record. + * @return The number of bytes read from the byte array. + */ + public int fillFields( byte[] data, int offset, EscherRecordFactory recordFactory ) + { + int bytesRemaining = readHeader( data, offset ); + int pos = offset + 8; + int size = 0; + field_1_shapeId = LittleEndian.getInt( data, pos + size ); size += 4; + field_2_flags = LittleEndian.getInt( data, pos + size ); size += 4; +// bytesRemaining -= size; +// remainingData = new byte[bytesRemaining]; +// System.arraycopy( data, pos + size, remainingData, 0, bytesRemaining ); + return getRecordSize(); + } + + /** + * This method serializes this escher record into a byte array. + * + * @param offset The offset into data to start writing the record data to. + * @param data The byte array to serialize to. + * @param listener A listener to retrieve start and end callbacks. Use a NullEscherSerailizationListener to ignore these events. + * @return The number of bytes written. + * + * @see NullEscherSerializationListener + */ + public int serialize( int offset, byte[] data, EscherSerializationListener listener ) + { + listener.beforeRecordSerialize( offset, getRecordId(), this ); + LittleEndian.putShort( data, offset, getOptions() ); + LittleEndian.putShort( data, offset + 2, getRecordId() ); + int remainingBytes = 8; + LittleEndian.putInt( data, offset + 4, remainingBytes ); + LittleEndian.putInt( data, offset + 8, field_1_shapeId ); + LittleEndian.putInt( data, offset + 12, field_2_flags ); +// System.arraycopy( remainingData, 0, data, offset + 26, remainingData.length ); +// int pos = offset + 8 + 18 + remainingData.length; + listener.afterRecordSerialize( offset + getRecordSize(), getRecordId(), getRecordSize(), this ); + return 8 + 8; + } + + /** + * Returns the number of bytes that are required to serialize this record. + * + * @return Number of bytes + */ + public int getRecordSize() + { + return 8 + 8; + } + + /** + * @return the 16 bit identifier for this record. + */ + public short getRecordId() + { + return RECORD_ID; + } + + /** + * The short name for this record + */ + public String getRecordName() + { + return "Sp"; + } + + /** + * @return the string representing this shape. + */ + public String toString() + { + String nl = System.getProperty("line.separator"); + + return getClass().getName() + ":" + nl + + " RecordId: 0x" + HexDump.toHex(RECORD_ID) + nl + + " Options: 0x" + HexDump.toHex(getOptions()) + nl + + " ShapeId: " + field_1_shapeId + nl + + " Flags: " + decodeFlags(field_2_flags) + " (0x" + HexDump.toHex(field_2_flags) + ")" + nl; + + } + + /** + * Converts the shape flags into a more descriptive name. + */ + private String decodeFlags( int flags ) + { + StringBuffer result = new StringBuffer(); + result.append( ( flags & FLAG_GROUP ) != 0 ? "|GROUP" : "" ); + result.append( ( flags & FLAG_CHILD ) != 0 ? "|CHILD" : "" ); + result.append( ( flags & FLAG_PATRIARCH ) != 0 ? "|PATRIARCH" : "" ); + result.append( ( flags & FLAG_DELETED ) != 0 ? "|DELETED" : "" ); + result.append( ( flags & FLAG_OLESHAPE ) != 0 ? "|OLESHAPE" : "" ); + result.append( ( flags & FLAG_HAVEMASTER ) != 0 ? "|HAVEMASTER" : "" ); + result.append( ( flags & FLAG_FLIPHORIZ ) != 0 ? "|FLIPHORIZ" : "" ); + result.append( ( flags & FLAG_FLIPVERT ) != 0 ? "|FLIPVERT" : "" ); + result.append( ( flags & FLAG_CONNECTOR ) != 0 ? "|CONNECTOR" : "" ); + result.append( ( flags & FLAG_HAVEANCHOR ) != 0 ? "|HAVEANCHOR" : "" ); + result.append( ( flags & FLAG_BACKGROUND ) != 0 ? "|BACKGROUND" : "" ); + result.append( ( flags & FLAG_HASSHAPETYPE ) != 0 ? "|HASSHAPETYPE" : "" ); + + result.deleteCharAt(0); + return result.toString(); + } + + /** + * @return A number that identifies this shape + */ + public int getShapeId() + { + return field_1_shapeId; + } + + /** + * Sets a number that identifies this shape. + */ + public void setShapeId( int field_1_shapeId ) + { + this.field_1_shapeId = field_1_shapeId; + } + + /** + * The flags that apply to this shape. + * + * @see #FLAG_GROUP + * @see #FLAG_CHILD + * @see #FLAG_PATRIARCH + * @see #FLAG_DELETED + * @see #FLAG_OLESHAPE + * @see #FLAG_HAVEMASTER + * @see #FLAG_FLIPHORIZ + * @see #FLAG_FLIPVERT + * @see #FLAG_CONNECTOR + * @see #FLAG_HAVEANCHOR + * @see #FLAG_BACKGROUND + * @see #FLAG_HASSHAPETYPE + */ + public int getFlags() + { + return field_2_flags; + } + + /** + * The flags that apply to this shape. + * + * @see #FLAG_GROUP + * @see #FLAG_CHILD + * @see #FLAG_PATRIARCH + * @see #FLAG_DELETED + * @see #FLAG_OLESHAPE + * @see #FLAG_HAVEMASTER + * @see #FLAG_FLIPHORIZ + * @see #FLAG_FLIPVERT + * @see #FLAG_CONNECTOR + * @see #FLAG_HAVEANCHOR + * @see #FLAG_BACKGROUND + * @see #FLAG_HASSHAPETYPE + */ + public void setFlags( int field_2_flags ) + { + this.field_2_flags = field_2_flags; + } +} diff --git a/src/java/org/apache/poi/ddf/EscherSpgrRecord.java b/src/java/org/apache/poi/ddf/EscherSpgrRecord.java new file mode 100644 index 0000000000..7e86108e0d --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherSpgrRecord.java @@ -0,0 +1,192 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.hssf.record.RecordFormatException; + +/** + * The spgr record defines information about a shape group. Groups in escher + * are simply another form of shape that you can't physically see. + * + * @author Glen Stampoultzis (glens at apache.org) + */ +public class EscherSpgrRecord + extends EscherRecord +{ + public static final short RECORD_ID = (short) 0xF009; + public static final String RECORD_DESCRIPTION = "MsofbtSpgr"; + + private int field_1_rectX1; + private int field_2_rectY1; + private int field_3_rectX2; + private int field_4_rectY2; + + /** + * This method deserializes the record from a byte array. + * + * @param data The byte array containing the escher record information + * @param offset The starting offset into data. + * @param recordFactory May be null since this is not a container record. + * @return The number of bytes read from the byte array. + */ + public int fillFields( byte[] data, int offset, EscherRecordFactory recordFactory ) + { + int bytesRemaining = readHeader( data, offset ); + int pos = offset + 8; + int size = 0; + field_1_rectX1 = LittleEndian.getInt( data, pos + size );size+=4; + field_2_rectY1 = LittleEndian.getInt( data, pos + size );size+=4; + field_3_rectX2 = LittleEndian.getInt( data, pos + size );size+=4; + field_4_rectY2 = LittleEndian.getInt( data, pos + size );size+=4; + bytesRemaining -= size; + if (bytesRemaining != 0) throw new RecordFormatException("Expected no remaining bytes but got " + bytesRemaining); +// remainingData = new byte[bytesRemaining]; +// System.arraycopy( data, pos + size, remainingData, 0, bytesRemaining ); + return 8 + size + bytesRemaining; + } + + /** + * This method serializes this escher record into a byte array. + * + * @param offset The offset into data to start writing the record data to. + * @param data The byte array to serialize to. + * @param listener A listener to retrieve start and end callbacks. Use a NullEscherSerailizationListener to ignore these events. + * @return The number of bytes written. + * + * @see NullEscherSerializationListener + */ + public int serialize( int offset, byte[] data, EscherSerializationListener listener ) + { + listener.beforeRecordSerialize( offset, getRecordId(), this ); + LittleEndian.putShort( data, offset, getOptions() ); + LittleEndian.putShort( data, offset + 2, getRecordId() ); + int remainingBytes = 16; + LittleEndian.putInt( data, offset + 4, remainingBytes ); + LittleEndian.putInt( data, offset + 8, field_1_rectX1 ); + LittleEndian.putInt( data, offset + 12, field_2_rectY1 ); + LittleEndian.putInt( data, offset + 16, field_3_rectX2 ); + LittleEndian.putInt( data, offset + 20, field_4_rectY2 ); +// System.arraycopy( remainingData, 0, data, offset + 26, remainingData.length ); +// int pos = offset + 8 + 18 + remainingData.length; + listener.afterRecordSerialize( offset + getRecordSize(), getRecordId(), offset + getRecordSize(), this ); + return 8 + 16; + } + + /** + * Returns the number of bytes that are required to serialize this record. + * + * @return Number of bytes + */ + public int getRecordSize() + { + return 8 + 16; + } + + /** + * The 16 bit identifier of this shape group record. + */ + public short getRecordId() + { + return RECORD_ID; + } + + /** + * The short name for this record + */ + public String getRecordName() + { + return "Spgr"; + } + + /** + * @return the string representation of this record. + */ + public String toString() + { + String nl = System.getProperty("line.separator"); + +// String extraData; +// ByteArrayOutputStream b = new ByteArrayOutputStream(); +// try +// { +// HexDump.dump(this.remainingData, 0, b, 0); +// extraData = b.toString(); +// } +// catch ( Exception e ) +// { +// extraData = "error"; +// } + return getClass().getName() + ":" + nl + + " RecordId: 0x" + HexDump.toHex(RECORD_ID) + nl + + " Options: 0x" + HexDump.toHex(getOptions()) + nl + + " RectX: " + field_1_rectX1 + nl + + " RectY: " + field_2_rectY1 + nl + + " RectWidth: " + field_3_rectX2 + nl + + " RectHeight: " + field_4_rectY2 + nl; + + } + + /** + * The starting top-left coordinate of child records. + */ + public int getRectX1() + { + return field_1_rectX1; + } + + /** + * The starting top-left coordinate of child records. + */ + public void setRectX1( int x1 ) + { + this.field_1_rectX1 = x1; + } + + /** + * The starting top-left coordinate of child records. + */ + public int getRectY1() + { + return field_2_rectY1; + } + + /** + * The starting top-left coordinate of child records. + */ + public void setRectY1( int y1 ) + { + this.field_2_rectY1 = y1; + } + + /** + * The starting bottom-right coordinate of child records. + */ + public int getRectX2() + { + return field_3_rectX2; + } + + /** + * The starting bottom-right coordinate of child records. + */ + public void setRectX2( int x2 ) + { + this.field_3_rectX2 = x2; + } + + /** + * The starting bottom-right coordinate of child records. + */ + public int getRectY2() + { + return field_4_rectY2; + } + + /** + * The starting bottom-right coordinate of child records. + */ + public void setRectY2( int field_4_rectY2 ) + { + this.field_4_rectY2 = field_4_rectY2; + } +} diff --git a/src/java/org/apache/poi/ddf/EscherSplitMenuColorsRecord.java b/src/java/org/apache/poi/ddf/EscherSplitMenuColorsRecord.java new file mode 100644 index 0000000000..2790d4eacf --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherSplitMenuColorsRecord.java @@ -0,0 +1,171 @@ +package org.apache.poi.ddf; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.hssf.record.RecordFormatException; + +/** + * A list of the most recently used colours for the drawings contained in + * this document. + * + * @author Glen Stampoultzis (glens at apache.org) + */ +public class EscherSplitMenuColorsRecord + extends EscherRecord +{ + public static final short RECORD_ID = (short) 0xF11E; + public static final String RECORD_DESCRIPTION = "MsofbtSplitMenuColors"; + + private int field_1_color1; + private int field_2_color2; + private int field_3_color3; + private int field_4_color4; + + /** + * This method deserializes the record from a byte array. + * + * @param data The byte array containing the escher record information + * @param offset The starting offset into data. + * @param recordFactory May be null since this is not a container record. + * @return The number of bytes read from the byte array. + */ + public int fillFields( byte[] data, int offset, EscherRecordFactory recordFactory ) + { + int bytesRemaining = readHeader( data, offset ); + int pos = offset + 8; + int size = 0; + field_1_color1 = LittleEndian.getInt( data, pos + size );size+=4; + field_2_color2 = LittleEndian.getInt( data, pos + size );size+=4; + field_3_color3 = LittleEndian.getInt( data, pos + size );size+=4; + field_4_color4 = LittleEndian.getInt( data, pos + size );size+=4; + bytesRemaining -= size; + if (bytesRemaining != 0) + throw new RecordFormatException("Expecting no remaining data but got " + bytesRemaining + " byte(s)."); + return 8 + size + bytesRemaining; + } + + /** + * This method serializes this escher record into a byte array. + * + * @param offset The offset into data to start writing the record data to. + * @param data The byte array to serialize to. + * @param listener A listener to retrieve start and end callbacks. Use a NullEscherSerailizationListener to ignore these events. + * @return The number of bytes written. + * + * @see NullEscherSerializationListener + */ + public int serialize( int offset, byte[] data, EscherSerializationListener listener ) + { +// int field_2_numIdClusters = field_5_fileIdClusters.length + 1; + listener.beforeRecordSerialize( offset, getRecordId(), this ); + + int pos = offset; + LittleEndian.putShort( data, pos, getOptions() ); pos += 2; + LittleEndian.putShort( data, pos, getRecordId() ); pos += 2; + int remainingBytes = getRecordSize() - 8; + + LittleEndian.putInt( data, pos, remainingBytes ); pos += 4; + LittleEndian.putInt( data, pos, field_1_color1 ); pos += 4; + LittleEndian.putInt( data, pos, field_2_color2 ); pos += 4; + LittleEndian.putInt( data, pos, field_3_color3 ); pos += 4; + LittleEndian.putInt( data, pos, field_4_color4 ); pos += 4; + listener.afterRecordSerialize( pos, getRecordId(), pos - offset, this ); + return getRecordSize(); + } + + /** + * Returns the number of bytes that are required to serialize this record. + * + * @return Number of bytes + */ + public int getRecordSize() + { + return 8 + 4 * 4; + } + + /** + * @return the 16 bit identifer for this record. + */ + public short getRecordId() + { + return RECORD_ID; + } + + /** + * The short name for this record + */ + public String getRecordName() + { + return "SplitMenuColors"; + } + + /** + * @return a string representation of this record. + */ + public String toString() + { + String nl = System.getProperty("line.separator"); + +// String extraData; +// ByteArrayOutputStream b = new ByteArrayOutputStream(); +// try +// { +// HexDump.dump(this.remainingData, 0, b, 0); +// extraData = b.toString(); +// } +// catch ( Exception e ) +// { +// extraData = "error"; +// } + return getClass().getName() + ":" + nl + + " RecordId: 0x" + HexDump.toHex(RECORD_ID) + nl + + " Options: 0x" + HexDump.toHex(getOptions()) + nl + + " Color1: 0x" + HexDump.toHex(field_1_color1) + nl + + " Color2: 0x" + HexDump.toHex(field_2_color2) + nl + + " Color3: 0x" + HexDump.toHex(field_3_color3) + nl + + " Color4: 0x" + HexDump.toHex(field_4_color4) + nl + + ""; + + } + + public int getColor1() + { + return field_1_color1; + } + + public void setColor1( int field_1_color1 ) + { + this.field_1_color1 = field_1_color1; + } + + public int getColor2() + { + return field_2_color2; + } + + public void setColor2( int field_2_color2 ) + { + this.field_2_color2 = field_2_color2; + } + + public int getColor3() + { + return field_3_color3; + } + + public void setColor3( int field_3_color3 ) + { + this.field_3_color3 = field_3_color3; + } + + public int getColor4() + { + return field_4_color4; + } + + public void setColor4( int field_4_color4 ) + { + this.field_4_color4 = field_4_color4; + } + +} diff --git a/src/java/org/apache/poi/ddf/EscherTextboxRecord.java b/src/java/org/apache/poi/ddf/EscherTextboxRecord.java new file mode 100644 index 0000000000..34dc4fca29 --- /dev/null +++ b/src/java/org/apache/poi/ddf/EscherTextboxRecord.java @@ -0,0 +1,215 @@ +/* ==================================================================== + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2002 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Apache" and "Apache Software Foundation" and + * "Apache POI" must not be used to endorse or promote products + * derived from this software without prior written permission. For + * written permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache", + * "Apache POI", nor may "Apache" appear in their name, without + * prior written permission of the Apache Software Foundation. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ +package org.apache.poi.ddf; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.hssf.record.RecordFormatException; + +import java.util.Iterator; +import java.util.List; +import java.util.ArrayList; + +/** + * Supports text boxes + * + * @author Glen Stampoultzis (glens at apache.org) + */ +public class EscherTextboxRecord extends EscherRecord +{ + public static final short RECORD_ID = (short)0xF00D; + public static final String RECORD_DESCRIPTION = "msofbtClientTextbox"; + + private static final byte[] NO_BYTES = new byte[0]; + + /** The data for this record not including the the 8 byte header */ + private byte[] thedata = NO_BYTES; + + public EscherTextboxRecord() + { + } + + /** + * This method deserializes the record from a byte array. + * + * @param data The byte array containing the escher record information + * @param offset The starting offset into data. + * @param recordFactory May be null since this is not a container record. + * @return The number of bytes read from the byte array. + */ + public int fillFields( byte[] data, int offset, EscherRecordFactory recordFactory ) + { + int bytesRemaining = readHeader( data, offset ); + if ( isContainerRecord() ) + { + int bytesWritten = 0; + thedata = new byte[0]; + offset += 8; + bytesWritten += 8; + while ( bytesRemaining > 0 ) + { + EscherRecord child = recordFactory.createRecord( data, offset ); + int childBytesWritten = child.fillFields( data, offset, recordFactory ); + bytesWritten += childBytesWritten; + offset += childBytesWritten; + bytesRemaining -= childBytesWritten; + getChildRecords().add( child ); + } + return bytesWritten; + } + else + { + thedata = new byte[bytesRemaining]; + System.arraycopy( data, offset + 8, thedata, 0, bytesRemaining ); + return bytesRemaining + 8; + } + } + + /** + * Writes this record and any contained records to the supplied byte + * array. + * + * @return the number of bytes written. + */ + public int serialize( int offset, byte[] data, EscherSerializationListener listener ) + { + listener.beforeRecordSerialize( offset, getRecordId(), this ); + + LittleEndian.putShort(data, offset, getOptions()); + LittleEndian.putShort(data, offset+2, getRecordId()); + int remainingBytes = thedata.length; + for ( Iterator iterator = getChildRecords().iterator(); iterator.hasNext(); ) + { + EscherRecord r = (EscherRecord) iterator.next(); + remainingBytes += r.getRecordSize(); + } + LittleEndian.putInt(data, offset+4, remainingBytes); + System.arraycopy(thedata, 0, data, offset+8, thedata.length); + int pos = offset+8+thedata.length; + for ( Iterator iterator = getChildRecords().iterator(); iterator.hasNext(); ) + { + EscherRecord r = (EscherRecord) iterator.next(); + pos += r.serialize(pos, data, listener ); + } + + listener.afterRecordSerialize( pos, getRecordId(), pos - offset, this ); + int size = pos - offset; + if (size != getRecordSize()) + throw new RecordFormatException(size + " bytes written but getRecordSize() reports " + getRecordSize()); + return size; + } + + /** + * Returns any extra data associated with this record. In practice excel + * does not seem to put anything here. + */ + public byte[] getData() + { + return thedata; + } + + /** + * Returns the number of bytes that are required to serialize this record. + * + * @return Number of bytes + */ + public int getRecordSize() + { + return 8 + thedata.length; + } + + public Object clone() + { + // shallow clone + return super.clone(); + } + + /** + * The short name for this record + */ + public String getRecordName() + { + return "ClientTextbox"; + } + + public String toString() + { + String nl = System.getProperty( "line.separator" ); + + String theDumpHex = ""; + try + { + if (thedata.length != 0) + { + theDumpHex = " Extra Data:" + nl; + theDumpHex += HexDump.dump(thedata, 0, 0); + } + } + catch ( Exception e ) + { + theDumpHex = "Error!!"; + } + + return getClass().getName() + ":" + nl + + " isContainer: " + isContainerRecord() + nl + + " options: 0x" + HexDump.toHex( getOptions() ) + nl + + " recordId: 0x" + HexDump.toHex( getRecordId() ) + nl + + " numchildren: " + getChildRecords().size() + nl + + theDumpHex; + } + +} + + + diff --git a/src/java/org/apache/poi/ddf/NullEscherSerializationListener.java b/src/java/org/apache/poi/ddf/NullEscherSerializationListener.java new file mode 100644 index 0000000000..10a1e798c4 --- /dev/null +++ b/src/java/org/apache/poi/ddf/NullEscherSerializationListener.java @@ -0,0 +1,20 @@ +package org.apache.poi.ddf; + +/** + * Ignores all serialization events. + * + * @author Glen Stampoultzis (glens at apache.org) + */ +public class NullEscherSerializationListener implements EscherSerializationListener +{ + public void beforeRecordSerialize( int offset, short recordId, EscherRecord record ) + { + // do nothing + } + + public void afterRecordSerialize( int offset, short recordId, int size, EscherRecord record ) + { + // do nothing + } + +} diff --git a/src/java/org/apache/poi/ddf/UnknownEscherRecord.java b/src/java/org/apache/poi/ddf/UnknownEscherRecord.java new file mode 100644 index 0000000000..e2d21fbabe --- /dev/null +++ b/src/java/org/apache/poi/ddf/UnknownEscherRecord.java @@ -0,0 +1,234 @@ +/* ==================================================================== + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2002 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Apache" and "Apache Software Foundation" and + * "Apache POI" must not be used to endorse or promote products + * derived from this software without prior written permission. For + * written permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache", + * "Apache POI", nor may "Apache" appear in their name, without + * prior written permission of the Apache Software Foundation. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ +package org.apache.poi.ddf; + +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; + +import java.util.Iterator; +import java.util.List; +import java.util.ArrayList; + +/** + * This record is used whenever a escher record is encountered that + * we do not explicitly support. + * + * @author Glen Stampoultzis (glens at apache.org) + */ +public class UnknownEscherRecord extends EscherRecord +{ + private static final byte[] NO_BYTES = new byte[0]; + + /** The data for this record not including the the 8 byte header */ + private byte[] thedata = NO_BYTES; + private List childRecords = new ArrayList(); + + public UnknownEscherRecord() + { + } + + /** + * This method deserializes the record from a byte array. + * + * @param data The byte array containing the escher record information + * @param offset The starting offset into data. + * @param recordFactory May be null since this is not a container record. + * @return The number of bytes read from the byte array. + */ + public int fillFields( byte[] data, int offset, EscherRecordFactory recordFactory ) + { + int bytesRemaining = readHeader( data, offset ); + if ( isContainerRecord() ) + { + int bytesWritten = 0; + thedata = new byte[0]; + offset += 8; + bytesWritten += 8; + while ( bytesRemaining > 0 ) + { + EscherRecord child = recordFactory.createRecord( data, offset ); + int childBytesWritten = child.fillFields( data, offset, recordFactory ); + bytesWritten += childBytesWritten; + offset += childBytesWritten; + bytesRemaining -= childBytesWritten; + getChildRecords().add( child ); + } + return bytesWritten; + } + else + { + thedata = new byte[bytesRemaining]; + System.arraycopy( data, offset + 8, thedata, 0, bytesRemaining ); + return bytesRemaining + 8; + } + } + + /** + * Writes this record and any contained records to the supplied byte + * array. + * + * @return the number of bytes written. + */ + public int serialize( int offset, byte[] data, EscherSerializationListener listener ) + { + listener.beforeRecordSerialize( offset, getRecordId(), this ); + + LittleEndian.putShort(data, offset, getOptions()); + LittleEndian.putShort(data, offset+2, getRecordId()); + int remainingBytes = thedata.length; + for ( Iterator iterator = getChildRecords().iterator(); iterator.hasNext(); ) + { + EscherRecord r = (EscherRecord) iterator.next(); + remainingBytes += r.getRecordSize(); + } + LittleEndian.putInt(data, offset+4, remainingBytes); + System.arraycopy(thedata, 0, data, offset+8, thedata.length); + int pos = offset+8+thedata.length; + for ( Iterator iterator = getChildRecords().iterator(); iterator.hasNext(); ) + { + EscherRecord r = (EscherRecord) iterator.next(); + pos += r.serialize(pos, data, listener ); + } + + listener.afterRecordSerialize( pos, getRecordId(), pos - offset, this ); + return pos - offset; + } + + public byte[] getData() + { + return thedata; + } + + /** + * Returns the number of bytes that are required to serialize this record. + * + * @return Number of bytes + */ + public int getRecordSize() + { + return 8 + thedata.length; + } + + public List getChildRecords() + { + return childRecords; + } + + public void setChildRecords( List childRecords ) + { + this.childRecords = childRecords; + } + + public Object clone() + { + // shallow clone + return super.clone(); + } + + /** + * The short name for this record + */ + public String getRecordName() + { + return "Unknown 0x" + HexDump.toHex(getRecordId()); + } + + public String toString() + { + String nl = System.getProperty( "line.separator" ); + + StringBuffer children = new StringBuffer(); + if ( getChildRecords().size() > 0 ) + { + children.append( " children: " + nl ); + for ( Iterator iterator = getChildRecords().iterator(); iterator.hasNext(); ) + { + EscherRecord record = (EscherRecord) iterator.next(); + children.append( record.toString() ); + children.append( nl ); + } + } + + String theDumpHex = ""; + try + { + if (thedata.length != 0) + { + theDumpHex = " Extra Data:" + nl; + theDumpHex += HexDump.dump(thedata, 0, 0); + } + } + catch ( Exception e ) + { + theDumpHex = "Error!!"; + } + + return getClass().getName() + ":" + nl + + " isContainer: " + isContainerRecord() + nl + + " options: 0x" + HexDump.toHex( getOptions() ) + nl + + " recordId: 0x" + HexDump.toHex( getRecordId() ) + nl + + " numchildren: " + getChildRecords().size() + nl + + theDumpHex + + children.toString(); + } + + public void addChildRecord( EscherRecord childRecord ) + { + getChildRecords().add( childRecord ); + } + +} + + + diff --git a/src/java/org/apache/poi/ddf/package.html b/src/java/org/apache/poi/ddf/package.html new file mode 100644 index 0000000000..3205d487d0 --- /dev/null +++ b/src/java/org/apache/poi/ddf/package.html @@ -0,0 +1,11 @@ + + + + +

This package contains classes for decoding the Microsoft Office + Drawing format otherwise known as escher henceforth known in POI + as the Dreadful Drawing Format. +

+ + + \ No newline at end of file -- 2.39.5