diff --git a/src/java/org/apache/poi/ddf/EscherOptRecord.java b/src/java/org/apache/poi/ddf/EscherOptRecord.java index f18e38f03c..d7de48edaf 100644 --- a/src/java/org/apache/poi/ddf/EscherOptRecord.java +++ b/src/java/org/apache/poi/ddf/EscherOptRecord.java @@ -18,11 +18,14 @@ package org.apache.poi.ddf; -import org.apache.poi.util.LittleEndian; -import org.apache.poi.util.HexDump; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; -import java.util.*; -import java.io.IOException; +import org.apache.poi.util.HexDump; +import org.apache.poi.util.LittleEndian; /** * The opt record is used to store property values for a shape. It is the key to determining diff --git a/src/java/org/apache/poi/hssf/model/Sheet.java b/src/java/org/apache/poi/hssf/model/Sheet.java index c3f93b0a01..59e5de3246 100644 --- a/src/java/org/apache/poi/hssf/model/Sheet.java +++ b/src/java/org/apache/poi/hssf/model/Sheet.java @@ -2663,12 +2663,26 @@ public class Sheet implements Model return margins; } - public int aggregateDrawingRecords(DrawingManager2 drawingManager) + /** + * Finds the DrawingRecord for our sheet, and + * attaches it to the DrawingManager (which knows about + * the overall DrawingGroup for our workbook). + * If requested, will create a new DrawRecord + * if none currently exist + * @param drawingManager The DrawingManager2 for our workbook + * @param createIfMissing Should one be created if missing? + */ + public int aggregateDrawingRecords(DrawingManager2 drawingManager, boolean createIfMissing) { int loc = findFirstRecordLocBySid(DrawingRecord.sid); - boolean noDrawingRecordsFound = loc == -1; + boolean noDrawingRecordsFound = (loc == -1); if (noDrawingRecordsFound) { + if(!createIfMissing) { + // None found, and not allowed to add in + return -1; + } + EscherAggregate aggregate = new EscherAggregate( drawingManager ); loc = findFirstRecordLocBySid(EscherAggregate.sid); if (loc == -1) diff --git a/src/java/org/apache/poi/hssf/model/Workbook.java b/src/java/org/apache/poi/hssf/model/Workbook.java index 2af77a7814..08c236cda1 100644 --- a/src/java/org/apache/poi/hssf/model/Workbook.java +++ b/src/java/org/apache/poi/hssf/model/Workbook.java @@ -2165,13 +2165,68 @@ public class Workbook implements Model } return palette; } + + /** + * Finds the primary drawing group, if one already exists + */ + public void findDrawingGroup() { + // Need to find a DrawingGroupRecord that + // contains a EscherDggRecord + for(Iterator rit = records.iterator(); rit.hasNext();) { + Record r = (Record)rit.next(); + + if(r instanceof DrawingGroupRecord) { + DrawingGroupRecord dg = (DrawingGroupRecord)r; + dg.processChildRecords(); + + EscherContainerRecord cr = + dg.getEscherContainer(); + if(cr == null) { + continue; + } + + EscherDggRecord dgg = null; + for(Iterator it = cr.getChildRecords().iterator(); it.hasNext();) { + Object er = it.next(); + if(er instanceof EscherDggRecord) { + dgg = (EscherDggRecord)er; + } + } + + if(dgg != null) { + drawingManager = new DrawingManager2(dgg); + return; + } + } + } + + // Look for the DrawingGroup record + int dgLoc = findFirstRecordLocBySid(DrawingGroupRecord.sid); + + // If there is one, does it have a EscherDggRecord? + if(dgLoc != -1) { + DrawingGroupRecord dg = + (DrawingGroupRecord)records.get(dgLoc); + EscherDggRecord dgg = null; + for(Iterator it = dg.getEscherRecords().iterator(); it.hasNext();) { + Object er = it.next(); + if(er instanceof EscherDggRecord) { + dgg = (EscherDggRecord)er; + } + } + + if(dgg != null) { + drawingManager = new DrawingManager2(dgg); + } + } + } /** - * Creates a drawing group record. If it already exists then it's modified. + * Creates a primary drawing group record. If it already + * exists then it's modified. */ public void createDrawingGroup() { - if (drawingManager == null) { EscherContainerRecord dggContainer = new EscherContainerRecord(); @@ -2235,7 +2290,6 @@ public class Workbook implements Model } } - } public WindowOneRecord getWindowOne() { diff --git a/src/java/org/apache/poi/hssf/record/AbstractEscherHolderRecord.java b/src/java/org/apache/poi/hssf/record/AbstractEscherHolderRecord.java index c9417a9c3a..0260fc915b 100644 --- a/src/java/org/apache/poi/hssf/record/AbstractEscherHolderRecord.java +++ b/src/java/org/apache/poi/hssf/record/AbstractEscherHolderRecord.java @@ -18,19 +18,18 @@ package org.apache.poi.hssf.record; +import java.io.ByteArrayInputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + import org.apache.poi.ddf.DefaultEscherRecordFactory; +import org.apache.poi.ddf.EscherContainerRecord; import org.apache.poi.ddf.EscherRecord; import org.apache.poi.ddf.EscherRecordFactory; import org.apache.poi.ddf.NullEscherSerializationListener; import org.apache.poi.util.LittleEndian; -import java.io.ByteArrayInputStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - /** * The escher container record is used to hold escher records. It is abstract and * must be subclassed for maximum benefit. @@ -97,6 +96,9 @@ public abstract class AbstractEscherHolderRecord } } + protected void convertRawBytesToEscherRecords() { + convertToEscherRecords(0, rawData.length, rawData); + } private void convertToEscherRecords( int offset, int size, byte[] data ) { EscherRecordFactory recordFactory = new DefaultEscherRecordFactory(); @@ -264,6 +266,54 @@ public abstract class AbstractEscherHolderRecord { escherRecords.clear(); } + + /** + * If we have a EscherContainerRecord as one of our + * children (and most top level escher holders do), + * then return that. + */ + public EscherContainerRecord getEscherContainer() { + for(Iterator it = escherRecords.iterator(); it.hasNext();) { + Object er = it.next(); + if(er instanceof EscherContainerRecord) { + return (EscherContainerRecord)er; + } + } + return null; + } + + /** + * Descends into all our children, returning the + * first EscherRecord with the given id, or null + * if none found + */ + public EscherRecord findFirstWithId(short id) { + return findFirstWithId(id, getEscherRecords()); + } + private EscherRecord findFirstWithId(short id, List records) { + // Check at our level + for(Iterator it = records.iterator(); it.hasNext();) { + EscherRecord r = (EscherRecord)it.next(); + if(r.getRecordId() == id) { + return r; + } + } + + // Then check our children in turn + for(Iterator it = records.iterator(); it.hasNext();) { + EscherRecord r = (EscherRecord)it.next(); + if(r.isContainerRecord()) { + EscherRecord found = + findFirstWithId(id, r.getChildRecords()); + if(found != null) { + return found; + } + } + } + + // Not found in this lot + return null; + } public EscherRecord getEscherRecord(int index) diff --git a/src/java/org/apache/poi/hssf/record/DrawingGroupRecord.java b/src/java/org/apache/poi/hssf/record/DrawingGroupRecord.java index 3782273a5d..ea083ee1e3 100644 --- a/src/java/org/apache/poi/hssf/record/DrawingGroupRecord.java +++ b/src/java/org/apache/poi/hssf/record/DrawingGroupRecord.java @@ -72,6 +72,16 @@ public class DrawingGroupRecord extends AbstractEscherHolderRecord return writeData( offset, data, buffer ); } } + + /** + * Process the bytes into escher records. + * (Not done by default in case we break things, + * unless you set the "poi.deserialize.escher" + * system property) + */ + public void processChildRecords() { + convertRawBytesToEscherRecords(); + } /** * Size of record (including 4 byte headers for all sections) diff --git a/src/java/org/apache/poi/hssf/record/EscherAggregate.java b/src/java/org/apache/poi/hssf/record/EscherAggregate.java index 28a717682a..4f5d18c239 100644 --- a/src/java/org/apache/poi/hssf/record/EscherAggregate.java +++ b/src/java/org/apache/poi/hssf/record/EscherAggregate.java @@ -523,7 +523,20 @@ public class EscherAggregate extends AbstractEscherHolderRecord { this.patriarch = patriarch; } - + + /** + * Converts the Records into UserModel + * objects on the bound HSSFPatriarch + */ + public void convertRecordsToUserModel() { + if(patriarch == null) { + throw new IllegalStateException("Must call setPatriarch() first"); + } + + // TODO: Support converting our records + // back into shapes + } + public void clear() { clearEscherRecords(); diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFPatriarch.java b/src/java/org/apache/poi/hssf/usermodel/HSSFPatriarch.java index e383ed5586..3df804da76 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFPatriarch.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFPatriarch.java @@ -21,6 +21,13 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import org.apache.poi.ddf.EscherComplexProperty; +import org.apache.poi.ddf.EscherOptRecord; +import org.apache.poi.ddf.EscherProperty; +import org.apache.poi.hssf.record.EscherAggregate; +import org.apache.poi.util.LittleEndian; +import org.apache.poi.util.StringUtil; + /** * The patriarch is the toplevel container for shapes in a sheet. It does * little other than act as a container for other shapes and groups. @@ -37,13 +44,21 @@ public class HSSFPatriarch int x2 = 1023; int y2 = 255; + /** + * The EscherAggregate we have been bound to. + * (This will handle writing us out into records, + * and building up our shapes from the records) + */ + private EscherAggregate boundAggregate; + /** * Creates the patriarch. * - * @param sheet the sheet this patriarch is stored in. + * @param sheet the sheet this patriarch is stored in. */ - HSSFPatriarch(HSSFSheet sheet) + HSSFPatriarch(HSSFSheet sheet, EscherAggregate boundAggregate) { + this.boundAggregate = boundAggregate; this.sheet = sheet; } @@ -173,6 +188,39 @@ public class HSSFPatriarch this.x2 = x2; this.y2 = y2; } + + /** + * Does this HSSFPatriarch contain a chart? + * (Technically a reference to a chart, since they + * get stored in a different block of records) + * FIXME - detect chart in all cases (only seems + * to work on some charts so far) + */ + public boolean containsChart() { + // TODO - support charts properly in usermodel + + // We're looking for a EscherOptRecord + EscherOptRecord optRecord = (EscherOptRecord) + boundAggregate.findFirstWithId(EscherOptRecord.RECORD_ID); + if(optRecord == null) { + // No opt record, can't have chart + return false; + } + + for(Iterator it = optRecord.getEscherProperties().iterator(); it.hasNext();) { + EscherProperty prop = (EscherProperty)it.next(); + if(prop.getPropertyNumber() == 896 && prop.isComplex()) { + EscherComplexProperty cp = (EscherComplexProperty)prop; + String str = StringUtil.getFromUnicodeLE(cp.getComplexData()); + System.err.println(str); + if(str.equals("Chart 1\0")) { + return true; + } + } + } + + return false; + } /** * The top left x coordinate of this group. diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFSheet.java b/src/java/org/apache/poi/hssf/usermodel/HSSFSheet.java index 6fc113c085..3ad8a94777 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFSheet.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFSheet.java @@ -1488,7 +1488,7 @@ public class HSSFSheet */ public void dumpDrawingRecords(boolean fat) { - sheet.aggregateDrawingRecords(book.getDrawingManager()); + sheet.aggregateDrawingRecords(book.getDrawingManager(), false); EscherAggregate r = (EscherAggregate) getSheet().findFirstRecordBySid(EscherAggregate.sid); List escherRecords = r.getEscherRecords(); @@ -1505,9 +1505,10 @@ public class HSSFSheet } /** - * Creates the toplevel drawing patriarch. This will have the effect of - * removing any existing drawings on this sheet. - * + * Creates the top-level drawing patriarch. This will have + * the effect of removing any existing drawings on this + * sheet. + * This may then be used to add graphics or charts * @return The new patriarch. */ public HSSFPatriarch createDrawingPatriarch() @@ -1515,14 +1516,43 @@ public class HSSFSheet // Create the drawing group if it doesn't already exist. book.createDrawingGroup(); - sheet.aggregateDrawingRecords(book.getDrawingManager()); + sheet.aggregateDrawingRecords(book.getDrawingManager(), true); EscherAggregate agg = (EscherAggregate) sheet.findFirstRecordBySid(EscherAggregate.sid); - HSSFPatriarch patriarch = new HSSFPatriarch(this); + HSSFPatriarch patriarch = new HSSFPatriarch(this, agg); agg.clear(); // Initially the behaviour will be to clear out any existing shapes in the sheet when // creating a new patriarch. agg.setPatriarch(patriarch); return patriarch; } + + /** + * Returns the top-level drawing patriach, if there is + * one. + * This will hold any graphics or charts for the sheet + */ + public HSSFPatriarch getDrawingPatriarch() { + book.findDrawingGroup(); + + // If there's now no drawing manager, then there's + // no drawing escher records on the workbook + if(book.getDrawingManager() == null) { + return null; + } + + int found = sheet.aggregateDrawingRecords( + book.getDrawingManager(), false + ); + if(found == -1) { + // Workbook has drawing stuff, but this sheet doesn't + return null; + } + + EscherAggregate agg = (EscherAggregate) sheet.findFirstRecordBySid(EscherAggregate.sid); + HSSFPatriarch patriarch = new HSSFPatriarch(this, agg); + agg.setPatriarch(patriarch); + agg.convertRecordsToUserModel(); + return patriarch; + } /** * Expands or collapses a column group. diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFSheet.java b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFSheet.java index 5f9ef53c89..f0deb68e0c 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFSheet.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFSheet.java @@ -407,6 +407,30 @@ public class TestHSSFSheet assertEquals(0, r6.getOutlineLevel()); } + public void testGetDrawings() throws Exception { + String filename = System.getProperty("HSSF.testdata.path"); + HSSFWorkbook wb1c = new HSSFWorkbook( + new FileInputStream(new File(filename,"WithChart.xls")) + ); + HSSFWorkbook wb2c = new HSSFWorkbook( + new FileInputStream(new File(filename,"WithTwoCharts.xls")) + ); + + // 1 chart sheet -> data on 1st, chart on 2nd + assertNotNull(wb1c.getSheetAt(0).getDrawingPatriarch()); + assertNotNull(wb1c.getSheetAt(1).getDrawingPatriarch()); + assertFalse(wb1c.getSheetAt(0).getDrawingPatriarch().containsChart()); + assertTrue(wb1c.getSheetAt(1).getDrawingPatriarch().containsChart()); + + // 2 chart sheet -> data on 1st, chart on 2nd+3rd + assertNotNull(wb2c.getSheetAt(0).getDrawingPatriarch()); + assertNotNull(wb2c.getSheetAt(1).getDrawingPatriarch()); + assertNotNull(wb2c.getSheetAt(2).getDrawingPatriarch()); + assertFalse(wb2c.getSheetAt(0).getDrawingPatriarch().containsChart()); + assertTrue(wb2c.getSheetAt(1).getDrawingPatriarch().containsChart()); + assertTrue(wb2c.getSheetAt(2).getDrawingPatriarch().containsChart()); + } + /** * Test that the ProtectRecord is included when creating or cloning a sheet */ diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFWorkbook.java b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFWorkbook.java index 6d5afca114..40a577240e 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFWorkbook.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFWorkbook.java @@ -16,6 +16,8 @@ */ package org.apache.poi.hssf.usermodel; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; @@ -129,4 +131,69 @@ public class TestHSSFWorkbook extends TestCase b.cloneSheet(0); assertEquals(2, b.getNumberOfSheets()); } + + public void testReadWriteWithCharts() throws Exception { + HSSFWorkbook b; + HSSFSheet s; + + // Single chart, two sheets + b = new HSSFWorkbook( + new FileInputStream(new File(filename,"44010-SingleChart.xls")) + ); + assertEquals(2, b.getNumberOfSheets()); + s = b.getSheetAt(1); + assertEquals(0, s.getFirstRowNum()); + assertEquals(0, s.getLastRowNum()); + + // Has chart on 1st sheet?? + // FIXME + assertNotNull(b.getSheetAt(0).getDrawingPatriarch()); + assertNull(b.getSheetAt(1).getDrawingPatriarch()); + assertFalse(b.getSheetAt(0).getDrawingPatriarch().containsChart()); + + b = writeRead(b); + assertEquals(2, b.getNumberOfSheets()); + s = b.getSheetAt(1); + assertEquals(0, s.getFirstRowNum()); + assertEquals(0, s.getLastRowNum()); + + + // Two charts, three sheets + b = new HSSFWorkbook( + new FileInputStream(new File(filename,"44010-TwoCharts.xls")) + ); + assertEquals(3, b.getNumberOfSheets()); + + s = b.getSheetAt(1); + assertEquals(0, s.getFirstRowNum()); + assertEquals(0, s.getLastRowNum()); + s = b.getSheetAt(2); + assertEquals(0, s.getFirstRowNum()); + assertEquals(0, s.getLastRowNum()); + + // Has chart on 1st sheet?? + // FIXME + assertNotNull(b.getSheetAt(0).getDrawingPatriarch()); + assertNull(b.getSheetAt(1).getDrawingPatriarch()); + assertNull(b.getSheetAt(2).getDrawingPatriarch()); + assertFalse(b.getSheetAt(0).getDrawingPatriarch().containsChart()); + + b = writeRead(b); + assertEquals(3, b.getNumberOfSheets()); + + s = b.getSheetAt(1); + assertEquals(0, s.getFirstRowNum()); + assertEquals(0, s.getLastRowNum()); + s = b.getSheetAt(2); + assertEquals(0, s.getFirstRowNum()); + assertEquals(0, s.getLastRowNum()); + } + + private HSSFWorkbook writeRead(HSSFWorkbook b) throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + b.write(baos); + return new HSSFWorkbook( + new ByteArrayInputStream(baos.toByteArray()) + ); + } } \ No newline at end of file