/* 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 */ } }