/** * 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 */ public abstract class EscherRecord { private static BitField fInstance = BitFieldFactory.getInstance(0xfff0); private static BitField fVersion = BitFieldFactory.getInstance(0x000f); private short _options; private short _recordId; /** Create a new instance */ public EscherRecord() { // fields uninitialised } /** * 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) { _options = LittleEndian.getShort(data, offset); _recordId = LittleEndian.getShort(data, offset + 2); int remainingBytes = LittleEndian.getInt(data, offset + 4); return remainingBytes; } /** * Read the options field from header and return instance part of it. * * @param data the byte array to read from * @param offset the offset to start reading from * @return value of instance part of options field */ protected static short readInstance(byte data[], int offset) { final short options = LittleEndian.getShort(data, offset); return fInstance.getShortValue(options); } /** * Determine whether this is a container record by inspecting the option field. * * @return true is this is a container field. */ public boolean isContainerRecord() { return getVersion() == (short) 0x000f; } /** * options</code> is an internal field. Use {@link #setInstance(short)} ()} and {@link * #setVersion(short)} ()} to set the actual fields. * * @return The options field for this record. All records have one. */ @Internal public short getOptions() { return _options; } /** * Set the options this this record. Container records should have the last nibble set to 0xF. * * <p * Note that <code>options</code> is an internal field. Use {@link #getInstance()} and * {@link #getVersion()} to access actual fields. */ @Internal public void setOptions(short options) { // call to handle correct/incorrect values setVersion(fVersion.getShortValue(options)); setInstance(fInstance.getShortValue(options)); _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 */ public abstract 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) { _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<EscherRecord> getChildRecords() { return Collections.emptyList(); } /** * 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<EscherRecord> childRecords) { throw new UnsupportedOperationException("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 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 fInstance.getShortValue(_options); } /** * Sets the instance part of record * * @param value instance part value */ public void setInstance(short value) { _options = fInstance.setShortValue(_options, value); } /** * Returns the version part of the option record. * * @return The version part of the option record */ public short getVersion() { return fVersion.getShortValue(_options); } /** * Sets the version part of record * * @param value version part value */ public void setVersion(short value) { _options = fVersion.setShortValue(_options, value); } /** * @param tab - each children must be a right of his parent * @return xml representation of this record */ public String toXml(String tab) { StringBuilder builder = new StringBuilder(); builder .append(tab) .append("<") .append(getClass().getSimpleName()) .append(">\n") .append(tab) .append("\t") .append("<RecordId>0x") .append(HexDump.toHex(_recordId)) .append("</RecordId>\n") .append(tab) .append("\t") .append("<Options>") .append(_options) .append("</Options>\n") .append(tab) .append("</") .append(getClass().getSimpleName()) .append(">\n"); return builder.toString(); } protected String formatXmlRecordHeader( String className, String recordId, String version, String instance) { StringBuilder builder = new StringBuilder(); builder .append("<") .append(className) .append(" recordId=\"0x") .append(recordId) .append("\" version=\"0x") .append(version) .append("\" instance=\"0x") .append(instance) .append("\" size=\"") .append(getRecordSize()) .append("\">\n"); return builder.toString(); } public String toXml() { return toXml(""); } }
/** * "Special Attributes" This seems to be a Misc Stuff and Junk record. One function it serves is in * SUM functions (i.e. SUM(A1:A3) causes an area PTG then an ATTR with the SUM option set) * * @author andy * @author Jason Height (jheight at chariot dot net dot au) */ public final class AttrPtg extends ControlPtg { public static final byte sid = 0x19; private static final int SIZE = 4; private final byte _options; private final short _data; /** only used for tAttrChoose: table of offsets to starts of args */ private final int[] _jumpTable; /** only used for tAttrChoose: offset to the tFuncVar for CHOOSE() */ private final int _chooseFuncOffset; // flags 'volatile' and 'space', can be combined. // OOO spec says other combinations are theoretically possible but not likely to occur. private static final BitField semiVolatile = BitFieldFactory.getInstance(0x01); private static final BitField optiIf = BitFieldFactory.getInstance(0x02); private static final BitField optiChoose = BitFieldFactory.getInstance(0x04); private static final BitField optiSkip = BitFieldFactory.getInstance(0x08); private static final BitField optiSum = BitFieldFactory.getInstance(0x10); private static final BitField baxcel = BitFieldFactory.getInstance(0x20); // 'assignment-style formula in a macro sheet' private static final BitField space = BitFieldFactory.getInstance(0x40); public static final AttrPtg SUM = new AttrPtg(0x0010, 0, null, -1); public static final class SpaceType { private SpaceType() { // no instances of this class } /** 00H = Spaces before the next token (not allowed before tParen token) */ public static final int SPACE_BEFORE = 0x00; /** 01H = Carriage returns before the next token (not allowed before tParen token) */ public static final int CR_BEFORE = 0x01; /** 02H = Spaces before opening parenthesis (only allowed before tParen token) */ public static final int SPACE_BEFORE_OPEN_PAREN = 0x02; /** 03H = Carriage returns before opening parenthesis (only allowed before tParen token) */ public static final int CR_BEFORE_OPEN_PAREN = 0x03; /** * 04H = Spaces before closing parenthesis (only allowed before tParen, tFunc, and tFuncVar * tokens) */ public static final int SPACE_BEFORE_CLOSE_PAREN = 0x04; /** * 05H = Carriage returns before closing parenthesis (only allowed before tParen, tFunc, and * tFuncVar tokens) */ public static final int CR_BEFORE_CLOSE_PAREN = 0x05; /** 06H = Spaces following the equality sign (only in macro sheets) */ public static final int SPACE_AFTER_EQUALITY = 0x06; } public AttrPtg(LittleEndianInput in) { _options = in.readByte(); _data = in.readShort(); if (isOptimizedChoose()) { int nCases = _data; int[] jumpTable = new int[nCases]; for (int i = 0; i < jumpTable.length; i++) { jumpTable[i] = in.readUShort(); } _jumpTable = jumpTable; _chooseFuncOffset = in.readUShort(); } else { _jumpTable = null; _chooseFuncOffset = -1; } } private AttrPtg(int options, int data, int[] jt, int chooseFuncOffset) { _options = (byte) options; _data = (short) data; _jumpTable = jt; _chooseFuncOffset = chooseFuncOffset; } /** * @param type a constant from <tt>SpaceType</tt> * @param count the number of space characters */ public static AttrPtg createSpace(int type, int count) { int data = type & 0x00FF | (count << 8) & 0x00FFFF; return new AttrPtg(space.set(0), data, null, -1); } /** * @param dist distance (in bytes) to start of either * <ul> * <li>false parameter * <li>tFuncVar(IF) token (when false parameter is not present) * </ul> */ public static AttrPtg createIf(int dist) { return new AttrPtg(optiIf.set(0), dist, null, -1); } /** @param dist distance (in bytes) to position behind tFuncVar(IF) token (minus 1) */ public static AttrPtg createSkip(int dist) { return new AttrPtg(optiSkip.set(0), dist, null, -1); } public static AttrPtg getSumSingle() { return new AttrPtg(optiSum.set(0), 0, null, -1); } public boolean isSemiVolatile() { return semiVolatile.isSet(_options); } public boolean isOptimizedIf() { return optiIf.isSet(_options); } public boolean isOptimizedChoose() { return optiChoose.isSet(_options); } public boolean isSum() { return optiSum.isSet(_options); } public boolean isSkip() { return optiSkip.isSet(_options); } // lets hope no one uses this anymore private boolean isBaxcel() { return baxcel.isSet(_options); } public boolean isSpace() { return space.isSet(_options); } public short getData() { return _data; } public int[] getJumpTable() { return _jumpTable.clone(); } public int getChooseFuncOffset() { if (_jumpTable == null) { throw new IllegalStateException("Not tAttrChoose"); } return _chooseFuncOffset; } public String toString() { StringBuffer sb = new StringBuffer(64); sb.append(getClass().getName()).append(" ["); if (isSemiVolatile()) { sb.append("volatile "); } if (isSpace()) { sb.append("space count=").append((_data >> 8) & 0x00FF); sb.append(" type=").append(_data & 0x00FF).append(" "); } // the rest seem to be mutually exclusive if (isOptimizedIf()) { sb.append("if dist=").append(_data); } else if (isOptimizedChoose()) { sb.append("choose nCases=").append(_data); } else if (isSkip()) { sb.append("skip dist=").append(_data); } else if (isSum()) { sb.append("sum "); } else if (isBaxcel()) { sb.append("assign "); } sb.append("]"); return sb.toString(); } public void write(LittleEndianOutput out) { out.writeByte(sid + getPtgClass()); out.writeByte(_options); out.writeShort(_data); int[] jt = _jumpTable; if (jt != null) { for (int i = 0; i < jt.length; i++) { out.writeShort(jt[i]); } out.writeShort(_chooseFuncOffset); } } public int getSize() { if (_jumpTable != null) { return SIZE + (_jumpTable.length + 1) * LittleEndian.SHORT_SIZE; } return SIZE; } public String toFormulaString(String[] operands) { if (space.isSet(_options)) { return operands[0]; } else if (optiIf.isSet(_options)) { return toFormulaString() + "(" + operands[0] + ")"; } else if (optiSkip.isSet(_options)) { return toFormulaString() + operands[0]; // goto isn't a real formula element should not show up } else { return toFormulaString() + "(" + operands[0] + ")"; } } public int getNumberOfOperands() { return 1; } public int getType() { return -1; } public String toFormulaString() { if (semiVolatile.isSet(_options)) { return "ATTR(semiVolatile)"; } if (optiIf.isSet(_options)) { return "IF"; } if (optiChoose.isSet(_options)) { return "CHOOSE"; } if (optiSkip.isSet(_options)) { return ""; } if (optiSum.isSet(_options)) { return "SUM"; } if (baxcel.isSet(_options)) { return "ATTR(baxcel)"; } if (space.isSet(_options)) { return ""; } return "UNKNOWN ATTRIBUTE"; } }
/** * Table Cell Descriptor. NOTE: This source is automatically generated please do not modify this * file. Either subclass or remove the record in src/records/definitions. * * @author S. Ryan Ackley */ public abstract class TCAbstractType implements HDFType { protected short field_1_rgf; private static BitField fFirstMerged = BitFieldFactory.getInstance(0x0001); private static BitField fMerged = BitFieldFactory.getInstance(0x0002); private static BitField fVertical = BitFieldFactory.getInstance(0x0004); private static BitField fBackward = BitFieldFactory.getInstance(0x0008); private static BitField fRotateFont = BitFieldFactory.getInstance(0x0010); private static BitField fVertMerge = BitFieldFactory.getInstance(0x0020); private static BitField fVertRestart = BitFieldFactory.getInstance(0x0040); private static BitField vertAlign = BitFieldFactory.getInstance(0x0180); protected short field_2_unused; protected BorderCode field_3_brcTop; protected BorderCode field_4_brcLeft; protected BorderCode field_5_brcBottom; protected BorderCode field_6_brcRight; public TCAbstractType() {} protected void fillFields(byte[] data, int offset) { field_1_rgf = LittleEndian.getShort(data, 0x0 + offset); field_2_unused = LittleEndian.getShort(data, 0x2 + offset); field_3_brcTop = new BorderCode(data, 0x4 + offset); field_4_brcLeft = new BorderCode(data, 0x8 + offset); field_5_brcBottom = new BorderCode(data, 0xc + offset); field_6_brcRight = new BorderCode(data, 0x10 + offset); } public void serialize(byte[] data, int offset) { LittleEndian.putShort(data, 0x0 + offset, field_1_rgf); LittleEndian.putShort(data, 0x2 + offset, field_2_unused); field_3_brcTop.serialize(data, 0x4 + offset); field_4_brcLeft.serialize(data, 0x8 + offset); field_5_brcBottom.serialize(data, 0xc + offset); field_6_brcRight.serialize(data, 0x10 + offset); } public String toString() { StringBuffer buffer = new StringBuffer(); buffer.append("[TC]\n"); buffer.append(" .rgf = "); buffer.append(" (").append(getRgf()).append(" )\n"); buffer.append(" .fFirstMerged = ").append(isFFirstMerged()).append('\n'); buffer.append(" .fMerged = ").append(isFMerged()).append('\n'); buffer.append(" .fVertical = ").append(isFVertical()).append('\n'); buffer.append(" .fBackward = ").append(isFBackward()).append('\n'); buffer.append(" .fRotateFont = ").append(isFRotateFont()).append('\n'); buffer.append(" .fVertMerge = ").append(isFVertMerge()).append('\n'); buffer.append(" .fVertRestart = ").append(isFVertRestart()).append('\n'); buffer.append(" .vertAlign = ").append(getVertAlign()).append('\n'); buffer.append(" .unused = "); buffer.append(" (").append(getUnused()).append(" )\n"); buffer.append(" .brcTop = "); buffer.append(" (").append(getBrcTop()).append(" )\n"); buffer.append(" .brcLeft = "); buffer.append(" (").append(getBrcLeft()).append(" )\n"); buffer.append(" .brcBottom = "); buffer.append(" (").append(getBrcBottom()).append(" )\n"); buffer.append(" .brcRight = "); buffer.append(" (").append(getBrcRight()).append(" )\n"); buffer.append("[/TC]\n"); return buffer.toString(); } /** Size of record (exluding 4 byte header) */ public int getSize() { return 4 + +2 + 2 + 4 + 4 + 4 + 4; } /** Get the rgf field for the TC record. */ public short getRgf() { return field_1_rgf; } /** Set the rgf field for the TC record. */ public void setRgf(short field_1_rgf) { this.field_1_rgf = field_1_rgf; } /** Get the unused field for the TC record. */ public short getUnused() { return field_2_unused; } /** Set the unused field for the TC record. */ public void setUnused(short field_2_unused) { this.field_2_unused = field_2_unused; } /** Get the brcTop field for the TC record. */ public BorderCode getBrcTop() { return field_3_brcTop; } /** Set the brcTop field for the TC record. */ public void setBrcTop(BorderCode field_3_brcTop) { this.field_3_brcTop = field_3_brcTop; } /** Get the brcLeft field for the TC record. */ public BorderCode getBrcLeft() { return field_4_brcLeft; } /** Set the brcLeft field for the TC record. */ public void setBrcLeft(BorderCode field_4_brcLeft) { this.field_4_brcLeft = field_4_brcLeft; } /** Get the brcBottom field for the TC record. */ public BorderCode getBrcBottom() { return field_5_brcBottom; } /** Set the brcBottom field for the TC record. */ public void setBrcBottom(BorderCode field_5_brcBottom) { this.field_5_brcBottom = field_5_brcBottom; } /** Get the brcRight field for the TC record. */ public BorderCode getBrcRight() { return field_6_brcRight; } /** Set the brcRight field for the TC record. */ public void setBrcRight(BorderCode field_6_brcRight) { this.field_6_brcRight = field_6_brcRight; } /** Sets the fFirstMerged field value. */ public void setFFirstMerged(boolean value) { field_1_rgf = (short) fFirstMerged.setBoolean(field_1_rgf, value); } /** @return the fFirstMerged field value. */ public boolean isFFirstMerged() { return fFirstMerged.isSet(field_1_rgf); } /** Sets the fMerged field value. */ public void setFMerged(boolean value) { field_1_rgf = (short) fMerged.setBoolean(field_1_rgf, value); } /** @return the fMerged field value. */ public boolean isFMerged() { return fMerged.isSet(field_1_rgf); } /** Sets the fVertical field value. */ public void setFVertical(boolean value) { field_1_rgf = (short) fVertical.setBoolean(field_1_rgf, value); } /** @return the fVertical field value. */ public boolean isFVertical() { return fVertical.isSet(field_1_rgf); } /** Sets the fBackward field value. */ public void setFBackward(boolean value) { field_1_rgf = (short) fBackward.setBoolean(field_1_rgf, value); } /** @return the fBackward field value. */ public boolean isFBackward() { return fBackward.isSet(field_1_rgf); } /** Sets the fRotateFont field value. */ public void setFRotateFont(boolean value) { field_1_rgf = (short) fRotateFont.setBoolean(field_1_rgf, value); } /** @return the fRotateFont field value. */ public boolean isFRotateFont() { return fRotateFont.isSet(field_1_rgf); } /** Sets the fVertMerge field value. */ public void setFVertMerge(boolean value) { field_1_rgf = (short) fVertMerge.setBoolean(field_1_rgf, value); } /** @return the fVertMerge field value. */ public boolean isFVertMerge() { return fVertMerge.isSet(field_1_rgf); } /** Sets the fVertRestart field value. */ public void setFVertRestart(boolean value) { field_1_rgf = (short) fVertRestart.setBoolean(field_1_rgf, value); } /** @return the fVertRestart field value. */ public boolean isFVertRestart() { return fVertRestart.isSet(field_1_rgf); } /** Sets the vertAlign field value. */ public void setVertAlign(byte value) { field_1_rgf = (short) vertAlign.setValue(field_1_rgf, value); } /** @return the vertAlign field value. */ public byte getVertAlign() { return (byte) vertAlign.getValue(field_1_rgf); } }
/** * This data structure is used by a paragraph to determine how it should drop its first letter. I * think its the visual effect that will show a giant first letter to a paragraph. I've seen this * used in the first paragraph of a book * * @author Ryan Ackley */ public final class DropCapSpecifier implements Cloneable { private short _fdct; private static BitField _lines = BitFieldFactory.getInstance(0xf8); private static BitField _type = BitFieldFactory.getInstance(0x07); public DropCapSpecifier() { this._fdct = 0; } public DropCapSpecifier(byte[] buf, int offset) { this(LittleEndian.getShort(buf, offset)); } public DropCapSpecifier(short fdct) { this._fdct = fdct; } @Override public DropCapSpecifier clone() { return new DropCapSpecifier(_fdct); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; DropCapSpecifier other = (DropCapSpecifier) obj; if (_fdct != other._fdct) return false; return true; } public byte getCountOfLinesToDrop() { return (byte) _lines.getValue(_fdct); } public byte getDropCapType() { return (byte) _type.getValue(_fdct); } @Override public int hashCode() { return _fdct; } public boolean isEmpty() { return _fdct == 0; } public void setCountOfLinesToDrop(byte value) { _fdct = (short) _lines.setValue(_fdct, value); } public void setDropCapType(byte value) { _fdct = (short) _type.setValue(_fdct, value); } public short toShort() { return _fdct; } @Override public String toString() { if (isEmpty()) return "[DCS] EMPTY"; return "[DCS] (type: " + getDropCapType() + "; count: " + getCountOfLinesToDrop() + ")"; } }
/** * The TXO record (0x01B6) is used to define the properties of a text box. It is followed by two or * more continue records unless there is no actual text. The first continue records contain the text * data and the last continue record contains the formatting runs. * * <p> * * @author Glen Stampoultzis (glens at apache.org) */ public final class TextObjectRecord extends ContinuableRecord { public static final short sid = 0x01B6; private static final int FORMAT_RUN_ENCODED_SIZE = 8; // 2 shorts and 4 bytes reserved private static final BitField HorizontalTextAlignment = BitFieldFactory.getInstance(0x000E); private static final BitField VerticalTextAlignment = BitFieldFactory.getInstance(0x0070); private static final BitField textLocked = BitFieldFactory.getInstance(0x0200); public static final short HORIZONTAL_TEXT_ALIGNMENT_LEFT_ALIGNED = 1; public static final short HORIZONTAL_TEXT_ALIGNMENT_CENTERED = 2; public static final short HORIZONTAL_TEXT_ALIGNMENT_RIGHT_ALIGNED = 3; public static final short HORIZONTAL_TEXT_ALIGNMENT_JUSTIFIED = 4; public static final short VERTICAL_TEXT_ALIGNMENT_TOP = 1; public static final short VERTICAL_TEXT_ALIGNMENT_CENTER = 2; public static final short VERTICAL_TEXT_ALIGNMENT_BOTTOM = 3; public static final short VERTICAL_TEXT_ALIGNMENT_JUSTIFY = 4; public static final short TEXT_ORIENTATION_NONE = 0; public static final short TEXT_ORIENTATION_TOP_TO_BOTTOM = 1; public static final short TEXT_ORIENTATION_ROT_RIGHT = 2; public static final short TEXT_ORIENTATION_ROT_LEFT = 3; private int field_1_options; private int field_2_textOrientation; private int field_3_reserved4; private int field_4_reserved5; private int field_5_reserved6; private int field_8_reserved7; private HSSFRichTextString _text; /* * Note - the next three fields are very similar to those on * EmbededObjectRefSubRecord(ftPictFmla 0x0009) * * some observed values for the 4 bytes preceding the formula: C0 5E 86 03 * C0 11 AC 02 80 F1 8A 03 D4 F0 8A 03 */ private int _unknownPreFormulaInt; /** expect tRef, tRef3D, tArea, tArea3D or tName */ private OperandPtg _linkRefPtg; /** * Not clear if needed . Excel seems to be OK if this byte is not present. Value is often the same * as the earlier firstColumn byte. */ private Byte _unknownPostFormulaByte; public TextObjectRecord() { // } public TextObjectRecord(RecordInputStream in) { field_1_options = in.readUShort(); field_2_textOrientation = in.readUShort(); field_3_reserved4 = in.readUShort(); field_4_reserved5 = in.readUShort(); field_5_reserved6 = in.readUShort(); int field_6_textLength = in.readUShort(); int field_7_formattingDataLength = in.readUShort(); field_8_reserved7 = in.readInt(); if (in.remaining() > 0) { // Text Objects can have simple reference formulas // (This bit not mentioned in the MS document) if (in.remaining() < 11) { throw new RecordFormatException("Not enough remaining data for a link formula"); } int formulaSize = in.readUShort(); _unknownPreFormulaInt = in.readInt(); Ptg[] ptgs = Ptg.readTokens(formulaSize, in); if (ptgs.length != 1) { throw new RecordFormatException("Read " + ptgs.length + " tokens but expected exactly 1"); } _linkRefPtg = (OperandPtg) ptgs[0]; if (in.remaining() > 0) { _unknownPostFormulaByte = Byte.valueOf(in.readByte()); } else { _unknownPostFormulaByte = null; } } else { _linkRefPtg = null; } if (in.remaining() > 0) { throw new RecordFormatException("Unused " + in.remaining() + " bytes at end of record"); } String text; if (field_6_textLength > 0) { text = readRawString(in, field_6_textLength); } else { text = ""; } _text = new HSSFRichTextString(text); if (field_7_formattingDataLength > 0) { processFontRuns(in, _text, field_7_formattingDataLength); } } private static String readRawString(RecordInputStream in, int textLength) { byte compressByte = in.readByte(); boolean isCompressed = (compressByte & 0x01) == 0; if (isCompressed) { return in.readCompressedUnicode(textLength); } return in.readUnicodeLEString(textLength); } private static void processFontRuns( RecordInputStream in, HSSFRichTextString str, int formattingRunDataLength) { if (formattingRunDataLength % FORMAT_RUN_ENCODED_SIZE != 0) { throw new RecordFormatException( "Bad format run data length " + formattingRunDataLength + ")"); } int nRuns = formattingRunDataLength / FORMAT_RUN_ENCODED_SIZE; for (int i = 0; i < nRuns; i++) { short index = in.readShort(); short iFont = in.readShort(); in.readInt(); // skip reserved. str.applyFont(index, str.length(), iFont); } } public short getSid() { return sid; } private void serializeTXORecord(ContinuableRecordOutput out) { out.writeShort(field_1_options); out.writeShort(field_2_textOrientation); out.writeShort(field_3_reserved4); out.writeShort(field_4_reserved5); out.writeShort(field_5_reserved6); out.writeShort(_text.length()); out.writeShort(getFormattingDataLength()); out.writeInt(field_8_reserved7); if (_linkRefPtg != null) { int formulaSize = _linkRefPtg.getSize(); out.writeShort(formulaSize); out.writeInt(_unknownPreFormulaInt); _linkRefPtg.write(out); if (_unknownPostFormulaByte != null) { out.writeByte(_unknownPostFormulaByte.byteValue()); } } } private void serializeTrailingRecords(ContinuableRecordOutput out) { out.writeContinue(); out.writeStringData(_text.getString()); out.writeContinue(); writeFormatData(out, _text); } protected void serialize(ContinuableRecordOutput out) { serializeTXORecord(out); if (_text.getString().length() > 0) { serializeTrailingRecords(out); } } private int getFormattingDataLength() { if (_text.length() < 1) { // important - no formatting data if text is empty return 0; } return (_text.numFormattingRuns() + 1) * FORMAT_RUN_ENCODED_SIZE; } private static void writeFormatData(ContinuableRecordOutput out, HSSFRichTextString str) { int nRuns = str.numFormattingRuns(); for (int i = 0; i < nRuns; i++) { out.writeShort(str.getIndexOfFormattingRun(i)); int fontIndex = str.getFontOfFormattingRun(i); out.writeShort(fontIndex == str.NO_FONT ? 0 : fontIndex); out.writeInt(0); // skip reserved } out.writeShort(str.length()); out.writeShort(0); out.writeInt(0); // skip reserved } /** Sets the Horizontal text alignment field value. */ public void setHorizontalTextAlignment(int value) { field_1_options = HorizontalTextAlignment.setValue(field_1_options, value); } /** @return the Horizontal text alignment field value. */ public int getHorizontalTextAlignment() { return HorizontalTextAlignment.getValue(field_1_options); } /** Sets the Vertical text alignment field value. */ public void setVerticalTextAlignment(int value) { field_1_options = VerticalTextAlignment.setValue(field_1_options, value); } /** @return the Vertical text alignment field value. */ public int getVerticalTextAlignment() { return VerticalTextAlignment.getValue(field_1_options); } /** Sets the text locked field value. */ public void setTextLocked(boolean value) { field_1_options = textLocked.setBoolean(field_1_options, value); } /** @return the text locked field value. */ public boolean isTextLocked() { return textLocked.isSet(field_1_options); } /** * Get the text orientation field for the TextObjectBase record. * * @return One of TEXT_ORIENTATION_NONE TEXT_ORIENTATION_TOP_TO_BOTTOM TEXT_ORIENTATION_ROT_RIGHT * TEXT_ORIENTATION_ROT_LEFT */ public int getTextOrientation() { return field_2_textOrientation; } /** * Set the text orientation field for the TextObjectBase record. * * @param textOrientation One of TEXT_ORIENTATION_NONE TEXT_ORIENTATION_TOP_TO_BOTTOM * TEXT_ORIENTATION_ROT_RIGHT TEXT_ORIENTATION_ROT_LEFT */ public void setTextOrientation(int textOrientation) { this.field_2_textOrientation = textOrientation; } public HSSFRichTextString getStr() { return _text; } public void setStr(HSSFRichTextString str) { _text = str; } public Ptg getLinkRefPtg() { return _linkRefPtg; } public String toString() { StringBuffer sb = new StringBuffer(); sb.append("[TXO]\n"); sb.append(" .options = ").append(HexDump.shortToHex(field_1_options)).append("\n"); sb.append(" .isHorizontal = ").append(getHorizontalTextAlignment()).append('\n'); sb.append(" .isVertical = ").append(getVerticalTextAlignment()).append('\n'); sb.append(" .textLocked = ").append(isTextLocked()).append('\n'); sb.append(" .textOrientation= ") .append(HexDump.shortToHex(getTextOrientation())) .append("\n"); sb.append(" .reserved4 = ").append(HexDump.shortToHex(field_3_reserved4)).append("\n"); sb.append(" .reserved5 = ").append(HexDump.shortToHex(field_4_reserved5)).append("\n"); sb.append(" .reserved6 = ").append(HexDump.shortToHex(field_5_reserved6)).append("\n"); sb.append(" .textLength = ").append(HexDump.shortToHex(_text.length())).append("\n"); sb.append(" .reserved7 = ").append(HexDump.intToHex(field_8_reserved7)).append("\n"); sb.append(" .string = ").append(_text).append('\n'); for (int i = 0; i < _text.numFormattingRuns(); i++) { sb.append(" .textrun = ").append(_text.getFontOfFormattingRun(i)).append('\n'); } sb.append("[/TXO]\n"); return sb.toString(); } public Object clone() { TextObjectRecord rec = new TextObjectRecord(); rec._text = _text; rec.field_1_options = field_1_options; rec.field_2_textOrientation = field_2_textOrientation; rec.field_3_reserved4 = field_3_reserved4; rec.field_4_reserved5 = field_4_reserved5; rec.field_5_reserved6 = field_5_reserved6; rec.field_8_reserved7 = field_8_reserved7; rec._text = _text; // clone needed? if (_linkRefPtg != null) { rec._unknownPreFormulaInt = _unknownPreFormulaInt; rec._linkRefPtg = _linkRefPtg.copy(); rec._unknownPostFormulaByte = _unknownPostFormulaByte; } return rec; } }
/** * Formula Record (0x0006). REFERENCE: PG 317/444 Microsoft Excel 97 Developer's Kit (ISBN: * 1-57231-498-2) * * <p> * * @author Andrew C. Oliver (acoliver at apache dot org) * @author Jason Height (jheight at chariot dot net dot au) */ public final class FormulaRecord extends CellRecord { public static final short sid = 0x0006; // docs say 406...because of a bug Microsoft support site article #Q184647) private static int FIXED_SIZE = 14; // double + short + int private static final BitField alwaysCalc = BitFieldFactory.getInstance(0x0001); private static final BitField calcOnLoad = BitFieldFactory.getInstance(0x0002); private static final BitField sharedFormula = BitFieldFactory.getInstance(0x0008); /** * Manages the cached formula result values of other types besides numeric. Excel encodes the same * 8 bytes that would be field_4_value with various NaN values that are decoded/encoded by this * class. */ private static final class SpecialCachedValue { /** deliberately chosen by Excel in order to encode other values within Double NaNs */ private static final long BIT_MARKER = 0xFFFF000000000000L; private static final int VARIABLE_DATA_LENGTH = 6; private static final int DATA_INDEX = 2; public static final int STRING = 0; public static final int BOOLEAN = 1; public static final int ERROR_CODE = 2; public static final int EMPTY = 3; private final byte[] _variableData; private SpecialCachedValue(byte[] data) { _variableData = data; } public int getTypeCode() { return _variableData[0]; } /** * @return <code>null</code> if the double value encoded by <tt>valueLongBits</tt> is a normal * (non NaN) double value. */ public static SpecialCachedValue create(long valueLongBits) { if ((BIT_MARKER & valueLongBits) != BIT_MARKER) { return null; } byte[] result = new byte[VARIABLE_DATA_LENGTH]; long x = valueLongBits; for (int i = 0; i < VARIABLE_DATA_LENGTH; i++) { result[i] = (byte) x; x >>= 8; } switch (result[0]) { case STRING: case BOOLEAN: case ERROR_CODE: case EMPTY: break; default: throw new RecordFormatException("Bad special value code (" + result[0] + ")"); } return new SpecialCachedValue(result); } public void serialize(LittleEndianOutput out) { out.write(_variableData); out.writeShort(0xFFFF); } public String formatDebugString() { return formatValue() + ' ' + HexDump.toHex(_variableData); } private String formatValue() { int typeCode = getTypeCode(); switch (typeCode) { case STRING: return "<string>"; case BOOLEAN: return getDataValue() == 0 ? "FALSE" : "TRUE"; case ERROR_CODE: return ErrorEval.getText(getDataValue()); case EMPTY: return "<empty>"; } return "#error(type=" + typeCode + ")#"; } private int getDataValue() { return _variableData[DATA_INDEX]; } public static SpecialCachedValue createCachedEmptyValue() { return create(EMPTY, 0); } public static SpecialCachedValue createForString() { return create(STRING, 0); } public static SpecialCachedValue createCachedBoolean(boolean b) { return create(BOOLEAN, b ? 1 : 0); } public static SpecialCachedValue createCachedErrorCode(int errorCode) { return create(ERROR_CODE, errorCode); } private static SpecialCachedValue create(int code, int data) { byte[] vd = { (byte) code, 0, (byte) data, 0, 0, 0, }; return new SpecialCachedValue(vd); } public String toString() { StringBuffer sb = new StringBuffer(64); sb.append(getClass().getName()); sb.append('[').append(formatValue()).append(']'); return sb.toString(); } public int getValueType() { int typeCode = getTypeCode(); switch (typeCode) { case STRING: return HSSFCell.CELL_TYPE_STRING; case BOOLEAN: return HSSFCell.CELL_TYPE_BOOLEAN; case ERROR_CODE: return HSSFCell.CELL_TYPE_ERROR; case EMPTY: return HSSFCell.CELL_TYPE_STRING; // is this correct? } throw new IllegalStateException("Unexpected type id (" + typeCode + ")"); } public boolean getBooleanValue() { if (getTypeCode() != BOOLEAN) { throw new IllegalStateException("Not a boolean cached value - " + formatValue()); } return getDataValue() != 0; } public int getErrorValue() { if (getTypeCode() != ERROR_CODE) { throw new IllegalStateException("Not an error cached value - " + formatValue()); } return getDataValue(); } } private double field_4_value; private short field_5_options; /** * Unused field. As it turns out this field is often not zero.. According to Microsoft Excel * Developer's Kit Page 318: when writing the chn field (offset 20), it's supposed to be 0 but * ignored on read */ private int field_6_zero; private Formula field_8_parsed_expr; /** * Since the NaN support seems sketchy (different constants) we'll store and spit it out directly */ private SpecialCachedValue specialCachedValue; /** Creates new FormulaRecord */ public FormulaRecord() { field_8_parsed_expr = Formula.create(Ptg.EMPTY_PTG_ARRAY); } public FormulaRecord(RecordInputStream ris) { super(ris); LittleEndianInput in = ris; long valueLongBits = in.readLong(); field_5_options = in.readShort(); specialCachedValue = SpecialCachedValue.create(valueLongBits); if (specialCachedValue == null) { field_4_value = Double.longBitsToDouble(valueLongBits); } field_6_zero = in.readInt(); int field_7_expression_len = in.readShort(); // this length does not include any extra array data int nBytesAvailable = in.available(); field_8_parsed_expr = Formula.read(field_7_expression_len, in, nBytesAvailable); } /** * set the calculated value of the formula * * @param value calculated value */ public void setValue(double value) { field_4_value = value; specialCachedValue = null; } public void setCachedResultTypeEmptyString() { specialCachedValue = SpecialCachedValue.createCachedEmptyValue(); } public void setCachedResultTypeString() { specialCachedValue = SpecialCachedValue.createForString(); } public void setCachedResultErrorCode(int errorCode) { specialCachedValue = SpecialCachedValue.createCachedErrorCode(errorCode); } public void setCachedResultBoolean(boolean value) { specialCachedValue = SpecialCachedValue.createCachedBoolean(value); } /** * @return <code>true</code> if this {@link FormulaRecord} is followed by a {@link StringRecord} * representing the cached text result of the formula evaluation. */ public boolean hasCachedResultString() { if (specialCachedValue == null) { return false; } return specialCachedValue.getTypeCode() == SpecialCachedValue.STRING; } public int getCachedResultType() { if (specialCachedValue == null) { return HSSFCell.CELL_TYPE_NUMERIC; } return specialCachedValue.getValueType(); } public boolean getCachedBooleanValue() { return specialCachedValue.getBooleanValue(); } public int getCachedErrorValue() { return specialCachedValue.getErrorValue(); } /** * set the option flags * * @param options bitmask */ public void setOptions(short options) { field_5_options = options; } /** * get the calculated value of the formula * * @return calculated value */ public double getValue() { return field_4_value; } /** * get the option flags * * @return bitmask */ public short getOptions() { return field_5_options; } public boolean isSharedFormula() { return sharedFormula.isSet(field_5_options); } public void setSharedFormula(boolean flag) { field_5_options = sharedFormula.setShortBoolean(field_5_options, flag); } public boolean isAlwaysCalc() { return alwaysCalc.isSet(field_5_options); } public void setAlwaysCalc(boolean flag) { field_5_options = alwaysCalc.setShortBoolean(field_5_options, flag); } public boolean isCalcOnLoad() { return calcOnLoad.isSet(field_5_options); } public void setCalcOnLoad(boolean flag) { field_5_options = calcOnLoad.setShortBoolean(field_5_options, flag); } /** @return the formula tokens. never <code>null</code> */ public Ptg[] getParsedExpression() { return field_8_parsed_expr.getTokens(); } public Formula getFormula() { return field_8_parsed_expr; } public void setParsedExpression(Ptg[] ptgs) { field_8_parsed_expr = Formula.create(ptgs); } public short getSid() { return sid; } @Override protected int getValueDataSize() { return FIXED_SIZE + field_8_parsed_expr.getEncodedSize(); } @Override protected void serializeValue(LittleEndianOutput out) { if (specialCachedValue == null) { out.writeDouble(field_4_value); } else { specialCachedValue.serialize(out); } out.writeShort(getOptions()); out.writeInt( field_6_zero); // may as well write original data back so as to minimise differences from // original field_8_parsed_expr.serialize(out); } @Override protected String getRecordName() { return "FORMULA"; } @Override protected void appendValueText(StringBuilder sb) { sb.append(" .value = "); if (specialCachedValue == null) { sb.append(field_4_value).append("\n"); } else { sb.append(specialCachedValue.formatDebugString()).append("\n"); } sb.append(" .options = ").append(HexDump.shortToHex(getOptions())).append("\n"); sb.append(" .alwaysCalc= ").append(isAlwaysCalc()).append("\n"); sb.append(" .calcOnLoad= ").append(isCalcOnLoad()).append("\n"); sb.append(" .shared = ").append(isSharedFormula()).append("\n"); sb.append(" .zero = ").append(HexDump.intToHex(field_6_zero)).append("\n"); Ptg[] ptgs = field_8_parsed_expr.getTokens(); for (int k = 0; k < ptgs.length; k++) { if (k > 0) { sb.append("\n"); } sb.append(" Ptg[").append(k).append("]="); Ptg ptg = ptgs[k]; sb.append(ptg.toString()).append(ptg.getRVAType()); } } public Object clone() { FormulaRecord rec = new FormulaRecord(); copyBaseFields(rec); rec.field_4_value = field_4_value; rec.field_5_options = field_5_options; rec.field_6_zero = field_6_zero; rec.field_8_parsed_expr = field_8_parsed_expr; rec.specialCachedValue = specialCachedValue; return rec; } }
/** * Specifies a rectangular area of cells A1:A4 for instance. * * @author andy * @author Jason Height (jheight at chariot dot net dot au) */ public abstract class AreaPtgBase extends OperandPtg implements AreaI { /** * TODO - (May-2008) fix subclasses of AreaPtg 'AreaN~' which are used in shared formulas. see * similar comment in ReferencePtg */ protected final RuntimeException notImplemented() { return new RuntimeException( "Coding Error: This method should never be called. This ptg should be converted"); } /** zero based, unsigned 16 bit */ private int field_1_first_row; /** zero based, unsigned 16 bit */ private int field_2_last_row; /** zero based, unsigned 8 bit */ private int field_3_first_column; // BitFields: (first row relative, first col relative, first column // number) /** zero based, unsigned 8 bit */ private int field_4_last_column; // BitFields: (last row relative, last col relative, last column number) private static final BitField rowRelative = BitFieldFactory.getInstance(0x8000); private static final BitField colRelative = BitFieldFactory.getInstance(0x4000); private static final BitField columnMask = BitFieldFactory.getInstance(0x3FFF); protected AreaPtgBase() { // do nothing } protected AreaPtgBase(AreaReference ar) { CellReference firstCell = ar.getFirstCell(); CellReference lastCell = ar.getLastCell(); setFirstRow(firstCell.getRow()); setFirstColumn(firstCell.getCol() == -1 ? 0 : firstCell.getCol()); setLastRow(lastCell.getRow()); setLastColumn(lastCell.getCol() == -1 ? 0xFF : lastCell.getCol()); setFirstColRelative(!firstCell.isColAbsolute()); setLastColRelative(!lastCell.isColAbsolute()); setFirstRowRelative(!firstCell.isRowAbsolute()); setLastRowRelative(!lastCell.isRowAbsolute()); } protected AreaPtgBase( int firstRow, int lastRow, int firstColumn, int lastColumn, boolean firstRowRelative, boolean lastRowRelative, boolean firstColRelative, boolean lastColRelative) { if (lastRow >= firstRow) { setFirstRow(firstRow); setLastRow(lastRow); setFirstRowRelative(firstRowRelative); setLastRowRelative(lastRowRelative); } else { setFirstRow(lastRow); setLastRow(firstRow); setFirstRowRelative(lastRowRelative); setLastRowRelative(firstRowRelative); } if (lastColumn >= firstColumn) { setFirstColumn(firstColumn); setLastColumn(lastColumn); setFirstColRelative(firstColRelative); setLastColRelative(lastColRelative); } else { setFirstColumn(lastColumn); setLastColumn(firstColumn); setFirstColRelative(lastColRelative); setLastColRelative(firstColRelative); } } /** * Sort the first and last row and columns in-place to the preferred (top left:bottom right) order * Note: Sort only occurs when an instance is constructed or when this method is called. * * <p>For example, <code>$E5:B$10</code> becomes <code>B5:$E$10</code> */ public void sortTopLeftToBottomRight() { if (getFirstRow() > getLastRow()) { // swap first row and last row numbers and relativity // Note: cannot just swap the fields because row relativity is stored in fields 3 and 4 final int firstRow = getFirstRow(); final boolean firstRowRel = isFirstRowRelative(); setFirstRow(getLastRow()); setFirstRowRelative(isLastRowRelative()); setLastRow(firstRow); setLastRowRelative(firstRowRel); } if (getFirstColumn() > getLastColumn()) { // swap first column and last column numbers and relativity // Note: cannot just swap the fields because row relativity is stored in fields 3 and 4 final int firstCol = getFirstColumn(); final boolean firstColRel = isFirstColRelative(); setFirstColumn(getLastColumn()); setFirstColRelative(isLastColRelative()); setLastColumn(firstCol); setLastColRelative(firstColRel); } } protected final void readCoordinates(LittleEndianInput in) { field_1_first_row = in.readUShort(); field_2_last_row = in.readUShort(); field_3_first_column = in.readUShort(); field_4_last_column = in.readUShort(); } protected final void writeCoordinates(LittleEndianOutput out) { out.writeShort(field_1_first_row); out.writeShort(field_2_last_row); out.writeShort(field_3_first_column); out.writeShort(field_4_last_column); } /** @return the first row in the area */ public final int getFirstRow() { return field_1_first_row; } /** * sets the first row * * @param rowIx number (0-based) */ public final void setFirstRow(int rowIx) { field_1_first_row = rowIx; } /** @return last row in the range (x2 in x1,y1-x2,y2) */ public final int getLastRow() { return field_2_last_row; } /** @param rowIx last row number in the area */ public final void setLastRow(int rowIx) { field_2_last_row = rowIx; } /** @return the first column number in the area. */ public final int getFirstColumn() { return columnMask.getValue(field_3_first_column); } /** @return the first column number + the options bit settings unstripped */ public final short getFirstColumnRaw() { return (short) field_3_first_column; // TODO } /** @return whether or not the first row is a relative reference or not. */ public final boolean isFirstRowRelative() { return rowRelative.isSet(field_3_first_column); } /** * sets the first row to relative or not * * @param rel is relative or not. */ public final void setFirstRowRelative(boolean rel) { field_3_first_column = rowRelative.setBoolean(field_3_first_column, rel); } /** @return isrelative first column to relative or not */ public final boolean isFirstColRelative() { return colRelative.isSet(field_3_first_column); } /** set whether the first column is relative */ public final void setFirstColRelative(boolean rel) { field_3_first_column = colRelative.setBoolean(field_3_first_column, rel); } /** set the first column in the area */ public final void setFirstColumn(int colIx) { field_3_first_column = columnMask.setValue(field_3_first_column, colIx); } /** set the first column irrespective of the bitmasks */ public final void setFirstColumnRaw(int column) { field_3_first_column = column; } /** @return lastcolumn in the area */ public final int getLastColumn() { return columnMask.getValue(field_4_last_column); } /** @return last column and bitmask (the raw field) */ public final short getLastColumnRaw() { return (short) field_4_last_column; } /** @return last row relative or not */ public final boolean isLastRowRelative() { return rowRelative.isSet(field_4_last_column); } /** * set whether the last row is relative or not * * @param rel <code>true</code> if the last row relative, else <code>false</code> */ public final void setLastRowRelative(boolean rel) { field_4_last_column = rowRelative.setBoolean(field_4_last_column, rel); } /** @return lastcol relative or not */ public final boolean isLastColRelative() { return colRelative.isSet(field_4_last_column); } /** set whether the last column should be relative or not */ public final void setLastColRelative(boolean rel) { field_4_last_column = colRelative.setBoolean(field_4_last_column, rel); } /** set the last column in the area */ public final void setLastColumn(int colIx) { field_4_last_column = columnMask.setValue(field_4_last_column, colIx); } /** set the last column irrespective of the bitmasks */ public final void setLastColumnRaw(short column) { field_4_last_column = column; } protected final String formatReferenceAsString() { CellReference topLeft = new CellReference( getFirstRow(), getFirstColumn(), !isFirstRowRelative(), !isFirstColRelative()); CellReference botRight = new CellReference( getLastRow(), getLastColumn(), !isLastRowRelative(), !isLastColRelative()); if (AreaReference.isWholeColumnReference(SpreadsheetVersion.EXCEL97, topLeft, botRight)) { return (new AreaReference(topLeft, botRight)).formatAsString(); } return topLeft.formatAsString() + ":" + botRight.formatAsString(); } public String toFormulaString() { return formatReferenceAsString(); } public byte getDefaultOperandClass() { return Ptg.CLASS_REF; } }
/** * Title: Unicode String * * <p>Description: Unicode String - just standard fields that are in several records. It is * considered more desirable then repeating it in all of them. * * <p>This is often called a XLUnicodeRichExtendedString in MS documentation. * * <p>REFERENCE: PG 264 Microsoft Excel 97 Developer's Kit (ISBN: 1-57231-498-2) * * <p>REFERENCE: PG 951 Excel Binary File Format (.xls) Structure Specification v20091214 */ public class UnicodeString implements Comparable< UnicodeString> { // TODO - make this final when the compatibility version is removed private short field_1_charCount; private byte field_2_optionflags; private String field_3_string; private List<FormatRun> field_4_format_runs; private ExtRst field_5_ext_rst; private static final BitField highByte = BitFieldFactory.getInstance(0x1); // 0x2 is reserved private static final BitField extBit = BitFieldFactory.getInstance(0x4); private static final BitField richText = BitFieldFactory.getInstance(0x8); public static class FormatRun implements Comparable<FormatRun> { final short _character; short _fontIndex; public FormatRun(short character, short fontIndex) { this._character = character; this._fontIndex = fontIndex; } public FormatRun(LittleEndianInput in) { this(in.readShort(), in.readShort()); } public short getCharacterPos() { return _character; } public short getFontIndex() { return _fontIndex; } public boolean equals(Object o) { if (!(o instanceof FormatRun)) { return false; } FormatRun other = (FormatRun) o; return _character == other._character && _fontIndex == other._fontIndex; } public int compareTo(FormatRun r) { if (_character == r._character && _fontIndex == r._fontIndex) { return 0; } if (_character == r._character) { return _fontIndex - r._fontIndex; } return _character - r._character; } public String toString() { return "character=" + _character + ",fontIndex=" + _fontIndex; } public void serialize(LittleEndianOutput out) { out.writeShort(_character); out.writeShort(_fontIndex); } } // See page 681 public static class ExtRst implements Comparable<ExtRst> { private short reserved; // This is a Phs (see page 881) private short formattingFontIndex; private short formattingOptions; // This is a RPHSSub (see page 894) private int numberOfRuns; private String phoneticText; // This is an array of PhRuns (see page 881) private PhRun[] phRuns; // Sometimes there's some cruft at the end private byte[] extraData; private void populateEmpty() { reserved = 1; phoneticText = ""; phRuns = new PhRun[0]; extraData = new byte[0]; } protected ExtRst() { populateEmpty(); } protected ExtRst(LittleEndianInput in, int expectedLength) { reserved = in.readShort(); // Old style detection (Reserved = 0xFF) if (reserved == -1) { populateEmpty(); return; } // Spot corrupt records if (reserved != 1) { System.err.println( "Warning - ExtRst was has wrong magic marker, expecting 1 but found " + reserved + " - ignoring"); // Grab all the remaining data, and ignore it for (int i = 0; i < expectedLength - 2; i++) { in.readByte(); } // And make us be empty populateEmpty(); return; } // Carry on reading in as normal short stringDataSize = in.readShort(); formattingFontIndex = in.readShort(); formattingOptions = in.readShort(); // RPHSSub numberOfRuns = in.readUShort(); short length1 = in.readShort(); // No really. Someone clearly forgot to read // the docs on their datastructure... short length2 = in.readShort(); // And sometimes they write out garbage :( if (length1 == 0 && length2 > 0) { length2 = 0; } if (length1 != length2) { throw new IllegalStateException( "The two length fields of the Phonetic Text don't agree! " + length1 + " vs " + length2); } phoneticText = StringUtil.readUnicodeLE(in, length1); int runData = stringDataSize - 4 - 6 - (2 * phoneticText.length()); int numRuns = (runData / 6); phRuns = new PhRun[numRuns]; for (int i = 0; i < phRuns.length; i++) { phRuns[i] = new PhRun(in); } int extraDataLength = runData - (numRuns * 6); if (extraDataLength < 0) { System.err.println("Warning - ExtRst overran by " + (0 - extraDataLength) + " bytes"); extraDataLength = 0; } extraData = new byte[extraDataLength]; for (int i = 0; i < extraData.length; i++) { extraData[i] = in.readByte(); } } /** Returns our size, excluding our 4 byte header */ protected int getDataSize() { return 4 + 6 + (2 * phoneticText.length()) + (6 * phRuns.length) + extraData.length; } protected void serialize(ContinuableRecordOutput out) { int dataSize = getDataSize(); out.writeContinueIfRequired(8); out.writeShort(reserved); out.writeShort(dataSize); out.writeShort(formattingFontIndex); out.writeShort(formattingOptions); out.writeContinueIfRequired(6); out.writeShort(numberOfRuns); out.writeShort(phoneticText.length()); out.writeShort(phoneticText.length()); out.writeContinueIfRequired(phoneticText.length() * 2); StringUtil.putUnicodeLE(phoneticText, out); for (int i = 0; i < phRuns.length; i++) { phRuns[i].serialize(out); } out.write(extraData); } public boolean equals(Object obj) { if (!(obj instanceof ExtRst)) { return false; } ExtRst other = (ExtRst) obj; return (compareTo(other) == 0); } public int compareTo(ExtRst o) { int result; result = reserved - o.reserved; if (result != 0) return result; result = formattingFontIndex - o.formattingFontIndex; if (result != 0) return result; result = formattingOptions - o.formattingOptions; if (result != 0) return result; result = numberOfRuns - o.numberOfRuns; if (result != 0) return result; result = phoneticText.compareTo(o.phoneticText); if (result != 0) return result; result = phRuns.length - o.phRuns.length; if (result != 0) return result; for (int i = 0; i < phRuns.length; i++) { result = phRuns[i].phoneticTextFirstCharacterOffset - o.phRuns[i].phoneticTextFirstCharacterOffset; if (result != 0) return result; result = phRuns[i].realTextFirstCharacterOffset - o.phRuns[i].realTextFirstCharacterOffset; if (result != 0) return result; result = phRuns[i].realTextFirstCharacterOffset - o.phRuns[i].realTextLength; if (result != 0) return result; } result = extraData.length - o.extraData.length; if (result != 0) return result; // If we get here, it's the same return 0; } protected ExtRst clone() { ExtRst ext = new ExtRst(); ext.reserved = reserved; ext.formattingFontIndex = formattingFontIndex; ext.formattingOptions = formattingOptions; ext.numberOfRuns = numberOfRuns; ext.phoneticText = new String(phoneticText); ext.phRuns = new PhRun[phRuns.length]; for (int i = 0; i < ext.phRuns.length; i++) { ext.phRuns[i] = new PhRun( phRuns[i].phoneticTextFirstCharacterOffset, phRuns[i].realTextFirstCharacterOffset, phRuns[i].realTextLength); } return ext; } } public static class PhRun { private int phoneticTextFirstCharacterOffset; private int realTextFirstCharacterOffset; private int realTextLength; public PhRun( int phoneticTextFirstCharacterOffset, int realTextFirstCharacterOffset, int realTextLength) { this.phoneticTextFirstCharacterOffset = phoneticTextFirstCharacterOffset; this.realTextFirstCharacterOffset = realTextFirstCharacterOffset; this.realTextLength = realTextLength; } private PhRun(LittleEndianInput in) { phoneticTextFirstCharacterOffset = in.readUShort(); realTextFirstCharacterOffset = in.readUShort(); realTextLength = in.readUShort(); } private void serialize(ContinuableRecordOutput out) { out.writeContinueIfRequired(6); out.writeShort(phoneticTextFirstCharacterOffset); out.writeShort(realTextFirstCharacterOffset); out.writeShort(realTextLength); } } private UnicodeString() { // Used for clone method. } public UnicodeString(String str) { setString(str); } public int hashCode() { int stringHash = 0; if (field_3_string != null) stringHash = field_3_string.hashCode(); return field_1_charCount + stringHash; } /** * Our handling of equals is inconsistent with compareTo. The trouble is because we don't truely * understand rich text fields yet it's difficult to make a sound comparison. * * @param o The object to compare. * @return true if the object is actually equal. */ public boolean equals(Object o) { if (!(o instanceof UnicodeString)) { return false; } UnicodeString other = (UnicodeString) o; // OK lets do this in stages to return a quickly, first check the actual string boolean eq = ((field_1_charCount == other.field_1_charCount) && (field_2_optionflags == other.field_2_optionflags) && field_3_string.equals(other.field_3_string)); if (!eq) return false; // OK string appears to be equal but now lets compare formatting runs if ((field_4_format_runs == null) && (other.field_4_format_runs == null)) // Strings are equal, and there are not formatting runs. return true; if (((field_4_format_runs == null) && (other.field_4_format_runs != null)) || (field_4_format_runs != null) && (other.field_4_format_runs == null)) // Strings are equal, but one or the other has formatting runs return false; // Strings are equal, so now compare formatting runs. int size = field_4_format_runs.size(); if (size != other.field_4_format_runs.size()) return false; for (int i = 0; i < size; i++) { FormatRun run1 = field_4_format_runs.get(i); FormatRun run2 = other.field_4_format_runs.get(i); if (!run1.equals(run2)) return false; } // Well the format runs are equal as well!, better check the ExtRst data if (field_5_ext_rst == null && other.field_5_ext_rst == null) { // Good } else if (field_5_ext_rst != null && other.field_5_ext_rst != null) { int extCmp = field_5_ext_rst.compareTo(other.field_5_ext_rst); if (extCmp == 0) { // Good } else { return false; } } else { return false; } // Phew!! After all of that we have finally worked out that the strings // are identical. return true; } /** * construct a unicode string record and fill its fields, ID is ignored * * @param in the RecordInputstream to read the record from */ public UnicodeString(RecordInputStream in) { field_1_charCount = in.readShort(); field_2_optionflags = in.readByte(); int runCount = 0; int extensionLength = 0; // Read the number of rich runs if rich text. if (isRichText()) { runCount = in.readShort(); } // Read the size of extended data if present. if (isExtendedText()) { extensionLength = in.readInt(); } boolean isCompressed = ((field_2_optionflags & 1) == 0); if (isCompressed) { field_3_string = in.readCompressedUnicode(getCharCount()); } else { field_3_string = in.readUnicodeLEString(getCharCount()); } if (isRichText() && (runCount > 0)) { field_4_format_runs = new ArrayList<FormatRun>(runCount); for (int i = 0; i < runCount; i++) { field_4_format_runs.add(new FormatRun(in)); } } if (isExtendedText() && (extensionLength > 0)) { field_5_ext_rst = new ExtRst(in, extensionLength); if (field_5_ext_rst.getDataSize() + 4 != extensionLength) { System.err.println( "ExtRst was supposed to be " + extensionLength + " bytes long, but seems to actually be " + (field_5_ext_rst.getDataSize() + 4)); } } } /** * get the number of characters in the string, as an un-wrapped int * * @return number of characters */ public int getCharCount() { if (field_1_charCount < 0) { return field_1_charCount + 65536; } return field_1_charCount; } /** * set the number of characters in the string * * @param cc - number of characters */ public void setCharCount(short cc) { field_1_charCount = cc; } /** * get the option flags which among other things return if this is a 16-bit or 8 bit string * * @return optionflags bitmask */ public byte getOptionFlags() { return field_2_optionflags; } /** @return the actual string this contains as a java String object */ public String getString() { return field_3_string; } /** * set the actual string this contains * * @param string the text */ public void setString(String string) { field_3_string = string; setCharCount((short) field_3_string.length()); // scan for characters greater than 255 ... if any are // present, we have to use 16-bit encoding. Otherwise, we // can use 8-bit encoding boolean useUTF16 = false; int strlen = string.length(); for (int j = 0; j < strlen; j++) { if (string.charAt(j) > 255) { useUTF16 = true; break; } } if (useUTF16) // Set the uncompressed bit field_2_optionflags = highByte.setByte(field_2_optionflags); else field_2_optionflags = highByte.clearByte(field_2_optionflags); } public int getFormatRunCount() { if (field_4_format_runs == null) return 0; return field_4_format_runs.size(); } public FormatRun getFormatRun(int index) { if (field_4_format_runs == null) { return null; } if (index < 0 || index >= field_4_format_runs.size()) { return null; } return field_4_format_runs.get(index); } private int findFormatRunAt(int characterPos) { int size = field_4_format_runs.size(); for (int i = 0; i < size; i++) { FormatRun r = field_4_format_runs.get(i); if (r._character == characterPos) return i; else if (r._character > characterPos) return -1; } return -1; } /** * Adds a font run to the formatted string. * * <p>If a font run exists at the current charcter location, then it is replaced with the font run * to be added. */ public void addFormatRun(FormatRun r) { if (field_4_format_runs == null) { field_4_format_runs = new ArrayList<FormatRun>(); } int index = findFormatRunAt(r._character); if (index != -1) field_4_format_runs.remove(index); field_4_format_runs.add(r); // Need to sort the font runs to ensure that the font runs appear in // character order Collections.sort(field_4_format_runs); // Make sure that we now say that we are a rich string field_2_optionflags = richText.setByte(field_2_optionflags); } public Iterator<FormatRun> formatIterator() { if (field_4_format_runs != null) { return field_4_format_runs.iterator(); } return null; } /** * unlike the real records we return the same as "getString()" rather than debug info * * @see #getDebugInfo() * @return String value of the record */ public String toString() { return getString(); } /** * return a character representation of the fields of this record * * @return String of output for biffviewer etc. */ public String getDebugInfo() { StringBuffer buffer = new StringBuffer(); buffer.append("[UNICODESTRING]\n"); buffer .append(" .charcount = ") .append(Integer.toHexString(getCharCount())) .append("\n"); buffer .append(" .optionflags = ") .append(Integer.toHexString(getOptionFlags())) .append("\n"); buffer.append(" .string = ").append(getString()).append("\n"); if (field_4_format_runs != null) { for (int i = 0; i < field_4_format_runs.size(); i++) { FormatRun r = field_4_format_runs.get(i); buffer.append(" .format_run" + i + " = ").append(r.toString()).append("\n"); } } if (field_5_ext_rst != null) { buffer.append(" .field_5_ext_rst = ").append("\n"); buffer.append(field_5_ext_rst.toString()).append("\n"); } buffer.append("[/UNICODESTRING]\n"); return buffer.toString(); } /** * Serialises out the String. There are special rules about where we can and can't split onto * Continue records. */ public void serialize(ContinuableRecordOutput out) { int numberOfRichTextRuns = 0; int extendedDataSize = 0; if (isRichText() && field_4_format_runs != null) { numberOfRichTextRuns = field_4_format_runs.size(); } if (isExtendedText() && field_5_ext_rst != null) { extendedDataSize = 4 + field_5_ext_rst.getDataSize(); } // Serialise the bulk of the String // The writeString handles tricky continue stuff for us out.writeString(field_3_string, numberOfRichTextRuns, extendedDataSize); if (numberOfRichTextRuns > 0) { // This will ensure that a run does not split a continue for (int i = 0; i < numberOfRichTextRuns; i++) { if (out.getAvailableSpace() < 4) { out.writeContinue(); } FormatRun r = field_4_format_runs.get(i); r.serialize(out); } } if (extendedDataSize > 0) { field_5_ext_rst.serialize(out); } } public int compareTo(UnicodeString str) { int result = getString().compareTo(str.getString()); // As per the equals method lets do this in stages if (result != 0) return result; // OK string appears to be equal but now lets compare formatting runs if ((field_4_format_runs == null) && (str.field_4_format_runs == null)) // Strings are equal, and there are no formatting runs. return 0; if ((field_4_format_runs == null) && (str.field_4_format_runs != null)) // Strings are equal, but one or the other has formatting runs return 1; if ((field_4_format_runs != null) && (str.field_4_format_runs == null)) // Strings are equal, but one or the other has formatting runs return -1; // Strings are equal, so now compare formatting runs. int size = field_4_format_runs.size(); if (size != str.field_4_format_runs.size()) return size - str.field_4_format_runs.size(); for (int i = 0; i < size; i++) { FormatRun run1 = field_4_format_runs.get(i); FormatRun run2 = str.field_4_format_runs.get(i); result = run1.compareTo(run2); if (result != 0) return result; } // Well the format runs are equal as well!, better check the ExtRst data if ((field_5_ext_rst == null) && (str.field_5_ext_rst == null)) return 0; if ((field_5_ext_rst == null) && (str.field_5_ext_rst != null)) return 1; if ((field_5_ext_rst != null) && (str.field_5_ext_rst == null)) return -1; result = field_5_ext_rst.compareTo(str.field_5_ext_rst); if (result != 0) return result; // Phew!! After all of that we have finally worked out that the strings // are identical. return 0; } private boolean isRichText() { return richText.isSet(getOptionFlags()); } private boolean isExtendedText() { return extBit.isSet(getOptionFlags()); } public Object clone() { UnicodeString str = new UnicodeString(); str.field_1_charCount = field_1_charCount; str.field_2_optionflags = field_2_optionflags; str.field_3_string = field_3_string; if (field_4_format_runs != null) { str.field_4_format_runs = new ArrayList<FormatRun>(); for (FormatRun r : field_4_format_runs) { str.field_4_format_runs.add(new FormatRun(r._character, r._fontIndex)); } } if (field_5_ext_rst != null) { str.field_5_ext_rst = field_5_ext_rst.clone(); } return str; } }