You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1002 lines
29 KiB
1002 lines
29 KiB
/*
|
|
* Copyright (c) 1997, 2008, Oracle and/or its affiliates. All rights reserved.
|
|
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*
|
|
*/
|
|
package javax.swing.text.rtf;
|
|
|
|
import java.lang.*;
|
|
import java.util.*;
|
|
import java.awt.Color;
|
|
import java.awt.Font;
|
|
import java.io.OutputStream;
|
|
import java.io.IOException;
|
|
|
|
import javax.swing.text.*;
|
|
|
|
/**
|
|
* Generates an RTF output stream (java.io.OutputStream) from rich text
|
|
* (handed off through a series of LTTextAcceptor calls). Can be used to
|
|
* generate RTF from any object which knows how to write to a text acceptor
|
|
* (e.g., LTAttributedText and LTRTFFilter).
|
|
*
|
|
* <p>Note that this is a lossy conversion since RTF's model of
|
|
* text does not exactly correspond with LightText's.
|
|
*
|
|
* @see LTAttributedText
|
|
* @see LTRTFFilter
|
|
* @see LTTextAcceptor
|
|
* @see java.io.OutputStream
|
|
*/
|
|
|
|
class RTFGenerator extends Object
|
|
{
|
|
/* These dictionaries map Colors, font names, or Style objects
|
|
to Integers */
|
|
Dictionary<Object, Integer> colorTable;
|
|
int colorCount;
|
|
Dictionary<String, Integer> fontTable;
|
|
int fontCount;
|
|
Dictionary<AttributeSet, Integer> styleTable;
|
|
int styleCount;
|
|
|
|
/* where all the text is going */
|
|
OutputStream outputStream;
|
|
|
|
boolean afterKeyword;
|
|
|
|
MutableAttributeSet outputAttributes;
|
|
|
|
/* the value of the last \\ucN keyword emitted */
|
|
int unicodeCount;
|
|
|
|
/* for efficiency's sake (ha) */
|
|
private Segment workingSegment;
|
|
|
|
int[] outputConversion;
|
|
|
|
/** The default color, used for text without an explicit color
|
|
* attribute. */
|
|
static public final Color defaultRTFColor = Color.black;
|
|
|
|
static public final float defaultFontSize = 12f;
|
|
|
|
static public final String defaultFontFamily = "Helvetica";
|
|
|
|
/* constants so we can avoid allocating objects in inner loops */
|
|
final static private Object MagicToken;
|
|
|
|
/* An array of character-keyword pairs. This could be done
|
|
as a dictionary (and lookup would be quicker), but that
|
|
would require allocating an object for every character
|
|
written (slow!). */
|
|
static class CharacterKeywordPair
|
|
{ public char character; public String keyword; }
|
|
static protected CharacterKeywordPair[] textKeywords;
|
|
|
|
static {
|
|
MagicToken = new Object();
|
|
|
|
Dictionary textKeywordDictionary = RTFReader.textKeywords;
|
|
Enumeration keys = textKeywordDictionary.keys();
|
|
Vector<CharacterKeywordPair> tempPairs = new Vector<CharacterKeywordPair>();
|
|
while(keys.hasMoreElements()) {
|
|
CharacterKeywordPair pair = new CharacterKeywordPair();
|
|
pair.keyword = (String)keys.nextElement();
|
|
pair.character = ((String)textKeywordDictionary.get(pair.keyword)).charAt(0);
|
|
tempPairs.addElement(pair);
|
|
}
|
|
textKeywords = new CharacterKeywordPair[tempPairs.size()];
|
|
tempPairs.copyInto(textKeywords);
|
|
}
|
|
|
|
static final char[] hexdigits = { '0', '1', '2', '3', '4', '5', '6', '7',
|
|
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
|
|
|
|
static public void writeDocument(Document d, OutputStream to)
|
|
throws IOException
|
|
{
|
|
RTFGenerator gen = new RTFGenerator(to);
|
|
Element root = d.getDefaultRootElement();
|
|
|
|
gen.examineElement(root);
|
|
gen.writeRTFHeader();
|
|
gen.writeDocumentProperties(d);
|
|
|
|
/* TODO this assumes a particular element structure; is there
|
|
a way to iterate more generically ? */
|
|
int max = root.getElementCount();
|
|
for(int idx = 0; idx < max; idx++)
|
|
gen.writeParagraphElement(root.getElement(idx));
|
|
|
|
gen.writeRTFTrailer();
|
|
}
|
|
|
|
public RTFGenerator(OutputStream to)
|
|
{
|
|
colorTable = new Hashtable<Object, Integer>();
|
|
colorTable.put(defaultRTFColor, Integer.valueOf(0));
|
|
colorCount = 1;
|
|
|
|
fontTable = new Hashtable<String, Integer>();
|
|
fontCount = 0;
|
|
|
|
styleTable = new Hashtable<AttributeSet, Integer>();
|
|
/* TODO: put default style in style table */
|
|
styleCount = 0;
|
|
|
|
workingSegment = new Segment();
|
|
|
|
outputStream = to;
|
|
|
|
unicodeCount = 1;
|
|
}
|
|
|
|
public void examineElement(Element el)
|
|
{
|
|
AttributeSet a = el.getAttributes();
|
|
String fontName;
|
|
Object foregroundColor, backgroundColor;
|
|
|
|
tallyStyles(a);
|
|
|
|
if (a != null) {
|
|
/* TODO: default color must be color 0! */
|
|
|
|
foregroundColor = StyleConstants.getForeground(a);
|
|
if (foregroundColor != null &&
|
|
colorTable.get(foregroundColor) == null) {
|
|
colorTable.put(foregroundColor, new Integer(colorCount));
|
|
colorCount ++;
|
|
}
|
|
|
|
backgroundColor = a.getAttribute(StyleConstants.Background);
|
|
if (backgroundColor != null &&
|
|
colorTable.get(backgroundColor) == null) {
|
|
colorTable.put(backgroundColor, new Integer(colorCount));
|
|
colorCount ++;
|
|
}
|
|
|
|
fontName = StyleConstants.getFontFamily(a);
|
|
|
|
if (fontName == null)
|
|
fontName = defaultFontFamily;
|
|
|
|
if (fontName != null &&
|
|
fontTable.get(fontName) == null) {
|
|
fontTable.put(fontName, new Integer(fontCount));
|
|
fontCount ++;
|
|
}
|
|
}
|
|
|
|
int el_count = el.getElementCount();
|
|
for(int el_idx = 0; el_idx < el_count; el_idx ++) {
|
|
examineElement(el.getElement(el_idx));
|
|
}
|
|
}
|
|
|
|
private void tallyStyles(AttributeSet a) {
|
|
while (a != null) {
|
|
if (a instanceof Style) {
|
|
Integer aNum = styleTable.get(a);
|
|
if (aNum == null) {
|
|
styleCount = styleCount + 1;
|
|
aNum = new Integer(styleCount);
|
|
styleTable.put(a, aNum);
|
|
}
|
|
}
|
|
a = a.getResolveParent();
|
|
}
|
|
}
|
|
|
|
private Style findStyle(AttributeSet a)
|
|
{
|
|
while(a != null) {
|
|
if (a instanceof Style) {
|
|
Object aNum = styleTable.get(a);
|
|
if (aNum != null)
|
|
return (Style)a;
|
|
}
|
|
a = a.getResolveParent();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private Integer findStyleNumber(AttributeSet a, String domain)
|
|
{
|
|
while(a != null) {
|
|
if (a instanceof Style) {
|
|
Integer aNum = styleTable.get(a);
|
|
if (aNum != null) {
|
|
if (domain == null ||
|
|
domain.equals(a.getAttribute(Constants.StyleType)))
|
|
return aNum;
|
|
}
|
|
|
|
}
|
|
a = a.getResolveParent();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static private Object attrDiff(MutableAttributeSet oldAttrs,
|
|
AttributeSet newAttrs,
|
|
Object key,
|
|
Object dfl)
|
|
{
|
|
Object oldValue, newValue;
|
|
|
|
oldValue = oldAttrs.getAttribute(key);
|
|
newValue = newAttrs.getAttribute(key);
|
|
|
|
if (newValue == oldValue)
|
|
return null;
|
|
if (newValue == null) {
|
|
oldAttrs.removeAttribute(key);
|
|
if (dfl != null && !dfl.equals(oldValue))
|
|
return dfl;
|
|
else
|
|
return null;
|
|
}
|
|
if (oldValue == null ||
|
|
!equalArraysOK(oldValue, newValue)) {
|
|
oldAttrs.addAttribute(key, newValue);
|
|
return newValue;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static private boolean equalArraysOK(Object a, Object b)
|
|
{
|
|
Object[] aa, bb;
|
|
if (a == b)
|
|
return true;
|
|
if (a == null || b == null)
|
|
return false;
|
|
if (a.equals(b))
|
|
return true;
|
|
if (!(a.getClass().isArray() && b.getClass().isArray()))
|
|
return false;
|
|
aa = (Object[])a;
|
|
bb = (Object[])b;
|
|
if (aa.length != bb.length)
|
|
return false;
|
|
|
|
int i;
|
|
int l = aa.length;
|
|
for(i = 0; i < l; i++) {
|
|
if (!equalArraysOK(aa[i], bb[i]))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Writes a line break to the output file, for ease in debugging */
|
|
public void writeLineBreak()
|
|
throws IOException
|
|
{
|
|
writeRawString("\n");
|
|
afterKeyword = false;
|
|
}
|
|
|
|
|
|
public void writeRTFHeader()
|
|
throws IOException
|
|
{
|
|
int index;
|
|
|
|
/* TODO: Should the writer attempt to examine the text it's writing
|
|
and pick a character set which will most compactly represent the
|
|
document? (currently the writer always uses the ansi character
|
|
set, which is roughly ISO-8859 Latin-1, and uses Unicode escapes
|
|
for all other characters. However Unicode is a relatively
|
|
recent addition to RTF, and not all readers will understand it.) */
|
|
writeBegingroup();
|
|
writeControlWord("rtf", 1);
|
|
writeControlWord("ansi");
|
|
outputConversion = outputConversionForName("ansi");
|
|
writeLineBreak();
|
|
|
|
/* write font table */
|
|
String[] sortedFontTable = new String[fontCount];
|
|
Enumeration<String> fonts = fontTable.keys();
|
|
String font;
|
|
while(fonts.hasMoreElements()) {
|
|
font = fonts.nextElement();
|
|
Integer num = fontTable.get(font);
|
|
sortedFontTable[num.intValue()] = font;
|
|
}
|
|
writeBegingroup();
|
|
writeControlWord("fonttbl");
|
|
for(index = 0; index < fontCount; index ++) {
|
|
writeControlWord("f", index);
|
|
writeControlWord("fnil"); /* TODO: supply correct font style */
|
|
writeText(sortedFontTable[index]);
|
|
writeText(";");
|
|
}
|
|
writeEndgroup();
|
|
writeLineBreak();
|
|
|
|
/* write color table */
|
|
if (colorCount > 1) {
|
|
Color[] sortedColorTable = new Color[colorCount];
|
|
Enumeration colors = colorTable.keys();
|
|
Color color;
|
|
while(colors.hasMoreElements()) {
|
|
color = (Color)colors.nextElement();
|
|
Integer num = colorTable.get(color);
|
|
sortedColorTable[num.intValue()] = color;
|
|
}
|
|
writeBegingroup();
|
|
writeControlWord("colortbl");
|
|
for(index = 0; index < colorCount; index ++) {
|
|
color = sortedColorTable[index];
|
|
if (color != null) {
|
|
writeControlWord("red", color.getRed());
|
|
writeControlWord("green", color.getGreen());
|
|
writeControlWord("blue", color.getBlue());
|
|
}
|
|
writeRawString(";");
|
|
}
|
|
writeEndgroup();
|
|
writeLineBreak();
|
|
}
|
|
|
|
/* write the style sheet */
|
|
if (styleCount > 1) {
|
|
writeBegingroup();
|
|
writeControlWord("stylesheet");
|
|
Enumeration<AttributeSet> styles = styleTable.keys();
|
|
while(styles.hasMoreElements()) {
|
|
Style style = (Style)styles.nextElement();
|
|
int styleNumber = styleTable.get(style).intValue();
|
|
writeBegingroup();
|
|
String styleType = (String)style.getAttribute(Constants.StyleType);
|
|
if (styleType == null)
|
|
styleType = Constants.STParagraph;
|
|
if (styleType.equals(Constants.STCharacter)) {
|
|
writeControlWord("*");
|
|
writeControlWord("cs", styleNumber);
|
|
} else if(styleType.equals(Constants.STSection)) {
|
|
writeControlWord("*");
|
|
writeControlWord("ds", styleNumber);
|
|
} else {
|
|
writeControlWord("s", styleNumber);
|
|
}
|
|
|
|
AttributeSet basis = style.getResolveParent();
|
|
MutableAttributeSet goat;
|
|
if (basis == null) {
|
|
goat = new SimpleAttributeSet();
|
|
} else {
|
|
goat = new SimpleAttributeSet(basis);
|
|
}
|
|
|
|
updateSectionAttributes(goat, style, false);
|
|
updateParagraphAttributes(goat, style, false);
|
|
updateCharacterAttributes(goat, style, false);
|
|
|
|
basis = style.getResolveParent();
|
|
if (basis != null && basis instanceof Style) {
|
|
Integer basedOn = styleTable.get(basis);
|
|
if (basedOn != null) {
|
|
writeControlWord("sbasedon", basedOn.intValue());
|
|
}
|
|
}
|
|
|
|
Style nextStyle = (Style)style.getAttribute(Constants.StyleNext);
|
|
if (nextStyle != null) {
|
|
Integer nextNum = styleTable.get(nextStyle);
|
|
if (nextNum != null) {
|
|
writeControlWord("snext", nextNum.intValue());
|
|
}
|
|
}
|
|
|
|
Boolean hidden = (Boolean)style.getAttribute(Constants.StyleHidden);
|
|
if (hidden != null && hidden.booleanValue())
|
|
writeControlWord("shidden");
|
|
|
|
Boolean additive = (Boolean)style.getAttribute(Constants.StyleAdditive);
|
|
if (additive != null && additive.booleanValue())
|
|
writeControlWord("additive");
|
|
|
|
|
|
writeText(style.getName());
|
|
writeText(";");
|
|
writeEndgroup();
|
|
}
|
|
writeEndgroup();
|
|
writeLineBreak();
|
|
}
|
|
|
|
outputAttributes = new SimpleAttributeSet();
|
|
}
|
|
|
|
void writeDocumentProperties(Document doc)
|
|
throws IOException
|
|
{
|
|
/* Write the document properties */
|
|
int i;
|
|
boolean wroteSomething = false;
|
|
|
|
for(i = 0; i < RTFAttributes.attributes.length; i++) {
|
|
RTFAttribute attr = RTFAttributes.attributes[i];
|
|
if (attr.domain() != RTFAttribute.D_DOCUMENT)
|
|
continue;
|
|
Object prop = doc.getProperty(attr.swingName());
|
|
boolean ok = attr.writeValue(prop, this, false);
|
|
if (ok)
|
|
wroteSomething = true;
|
|
}
|
|
|
|
if (wroteSomething)
|
|
writeLineBreak();
|
|
}
|
|
|
|
public void writeRTFTrailer()
|
|
throws IOException
|
|
{
|
|
writeEndgroup();
|
|
writeLineBreak();
|
|
}
|
|
|
|
protected void checkNumericControlWord(MutableAttributeSet currentAttributes,
|
|
AttributeSet newAttributes,
|
|
Object attrName,
|
|
String controlWord,
|
|
float dflt, float scale)
|
|
throws IOException
|
|
{
|
|
Object parm;
|
|
|
|
if ((parm = attrDiff(currentAttributes, newAttributes,
|
|
attrName, MagicToken)) != null) {
|
|
float targ;
|
|
if (parm == MagicToken)
|
|
targ = dflt;
|
|
else
|
|
targ = ((Number)parm).floatValue();
|
|
writeControlWord(controlWord, Math.round(targ * scale));
|
|
}
|
|
}
|
|
|
|
protected void checkControlWord(MutableAttributeSet currentAttributes,
|
|
AttributeSet newAttributes,
|
|
RTFAttribute word)
|
|
throws IOException
|
|
{
|
|
Object parm;
|
|
|
|
if ((parm = attrDiff(currentAttributes, newAttributes,
|
|
word.swingName(), MagicToken)) != null) {
|
|
if (parm == MagicToken)
|
|
parm = null;
|
|
word.writeValue(parm, this, true);
|
|
}
|
|
}
|
|
|
|
protected void checkControlWords(MutableAttributeSet currentAttributes,
|
|
AttributeSet newAttributes,
|
|
RTFAttribute words[],
|
|
int domain)
|
|
throws IOException
|
|
{
|
|
int wordIndex;
|
|
int wordCount = words.length;
|
|
for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
|
|
RTFAttribute attr = words[wordIndex];
|
|
if (attr.domain() == domain)
|
|
checkControlWord(currentAttributes, newAttributes, attr);
|
|
}
|
|
}
|
|
|
|
void updateSectionAttributes(MutableAttributeSet current,
|
|
AttributeSet newAttributes,
|
|
boolean emitStyleChanges)
|
|
throws IOException
|
|
{
|
|
if (emitStyleChanges) {
|
|
Object oldStyle = current.getAttribute("sectionStyle");
|
|
Object newStyle = findStyleNumber(newAttributes, Constants.STSection);
|
|
if (oldStyle != newStyle) {
|
|
if (oldStyle != null) {
|
|
resetSectionAttributes(current);
|
|
}
|
|
if (newStyle != null) {
|
|
writeControlWord("ds", ((Integer)newStyle).intValue());
|
|
current.addAttribute("sectionStyle", newStyle);
|
|
} else {
|
|
current.removeAttribute("sectionStyle");
|
|
}
|
|
}
|
|
}
|
|
|
|
checkControlWords(current, newAttributes,
|
|
RTFAttributes.attributes, RTFAttribute.D_SECTION);
|
|
}
|
|
|
|
protected void resetSectionAttributes(MutableAttributeSet currentAttributes)
|
|
throws IOException
|
|
{
|
|
writeControlWord("sectd");
|
|
|
|
int wordIndex;
|
|
int wordCount = RTFAttributes.attributes.length;
|
|
for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
|
|
RTFAttribute attr = RTFAttributes.attributes[wordIndex];
|
|
if (attr.domain() == RTFAttribute.D_SECTION)
|
|
attr.setDefault(currentAttributes);
|
|
}
|
|
|
|
currentAttributes.removeAttribute("sectionStyle");
|
|
}
|
|
|
|
void updateParagraphAttributes(MutableAttributeSet current,
|
|
AttributeSet newAttributes,
|
|
boolean emitStyleChanges)
|
|
throws IOException
|
|
{
|
|
Object parm;
|
|
Object oldStyle, newStyle;
|
|
|
|
/* The only way to get rid of tabs or styles is with the \pard keyword,
|
|
emitted by resetParagraphAttributes(). Ideally we should avoid
|
|
emitting \pard if the new paragraph's tabs are a superset of the old
|
|
paragraph's tabs. */
|
|
|
|
if (emitStyleChanges) {
|
|
oldStyle = current.getAttribute("paragraphStyle");
|
|
newStyle = findStyleNumber(newAttributes, Constants.STParagraph);
|
|
if (oldStyle != newStyle) {
|
|
if (oldStyle != null) {
|
|
resetParagraphAttributes(current);
|
|
oldStyle = null;
|
|
}
|
|
}
|
|
} else {
|
|
oldStyle = null;
|
|
newStyle = null;
|
|
}
|
|
|
|
Object oldTabs = current.getAttribute(Constants.Tabs);
|
|
Object newTabs = newAttributes.getAttribute(Constants.Tabs);
|
|
if (oldTabs != newTabs) {
|
|
if (oldTabs != null) {
|
|
resetParagraphAttributes(current);
|
|
oldTabs = null;
|
|
oldStyle = null;
|
|
}
|
|
}
|
|
|
|
if (oldStyle != newStyle && newStyle != null) {
|
|
writeControlWord("s", ((Integer)newStyle).intValue());
|
|
current.addAttribute("paragraphStyle", newStyle);
|
|
}
|
|
|
|
checkControlWords(current, newAttributes,
|
|
RTFAttributes.attributes, RTFAttribute.D_PARAGRAPH);
|
|
|
|
if (oldTabs != newTabs && newTabs != null) {
|
|
TabStop tabs[] = (TabStop[])newTabs;
|
|
int index;
|
|
for(index = 0; index < tabs.length; index ++) {
|
|
TabStop tab = tabs[index];
|
|
switch (tab.getAlignment()) {
|
|
case TabStop.ALIGN_LEFT:
|
|
case TabStop.ALIGN_BAR:
|
|
break;
|
|
case TabStop.ALIGN_RIGHT:
|
|
writeControlWord("tqr");
|
|
break;
|
|
case TabStop.ALIGN_CENTER:
|
|
writeControlWord("tqc");
|
|
break;
|
|
case TabStop.ALIGN_DECIMAL:
|
|
writeControlWord("tqdec");
|
|
break;
|
|
}
|
|
switch (tab.getLeader()) {
|
|
case TabStop.LEAD_NONE:
|
|
break;
|
|
case TabStop.LEAD_DOTS:
|
|
writeControlWord("tldot");
|
|
break;
|
|
case TabStop.LEAD_HYPHENS:
|
|
writeControlWord("tlhyph");
|
|
break;
|
|
case TabStop.LEAD_UNDERLINE:
|
|
writeControlWord("tlul");
|
|
break;
|
|
case TabStop.LEAD_THICKLINE:
|
|
writeControlWord("tlth");
|
|
break;
|
|
case TabStop.LEAD_EQUALS:
|
|
writeControlWord("tleq");
|
|
break;
|
|
}
|
|
int twips = Math.round(20f * tab.getPosition());
|
|
if (tab.getAlignment() == TabStop.ALIGN_BAR) {
|
|
writeControlWord("tb", twips);
|
|
} else {
|
|
writeControlWord("tx", twips);
|
|
}
|
|
}
|
|
current.addAttribute(Constants.Tabs, tabs);
|
|
}
|
|
}
|
|
|
|
public void writeParagraphElement(Element el)
|
|
throws IOException
|
|
{
|
|
updateParagraphAttributes(outputAttributes, el.getAttributes(), true);
|
|
|
|
int sub_count = el.getElementCount();
|
|
for(int idx = 0; idx < sub_count; idx ++) {
|
|
writeTextElement(el.getElement(idx));
|
|
}
|
|
|
|
writeControlWord("par");
|
|
writeLineBreak(); /* makes the raw file more readable */
|
|
}
|
|
|
|
/* debugging. TODO: remove.
|
|
private static String tabdump(Object tso)
|
|
{
|
|
String buf;
|
|
int i;
|
|
|
|
if (tso == null)
|
|
return "[none]";
|
|
|
|
TabStop[] ts = (TabStop[])tso;
|
|
|
|
buf = "[";
|
|
for(i = 0; i < ts.length; i++) {
|
|
buf = buf + ts[i].toString();
|
|
if ((i+1) < ts.length)
|
|
buf = buf + ",";
|
|
}
|
|
return buf + "]";
|
|
}
|
|
*/
|
|
|
|
protected void resetParagraphAttributes(MutableAttributeSet currentAttributes)
|
|
throws IOException
|
|
{
|
|
writeControlWord("pard");
|
|
|
|
currentAttributes.addAttribute(StyleConstants.Alignment, Integer.valueOf(0));
|
|
|
|
int wordIndex;
|
|
int wordCount = RTFAttributes.attributes.length;
|
|
for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
|
|
RTFAttribute attr = RTFAttributes.attributes[wordIndex];
|
|
if (attr.domain() == RTFAttribute.D_PARAGRAPH)
|
|
attr.setDefault(currentAttributes);
|
|
}
|
|
|
|
currentAttributes.removeAttribute("paragraphStyle");
|
|
currentAttributes.removeAttribute(Constants.Tabs);
|
|
}
|
|
|
|
void updateCharacterAttributes(MutableAttributeSet current,
|
|
AttributeSet newAttributes,
|
|
boolean updateStyleChanges)
|
|
throws IOException
|
|
{
|
|
Object parm;
|
|
|
|
if (updateStyleChanges) {
|
|
Object oldStyle = current.getAttribute("characterStyle");
|
|
Object newStyle = findStyleNumber(newAttributes,
|
|
Constants.STCharacter);
|
|
if (oldStyle != newStyle) {
|
|
if (oldStyle != null) {
|
|
resetCharacterAttributes(current);
|
|
}
|
|
if (newStyle != null) {
|
|
writeControlWord("cs", ((Integer)newStyle).intValue());
|
|
current.addAttribute("characterStyle", newStyle);
|
|
} else {
|
|
current.removeAttribute("characterStyle");
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((parm = attrDiff(current, newAttributes,
|
|
StyleConstants.FontFamily, null)) != null) {
|
|
Integer fontNum = fontTable.get(parm);
|
|
writeControlWord("f", fontNum.intValue());
|
|
}
|
|
|
|
checkNumericControlWord(current, newAttributes,
|
|
StyleConstants.FontSize, "fs",
|
|
defaultFontSize, 2f);
|
|
|
|
checkControlWords(current, newAttributes,
|
|
RTFAttributes.attributes, RTFAttribute.D_CHARACTER);
|
|
|
|
checkNumericControlWord(current, newAttributes,
|
|
StyleConstants.LineSpacing, "sl",
|
|
0, 20f); /* TODO: sl wackiness */
|
|
|
|
if ((parm = attrDiff(current, newAttributes,
|
|
StyleConstants.Background, MagicToken)) != null) {
|
|
int colorNum;
|
|
if (parm == MagicToken)
|
|
colorNum = 0;
|
|
else
|
|
colorNum = colorTable.get(parm).intValue();
|
|
writeControlWord("cb", colorNum);
|
|
}
|
|
|
|
if ((parm = attrDiff(current, newAttributes,
|
|
StyleConstants.Foreground, null)) != null) {
|
|
int colorNum;
|
|
if (parm == MagicToken)
|
|
colorNum = 0;
|
|
else
|
|
colorNum = colorTable.get(parm).intValue();
|
|
writeControlWord("cf", colorNum);
|
|
}
|
|
}
|
|
|
|
protected void resetCharacterAttributes(MutableAttributeSet currentAttributes)
|
|
throws IOException
|
|
{
|
|
writeControlWord("plain");
|
|
|
|
int wordIndex;
|
|
int wordCount = RTFAttributes.attributes.length;
|
|
for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
|
|
RTFAttribute attr = RTFAttributes.attributes[wordIndex];
|
|
if (attr.domain() == RTFAttribute.D_CHARACTER)
|
|
attr.setDefault(currentAttributes);
|
|
}
|
|
|
|
StyleConstants.setFontFamily(currentAttributes, defaultFontFamily);
|
|
currentAttributes.removeAttribute(StyleConstants.FontSize); /* =default */
|
|
currentAttributes.removeAttribute(StyleConstants.Background);
|
|
currentAttributes.removeAttribute(StyleConstants.Foreground);
|
|
currentAttributes.removeAttribute(StyleConstants.LineSpacing);
|
|
currentAttributes.removeAttribute("characterStyle");
|
|
}
|
|
|
|
public void writeTextElement(Element el)
|
|
throws IOException
|
|
{
|
|
updateCharacterAttributes(outputAttributes, el.getAttributes(), true);
|
|
|
|
if (el.isLeaf()) {
|
|
try {
|
|
el.getDocument().getText(el.getStartOffset(),
|
|
el.getEndOffset() - el.getStartOffset(),
|
|
this.workingSegment);
|
|
} catch (BadLocationException ble) {
|
|
/* TODO is this the correct error to raise? */
|
|
ble.printStackTrace();
|
|
throw new InternalError(ble.getMessage());
|
|
}
|
|
writeText(this.workingSegment);
|
|
} else {
|
|
int sub_count = el.getElementCount();
|
|
for(int idx = 0; idx < sub_count; idx ++)
|
|
writeTextElement(el.getElement(idx));
|
|
}
|
|
}
|
|
|
|
public void writeText(Segment s)
|
|
throws IOException
|
|
{
|
|
int pos, end;
|
|
char[] array;
|
|
|
|
pos = s.offset;
|
|
end = pos + s.count;
|
|
array = s.array;
|
|
for( ; pos < end; pos ++)
|
|
writeCharacter(array[pos]);
|
|
}
|
|
|
|
public void writeText(String s)
|
|
throws IOException
|
|
{
|
|
int pos, end;
|
|
|
|
pos = 0;
|
|
end = s.length();
|
|
for( ; pos < end; pos ++)
|
|
writeCharacter(s.charAt(pos));
|
|
}
|
|
|
|
public void writeRawString(String str)
|
|
throws IOException
|
|
{
|
|
int strlen = str.length();
|
|
for (int offset = 0; offset < strlen; offset ++)
|
|
outputStream.write((int)str.charAt(offset));
|
|
}
|
|
|
|
public void writeControlWord(String keyword)
|
|
throws IOException
|
|
{
|
|
outputStream.write('\\');
|
|
writeRawString(keyword);
|
|
afterKeyword = true;
|
|
}
|
|
|
|
public void writeControlWord(String keyword, int arg)
|
|
throws IOException
|
|
{
|
|
outputStream.write('\\');
|
|
writeRawString(keyword);
|
|
writeRawString(String.valueOf(arg)); /* TODO: correct in all cases? */
|
|
afterKeyword = true;
|
|
}
|
|
|
|
public void writeBegingroup()
|
|
throws IOException
|
|
{
|
|
outputStream.write('{');
|
|
afterKeyword = false;
|
|
}
|
|
|
|
public void writeEndgroup()
|
|
throws IOException
|
|
{
|
|
outputStream.write('}');
|
|
afterKeyword = false;
|
|
}
|
|
|
|
public void writeCharacter(char ch)
|
|
throws IOException
|
|
{
|
|
/* Nonbreaking space is in most RTF encodings, but the keyword is
|
|
preferable; same goes for tabs */
|
|
if (ch == 0xA0) { /* nonbreaking space */
|
|
outputStream.write(0x5C); /* backslash */
|
|
outputStream.write(0x7E); /* tilde */
|
|
afterKeyword = false; /* non-alpha keywords are self-terminating */
|
|
return;
|
|
}
|
|
|
|
if (ch == 0x09) { /* horizontal tab */
|
|
writeControlWord("tab");
|
|
return;
|
|
}
|
|
|
|
if (ch == 10 || ch == 13) { /* newline / paragraph */
|
|
/* ignore CRs, we'll write a paragraph element soon enough */
|
|
return;
|
|
}
|
|
|
|
int b = convertCharacter(outputConversion, ch);
|
|
if (b == 0) {
|
|
/* Unicode characters which have corresponding RTF keywords */
|
|
int i;
|
|
for(i = 0; i < textKeywords.length; i++) {
|
|
if (textKeywords[i].character == ch) {
|
|
writeControlWord(textKeywords[i].keyword);
|
|
return;
|
|
}
|
|
}
|
|
/* In some cases it would be reasonable to check to see if the
|
|
glyph being written out is in the Symbol encoding, and if so,
|
|
to switch to the Symbol font for this character. TODO. */
|
|
/* Currently all unrepresentable characters are written as
|
|
Unicode escapes. */
|
|
String approximation = approximationForUnicode(ch);
|
|
if (approximation.length() != unicodeCount) {
|
|
unicodeCount = approximation.length();
|
|
writeControlWord("uc", unicodeCount);
|
|
}
|
|
writeControlWord("u", (int)ch);
|
|
writeRawString(" ");
|
|
writeRawString(approximation);
|
|
afterKeyword = false;
|
|
return;
|
|
}
|
|
|
|
if (b > 127) {
|
|
int nybble;
|
|
outputStream.write('\\');
|
|
outputStream.write('\'');
|
|
nybble = ( b & 0xF0 ) >>> 4;
|
|
outputStream.write(hexdigits[nybble]);
|
|
nybble = ( b & 0x0F );
|
|
outputStream.write(hexdigits[nybble]);
|
|
afterKeyword = false;
|
|
return;
|
|
}
|
|
|
|
switch (b) {
|
|
case '}':
|
|
case '{':
|
|
case '\\':
|
|
outputStream.write(0x5C); /* backslash */
|
|
afterKeyword = false; /* in a keyword, actually ... */
|
|
/* fall through */
|
|
default:
|
|
if (afterKeyword) {
|
|
outputStream.write(0x20); /* space */
|
|
afterKeyword = false;
|
|
}
|
|
outputStream.write(b);
|
|
break;
|
|
}
|
|
}
|
|
|
|
String approximationForUnicode(char ch)
|
|
{
|
|
/* TODO: Find reasonable approximations for all Unicode characters
|
|
in all RTF code pages... heh, heh... */
|
|
return "?";
|
|
}
|
|
|
|
/** Takes a translation table (a 256-element array of characters)
|
|
* and creates an output conversion table for use by
|
|
* convertCharacter(). */
|
|
/* Not very efficient at all. Could be changed to sort the table
|
|
for binary search. TODO. (Even though this is inefficient however,
|
|
writing RTF is still much faster than reading it.) */
|
|
static int[] outputConversionFromTranslationTable(char[] table)
|
|
{
|
|
int[] conversion = new int[2 * table.length];
|
|
|
|
int index;
|
|
|
|
for(index = 0; index < table.length; index ++) {
|
|
conversion[index * 2] = table[index];
|
|
conversion[(index * 2) + 1] = index;
|
|
}
|
|
|
|
return conversion;
|
|
}
|
|
|
|
static int[] outputConversionForName(String name)
|
|
throws IOException
|
|
{
|
|
char[] table = (char[])RTFReader.getCharacterSet(name);
|
|
return outputConversionFromTranslationTable(table);
|
|
}
|
|
|
|
/** Takes a char and a conversion table (an int[] in the current
|
|
* implementation, but conversion tables should be treated as an opaque
|
|
* type) and returns the
|
|
* corresponding byte value (as an int, since bytes are signed).
|
|
*/
|
|
/* Not very efficient. TODO. */
|
|
static protected int convertCharacter(int[] conversion, char ch)
|
|
{
|
|
int index;
|
|
|
|
for(index = 0; index < conversion.length; index += 2) {
|
|
if(conversion[index] == ch)
|
|
return conversion[index + 1];
|
|
}
|
|
|
|
return 0; /* 0 indicates an unrepresentable character */
|
|
}
|
|
|
|
}
|