]> source.dussan.org Git - poi.git/commitdiff
The escher records themselves
authorGlen Stampoultzis <glens@apache.org>
Tue, 10 Feb 2004 22:03:32 +0000 (22:03 +0000)
committerGlen Stampoultzis <glens@apache.org>
Tue, 10 Feb 2004 22:03:32 +0000 (22:03 +0000)
git-svn-id: https://svn.apache.org/repos/asf/jakarta/poi/branches/REL_2_BRANCH@353499 13f79535-47bb-0310-9956-ffa450edef68

31 files changed:
src/java/org/apache/poi/ddf/DefaultEscherRecordFactory.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherArrayProperty.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherBSERecord.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherBlipRecord.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherBoolProperty.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherChildAnchorRecord.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherClientAnchorRecord.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherClientDataRecord.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherComplexProperty.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherContainerRecord.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherDgRecord.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherDggRecord.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherDump.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherOptRecord.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherProperties.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherProperty.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherPropertyFactory.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherPropertyMetaData.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherRGBProperty.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherRecord.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherRecordFactory.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherSerializationListener.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherShapePathProperty.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherSimpleProperty.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherSpRecord.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherSpgrRecord.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherSplitMenuColorsRecord.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/EscherTextboxRecord.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/NullEscherSerializationListener.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/UnknownEscherRecord.java [new file with mode: 0644]
src/java/org/apache/poi/ddf/package.html [new file with mode: 0644]

diff --git a/src/java/org/apache/poi/ddf/DefaultEscherRecordFactory.java b/src/java/org/apache/poi/ddf/DefaultEscherRecordFactory.java
new file mode 100644 (file)
index 0000000..09d8c5f
--- /dev/null
@@ -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 (file)
index 0000000..8979b30
--- /dev/null
@@ -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 (file)
index 0000000..2d170ab
--- /dev/null
@@ -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 <code>EscherBlipRecord</code> 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 <code>data</code>.
+     * @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 <code>data</code> 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 <code>NullEscherSerailizationListener</code> 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 (file)
index 0000000..ca8be71
--- /dev/null
@@ -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 <code>data</code>.
+     * @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 <code>data</code> 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 <code>NullEscherSerailizationListener</code> 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 (file)
index 0000000..86a7a0a
--- /dev/null
@@ -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 (file)
index 0000000..f53ba7d
--- /dev/null
@@ -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 <code>data</code>.
+     * @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 <code>data</code> 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 <code>NullEscherSerailizationListener</code> 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 (file)
index 0000000..d61d77b
--- /dev/null
@@ -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 <code>data</code>.
+     * @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 <code>data</code> 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 <code>NullEscherSerailizationListener</code> 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 (file)
index 0000000..d568996
--- /dev/null
@@ -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 <code>data</code>.
+     * @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 <code>data</code> 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 <code>NullEscherSerailizationListener</code> 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 (file)
index 0000000..4788a66
--- /dev/null
@@ -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 (file)
index 0000000..41c7a7e
--- /dev/null
@@ -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 (file)
index 0000000..049bf38
--- /dev/null
@@ -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 <code>data</code>.
+     * @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 <code>data</code> 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 <code>NullEscherSerailizationListener</code> 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 (file)
index 0000000..1adcf82
--- /dev/null
@@ -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 <code>data</code>.
+     * @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 <code>data</code> 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 <code>NullEscherSerailizationListener</code> 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 (file)
index 0000000..7930486
--- /dev/null
@@ -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
+ * <http://www.apache.org/>.
+ */
+
+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 (file)
index 0000000..4efab8b
--- /dev/null
@@ -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 <code>data</code>.
+     * @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 <code>data</code> 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 <code>NullEscherSerailizationListener</code> 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 (file)
index 0000000..ea1c874
--- /dev/null
@@ -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 (file)
index 0000000..787ccf2
--- /dev/null
@@ -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 (file)
index 0000000..cc6b7ba
--- /dev/null
@@ -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 (file)
index 0000000..9757606
--- /dev/null
@@ -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 (file)
index 0000000..1b8c9d2
--- /dev/null
@@ -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 (file)
index 0000000..0001bbf
--- /dev/null
@@ -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 <code>options</code>
+     * and <code>recordId</code> 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 (file)
index 0000000..4c6e22c
--- /dev/null
@@ -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 (file)
index 0000000..ddea267
--- /dev/null
@@ -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 (file)
index 0000000..df70e9c
--- /dev/null
@@ -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 (file)
index 0000000..b61474e
--- /dev/null
@@ -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 (file)
index 0000000..a5e2be9
--- /dev/null
@@ -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 <code>data</code>.
+     * @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 <code>data</code> 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 <code>NullEscherSerailizationListener</code> 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 (file)
index 0000000..7e86108
--- /dev/null
@@ -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 <code>data</code>.
+     * @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 <code>data</code> 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 <code>NullEscherSerailizationListener</code> 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 (file)
index 0000000..2790d4e
--- /dev/null
@@ -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 <code>data</code>.
+     * @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 <code>data</code> 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 <code>NullEscherSerailizationListener</code> 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 (file)
index 0000000..34dc4fc
--- /dev/null
@@ -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
+ * <http://www.apache.org/>.
+ */
+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 <code>data</code>.
+     * @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 (file)
index 0000000..10a1e79
--- /dev/null
@@ -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 (file)
index 0000000..e2d21fb
--- /dev/null
@@ -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
+ * <http://www.apache.org/>.
+ */
+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 <code>data</code>.
+     * @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 (file)
index 0000000..3205d48
--- /dev/null
@@ -0,0 +1,11 @@
+<html>
+
+<body>
+
+    <p>This package contains classes for decoding the Microsoft Office
+       Drawing format otherwise known as escher henceforth known in POI
+       as the Dreadful Drawing Format.
+    </p>
+
+</body>
+</html>
\ No newline at end of file