/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option) any
later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along
with this program; if not, see http://www.gnu.org/licenses or write to the Free
Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
*/
package com.servoy.j2db.util.rtf;
import java.awt.Color;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.Segment;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.TabStop;
import com.servoy.j2db.util.Debug;
/**
* 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 colorTable;
int colorCount;
Dictionary fontTable;
int fontCount;
Dictionary 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 final 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"; //$NON-NLS-1$
/* constants so we can avoid allocating objects in inner loops */
/* these should all be final, but javac seems to be a bit buggy */
static protected Integer One, Zero;
static protected Boolean False;
static protected Float ZeroPointZero;
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
{
One = new Integer(1);
Zero = new Integer(0);
False = new Boolean(false);
MagicToken = new Object();
ZeroPointZero = new Float(0);
Dictionary textKeywordDictionary = RTFReader.textKeywords;
Enumeration keys = textKeywordDictionary.keys();
Vector tempPairs = new Vector();
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), idx == max - 1);
gen.writeRTFTrailer();
}
public RTFGenerator(OutputStream to)
{
colorTable = new Hashtable();
colorTable.put(defaultRTFColor, new Integer(0));
colorCount = 1;
fontTable = new Hashtable();
fontCount = 0;
styleTable = new Hashtable();
/* 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 = (Integer)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 = (Integer)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"); //$NON-NLS-1$
// 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); //$NON-NLS-1$
writeControlWord("ansi"); //$NON-NLS-1$
outputConversion = outputConversionForName("ansi"); //$NON-NLS-1$
/* write font table */
String[] sortedFontTable = new String[fontCount];
Enumeration fonts = fontTable.keys();
String font;
while (fonts.hasMoreElements())
{
font = (String)fonts.nextElement();
Integer num = (Integer)(fontTable.get(font));
sortedFontTable[num.intValue()] = font;
}
writeBegingroup();
writeControlWord("fonttbl"); //$NON-NLS-1$
for (index = 0; index < fontCount; index++)
{
writeControlWord("f", index); //$NON-NLS-1$
writeControlWord("fnil"); /* TODO: supply correct font style *///$NON-NLS-1$
writeText(sortedFontTable[index]);
writeText(";"); //$NON-NLS-1$
}
writeEndgroup();
/* 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 = (Integer)(colorTable.get(color));
sortedColorTable[num.intValue()] = color;
}
writeBegingroup();
writeControlWord("colortbl"); //$NON-NLS-1$
for (index = 0; index < colorCount; index++)
{
color = sortedColorTable[index];
if (color != null)
{
writeControlWord("red", color.getRed()); //$NON-NLS-1$
writeControlWord("green", color.getGreen()); //$NON-NLS-1$
writeControlWord("blue", color.getBlue()); //$NON-NLS-1$
}
writeRawString(";"); //$NON-NLS-1$
}
writeEndgroup();
}
/* write the style sheet */
if (styleCount > 1)
{
writeBegingroup();
writeControlWord("stylesheet"); //$NON-NLS-1$
Enumeration styles = styleTable.keys();
while (styles.hasMoreElements())
{
Style style = (Style)styles.nextElement();
int styleNumber = ((Integer)styleTable.get(style)).intValue();
writeBegingroup();
String styleType = (String)style.getAttribute(Constants.StyleType);
if (styleType == null) styleType = Constants.STParagraph;
if (styleType.equals(Constants.STCharacter))
{
writeControlWord("*"); //$NON-NLS-1$
writeControlWord("cs", styleNumber); //$NON-NLS-1$
}
else if (styleType.equals(Constants.STSection))
{
writeControlWord("*"); //$NON-NLS-1$
writeControlWord("ds", styleNumber); //$NON-NLS-1$
}
else
{
writeControlWord("s", styleNumber); //$NON-NLS-1$
}
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 = (Integer)styleTable.get(basis);
if (basedOn != null)
{
writeControlWord("sbasedon", basedOn.intValue()); //$NON-NLS-1$
}
}
Style nextStyle = (Style)style.getAttribute(Constants.StyleNext);
if (nextStyle != null)
{
Integer nextNum = (Integer)styleTable.get(nextStyle);
if (nextNum != null)
{
writeControlWord("snext", nextNum.intValue()); //$NON-NLS-1$
}
}
Boolean hidden = (Boolean)style.getAttribute(Constants.StyleHidden);
if (hidden != null && hidden.booleanValue()) writeControlWord("shidden"); //$NON-NLS-1$
Boolean additive = (Boolean)style.getAttribute(Constants.StyleAdditive);
if (additive != null && additive.booleanValue()) writeControlWord("additive"); //$NON-NLS-1$
writeText(style.getName());
writeText(";"); //$NON-NLS-1$
writeEndgroup();
}
writeEndgroup();
}
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;
}
}
public void writeRTFTrailer() throws IOException
{
writeEndgroup();
}
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 checkControlWord(MutableAttributeSet currentAttributes, AttributeSet newAttributes, RTFAttribute word) throws IOException
{
Object parm = attrDiff(currentAttributes, newAttributes, word.swingName(), MagicToken);
if (word instanceof RTFAttributes.AssertiveAttribute)
{
word.writeValue(currentAttributes.getAttribute(word.swingName()), this, false);
}
else
{
if (parm != 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"); //$NON-NLS-1$
Object newStyle = findStyleNumber(newAttributes, Constants.STSection);
if (oldStyle != newStyle)
{
if (oldStyle != null)
{
resetSectionAttributes(current);
}
if (newStyle != null)
{
writeControlWord("ds", ((Integer)newStyle).intValue()); //$NON-NLS-1$
current.addAttribute("sectionStyle", newStyle); //$NON-NLS-1$
}
else
{
current.removeAttribute("sectionStyle"); //$NON-NLS-1$
}
}
}
checkControlWords(current, newAttributes, RTFAttributes.attributes, RTFAttribute.D_SECTION);
}
protected void resetSectionAttributes(MutableAttributeSet currentAttributes) throws IOException
{
writeControlWord("sectd"); //$NON-NLS-1$
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"); //$NON-NLS-1$
}
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"); //$NON-NLS-1$
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()); //$NON-NLS-1$
current.addAttribute("paragraphStyle", newStyle); //$NON-NLS-1$
}
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"); //$NON-NLS-1$
break;
case TabStop.ALIGN_CENTER :
writeControlWord("tqc"); //$NON-NLS-1$
break;
case TabStop.ALIGN_DECIMAL :
writeControlWord("tqdec"); //$NON-NLS-1$
break;
}
switch (tab.getLeader())
{
case TabStop.LEAD_NONE :
break;
case TabStop.LEAD_DOTS :
writeControlWord("tldot"); //$NON-NLS-1$
break;
case TabStop.LEAD_HYPHENS :
writeControlWord("tlhyph"); //$NON-NLS-1$
break;
case TabStop.LEAD_UNDERLINE :
writeControlWord("tlul"); //$NON-NLS-1$
break;
case TabStop.LEAD_THICKLINE :
writeControlWord("tlth"); //$NON-NLS-1$
break;
case TabStop.LEAD_EQUALS :
writeControlWord("tleq"); //$NON-NLS-1$
break;
}
int twips = Math.round(20f * tab.getPosition());
if (tab.getAlignment() == TabStop.ALIGN_BAR)
{
writeControlWord("tb", twips); //$NON-NLS-1$
}
else
{
writeControlWord("tx", twips); //$NON-NLS-1$
}
}
current.addAttribute(Constants.Tabs, tabs);
}
}
public void writeParagraphElement(Element el, boolean lastElement) throws IOException
{
updateParagraphAttributes(outputAttributes, el.getAttributes(), true);
int sub_count = el.getElementCount();
for (int idx = 0; idx < sub_count; idx++)
{
writeTextElement(el.getElement(idx));
}
if (!lastElement) writeControlWord("par"); //$NON-NLS-1$
}
/*
* 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"); //$NON-NLS-1$
currentAttributes.addAttribute(StyleConstants.Alignment, Zero);
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"); //$NON-NLS-1$
currentAttributes.removeAttribute(Constants.Tabs);
}
void updateCharacterAttributes(MutableAttributeSet current, AttributeSet newAttributes, boolean updateStyleChanges) throws IOException
{
Object parm;
if (updateStyleChanges)
{
Object oldStyle = current.getAttribute("characterStyle"); //$NON-NLS-1$
Object newStyle = findStyleNumber(newAttributes, Constants.STCharacter);
if (oldStyle != newStyle)
{
if (oldStyle != null)
{
resetCharacterAttributes(current);
}
if (newStyle != null)
{
writeControlWord("cs", ((Integer)newStyle).intValue()); //$NON-NLS-1$
current.addAttribute("characterStyle", newStyle); //$NON-NLS-1$
}
else
{
current.removeAttribute("characterStyle"); //$NON-NLS-1$
}
}
}
if ((parm = attrDiff(current, newAttributes, StyleConstants.FontFamily, null)) != null)
{
Number fontNum = (Number)fontTable.get(parm);
writeControlWord("f", fontNum.intValue()); //$NON-NLS-1$
}
checkNumericControlWord(current, newAttributes, StyleConstants.FontSize, "fs", //$NON-NLS-1$
defaultFontSize, 2f);
checkControlWords(current, newAttributes, RTFAttributes.attributes, RTFAttribute.D_CHARACTER);
checkNumericControlWord(current, newAttributes, StyleConstants.LineSpacing, "sl", //$NON-NLS-1$
0, 20f); /* TODO: sl wackiness */
if ((parm = attrDiff(current, newAttributes, StyleConstants.Background, MagicToken)) != null)
{
int colorNum;
if (parm == MagicToken) colorNum = 0;
else colorNum = ((Number)colorTable.get(parm)).intValue();
writeControlWord("cb", colorNum); //$NON-NLS-1$
}
if ((parm = attrDiff(current, newAttributes, StyleConstants.Foreground, null)) != null)
{
int colorNum;
if (parm == MagicToken) colorNum = 0;
else colorNum = ((Number)colorTable.get(parm)).intValue();
writeControlWord("cf", colorNum); //$NON-NLS-1$
}
}
protected void resetCharacterAttributes(MutableAttributeSet currentAttributes) throws IOException
{
writeControlWord("plain"); //$NON-NLS-1$
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"); //$NON-NLS-1$
}
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? */
Debug.error(ble);
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(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"); //$NON-NLS-1$
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); //$NON-NLS-1$
}
writeControlWord("u", ch); //$NON-NLS-1$
writeRawString(" "); //$NON-NLS-1$
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 "?"; //$NON-NLS-1$
}
/**
* 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 */
}
}