386 lines
14 KiB
Java
386 lines
14 KiB
Java
/* ====================================================================
|
|
Licensed to the Apache Software Foundation (ASF) under one or more
|
|
contributor license agreements. See the NOTICE file distributed with
|
|
this work for additional information regarding copyright ownership.
|
|
The ASF licenses this file to You under the Apache License, Version 2.0
|
|
(the "License"); you may not use this file except in compliance with
|
|
the License. You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
==================================================================== */
|
|
|
|
package org.apache.poi.hslf.model.textproperties;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.IOException;
|
|
import java.io.OutputStream;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
import org.apache.poi.hslf.exceptions.HSLFException;
|
|
import org.apache.poi.hslf.record.StyleTextPropAtom;
|
|
import org.apache.poi.util.HexDump;
|
|
import org.apache.poi.util.LittleEndian;
|
|
|
|
/**
|
|
* For a given run of characters, holds the properties (which could
|
|
* be paragraph properties or character properties).
|
|
* Used to hold the number of characters affected, the list of active
|
|
* properties, and the indent level if required.
|
|
*/
|
|
public class TextPropCollection {
|
|
/** All the different kinds of paragraph properties we might handle */
|
|
private static final TextProp[] paragraphTextPropTypes = {
|
|
// TextProp order is according to 2.9.20 TextPFException,
|
|
// bitmask order can be different
|
|
new ParagraphFlagsTextProp(),
|
|
new TextProp(2, 0x80, "bullet.char"),
|
|
new TextProp(2, 0x10, "bullet.font"),
|
|
new TextProp(2, 0x40, "bullet.size"),
|
|
new TextProp(4, 0x20, "bullet.color"),
|
|
new TextAlignmentProp(),
|
|
new TextProp(2, 0x1000, "linespacing"),
|
|
new TextProp(2, 0x2000, "spacebefore"),
|
|
new TextProp(2, 0x4000, "spaceafter"),
|
|
new TextProp(2, 0x100, "text.offset"), // left margin
|
|
// 0x200 - Undefined and MUST be ignored
|
|
new TextProp(2, 0x400, "bullet.offset"), // indent
|
|
new TextProp(2, 0x8000, "defaultTabSize"),
|
|
new TabStopPropCollection(), // tabstops size is variable!
|
|
new FontAlignmentProp(),
|
|
new WrapFlagsTextProp(),
|
|
new TextProp(2, 0x200000, "textDirection"),
|
|
// 0x400000 MUST be zero and MUST be ignored
|
|
new TextProp(0, 0x800000, "bullet.blip"), // TODO: check size
|
|
new TextProp(0, 0x1000000, "bullet.scheme"), // TODO: check size
|
|
new TextProp(0, 0x2000000, "hasBulletScheme"), // TODO: check size
|
|
// 0xFC000000 MUST be zero and MUST be ignored
|
|
};
|
|
|
|
/** All the different kinds of character properties we might handle */
|
|
private static final TextProp[] characterTextPropTypes = new TextProp[] {
|
|
new TextProp(0, 0x100000, "pp10ext"),
|
|
new TextProp(0, 0x1000000, "newAsian.font.index"), // A bit that specifies whether the newEAFontRef field of the TextCFException10 structure that contains this CFMasks exists.
|
|
new TextProp(0, 0x2000000, "cs.font.index"), // A bit that specifies whether the csFontRef field of the TextCFException10 structure that contains this CFMasks exists.
|
|
new TextProp(0, 0x4000000, "pp11ext"), // A bit that specifies whether the pp11ext field of the TextCFException10 structure that contains this CFMasks exists.
|
|
new CharFlagsTextProp(),
|
|
new TextProp(2, 0x10000, "font.index"),
|
|
new TextProp(2, 0x200000, "asian.font.index"),
|
|
new TextProp(2, 0x400000, "ansi.font.index"),
|
|
new TextProp(2, 0x800000, "symbol.font.index"),
|
|
new TextProp(2, 0x20000, "font.size"),
|
|
new TextProp(4, 0x40000, "font.color"),
|
|
new TextProp(2, 0x80000, "superscript")
|
|
};
|
|
|
|
public enum TextPropType {
|
|
paragraph, character;
|
|
}
|
|
|
|
private int charactersCovered;
|
|
|
|
// indentLevel is only valid for paragraph collection
|
|
// if it's set to -1, it must be omitted - see 2.9.36 TextMasterStyleLevel
|
|
private short indentLevel = 0;
|
|
private final Map<String,TextProp> textProps = new HashMap<String,TextProp>();
|
|
private int maskSpecial = 0;
|
|
private final TextPropType textPropType;
|
|
|
|
/**
|
|
* Create a new collection of text properties (be they paragraph
|
|
* or character) which will be groked via a subsequent call to
|
|
* buildTextPropList().
|
|
*/
|
|
public TextPropCollection(int charactersCovered, TextPropType textPropType) {
|
|
this.charactersCovered = charactersCovered;
|
|
this.textPropType = textPropType;
|
|
}
|
|
|
|
public int getSpecialMask() {
|
|
return maskSpecial;
|
|
}
|
|
|
|
/** Fetch the number of characters this styling applies to */
|
|
public int getCharactersCovered() {
|
|
return charactersCovered;
|
|
}
|
|
|
|
/** Fetch the TextProps that define this styling in the record order */
|
|
public List<TextProp> getTextPropList() {
|
|
List<TextProp> orderedList = new ArrayList<TextProp>();
|
|
for (TextProp potProp : getPotentialProperties()) {
|
|
TextProp textProp = textProps.get(potProp.getName());
|
|
if (textProp != null) {
|
|
orderedList.add(textProp);
|
|
}
|
|
}
|
|
return orderedList;
|
|
}
|
|
|
|
/** Fetch the TextProp with this name, or null if it isn't present */
|
|
public final TextProp findByName(String textPropName) {
|
|
return textProps.get(textPropName);
|
|
}
|
|
|
|
public final TextProp removeByName(String name) {
|
|
return textProps.remove(name);
|
|
}
|
|
|
|
public final TextPropType getTextPropType() {
|
|
return textPropType;
|
|
}
|
|
|
|
private TextProp[] getPotentialProperties() {
|
|
return (textPropType == TextPropType.paragraph) ? paragraphTextPropTypes : characterTextPropTypes;
|
|
}
|
|
|
|
/**
|
|
* Checks the paragraph or character properties for the given property name.
|
|
* Throws a HSLFException, if the name doesn't belong into this set of properties
|
|
*
|
|
* @param name the property name
|
|
* @return if found, the property template to copy from
|
|
*/
|
|
private TextProp validatePropName(String name) {
|
|
for (TextProp tp : getPotentialProperties()) {
|
|
if (tp.getName().equals(name)) {
|
|
return tp;
|
|
}
|
|
}
|
|
String errStr =
|
|
"No TextProp with name " + name + " is defined to add from. " +
|
|
"Character and paragraphs have their own properties/names.";
|
|
throw new HSLFException(errStr);
|
|
}
|
|
|
|
/** Add the TextProp with this name to the list */
|
|
public final TextProp addWithName(String name) {
|
|
// Find the base TextProp to base on
|
|
TextProp existing = findByName(name);
|
|
if (existing != null) return existing;
|
|
|
|
// Add a copy of this property
|
|
TextProp textProp = validatePropName(name).clone();
|
|
textProps.put(name,textProp);
|
|
return textProp;
|
|
}
|
|
|
|
/**
|
|
* Add the property at the correct position. Replaces an existing property with the same name.
|
|
*
|
|
* @param textProp the property to be added
|
|
*/
|
|
public final void addProp(TextProp textProp) {
|
|
if (textProp == null) {
|
|
throw new HSLFException("TextProp must not be null");
|
|
}
|
|
|
|
String propName = textProp.getName();
|
|
validatePropName(propName);
|
|
|
|
textProps.put(propName, textProp);
|
|
}
|
|
|
|
/**
|
|
* For an existing set of text properties, build the list of
|
|
* properties coded for in a given run of properties.
|
|
* @return the number of bytes that were used encoding the properties list
|
|
*/
|
|
public int buildTextPropList(int containsField, byte[] data, int dataOffset) {
|
|
int bytesPassed = 0;
|
|
|
|
// For each possible entry, see if we match the mask
|
|
// If we do, decode that, save it, and shuffle on
|
|
for(TextProp tp : getPotentialProperties()) {
|
|
// Check there's still data left to read
|
|
|
|
// Check if this property is found in the mask
|
|
if((containsField & tp.getMask()) != 0) {
|
|
if(dataOffset+bytesPassed >= data.length) {
|
|
// Out of data, can't be any more properties to go
|
|
// remember the mask and return
|
|
maskSpecial |= tp.getMask();
|
|
return bytesPassed;
|
|
}
|
|
|
|
// Bingo, data contains this property
|
|
TextProp prop = tp.clone();
|
|
int val = 0;
|
|
if (prop instanceof TabStopPropCollection) {
|
|
((TabStopPropCollection)prop).parseProperty(data, dataOffset+bytesPassed);
|
|
} else if (prop.getSize() == 2) {
|
|
val = LittleEndian.getShort(data,dataOffset+bytesPassed);
|
|
} else if(prop.getSize() == 4) {
|
|
val = LittleEndian.getInt(data,dataOffset+bytesPassed);
|
|
} else if (prop.getSize() == 0) {
|
|
//remember "special" bits.
|
|
maskSpecial |= tp.getMask();
|
|
continue;
|
|
}
|
|
|
|
if (prop instanceof BitMaskTextProp) {
|
|
((BitMaskTextProp)prop).setValueWithMask(val, containsField);
|
|
} else {
|
|
prop.setValue(val);
|
|
}
|
|
bytesPassed += prop.getSize();
|
|
addProp(prop);
|
|
}
|
|
}
|
|
|
|
// Return how many bytes were used
|
|
return bytesPassed;
|
|
}
|
|
|
|
/**
|
|
* Clones the given text properties
|
|
*/
|
|
public void copy(TextPropCollection other) {
|
|
if (other == null) {
|
|
throw new HSLFException("trying to copy null TextPropCollection");
|
|
}
|
|
if (this == other) return;
|
|
this.charactersCovered = other.charactersCovered;
|
|
this.indentLevel = other.indentLevel;
|
|
this.maskSpecial = other.maskSpecial;
|
|
this.textProps.clear();
|
|
for (TextProp tp : other.textProps.values()) {
|
|
TextProp tpCopy = (tp instanceof BitMaskTextProp)
|
|
? ((BitMaskTextProp)tp).cloneAll()
|
|
: tp.clone();
|
|
addProp(tpCopy);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the size of the text that this set of properties
|
|
* applies to
|
|
*/
|
|
public void updateTextSize(int textSize) {
|
|
charactersCovered = textSize;
|
|
}
|
|
|
|
/**
|
|
* Writes out to disk the header, and then all the properties
|
|
*/
|
|
public void writeOut(OutputStream o) throws IOException {
|
|
writeOut(o, false);
|
|
}
|
|
|
|
/**
|
|
* Writes out to disk the header, and then all the properties
|
|
*/
|
|
public void writeOut(OutputStream o, boolean isMasterStyle) throws IOException {
|
|
if (!isMasterStyle) {
|
|
// First goes the number of characters we affect
|
|
// MasterStyles don't have this field
|
|
StyleTextPropAtom.writeLittleEndian(charactersCovered,o);
|
|
}
|
|
|
|
// Then we have the indentLevel field if it's a paragraph collection
|
|
if (textPropType == TextPropType.paragraph && indentLevel > -1) {
|
|
StyleTextPropAtom.writeLittleEndian(indentLevel, o);
|
|
}
|
|
|
|
// Then the mask field
|
|
int mask = maskSpecial;
|
|
for (TextProp textProp : textProps.values()) {
|
|
mask |= textProp.getWriteMask();
|
|
}
|
|
StyleTextPropAtom.writeLittleEndian(mask,o);
|
|
|
|
// Then the contents of all the properties
|
|
for (TextProp textProp : getTextPropList()) {
|
|
int val = textProp.getValue();
|
|
if (textProp instanceof BitMaskTextProp && textProp.getWriteMask() == 0) {
|
|
// don't add empty properties, as they can't be recognized while reading
|
|
continue;
|
|
} else if (textProp.getSize() == 2) {
|
|
StyleTextPropAtom.writeLittleEndian((short)val,o);
|
|
} else if (textProp.getSize() == 4) {
|
|
StyleTextPropAtom.writeLittleEndian(val,o);
|
|
}
|
|
}
|
|
}
|
|
|
|
public short getIndentLevel(){
|
|
return indentLevel;
|
|
}
|
|
|
|
public void setIndentLevel(short indentLevel) {
|
|
if (textPropType == TextPropType.character) {
|
|
throw new RuntimeException("trying to set an indent on a character collection.");
|
|
}
|
|
this.indentLevel = indentLevel;
|
|
}
|
|
|
|
public int hashCode() {
|
|
final int prime = 31;
|
|
int result = 1;
|
|
result = prime * result + charactersCovered;
|
|
result = prime * result + maskSpecial;
|
|
result = prime * result + indentLevel;
|
|
result = prime * result + ((textProps == null) ? 0 : textProps.hashCode());
|
|
return result;
|
|
}
|
|
/**
|
|
* compares most properties apart of the covered characters length
|
|
*/
|
|
public boolean equals(Object other) {
|
|
if (this == other) return true;
|
|
if (other == null) return false;
|
|
if (getClass() != other.getClass()) return false;
|
|
|
|
TextPropCollection o = (TextPropCollection)other;
|
|
if (o.maskSpecial != this.maskSpecial || o.indentLevel != this.indentLevel) {
|
|
return false;
|
|
}
|
|
|
|
return textProps.equals(o.textProps);
|
|
}
|
|
|
|
public String toString() {
|
|
StringBuilder out = new StringBuilder();
|
|
out.append(" chars covered: " + getCharactersCovered());
|
|
out.append(" special mask flags: 0x" + HexDump.toHex(getSpecialMask()) + "\n");
|
|
if (textPropType == TextPropType.paragraph) {
|
|
out.append(" indent level: "+getIndentLevel()+"\n");
|
|
}
|
|
for(TextProp p : getTextPropList()) {
|
|
out.append(" " + p.getName() + " = " + p.getValue() );
|
|
out.append(" (0x" + HexDump.toHex(p.getValue()) + ")\n");
|
|
if (p instanceof BitMaskTextProp) {
|
|
BitMaskTextProp bm = (BitMaskTextProp)p;
|
|
int i = 0;
|
|
for (String s : bm.getSubPropNames()) {
|
|
if (bm.getSubPropMatches()[i]) {
|
|
out.append(" " + s + " = " + bm.getSubValue(i) + "\n");
|
|
}
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
out.append(" bytes that would be written: \n");
|
|
|
|
try {
|
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
writeOut(baos);
|
|
byte[] b = baos.toByteArray();
|
|
out.append(HexDump.dump(b, 0, 0));
|
|
} catch (Exception e ) {
|
|
e.printStackTrace();
|
|
}
|
|
|
|
return out.toString();
|
|
}
|
|
}
|