| /* ==================================================================== |
| 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 static org.apache.poi.hssf.record.RecordInputStream.MAX_RECORD_DATA_SIZE; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.function.Supplier; |
| |
| 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.EscherSerializationListener; |
| import org.apache.poi.ddf.EscherSpRecord; |
| import org.apache.poi.ddf.EscherSpgrRecord; |
| import org.apache.poi.ddf.EscherTextboxRecord; |
| import org.apache.poi.util.GenericRecordXmlWriter; |
| import org.apache.poi.util.IOUtils; |
| import org.apache.poi.util.RecordFormatException; |
| |
| /** |
| * 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 { |
| // not a real sid - dummy value |
| public static final short sid = 9876; |
| //arbitrarily selected; may need to increase |
| private static final int MAX_RECORD_LENGTH = 100_000_000; |
| |
| |
| 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<>(); |
| |
| /** |
| * list of "tail" records that need to be serialized after all drawing group records |
| */ |
| private final Map<Integer, NoteRecord> tailRec = new LinkedHashMap<>(); |
| |
| /** |
| * 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(); |
| } |
| } |
| |
| public EscherAggregate(EscherAggregate other) { |
| super(other); |
| // shallow copy, because the aggregates doesn't own the records |
| shapeToObj.putAll(other.shapeToObj); |
| tailRec.putAll(other.tailRec); |
| } |
| |
| /** |
| * @return Returns the current sid. |
| */ |
| public short getSid() { |
| return sid; |
| } |
| |
| /** |
| * 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) { |
| return GenericRecordXmlWriter.marshal(this); |
| } |
| |
| /** |
| * 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(final List<RecordBase> records, final int locFirstDrawingRecord) { |
| EscherAggregate agg = new EscherAggregate(false); |
| |
| ShapeCollector recordFactory = new ShapeCollector(); |
| List<Record> objectRecords = new ArrayList<>(); |
| |
| int nextIdx = locFirstDrawingRecord; |
| for (RecordBase rb : records.subList(locFirstDrawingRecord, records.size())) { |
| nextIdx++; |
| switch (sid(rb)) { |
| case DrawingRecord.sid: |
| recordFactory.addBytes(((DrawingRecord)rb).getRecordData()); |
| continue; |
| case ContinueRecord.sid: |
| recordFactory.addBytes(((ContinueRecord)rb).getData()); |
| continue; |
| case ObjRecord.sid: |
| case TextObjectRecord.sid: |
| objectRecords.add((org.apache.poi.hssf.record.Record)rb); |
| continue; |
| case NoteRecord.sid: |
| // any NoteRecords that follow the drawing block must be aggregated and saved in the tailRec collection |
| NoteRecord r = (NoteRecord)rb; |
| agg.tailRec.put(r.getShapeId(), r); |
| continue; |
| default: |
| nextIdx--; |
| break; |
| } |
| break; |
| } |
| |
| // replace drawing block with the created EscherAggregate |
| records.set(locFirstDrawingRecord, agg); |
| if (locFirstDrawingRecord+1 <= nextIdx) { |
| records.subList(locFirstDrawingRecord + 1, nextIdx).clear(); |
| } |
| |
| // Decode the shapes |
| Iterator<EscherRecord> shapeIter = recordFactory.parse(agg).iterator(); |
| |
| // Associate the object records with the shapes |
| objectRecords.forEach(or -> agg.shapeToObj.put(shapeIter.next(), or)); |
| |
| return agg; |
| } |
| |
| private static class ShapeCollector extends DefaultEscherRecordFactory { |
| final List<EscherRecord> objShapes = new ArrayList<>(); |
| final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); |
| |
| void addBytes(byte[] data) { |
| try { |
| buffer.write(data); |
| } catch (IOException e) { |
| throw new RuntimeException("Couldn't get data from drawing/continue records", e); |
| } |
| } |
| |
| public EscherRecord createRecord(byte[] data, int offset) { |
| EscherRecord r = super.createRecord(data, offset); |
| short rid = r.getRecordId(); |
| if (rid == EscherClientDataRecord.RECORD_ID || rid == EscherTextboxRecord.RECORD_ID) { |
| objShapes.add(r); |
| } |
| return r; |
| } |
| |
| List<EscherRecord> parse(EscherAggregate agg) { |
| byte[] buf = buffer.toByteArray(); |
| for (int pos = 0, bytesRead; pos < buf.length; pos += bytesRead) { |
| EscherRecord r = createRecord(buf, pos); |
| bytesRead = r.fillFields(buf, pos, this); |
| agg.addEscherRecord(r); |
| } |
| return objShapes; |
| } |
| } |
| |
| /** |
| * 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(final int offset, final 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<>(); |
| final List <EscherRecord> shapes = new ArrayList<>(); |
| 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; |
| boolean isFirst = true; |
| int endOffset = 0; |
| for (int i = 1; i < shapes.size(); i++) { |
| int startOffset = endOffset; |
| endOffset = spEndingOffsets.get(i); |
| |
| byte[] drawingData = Arrays.copyOfRange(buffer, startOffset, endOffset); |
| pos += writeDataIntoDrawingRecord(drawingData, writtenEscherBytes, pos, data, isFirst); |
| |
| writtenEscherBytes += drawingData.length; |
| |
| // Write the matching OBJ record |
| Record obj = shapeToObj.get(shapes.get(i)); |
| pos += obj.serialize(pos, data); |
| |
| isFirst = false; |
| } |
| |
| if (endOffset < buffer.length - 1) { |
| byte[] drawingData = Arrays.copyOfRange(buffer, endOffset, buffer.length); |
| pos += writeDataIntoDrawingRecord(drawingData, writtenEscherBytes, pos, data, isFirst); |
| } |
| |
| for (NoteRecord noteRecord : tailRec.values()) { |
| pos += noteRecord.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 isFirst - is it the first shape, saved into data array |
| * @return offset of data array after serialization |
| */ |
| private int writeDataIntoDrawingRecord(final byte[] drawingData, final int writtenEscherBytes, final int pos, final byte[] data, final boolean isFirst) { |
| int temp = 0; |
| //First record in drawing layer MUST be DrawingRecord |
| boolean useDrawingRecord = isFirst || (writtenEscherBytes + drawingData.length) <= MAX_RECORD_DATA_SIZE; |
| |
| for (int j = 0; j < drawingData.length; j += MAX_RECORD_DATA_SIZE) { |
| byte[] buf = Arrays.copyOfRange(drawingData, j, Math.min(j+MAX_RECORD_DATA_SIZE, drawingData.length)); |
| Record drawing = (useDrawingRecord) ? new DrawingRecord(buf) : new ContinueRecord(buf); |
| temp += drawing.serialize(pos + temp, data); |
| useDrawingRecord = false; |
| } |
| 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 = IOUtils.safelyAllocate(rawEscherSize, MAX_RECORD_LENGTH); |
| final List<Integer> spEndingOffsets = new ArrayList<>(); |
| 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) <= MAX_RECORD_DATA_SIZE) { |
| continue; |
| } |
| continueRecordsHeadersSize += ((spEndingOffsets.get(i) - spEndingOffsets.get(i - 1)) / MAX_RECORD_DATA_SIZE) * 4; |
| } |
| |
| int drawingRecordSize = rawEscherSize + (shapeToObj.size()) * 4; |
| if (rawEscherSize != 0 && spEndingOffsets.size() == 1) { |
| // EMPTY |
| continueRecordsHeadersSize += 4; |
| } |
| int objRecordSize = 0; |
| for (org.apache.poi.hssf.record.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 ======================== |
| |
| /** |
| * 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); |
| if (dg != null) { |
| 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 spgrContainer = dgContainer.getChildById(EscherContainerRecord.SPGR_CONTAINER); |
| if (spgrContainer != null) { |
| EscherContainerRecord spContainer = (EscherContainerRecord) spgrContainer.getChild(0); |
| EscherSpRecord sp = spContainer.getChildById(EscherSpRecord.RECORD_ID); |
| if (sp != null) { |
| sp.setShapeId(shapeId); |
| } |
| } |
| } |
| |
| /** |
| * @param record the record to look into |
| * @return sid of the record |
| */ |
| private static short sid(RecordBase record) { |
| // Aggregates don't have a sid |
| // We could step into them, but for these needs we don't care |
| return (record instanceof org.apache.poi.hssf.record.Record) |
| ? ((org.apache.poi.hssf.record.Record)record).getSid() |
| : -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()); |
| } |
| |
| @Override |
| public EscherAggregate copy() { |
| return new EscherAggregate(this); |
| } |
| |
| @Override |
| public HSSFRecordTypes getGenericRecordType() { |
| return HSSFRecordTypes.ESCHER_AGGREGATE; |
| } |
| |
| @Override |
| public Map<String, Supplier<?>> getGenericProperties() { |
| return null; |
| } |
| } |