814 lines
37 KiB
Java
814 lines
37 KiB
Java
/* ====================================================================
|
|
Licensed to the Apache Software Foundation (ASF) under one or more
|
|
contributor license agreements. See the NOTICE file distributed with
|
|
this work for additional information regarding copyright ownership.
|
|
The ASF licenses this file to You under the Apache License, Version 2.0
|
|
(the "License"); you may not use this file except in compliance with
|
|
the License. You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
==================================================================== */
|
|
|
|
package org.apache.poi.hssf.record;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
import org.apache.poi.ddf.DefaultEscherRecordFactory;
|
|
import org.apache.poi.ddf.EscherClientDataRecord;
|
|
import org.apache.poi.ddf.EscherContainerRecord;
|
|
import org.apache.poi.ddf.EscherDgRecord;
|
|
import org.apache.poi.ddf.EscherRecord;
|
|
import org.apache.poi.ddf.EscherRecordFactory;
|
|
import org.apache.poi.ddf.EscherSerializationListener;
|
|
import org.apache.poi.ddf.EscherSpRecord;
|
|
import org.apache.poi.ddf.EscherSpgrRecord;
|
|
import org.apache.poi.ddf.EscherTextboxRecord;
|
|
import org.apache.poi.util.POILogFactory;
|
|
import org.apache.poi.util.POILogger;
|
|
|
|
/**
|
|
* This class is used to aggregate the MSODRAWING and OBJ record
|
|
* combinations. This is necessary due to the bizare way in which
|
|
* these records are serialized. What happens is that you get a
|
|
* combination of MSODRAWING -> OBJ -> MSODRAWING -> OBJ records
|
|
* but the escher records are serialized _across_ the MSODRAWING
|
|
* records.
|
|
* <p>
|
|
* It gets even worse when you start looking at TXO records.
|
|
* <p>
|
|
* So what we do with this class is aggregate lazily. That is
|
|
* we don't aggregate the MSODRAWING -> OBJ records unless we
|
|
* need to modify them.
|
|
* <p>
|
|
* At first document contains 4 types of records which belong to drawing layer.
|
|
* There are can be such sequence of record:
|
|
* <p>
|
|
* DrawingRecord
|
|
* ContinueRecord
|
|
* ...
|
|
* ContinueRecord
|
|
* ObjRecord | TextObjectRecord
|
|
* .....
|
|
* ContinueRecord
|
|
* ...
|
|
* ContinueRecord
|
|
* ObjRecord | TextObjectRecord
|
|
* NoteRecord
|
|
* ...
|
|
* NoteRecord
|
|
* <p>
|
|
* To work with shapes we have to read data from Drawing and Continue records into single array of bytes and
|
|
* build escher(office art) records tree from this array.
|
|
* Each shape in drawing layer matches corresponding ObjRecord
|
|
* Each textbox matches corresponding TextObjectRecord
|
|
* <p>
|
|
* ObjRecord contains information about shape. Thus each ObjRecord corresponds EscherContainerRecord(SPGR)
|
|
* <p>
|
|
* EscherAggrefate contains also NoteRecords
|
|
* NoteRecords must be serial
|
|
*/
|
|
|
|
public final class EscherAggregate extends AbstractEscherHolderRecord {
|
|
public static final short sid = 9876; // not a real sid - dummy value
|
|
private static POILogger log = POILogFactory.getLogger(EscherAggregate.class);
|
|
|
|
public static final short ST_MIN = (short) 0;
|
|
public static final short ST_NOT_PRIMATIVE = ST_MIN;
|
|
public static final short ST_RECTANGLE = (short) 1;
|
|
public static final short ST_ROUNDRECTANGLE = (short) 2;
|
|
public static final short ST_ELLIPSE = (short) 3;
|
|
public static final short ST_DIAMOND = (short) 4;
|
|
public static final short ST_ISOCELESTRIANGLE = (short) 5;
|
|
public static final short ST_RIGHTTRIANGLE = (short) 6;
|
|
public static final short ST_PARALLELOGRAM = (short) 7;
|
|
public static final short ST_TRAPEZOID = (short) 8;
|
|
public static final short ST_HEXAGON = (short) 9;
|
|
public static final short ST_OCTAGON = (short) 10;
|
|
public static final short ST_PLUS = (short) 11;
|
|
public static final short ST_STAR = (short) 12;
|
|
public static final short ST_ARROW = (short) 13;
|
|
public static final short ST_THICKARROW = (short) 14;
|
|
public static final short ST_HOMEPLATE = (short) 15;
|
|
public static final short ST_CUBE = (short) 16;
|
|
public static final short ST_BALLOON = (short) 17;
|
|
public static final short ST_SEAL = (short) 18;
|
|
public static final short ST_ARC = (short) 19;
|
|
public static final short ST_LINE = (short) 20;
|
|
public static final short ST_PLAQUE = (short) 21;
|
|
public static final short ST_CAN = (short) 22;
|
|
public static final short ST_DONUT = (short) 23;
|
|
public static final short ST_TEXTSIMPLE = (short) 24;
|
|
public static final short ST_TEXTOCTAGON = (short) 25;
|
|
public static final short ST_TEXTHEXAGON = (short) 26;
|
|
public static final short ST_TEXTCURVE = (short) 27;
|
|
public static final short ST_TEXTWAVE = (short) 28;
|
|
public static final short ST_TEXTRING = (short) 29;
|
|
public static final short ST_TEXTONCURVE = (short) 30;
|
|
public static final short ST_TEXTONRING = (short) 31;
|
|
public static final short ST_STRAIGHTCONNECTOR1 = (short) 32;
|
|
public static final short ST_BENTCONNECTOR2 = (short) 33;
|
|
public static final short ST_BENTCONNECTOR3 = (short) 34;
|
|
public static final short ST_BENTCONNECTOR4 = (short) 35;
|
|
public static final short ST_BENTCONNECTOR5 = (short) 36;
|
|
public static final short ST_CURVEDCONNECTOR2 = (short) 37;
|
|
public static final short ST_CURVEDCONNECTOR3 = (short) 38;
|
|
public static final short ST_CURVEDCONNECTOR4 = (short) 39;
|
|
public static final short ST_CURVEDCONNECTOR5 = (short) 40;
|
|
public static final short ST_CALLOUT1 = (short) 41;
|
|
public static final short ST_CALLOUT2 = (short) 42;
|
|
public static final short ST_CALLOUT3 = (short) 43;
|
|
public static final short ST_ACCENTCALLOUT1 = (short) 44;
|
|
public static final short ST_ACCENTCALLOUT2 = (short) 45;
|
|
public static final short ST_ACCENTCALLOUT3 = (short) 46;
|
|
public static final short ST_BORDERCALLOUT1 = (short) 47;
|
|
public static final short ST_BORDERCALLOUT2 = (short) 48;
|
|
public static final short ST_BORDERCALLOUT3 = (short) 49;
|
|
public static final short ST_ACCENTBORDERCALLOUT1 = (short) 50;
|
|
public static final short ST_ACCENTBORDERCALLOUT2 = (short) 51;
|
|
public static final short ST_ACCENTBORDERCALLOUT3 = (short) 52;
|
|
public static final short ST_RIBBON = (short) 53;
|
|
public static final short ST_RIBBON2 = (short) 54;
|
|
public static final short ST_CHEVRON = (short) 55;
|
|
public static final short ST_PENTAGON = (short) 56;
|
|
public static final short ST_NOSMOKING = (short) 57;
|
|
public static final short ST_SEAL8 = (short) 58;
|
|
public static final short ST_SEAL16 = (short) 59;
|
|
public static final short ST_SEAL32 = (short) 60;
|
|
public static final short ST_WEDGERECTCALLOUT = (short) 61;
|
|
public static final short ST_WEDGERRECTCALLOUT = (short) 62;
|
|
public static final short ST_WEDGEELLIPSECALLOUT = (short) 63;
|
|
public static final short ST_WAVE = (short) 64;
|
|
public static final short ST_FOLDEDCORNER = (short) 65;
|
|
public static final short ST_LEFTARROW = (short) 66;
|
|
public static final short ST_DOWNARROW = (short) 67;
|
|
public static final short ST_UPARROW = (short) 68;
|
|
public static final short ST_LEFTRIGHTARROW = (short) 69;
|
|
public static final short ST_UPDOWNARROW = (short) 70;
|
|
public static final short ST_IRREGULARSEAL1 = (short) 71;
|
|
public static final short ST_IRREGULARSEAL2 = (short) 72;
|
|
public static final short ST_LIGHTNINGBOLT = (short) 73;
|
|
public static final short ST_HEART = (short) 74;
|
|
public static final short ST_PICTUREFRAME = (short) 75;
|
|
public static final short ST_QUADARROW = (short) 76;
|
|
public static final short ST_LEFTARROWCALLOUT = (short) 77;
|
|
public static final short ST_RIGHTARROWCALLOUT = (short) 78;
|
|
public static final short ST_UPARROWCALLOUT = (short) 79;
|
|
public static final short ST_DOWNARROWCALLOUT = (short) 80;
|
|
public static final short ST_LEFTRIGHTARROWCALLOUT = (short) 81;
|
|
public static final short ST_UPDOWNARROWCALLOUT = (short) 82;
|
|
public static final short ST_QUADARROWCALLOUT = (short) 83;
|
|
public static final short ST_BEVEL = (short) 84;
|
|
public static final short ST_LEFTBRACKET = (short) 85;
|
|
public static final short ST_RIGHTBRACKET = (short) 86;
|
|
public static final short ST_LEFTBRACE = (short) 87;
|
|
public static final short ST_RIGHTBRACE = (short) 88;
|
|
public static final short ST_LEFTUPARROW = (short) 89;
|
|
public static final short ST_BENTUPARROW = (short) 90;
|
|
public static final short ST_BENTARROW = (short) 91;
|
|
public static final short ST_SEAL24 = (short) 92;
|
|
public static final short ST_STRIPEDRIGHTARROW = (short) 93;
|
|
public static final short ST_NOTCHEDRIGHTARROW = (short) 94;
|
|
public static final short ST_BLOCKARC = (short) 95;
|
|
public static final short ST_SMILEYFACE = (short) 96;
|
|
public static final short ST_VERTICALSCROLL = (short) 97;
|
|
public static final short ST_HORIZONTALSCROLL = (short) 98;
|
|
public static final short ST_CIRCULARARROW = (short) 99;
|
|
public static final short ST_NOTCHEDCIRCULARARROW = (short) 100;
|
|
public static final short ST_UTURNARROW = (short) 101;
|
|
public static final short ST_CURVEDRIGHTARROW = (short) 102;
|
|
public static final short ST_CURVEDLEFTARROW = (short) 103;
|
|
public static final short ST_CURVEDUPARROW = (short) 104;
|
|
public static final short ST_CURVEDDOWNARROW = (short) 105;
|
|
public static final short ST_CLOUDCALLOUT = (short) 106;
|
|
public static final short ST_ELLIPSERIBBON = (short) 107;
|
|
public static final short ST_ELLIPSERIBBON2 = (short) 108;
|
|
public static final short ST_FLOWCHARTPROCESS = (short) 109;
|
|
public static final short ST_FLOWCHARTDECISION = (short) 110;
|
|
public static final short ST_FLOWCHARTINPUTOUTPUT = (short) 111;
|
|
public static final short ST_FLOWCHARTPREDEFINEDPROCESS = (short) 112;
|
|
public static final short ST_FLOWCHARTINTERNALSTORAGE = (short) 113;
|
|
public static final short ST_FLOWCHARTDOCUMENT = (short) 114;
|
|
public static final short ST_FLOWCHARTMULTIDOCUMENT = (short) 115;
|
|
public static final short ST_FLOWCHARTTERMINATOR = (short) 116;
|
|
public static final short ST_FLOWCHARTPREPARATION = (short) 117;
|
|
public static final short ST_FLOWCHARTMANUALINPUT = (short) 118;
|
|
public static final short ST_FLOWCHARTMANUALOPERATION = (short) 119;
|
|
public static final short ST_FLOWCHARTCONNECTOR = (short) 120;
|
|
public static final short ST_FLOWCHARTPUNCHEDCARD = (short) 121;
|
|
public static final short ST_FLOWCHARTPUNCHEDTAPE = (short) 122;
|
|
public static final short ST_FLOWCHARTSUMMINGJUNCTION = (short) 123;
|
|
public static final short ST_FLOWCHARTOR = (short) 124;
|
|
public static final short ST_FLOWCHARTCOLLATE = (short) 125;
|
|
public static final short ST_FLOWCHARTSORT = (short) 126;
|
|
public static final short ST_FLOWCHARTEXTRACT = (short) 127;
|
|
public static final short ST_FLOWCHARTMERGE = (short) 128;
|
|
public static final short ST_FLOWCHARTOFFLINESTORAGE = (short) 129;
|
|
public static final short ST_FLOWCHARTONLINESTORAGE = (short) 130;
|
|
public static final short ST_FLOWCHARTMAGNETICTAPE = (short) 131;
|
|
public static final short ST_FLOWCHARTMAGNETICDISK = (short) 132;
|
|
public static final short ST_FLOWCHARTMAGNETICDRUM = (short) 133;
|
|
public static final short ST_FLOWCHARTDISPLAY = (short) 134;
|
|
public static final short ST_FLOWCHARTDELAY = (short) 135;
|
|
public static final short ST_TEXTPLAINTEXT = (short) 136;
|
|
public static final short ST_TEXTSTOP = (short) 137;
|
|
public static final short ST_TEXTTRIANGLE = (short) 138;
|
|
public static final short ST_TEXTTRIANGLEINVERTED = (short) 139;
|
|
public static final short ST_TEXTCHEVRON = (short) 140;
|
|
public static final short ST_TEXTCHEVRONINVERTED = (short) 141;
|
|
public static final short ST_TEXTRINGINSIDE = (short) 142;
|
|
public static final short ST_TEXTRINGOUTSIDE = (short) 143;
|
|
public static final short ST_TEXTARCHUPCURVE = (short) 144;
|
|
public static final short ST_TEXTARCHDOWNCURVE = (short) 145;
|
|
public static final short ST_TEXTCIRCLECURVE = (short) 146;
|
|
public static final short ST_TEXTBUTTONCURVE = (short) 147;
|
|
public static final short ST_TEXTARCHUPPOUR = (short) 148;
|
|
public static final short ST_TEXTARCHDOWNPOUR = (short) 149;
|
|
public static final short ST_TEXTCIRCLEPOUR = (short) 150;
|
|
public static final short ST_TEXTBUTTONPOUR = (short) 151;
|
|
public static final short ST_TEXTCURVEUP = (short) 152;
|
|
public static final short ST_TEXTCURVEDOWN = (short) 153;
|
|
public static final short ST_TEXTCASCADEUP = (short) 154;
|
|
public static final short ST_TEXTCASCADEDOWN = (short) 155;
|
|
public static final short ST_TEXTWAVE1 = (short) 156;
|
|
public static final short ST_TEXTWAVE2 = (short) 157;
|
|
public static final short ST_TEXTWAVE3 = (short) 158;
|
|
public static final short ST_TEXTWAVE4 = (short) 159;
|
|
public static final short ST_TEXTINFLATE = (short) 160;
|
|
public static final short ST_TEXTDEFLATE = (short) 161;
|
|
public static final short ST_TEXTINFLATEBOTTOM = (short) 162;
|
|
public static final short ST_TEXTDEFLATEBOTTOM = (short) 163;
|
|
public static final short ST_TEXTINFLATETOP = (short) 164;
|
|
public static final short ST_TEXTDEFLATETOP = (short) 165;
|
|
public static final short ST_TEXTDEFLATEINFLATE = (short) 166;
|
|
public static final short ST_TEXTDEFLATEINFLATEDEFLATE = (short) 167;
|
|
public static final short ST_TEXTFADERIGHT = (short) 168;
|
|
public static final short ST_TEXTFADELEFT = (short) 169;
|
|
public static final short ST_TEXTFADEUP = (short) 170;
|
|
public static final short ST_TEXTFADEDOWN = (short) 171;
|
|
public static final short ST_TEXTSLANTUP = (short) 172;
|
|
public static final short ST_TEXTSLANTDOWN = (short) 173;
|
|
public static final short ST_TEXTCANUP = (short) 174;
|
|
public static final short ST_TEXTCANDOWN = (short) 175;
|
|
public static final short ST_FLOWCHARTALTERNATEPROCESS = (short) 176;
|
|
public static final short ST_FLOWCHARTOFFPAGECONNECTOR = (short) 177;
|
|
public static final short ST_CALLOUT90 = (short) 178;
|
|
public static final short ST_ACCENTCALLOUT90 = (short) 179;
|
|
public static final short ST_BORDERCALLOUT90 = (short) 180;
|
|
public static final short ST_ACCENTBORDERCALLOUT90 = (short) 181;
|
|
public static final short ST_LEFTRIGHTUPARROW = (short) 182;
|
|
public static final short ST_SUN = (short) 183;
|
|
public static final short ST_MOON = (short) 184;
|
|
public static final short ST_BRACKETPAIR = (short) 185;
|
|
public static final short ST_BRACEPAIR = (short) 186;
|
|
public static final short ST_SEAL4 = (short) 187;
|
|
public static final short ST_DOUBLEWAVE = (short) 188;
|
|
public static final short ST_ACTIONBUTTONBLANK = (short) 189;
|
|
public static final short ST_ACTIONBUTTONHOME = (short) 190;
|
|
public static final short ST_ACTIONBUTTONHELP = (short) 191;
|
|
public static final short ST_ACTIONBUTTONINFORMATION = (short) 192;
|
|
public static final short ST_ACTIONBUTTONFORWARDNEXT = (short) 193;
|
|
public static final short ST_ACTIONBUTTONBACKPREVIOUS = (short) 194;
|
|
public static final short ST_ACTIONBUTTONEND = (short) 195;
|
|
public static final short ST_ACTIONBUTTONBEGINNING = (short) 196;
|
|
public static final short ST_ACTIONBUTTONRETURN = (short) 197;
|
|
public static final short ST_ACTIONBUTTONDOCUMENT = (short) 198;
|
|
public static final short ST_ACTIONBUTTONSOUND = (short) 199;
|
|
public static final short ST_ACTIONBUTTONMOVIE = (short) 200;
|
|
public static final short ST_HOSTCONTROL = (short) 201;
|
|
public static final short ST_TEXTBOX = (short) 202;
|
|
public static final short ST_NIL = (short) 0x0FFF;
|
|
|
|
/**
|
|
* Maps shape container objects to their {@link TextObjectRecord} or {@link ObjRecord}
|
|
*/
|
|
private final Map<EscherRecord, Record> shapeToObj = new HashMap<EscherRecord, Record>();
|
|
|
|
/**
|
|
* list of "tail" records that need to be serialized after all drawing group records
|
|
*/
|
|
private final Map<Integer, NoteRecord> tailRec = new LinkedHashMap<Integer, NoteRecord>();
|
|
|
|
/**
|
|
* create new EscherAggregate
|
|
* @param createDefaultTree if true creates base tree of the escher records, see EscherAggregate.buildBaseTree()
|
|
* else return empty escher aggregate
|
|
*/
|
|
public EscherAggregate(boolean createDefaultTree) {
|
|
if (createDefaultTree){
|
|
buildBaseTree();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return Returns the current sid.
|
|
*/
|
|
public short getSid() {
|
|
return sid;
|
|
}
|
|
|
|
/**
|
|
* Calculates the string representation of this record. This is
|
|
* simply a dump of all the records.
|
|
*/
|
|
public String toString() {
|
|
String nl = System.getProperty("line.separtor");
|
|
|
|
StringBuilder result = new StringBuilder();
|
|
result.append('[').append(getRecordName()).append(']').append(nl);
|
|
for (EscherRecord escherRecord : getEscherRecords()) {
|
|
result.append(escherRecord);
|
|
}
|
|
result.append("[/").append(getRecordName()).append(']').append(nl);
|
|
|
|
return result.toString();
|
|
}
|
|
|
|
/**
|
|
* Calculates the xml representation of this record. This is
|
|
* simply a dump of all the records.
|
|
* @param tab - string which must be added before each line (used by default '\t')
|
|
* @return xml representation of the all aggregated records
|
|
*/
|
|
public String toXml(String tab) {
|
|
StringBuilder builder = new StringBuilder();
|
|
builder.append(tab).append("<").append(getRecordName()).append(">\n");
|
|
for (EscherRecord escherRecord : getEscherRecords()) {
|
|
builder.append(escherRecord.toXml(tab + "\t"));
|
|
}
|
|
builder.append(tab).append("</").append(getRecordName()).append(">\n");
|
|
return builder.toString();
|
|
}
|
|
|
|
/**
|
|
* @param sid - record sid we want to check if it belongs to drawing layer
|
|
* @return true if record is instance of DrawingRecord or ContinueRecord or ObjRecord or TextObjRecord
|
|
*/
|
|
private static boolean isDrawingLayerRecord(final short sid) {
|
|
return sid == DrawingRecord.sid ||
|
|
sid == ContinueRecord.sid ||
|
|
sid == ObjRecord.sid ||
|
|
sid == TextObjectRecord.sid;
|
|
}
|
|
|
|
/**
|
|
* Collapses the drawing records into an aggregate.
|
|
* read Drawing, Obj, TxtObj, Note and Continue records into single byte array,
|
|
* create Escher tree from byte array, create map <EscherRecord, Record>
|
|
*
|
|
* @param records - list of all records inside sheet
|
|
* @param locFirstDrawingRecord - location of the first DrawingRecord inside sheet
|
|
* @return new EscherAggregate create from all aggregated records which belong to drawing layer
|
|
*/
|
|
public static EscherAggregate createAggregate(List<RecordBase> records, int locFirstDrawingRecord) {
|
|
// Keep track of any shape records created so we can match them back to the object id's.
|
|
// Textbox objects are also treated as shape objects.
|
|
final List<EscherRecord> shapeRecords = new ArrayList<EscherRecord>();
|
|
EscherRecordFactory recordFactory = new DefaultEscherRecordFactory() {
|
|
public EscherRecord createRecord(byte[] data, int offset) {
|
|
EscherRecord r = super.createRecord(data, offset);
|
|
if (r.getRecordId() == EscherClientDataRecord.RECORD_ID || r.getRecordId() == EscherTextboxRecord.RECORD_ID) {
|
|
shapeRecords.add(r);
|
|
}
|
|
return r;
|
|
}
|
|
};
|
|
|
|
// Create one big buffer
|
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
|
EscherAggregate agg = new EscherAggregate(false);
|
|
int loc = locFirstDrawingRecord;
|
|
while (loc + 1 < records.size()
|
|
&& (isDrawingLayerRecord(sid(records, loc)))) {
|
|
try {
|
|
if (!(sid(records, loc) == DrawingRecord.sid || sid(records, loc) == ContinueRecord.sid)) {
|
|
loc++;
|
|
continue;
|
|
}
|
|
if (sid(records, loc) == DrawingRecord.sid) {
|
|
buffer.write(((DrawingRecord) records.get(loc)).getRecordData());
|
|
} else {
|
|
buffer.write(((ContinueRecord) records.get(loc)).getData());
|
|
}
|
|
} catch (IOException e) {
|
|
throw new RuntimeException("Couldn't get data from drawing/continue records", e);
|
|
}
|
|
loc++;
|
|
}
|
|
|
|
// Decode the shapes
|
|
// agg.escherRecords = new ArrayList();
|
|
int pos = 0;
|
|
while (pos < buffer.size()) {
|
|
EscherRecord r = recordFactory.createRecord(buffer.toByteArray(), pos);
|
|
int bytesRead = r.fillFields(buffer.toByteArray(), pos, recordFactory);
|
|
agg.addEscherRecord(r);
|
|
pos += bytesRead;
|
|
}
|
|
|
|
// Associate the object records with the shapes
|
|
loc = locFirstDrawingRecord + 1;
|
|
int shapeIndex = 0;
|
|
while (loc < records.size()
|
|
&& (isDrawingLayerRecord(sid(records, loc)))) {
|
|
if (!isObjectRecord(records, loc)) {
|
|
loc++;
|
|
continue;
|
|
}
|
|
Record objRecord = (Record) records.get(loc);
|
|
agg.shapeToObj.put(shapeRecords.get(shapeIndex++), objRecord);
|
|
loc++;
|
|
}
|
|
|
|
// any NoteRecords that follow the drawing block must be aggregated and and saved in the tailRec collection
|
|
while (loc < records.size()) {
|
|
if (sid(records, loc) == NoteRecord.sid) {
|
|
NoteRecord r = (NoteRecord) records.get(loc);
|
|
agg.tailRec.put(r.getShapeId(), r);
|
|
} else {
|
|
break;
|
|
}
|
|
loc++;
|
|
}
|
|
|
|
int locLastDrawingRecord = loc;
|
|
// replace drawing block with the created EscherAggregate
|
|
records.subList(locFirstDrawingRecord, locLastDrawingRecord).clear();
|
|
records.add(locFirstDrawingRecord, agg);
|
|
return agg;
|
|
}
|
|
|
|
/**
|
|
* Serializes this aggregate to a byte array. Since this is an aggregate
|
|
* record it will effectively serialize the aggregated records.
|
|
*
|
|
* @param offset The offset into the start of the array.
|
|
* @param data The byte array to serialize to.
|
|
* @return The number of bytes serialized.
|
|
*/
|
|
public int serialize(int offset, byte[] data) {
|
|
// Determine buffer size
|
|
List <EscherRecord>records = getEscherRecords();
|
|
int size = getEscherRecordSize(records);
|
|
byte[] buffer = new byte[size];
|
|
|
|
// Serialize escher records into one big data structure and keep note of ending offsets.
|
|
final List <Integer>spEndingOffsets = new ArrayList<Integer>();
|
|
final List <EscherRecord> shapes = new ArrayList<EscherRecord>();
|
|
int pos = 0;
|
|
for (Object record : records) {
|
|
EscherRecord e = (EscherRecord) record;
|
|
pos += e.serialize(pos, buffer, new EscherSerializationListener() {
|
|
public void beforeRecordSerialize(int offset, short recordId, EscherRecord record) {
|
|
}
|
|
|
|
public void afterRecordSerialize(int offset, short recordId, int size, EscherRecord record) {
|
|
if (recordId == EscherClientDataRecord.RECORD_ID || recordId == EscherTextboxRecord.RECORD_ID) {
|
|
spEndingOffsets.add(offset);
|
|
shapes.add(record);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
shapes.add(0, null);
|
|
spEndingOffsets.add(0, 0);
|
|
|
|
// Split escher records into separate MSODRAWING and OBJ, TXO records. (We don't break on
|
|
// the first one because it's the patriach).
|
|
pos = offset;
|
|
int writtenEscherBytes = 0;
|
|
int i;
|
|
for (i = 1; i < shapes.size(); i++) {
|
|
int endOffset = spEndingOffsets.get(i) - 1;
|
|
int startOffset;
|
|
if (i == 1)
|
|
startOffset = 0;
|
|
else
|
|
startOffset = spEndingOffsets.get(i - 1);
|
|
|
|
byte[] drawingData = new byte[endOffset - startOffset + 1];
|
|
System.arraycopy(buffer, startOffset, drawingData, 0, drawingData.length);
|
|
pos += writeDataIntoDrawingRecord(drawingData, writtenEscherBytes, pos, data, i);
|
|
|
|
writtenEscherBytes += drawingData.length;
|
|
|
|
// Write the matching OBJ record
|
|
Record obj = shapeToObj.get(shapes.get(i));
|
|
pos += obj.serialize(pos, data);
|
|
|
|
if (i == shapes.size() - 1 && endOffset < buffer.length - 1) {
|
|
drawingData = new byte[buffer.length - endOffset - 1];
|
|
System.arraycopy(buffer, endOffset + 1, drawingData, 0, drawingData.length);
|
|
pos += writeDataIntoDrawingRecord(drawingData, writtenEscherBytes, pos, data, i);
|
|
}
|
|
}
|
|
if ((pos - offset) < buffer.length - 1) {
|
|
byte[] drawingData = new byte[buffer.length - (pos - offset)];
|
|
System.arraycopy(buffer, (pos - offset), drawingData, 0, drawingData.length);
|
|
pos += writeDataIntoDrawingRecord(drawingData, writtenEscherBytes, pos, data, i);
|
|
}
|
|
|
|
for (NoteRecord noteRecord : tailRec.values()) {
|
|
Record rec = (Record) noteRecord;
|
|
pos += rec.serialize(pos, data);
|
|
}
|
|
int bytesWritten = pos - offset;
|
|
if (bytesWritten != getRecordSize())
|
|
throw new RecordFormatException(bytesWritten + " bytes written but getRecordSize() reports " + getRecordSize());
|
|
return bytesWritten;
|
|
}
|
|
|
|
/**
|
|
* @param drawingData - escher records saved into single byte array
|
|
* @param writtenEscherBytes - count of bytes already saved into drawing records (we should know it to decide create
|
|
* drawing or continue record)
|
|
* @param pos current position of data array
|
|
* @param data - array of bytes where drawing records must be serialized
|
|
* @param i - number of shape, saved into data array
|
|
* @return offset of data array after serialization
|
|
*/
|
|
private int writeDataIntoDrawingRecord(byte[] drawingData, int writtenEscherBytes, int pos, byte[] data, int i) {
|
|
int temp = 0;
|
|
//First record in drawing layer MUST be DrawingRecord
|
|
if (writtenEscherBytes + drawingData.length > RecordInputStream.MAX_RECORD_DATA_SIZE && i != 1) {
|
|
for (int j = 0; j < drawingData.length; j += RecordInputStream.MAX_RECORD_DATA_SIZE) {
|
|
byte[] buf = new byte[Math.min(RecordInputStream.MAX_RECORD_DATA_SIZE, drawingData.length - j)];
|
|
System.arraycopy(drawingData, j, buf, 0, Math.min(RecordInputStream.MAX_RECORD_DATA_SIZE, drawingData.length - j));
|
|
ContinueRecord drawing = new ContinueRecord(buf);
|
|
temp += drawing.serialize(pos + temp, data);
|
|
}
|
|
} else {
|
|
for (int j = 0; j < drawingData.length; j += RecordInputStream.MAX_RECORD_DATA_SIZE) {
|
|
if (j == 0) {
|
|
DrawingRecord drawing = new DrawingRecord();
|
|
byte[] buf = new byte[Math.min(RecordInputStream.MAX_RECORD_DATA_SIZE, drawingData.length - j)];
|
|
System.arraycopy(drawingData, j, buf, 0, Math.min(RecordInputStream.MAX_RECORD_DATA_SIZE, drawingData.length - j));
|
|
drawing.setData(buf);
|
|
temp += drawing.serialize(pos + temp, data);
|
|
} else {
|
|
byte[] buf = new byte[Math.min(RecordInputStream.MAX_RECORD_DATA_SIZE, drawingData.length - j)];
|
|
System.arraycopy(drawingData, j, buf, 0, Math.min(RecordInputStream.MAX_RECORD_DATA_SIZE, drawingData.length - j));
|
|
ContinueRecord drawing = new ContinueRecord(buf);
|
|
temp += drawing.serialize(pos + temp, data);
|
|
}
|
|
}
|
|
}
|
|
return temp;
|
|
}
|
|
|
|
/**
|
|
* How many bytes do the raw escher records contain.
|
|
*
|
|
* @param records List of escher records
|
|
* @return the number of bytes
|
|
*/
|
|
private int getEscherRecordSize(List<EscherRecord> records) {
|
|
int size = 0;
|
|
for (EscherRecord record : records){
|
|
size += record.getRecordSize();
|
|
}
|
|
return size;
|
|
}
|
|
|
|
/**
|
|
* @return record size, including header size of obj, text, note, drawing, continue records
|
|
*/
|
|
public int getRecordSize() {
|
|
// To determine size of aggregate record we have to know size of each DrawingRecord because if DrawingRecord
|
|
// is split into several continue records we have to add header size to total EscherAggregate size
|
|
int continueRecordsHeadersSize = 0;
|
|
// Determine buffer size
|
|
List<EscherRecord> records = getEscherRecords();
|
|
int rawEscherSize = getEscherRecordSize(records);
|
|
byte[] buffer = new byte[rawEscherSize];
|
|
final List<Integer> spEndingOffsets = new ArrayList<Integer>();
|
|
int pos = 0;
|
|
for (EscherRecord e : records) {
|
|
pos += e.serialize(pos, buffer, new EscherSerializationListener() {
|
|
public void beforeRecordSerialize(int offset, short recordId, EscherRecord record) {
|
|
}
|
|
|
|
public void afterRecordSerialize(int offset, short recordId, int size, EscherRecord record) {
|
|
if (recordId == EscherClientDataRecord.RECORD_ID || recordId == EscherTextboxRecord.RECORD_ID) {
|
|
spEndingOffsets.add(offset);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
spEndingOffsets.add(0, 0);
|
|
|
|
for (int i = 1; i < spEndingOffsets.size(); i++) {
|
|
if (i == spEndingOffsets.size() - 1 && spEndingOffsets.get(i) < pos) {
|
|
continueRecordsHeadersSize += 4;
|
|
}
|
|
if (spEndingOffsets.get(i) - spEndingOffsets.get(i - 1) <= RecordInputStream.MAX_RECORD_DATA_SIZE) {
|
|
continue;
|
|
}
|
|
continueRecordsHeadersSize += ((spEndingOffsets.get(i) - spEndingOffsets.get(i - 1)) / RecordInputStream.MAX_RECORD_DATA_SIZE) * 4;
|
|
}
|
|
|
|
int drawingRecordSize = rawEscherSize + (shapeToObj.size()) * 4;
|
|
if (rawEscherSize != 0 && spEndingOffsets.size() == 1/**EMPTY**/) {
|
|
continueRecordsHeadersSize += 4;
|
|
}
|
|
int objRecordSize = 0;
|
|
for (Record r : shapeToObj.values()) {
|
|
objRecordSize += r.getRecordSize();
|
|
}
|
|
int tailRecordSize = 0;
|
|
for (NoteRecord noteRecord : tailRec.values()) {
|
|
tailRecordSize += noteRecord.getRecordSize();
|
|
}
|
|
return drawingRecordSize + objRecordSize + tailRecordSize + continueRecordsHeadersSize;
|
|
}
|
|
|
|
/**
|
|
* Associates an escher record to an OBJ record or a TXO record.
|
|
* @param r - ClientData or Textbox record
|
|
* @param objRecord - Obj or TextObj record
|
|
*/
|
|
public void associateShapeToObjRecord(EscherRecord r, Record objRecord) {
|
|
shapeToObj.put(r, objRecord);
|
|
}
|
|
|
|
/**
|
|
* Remove echerRecord and associated to it Obj or TextObj record
|
|
* @param rec - clientData or textbox record to be removed
|
|
*/
|
|
public void removeShapeToObjRecord(EscherRecord rec) {
|
|
shapeToObj.remove(rec);
|
|
}
|
|
|
|
/**
|
|
* @return "ESCHERAGGREGATE"
|
|
*/
|
|
protected String getRecordName() {
|
|
return "ESCHERAGGREGATE";
|
|
}
|
|
|
|
// =============== Private methods ========================
|
|
|
|
/**
|
|
*
|
|
* @param records list of the record to look inside
|
|
* @param loc location of the checked record
|
|
* @return true if record is instance of ObjRecord or TextObjectRecord
|
|
*/
|
|
private static boolean isObjectRecord(List <RecordBase>records, int loc) {
|
|
return sid(records, loc) == ObjRecord.sid || sid(records, loc) == TextObjectRecord.sid;
|
|
}
|
|
|
|
/**
|
|
* create base tree with such structure:
|
|
* EscherDgContainer
|
|
* -EscherSpgrContainer
|
|
* --EscherSpContainer
|
|
* ---EscherSpRecord
|
|
* ---EscherSpgrRecord
|
|
* ---EscherSpRecord
|
|
* -EscherDgRecord
|
|
*
|
|
* id of DgRecord and SpRecord are empty and must be set later by HSSFPatriarch
|
|
*/
|
|
private void buildBaseTree() {
|
|
EscherContainerRecord dgContainer = new EscherContainerRecord();
|
|
EscherContainerRecord spgrContainer = new EscherContainerRecord();
|
|
EscherContainerRecord spContainer1 = new EscherContainerRecord();
|
|
EscherSpgrRecord spgr = new EscherSpgrRecord();
|
|
EscherSpRecord sp1 = new EscherSpRecord();
|
|
dgContainer.setRecordId(EscherContainerRecord.DG_CONTAINER);
|
|
dgContainer.setOptions((short) 0x000F);
|
|
EscherDgRecord dg = new EscherDgRecord();
|
|
dg.setRecordId(EscherDgRecord.RECORD_ID);
|
|
short dgId = 1;
|
|
dg.setOptions((short) (dgId << 4));
|
|
dg.setNumShapes(0);
|
|
dg.setLastMSOSPID(1024);
|
|
spgrContainer.setRecordId(EscherContainerRecord.SPGR_CONTAINER);
|
|
spgrContainer.setOptions((short) 0x000F);
|
|
spContainer1.setRecordId(EscherContainerRecord.SP_CONTAINER);
|
|
spContainer1.setOptions((short) 0x000F);
|
|
spgr.setRecordId(EscherSpgrRecord.RECORD_ID);
|
|
spgr.setOptions((short) 0x0001); // version
|
|
spgr.setRectX1(0);
|
|
spgr.setRectY1(0);
|
|
spgr.setRectX2(1023);
|
|
spgr.setRectY2(255);
|
|
sp1.setRecordId(EscherSpRecord.RECORD_ID);
|
|
|
|
sp1.setOptions((short) 0x0002);
|
|
sp1.setVersion((short) 0x2);
|
|
sp1.setShapeId(-1);
|
|
sp1.setFlags(EscherSpRecord.FLAG_GROUP | EscherSpRecord.FLAG_PATRIARCH);
|
|
dgContainer.addChildRecord(dg);
|
|
dgContainer.addChildRecord(spgrContainer);
|
|
spgrContainer.addChildRecord(spContainer1);
|
|
spContainer1.addChildRecord(spgr);
|
|
spContainer1.addChildRecord(sp1);
|
|
addEscherRecord(dgContainer);
|
|
}
|
|
|
|
/**
|
|
* EscherDgContainer
|
|
* -EscherSpgrContainer
|
|
* -EscherDgRecord - set id for this record
|
|
* set id for DgRecord of DgContainer
|
|
* @param dgId - id which must be set
|
|
*/
|
|
public void setDgId(short dgId) {
|
|
EscherContainerRecord dgContainer = getEscherContainer();
|
|
EscherDgRecord dg = dgContainer.getChildById(EscherDgRecord.RECORD_ID);
|
|
dg.setOptions((short) (dgId << 4));
|
|
}
|
|
|
|
/**
|
|
* EscherDgContainer
|
|
* -EscherSpgrContainer
|
|
* --EscherSpContainer
|
|
* ---EscherSpRecord -set id for this record
|
|
* ---***
|
|
* --***
|
|
* -EscherDgRecord
|
|
* set id for the sp record of the first spContainer in main spgrConatiner
|
|
* @param shapeId - id which must be set
|
|
*/
|
|
public void setMainSpRecordId(int shapeId) {
|
|
EscherContainerRecord dgContainer = getEscherContainer();
|
|
EscherContainerRecord spgrConatiner = (EscherContainerRecord) dgContainer.getChildById(EscherContainerRecord.SPGR_CONTAINER);
|
|
EscherContainerRecord spContainer = (EscherContainerRecord) spgrConatiner.getChild(0);
|
|
EscherSpRecord sp = (EscherSpRecord) spContainer.getChildById(EscherSpRecord.RECORD_ID);
|
|
sp.setShapeId(shapeId);
|
|
}
|
|
|
|
/**
|
|
* @param records list of records to look into
|
|
* @param loc - location of the record which sid must be returned
|
|
* @return sid of the record with selected location
|
|
*/
|
|
private static short sid(List<RecordBase> records, int loc) {
|
|
RecordBase record = records.get(loc);
|
|
if (record instanceof Record) {
|
|
return ((Record)record).getSid();
|
|
} else {
|
|
// Aggregates don't have a sid
|
|
// We could step into them, but for these needs we don't care
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return unmodifiable copy of the mapping of {@link EscherClientDataRecord} and {@link EscherTextboxRecord}
|
|
* to their {@link TextObjectRecord} or {@link ObjRecord} .
|
|
* <p>
|
|
* We need to access it outside of EscherAggregate when building shapes
|
|
*/
|
|
public Map<EscherRecord, Record> getShapeToObjMapping() {
|
|
return Collections.unmodifiableMap(shapeToObj);
|
|
}
|
|
|
|
/**
|
|
* @return unmodifiable copy of tail records. We need to access them when building shapes.
|
|
* Every HSSFComment shape has a link to a NoteRecord from the tailRec collection.
|
|
*/
|
|
public Map<Integer, NoteRecord> getTailRecords() {
|
|
return Collections.unmodifiableMap(tailRec);
|
|
}
|
|
|
|
/**
|
|
* @param obj - ObjRecord with id == NoteRecord.id
|
|
* @return null if note record is not found else returns note record with id == obj.id
|
|
*/
|
|
public NoteRecord getNoteRecordByObj(ObjRecord obj) {
|
|
CommonObjectDataSubRecord cod = (CommonObjectDataSubRecord) obj.getSubRecords().get(0);
|
|
return tailRec.get(cod.getObjectId());
|
|
}
|
|
|
|
/**
|
|
* Add tail record to existing map
|
|
* @param note to be added
|
|
*/
|
|
public void addTailRecord(NoteRecord note) {
|
|
tailRec.put(note.getShapeId(), note);
|
|
}
|
|
|
|
/**
|
|
* Remove tail record from the existing map
|
|
* @param note to be removed
|
|
*/
|
|
public void removeTailRecord(NoteRecord note) {
|
|
tailRec.remove(note.getShapeId());
|
|
}
|
|
}
|