/* * Copyright (c) 1997, 2008, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code 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 General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javax.swing.text.rtf; import java.lang.*; import java.util.*; import java.io.*; import java.awt.Font; import java.awt.Color; import javax.swing.text.*; /** * Takes a sequence of RTF tokens and text and appends the text * described by the RTF to a <code>StyledDocument</code> (the <em>target</em>). * The RTF is lexed * from the character stream by the <code>RTFParser</code> which is this class's * superclass. * * This class is an indirect subclass of OutputStream. It must be closed * in order to guarantee that all of the text has been sent to * the text acceptor. * * @see RTFParser * @see java.io.OutputStream */ class RTFReader extends RTFParser { /** The object to which the parsed text is sent. */ StyledDocument target; /** Miscellaneous information about the parser's state. This * dictionary is saved and restored when an RTF group begins * or ends. */ Dictionary<Object, Object> parserState; /* Current parser state */ /** This is the "dst" item from parserState. rtfDestination * is the current rtf destination. It is cached in an instance * variable for speed. */ Destination rtfDestination; /** This holds the current document attributes. */ MutableAttributeSet documentAttributes; /** This Dictionary maps Integer font numbers to String font names. */ Dictionary<Integer, String> fontTable; /** This array maps color indices to Color objects. */ Color[] colorTable; /** This array maps character style numbers to Style objects. */ Style[] characterStyles; /** This array maps paragraph style numbers to Style objects. */ Style[] paragraphStyles; /** This array maps section style numbers to Style objects. */ Style[] sectionStyles; /** This is the RTF version number, extracted from the \rtf keyword. * The version information is currently not used. */ int rtfversion; /** <code>true</code> to indicate that if the next keyword is unknown, * the containing group should be ignored. */ boolean ignoreGroupIfUnknownKeyword; /** The parameter of the most recently parsed \\ucN keyword, * used for skipping alternative representations after a * Unicode character. */ int skippingCharacters; static private Dictionary<String, RTFAttribute> straightforwardAttributes; static { straightforwardAttributes = RTFAttributes.attributesByKeyword(); } private MockAttributeSet mockery; /* this should be final, but there's a bug in javac... */ /** textKeywords maps RTF keywords to single-character strings, * for those keywords which simply insert some text. */ static Dictionary<String, String> textKeywords = null; static { textKeywords = new Hashtable<String, String>(); textKeywords.put("\\", "\\"); textKeywords.put("{", "{"); textKeywords.put("}", "}"); textKeywords.put(" ", "\u00A0"); /* not in the spec... */ textKeywords.put("~", "\u00A0"); /* nonbreaking space */ textKeywords.put("_", "\u2011"); /* nonbreaking hyphen */ textKeywords.put("bullet", "\u2022"); textKeywords.put("emdash", "\u2014"); textKeywords.put("emspace", "\u2003"); textKeywords.put("endash", "\u2013"); textKeywords.put("enspace", "\u2002"); textKeywords.put("ldblquote", "\u201C"); textKeywords.put("lquote", "\u2018"); textKeywords.put("ltrmark", "\u200E"); textKeywords.put("rdblquote", "\u201D"); textKeywords.put("rquote", "\u2019"); textKeywords.put("rtlmark", "\u200F"); textKeywords.put("tab", "\u0009"); textKeywords.put("zwj", "\u200D"); textKeywords.put("zwnj", "\u200C"); /* There is no Unicode equivalent to an optional hyphen, as far as I can tell. */ textKeywords.put("-", "\u2027"); /* TODO: optional hyphen */ } /* some entries in parserState */ static final String TabAlignmentKey = "tab_alignment"; static final String TabLeaderKey = "tab_leader"; static Dictionary<String, char[]> characterSets; static boolean useNeXTForAnsi = false; static { characterSets = new Hashtable<String, char[]>(); } /* TODO: per-font font encodings ( \fcharset control word ) ? */ /** * Creates a new RTFReader instance. Text will be sent to * the specified TextAcceptor. * * @param destination The TextAcceptor which is to receive the text. */ public RTFReader(StyledDocument destination) { int i; target = destination; parserState = new Hashtable<Object, Object>(); fontTable = new Hashtable<Integer, String>(); rtfversion = -1; mockery = new MockAttributeSet(); documentAttributes = new SimpleAttributeSet(); } /** Called when the RTFParser encounters a bin keyword in the * RTF stream. * * @see RTFParser */ public void handleBinaryBlob(byte[] data) { if (skippingCharacters > 0) { /* a blob only counts as one character for skipping purposes */ skippingCharacters --; return; } /* someday, someone will want to do something with blobs */ } /** * Handles any pure text (containing no control characters) in the input * stream. Called by the superclass. */ public void handleText(String text) { if (skippingCharacters > 0) { if (skippingCharacters >= text.length()) { skippingCharacters -= text.length(); return; } else { text = text.substring(skippingCharacters); skippingCharacters = 0; } } if (rtfDestination != null) { rtfDestination.handleText(text); return; } warning("Text with no destination. oops."); } /** The default color for text which has no specified color. */ Color defaultColor() { return Color.black; } /** Called by the superclass when a new RTF group is begun. * This implementation saves the current <code>parserState</code>, and gives * the current destination a chance to save its own state. * @see RTFParser#begingroup */ public void begingroup() { if (skippingCharacters > 0) { /* TODO this indicates an error in the RTF. Log it? */ skippingCharacters = 0; } /* we do this little dance to avoid cloning the entire state stack and immediately throwing it away. */ Object oldSaveState = parserState.get("_savedState"); if (oldSaveState != null) parserState.remove("_savedState"); Dictionary<String, Object> saveState = (Dictionary<String, Object>)((Hashtable)parserState).clone(); if (oldSaveState != null) saveState.put("_savedState", oldSaveState); parserState.put("_savedState", saveState); if (rtfDestination != null) rtfDestination.begingroup(); } /** Called by the superclass when the current RTF group is closed. * This restores the parserState saved by <code>begingroup()</code> * as well as invoking the endgroup method of the current * destination. * @see RTFParser#endgroup */ public void endgroup() { if (skippingCharacters > 0) { /* NB this indicates an error in the RTF. Log it? */ skippingCharacters = 0; } Dictionary<Object, Object> restoredState = (Dictionary<Object, Object>)parserState.get("_savedState"); Destination restoredDestination = (Destination)restoredState.get("dst"); if (restoredDestination != rtfDestination) { rtfDestination.close(); /* allow the destination to clean up */ rtfDestination = restoredDestination; } Dictionary oldParserState = parserState; parserState = restoredState; if (rtfDestination != null) rtfDestination.endgroup(oldParserState); } protected void setRTFDestination(Destination newDestination) { /* Check that setting the destination won't close the current destination (should never happen) */ Dictionary previousState = (Dictionary)parserState.get("_savedState"); if (previousState != null) { if (rtfDestination != previousState.get("dst")) { warning("Warning, RTF destination overridden, invalid RTF."); rtfDestination.close(); } } rtfDestination = newDestination; parserState.put("dst", rtfDestination); } /** Called by the user when there is no more input (<i>i.e.</i>, * at the end of the RTF file.) * * @see OutputStream#close */ public void close() throws IOException { Enumeration docProps = documentAttributes.getAttributeNames(); while(docProps.hasMoreElements()) { Object propName = docProps.nextElement(); target.putProperty(propName, documentAttributes.getAttribute(propName)); } /* RTFParser should have ensured that all our groups are closed */ warning("RTF filter done."); super.close(); } /** * Handles a parameterless RTF keyword. This is called by the superclass * (RTFParser) when a keyword is found in the input stream. * * @returns <code>true</code> if the keyword is recognized and handled; * <code>false</code> otherwise * @see RTFParser#handleKeyword */ public boolean handleKeyword(String keyword) { String item; boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword; if (skippingCharacters > 0) { skippingCharacters --; return true; } ignoreGroupIfUnknownKeyword = false; if ((item = textKeywords.get(keyword)) != null) { handleText(item); return true; } if (keyword.equals("fonttbl")) { setRTFDestination(new FonttblDestination()); return true; } if (keyword.equals("colortbl")) { setRTFDestination(new ColortblDestination()); return true; } if (keyword.equals("stylesheet")) { setRTFDestination(new StylesheetDestination()); return true; } if (keyword.equals("info")) { setRTFDestination(new InfoDestination()); return false; } if (keyword.equals("mac")) { setCharacterSet("mac"); return true; } if (keyword.equals("ansi")) { if (useNeXTForAnsi) setCharacterSet("NeXT"); else setCharacterSet("ansi"); return true; } if (keyword.equals("next")) { setCharacterSet("NeXT"); return true; } if (keyword.equals("pc")) { setCharacterSet("cpg437"); /* IBM Code Page 437 */ return true; } if (keyword.equals("pca")) { setCharacterSet("cpg850"); /* IBM Code Page 850 */ return true; } if (keyword.equals("*")) { ignoreGroupIfUnknownKeyword = true; return true; } if (rtfDestination != null) { if(rtfDestination.handleKeyword(keyword)) return true; } /* this point is reached only if the keyword is unrecognized */ /* other destinations we don't understand and therefore ignore */ if (keyword.equals("aftncn") || keyword.equals("aftnsep") || keyword.equals("aftnsepc") || keyword.equals("annotation") || keyword.equals("atnauthor") || keyword.equals("atnicn") || keyword.equals("atnid") || keyword.equals("atnref") || keyword.equals("atntime") || keyword.equals("atrfend") || keyword.equals("atrfstart") || keyword.equals("bkmkend") || keyword.equals("bkmkstart") || keyword.equals("datafield") || keyword.equals("do") || keyword.equals("dptxbxtext") || keyword.equals("falt") || keyword.equals("field") || keyword.equals("file") || keyword.equals("filetbl") || keyword.equals("fname") || keyword.equals("fontemb") || keyword.equals("fontfile") || keyword.equals("footer") || keyword.equals("footerf") || keyword.equals("footerl") || keyword.equals("footerr") || keyword.equals("footnote") || keyword.equals("ftncn") || keyword.equals("ftnsep") || keyword.equals("ftnsepc") || keyword.equals("header") || keyword.equals("headerf") || keyword.equals("headerl") || keyword.equals("headerr") || keyword.equals("keycode") || keyword.equals("nextfile") || keyword.equals("object") || keyword.equals("pict") || keyword.equals("pn") || keyword.equals("pnseclvl") || keyword.equals("pntxtb") || keyword.equals("pntxta") || keyword.equals("revtbl") || keyword.equals("rxe") || keyword.equals("tc") || keyword.equals("template") || keyword.equals("txe") || keyword.equals("xe")) { ignoreGroupIfUnknownKeywordSave = true; } if (ignoreGroupIfUnknownKeywordSave) { setRTFDestination(new DiscardingDestination()); } return false; } /** * Handles an RTF keyword and its integer parameter. * This is called by the superclass * (RTFParser) when a keyword is found in the input stream. * * @returns <code>true</code> if the keyword is recognized and handled; * <code>false</code> otherwise * @see RTFParser#handleKeyword */ public boolean handleKeyword(String keyword, int parameter) { boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword; if (skippingCharacters > 0) { skippingCharacters --; return true; } ignoreGroupIfUnknownKeyword = false; if (keyword.equals("uc")) { /* count of characters to skip after a unicode character */ parserState.put("UnicodeSkip", Integer.valueOf(parameter)); return true; } if (keyword.equals("u")) { if (parameter < 0) parameter = parameter + 65536; handleText((char)parameter); Number skip = (Number)(parserState.get("UnicodeSkip")); if (skip != null) { skippingCharacters = skip.intValue(); } else { skippingCharacters = 1; } return true; } if (keyword.equals("rtf")) { rtfversion = parameter; setRTFDestination(new DocumentDestination()); return true; } if (keyword.startsWith("NeXT") || keyword.equals("private")) ignoreGroupIfUnknownKeywordSave = true; if (rtfDestination != null) { if(rtfDestination.handleKeyword(keyword, parameter)) return true; } /* this point is reached only if the keyword is unrecognized */ if (ignoreGroupIfUnknownKeywordSave) { setRTFDestination(new DiscardingDestination()); } return false; } private void setTargetAttribute(String name, Object value) { // target.changeAttributes(new LFDictionary(LFArray.arrayWithObject(value), LFArray.arrayWithObject(name))); } /** * setCharacterSet sets the current translation table to correspond with * the named character set. The character set is loaded if necessary. * * @see AbstractFilter */ public void setCharacterSet(String name) { Object set; try { set = getCharacterSet(name); } catch (Exception e) { warning("Exception loading RTF character set \"" + name + "\": " + e); set = null; } if (set != null) { translationTable = (char[])set; } else { warning("Unknown RTF character set \"" + name + "\""); if (!name.equals("ansi")) { try { translationTable = (char[])getCharacterSet("ansi"); } catch (IOException e) { throw new InternalError("RTFReader: Unable to find character set resources (" + e + ")", e); } } } setTargetAttribute(Constants.RTFCharacterSet, name); } /** Adds a character set to the RTFReader's list * of known character sets */ public static void defineCharacterSet(String name, char[] table) { if (table.length < 256) throw new IllegalArgumentException("Translation table must have 256 entries."); characterSets.put(name, table); } /** Looks up a named character set. A character set is a 256-entry * array of characters, mapping unsigned byte values to their Unicode * equivalents. The character set is loaded if necessary. * * @returns the character set */ public static Object getCharacterSet(final String name) throws IOException { char[] set = characterSets.get(name); if (set == null) { InputStream charsetStream; charsetStream = java.security.AccessController. doPrivileged(new java.security.PrivilegedAction<InputStream>() { public InputStream run() { return RTFReader.class.getResourceAsStream ("charsets/" + name + ".txt"); } }); set = readCharset(charsetStream); defineCharacterSet(name, set); } return set; } /** Parses a character set from an InputStream. The character set * must contain 256 decimal integers, separated by whitespace, with * no punctuation. B- and C- style comments are allowed. * * @returns the newly read character set */ static char[] readCharset(InputStream strm) throws IOException { char[] values = new char[256]; int i; StreamTokenizer in = new StreamTokenizer(new BufferedReader( new InputStreamReader(strm, "ISO-8859-1"))); in.eolIsSignificant(false); in.commentChar('#'); in.slashSlashComments(true); in.slashStarComments(true); i = 0; while (i < 256) { int ttype; try { ttype = in.nextToken(); } catch (Exception e) { throw new IOException("Unable to read from character set file (" + e + ")"); } if (ttype != in.TT_NUMBER) { // System.out.println("Bad token: type=" + ttype + " tok=" + in.sval); throw new IOException("Unexpected token in character set file"); // continue; } values[i] = (char)(in.nval); i++; } return values; } static char[] readCharset(java.net.URL href) throws IOException { return readCharset(href.openStream()); } /** An interface (could be an entirely abstract class) describing * a destination. The RTF reader always has a current destination * which is where text is sent. * * @see RTFReader */ interface Destination { void handleBinaryBlob(byte[] data); void handleText(String text); boolean handleKeyword(String keyword); boolean handleKeyword(String keyword, int parameter); void begingroup(); void endgroup(Dictionary oldState); void close(); } /** This data-sink class is used to implement ignored destinations * (e.g. {\*\blegga blah blah blah} ) * It accepts all keywords and text but does nothing with them. */ class DiscardingDestination implements Destination { public void handleBinaryBlob(byte[] data) { /* Discard binary blobs. */ } public void handleText(String text) { /* Discard text. */ } public boolean handleKeyword(String text) { /* Accept and discard keywords. */ return true; } public boolean handleKeyword(String text, int parameter) { /* Accept and discard parameterized keywords. */ return true; } public void begingroup() { /* Ignore groups --- the RTFReader will keep track of the current group level as necessary */ } public void endgroup(Dictionary oldState) { /* Ignore groups */ } public void close() { /* No end-of-destination cleanup needed */ } } /** Reads the fonttbl group, inserting fonts into the RTFReader's * fontTable dictionary. */ class FonttblDestination implements Destination { int nextFontNumber; Integer fontNumberKey = null; String nextFontFamily; public void handleBinaryBlob(byte[] data) { /* Discard binary blobs. */ } public void handleText(String text) { int semicolon = text.indexOf(';'); String fontName; if (semicolon > -1) fontName = text.substring(0, semicolon); else fontName = text; /* TODO: do something with the font family. */ if (nextFontNumber == -1 && fontNumberKey != null) { //font name might be broken across multiple calls fontName = fontTable.get(fontNumberKey) + fontName; } else { fontNumberKey = Integer.valueOf(nextFontNumber); } fontTable.put(fontNumberKey, fontName); nextFontNumber = -1; nextFontFamily = null; } public boolean handleKeyword(String keyword) { if (keyword.charAt(0) == 'f') { nextFontFamily = keyword.substring(1); return true; } return false; } public boolean handleKeyword(String keyword, int parameter) { if (keyword.equals("f")) { nextFontNumber = parameter; return true; } return false; } /* Groups are irrelevant. */ public void begingroup() {} public void endgroup(Dictionary oldState) {} /* currently, the only thing we do when the font table ends is dump its contents to the debugging log. */ public void close() { Enumeration<Integer> nums = fontTable.keys(); warning("Done reading font table."); while(nums.hasMoreElements()) { Integer num = nums.nextElement(); warning("Number " + num + ": " + fontTable.get(num)); } } } /** Reads the colortbl group. Upon end-of-group, the RTFReader's * color table is set to an array containing the read colors. */ class ColortblDestination implements Destination { int red, green, blue; Vector<Color> proTemTable; public ColortblDestination() { red = 0; green = 0; blue = 0; proTemTable = new Vector<Color>(); } public void handleText(String text) { int index; for (index = 0; index < text.length(); index ++) { if (text.charAt(index) == ';') { Color newColor; newColor = new Color(red, green, blue); proTemTable.addElement(newColor); } } } public void close() { int count = proTemTable.size(); warning("Done reading color table, " + count + " entries."); colorTable = new Color[count]; proTemTable.copyInto(colorTable); } public boolean handleKeyword(String keyword, int parameter) { if (keyword.equals("red")) red = parameter; else if (keyword.equals("green")) green = parameter; else if (keyword.equals("blue")) blue = parameter; else return false; return true; } /* Colortbls don't understand any parameterless keywords */ public boolean handleKeyword(String keyword) { return false; } /* Groups are irrelevant. */ public void begingroup() {} public void endgroup(Dictionary oldState) {} /* Shouldn't see any binary blobs ... */ public void handleBinaryBlob(byte[] data) {} } /** Handles the stylesheet keyword. Styles are read and sorted * into the three style arrays in the RTFReader. */ class StylesheetDestination extends DiscardingDestination implements Destination { Dictionary<Integer, StyleDefiningDestination> definedStyles; public StylesheetDestination() { definedStyles = new Hashtable<Integer, StyleDefiningDestination>(); } public void begingroup() { setRTFDestination(new StyleDefiningDestination()); } public void close() { Vector<Style> chrStyles = new Vector<Style>(); Vector<Style> pgfStyles = new Vector<Style>(); Vector<Style> secStyles = new Vector<Style>(); Enumeration<StyleDefiningDestination> styles = definedStyles.elements(); while(styles.hasMoreElements()) { StyleDefiningDestination style; Style defined; style = styles.nextElement(); defined = style.realize(); warning("Style "+style.number+" ("+style.styleName+"): "+defined); String stype = (String)defined.getAttribute(Constants.StyleType); Vector<Style> toSet; if (stype.equals(Constants.STSection)) { toSet = secStyles; } else if (stype.equals(Constants.STCharacter)) { toSet = chrStyles; } else { toSet = pgfStyles; } if (toSet.size() <= style.number) toSet.setSize(style.number + 1); toSet.setElementAt(defined, style.number); } if (!(chrStyles.isEmpty())) { Style[] styleArray = new Style[chrStyles.size()]; chrStyles.copyInto(styleArray); characterStyles = styleArray; } if (!(pgfStyles.isEmpty())) { Style[] styleArray = new Style[pgfStyles.size()]; pgfStyles.copyInto(styleArray); paragraphStyles = styleArray; } if (!(secStyles.isEmpty())) { Style[] styleArray = new Style[secStyles.size()]; secStyles.copyInto(styleArray); sectionStyles = styleArray; } /* (old debugging code) int i, m; if (characterStyles != null) { m = characterStyles.length; for(i=0;i<m;i++) warnings.println("chrStyle["+i+"]="+characterStyles[i]); } else warnings.println("No character styles."); if (paragraphStyles != null) { m = paragraphStyles.length; for(i=0;i<m;i++) warnings.println("pgfStyle["+i+"]="+paragraphStyles[i]); } else warnings.println("No paragraph styles."); if (sectionStyles != null) { m = characterStyles.length; for(i=0;i<m;i++) warnings.println("secStyle["+i+"]="+sectionStyles[i]); } else warnings.println("No section styles."); */ } /** This subclass handles an individual style */ class StyleDefiningDestination extends AttributeTrackingDestination implements Destination { final int STYLENUMBER_NONE = 222; boolean additive; boolean characterStyle; boolean sectionStyle; public String styleName; public int number; int basedOn; int nextStyle; boolean hidden; Style realizedStyle; public StyleDefiningDestination() { additive = false; characterStyle = false; sectionStyle = false; styleName = null; number = 0; basedOn = STYLENUMBER_NONE; nextStyle = STYLENUMBER_NONE; hidden = false; } public void handleText(String text) { if (styleName != null) styleName = styleName + text; else styleName = text; } public void close() { int semicolon = (styleName == null) ? 0 : styleName.indexOf(';'); if (semicolon > 0) styleName = styleName.substring(0, semicolon); definedStyles.put(Integer.valueOf(number), this); super.close(); } public boolean handleKeyword(String keyword) { if (keyword.equals("additive")) { additive = true; return true; } if (keyword.equals("shidden")) { hidden = true; return true; } return super.handleKeyword(keyword); } public boolean handleKeyword(String keyword, int parameter) { if (keyword.equals("s")) { characterStyle = false; sectionStyle = false; number = parameter; } else if (keyword.equals("cs")) { characterStyle = true; sectionStyle = false; number = parameter; } else if (keyword.equals("ds")) { characterStyle = false; sectionStyle = true; number = parameter; } else if (keyword.equals("sbasedon")) { basedOn = parameter; } else if (keyword.equals("snext")) { nextStyle = parameter; } else { return super.handleKeyword(keyword, parameter); } return true; } public Style realize() { Style basis = null; Style next = null; if (realizedStyle != null) return realizedStyle; if (basedOn != STYLENUMBER_NONE) { StyleDefiningDestination styleDest; styleDest = definedStyles.get(Integer.valueOf(basedOn)); if (styleDest != null && styleDest != this) { basis = styleDest.realize(); } } /* NB: Swing StyleContext doesn't allow distinct styles with the same name; RTF apparently does. This may confuse the user. */ realizedStyle = target.addStyle(styleName, basis); if (characterStyle) { realizedStyle.addAttributes(currentTextAttributes()); realizedStyle.addAttribute(Constants.StyleType, Constants.STCharacter); } else if (sectionStyle) { realizedStyle.addAttributes(currentSectionAttributes()); realizedStyle.addAttribute(Constants.StyleType, Constants.STSection); } else { /* must be a paragraph style */ realizedStyle.addAttributes(currentParagraphAttributes()); realizedStyle.addAttribute(Constants.StyleType, Constants.STParagraph); } if (nextStyle != STYLENUMBER_NONE) { StyleDefiningDestination styleDest; styleDest = definedStyles.get(Integer.valueOf(nextStyle)); if (styleDest != null) { next = styleDest.realize(); } } if (next != null) realizedStyle.addAttribute(Constants.StyleNext, next); realizedStyle.addAttribute(Constants.StyleAdditive, Boolean.valueOf(additive)); realizedStyle.addAttribute(Constants.StyleHidden, Boolean.valueOf(hidden)); return realizedStyle; } } } /** Handles the info group. Currently no info keywords are recognized * so this is a subclass of DiscardingDestination. */ class InfoDestination extends DiscardingDestination implements Destination { } /** RTFReader.TextHandlingDestination is an abstract RTF destination * which simply tracks the attributes specified by the RTF control words * in internal form and can produce acceptable AttributeSets for the * current character, paragraph, and section attributes. It is up * to the subclasses to determine what is done with the actual text. */ abstract class AttributeTrackingDestination implements Destination { /** This is the "chr" element of parserState, cached for * more efficient use */ MutableAttributeSet characterAttributes; /** This is the "pgf" element of parserState, cached for * more efficient use */ MutableAttributeSet paragraphAttributes; /** This is the "sec" element of parserState, cached for * more efficient use */ MutableAttributeSet sectionAttributes; public AttributeTrackingDestination() { characterAttributes = rootCharacterAttributes(); parserState.put("chr", characterAttributes); paragraphAttributes = rootParagraphAttributes(); parserState.put("pgf", paragraphAttributes); sectionAttributes = rootSectionAttributes(); parserState.put("sec", sectionAttributes); } abstract public void handleText(String text); public void handleBinaryBlob(byte[] data) { /* This should really be in TextHandlingDestination, but * since *nobody* does anything with binary blobs, this * is more convenient. */ warning("Unexpected binary data in RTF file."); } public void begingroup() { AttributeSet characterParent = currentTextAttributes(); AttributeSet paragraphParent = currentParagraphAttributes(); AttributeSet sectionParent = currentSectionAttributes(); /* It would probably be more efficient to use the * resolver property of the attributes set for * implementing rtf groups, * but that's needed for styles. */ /* update the cached attribute dictionaries */ characterAttributes = new SimpleAttributeSet(); characterAttributes.addAttributes(characterParent); parserState.put("chr", characterAttributes); paragraphAttributes = new SimpleAttributeSet(); paragraphAttributes.addAttributes(paragraphParent); parserState.put("pgf", paragraphAttributes); sectionAttributes = new SimpleAttributeSet(); sectionAttributes.addAttributes(sectionParent); parserState.put("sec", sectionAttributes); } public void endgroup(Dictionary oldState) { characterAttributes = (MutableAttributeSet)parserState.get("chr"); paragraphAttributes = (MutableAttributeSet)parserState.get("pgf"); sectionAttributes = (MutableAttributeSet)parserState.get("sec"); } public void close() { } public boolean handleKeyword(String keyword) { if (keyword.equals("ulnone")) { return handleKeyword("ul", 0); } { RTFAttribute attr = straightforwardAttributes.get(keyword); if (attr != null) { boolean ok; switch(attr.domain()) { case RTFAttribute.D_CHARACTER: ok = attr.set(characterAttributes); break; case RTFAttribute.D_PARAGRAPH: ok = attr.set(paragraphAttributes); break; case RTFAttribute.D_SECTION: ok = attr.set(sectionAttributes); break; case RTFAttribute.D_META: mockery.backing = parserState; ok = attr.set(mockery); mockery.backing = null; break; case RTFAttribute.D_DOCUMENT: ok = attr.set(documentAttributes); break; default: /* should never happen */ ok = false; break; } if (ok) return true; } } if (keyword.equals("plain")) { resetCharacterAttributes(); return true; } if (keyword.equals("pard")) { resetParagraphAttributes(); return true; } if (keyword.equals("sectd")) { resetSectionAttributes(); return true; } return false; } public boolean handleKeyword(String keyword, int parameter) { boolean booleanParameter = (parameter != 0); if (keyword.equals("fc")) keyword = "cf"; /* whatEVER, dude. */ if (keyword.equals("f")) { parserState.put(keyword, Integer.valueOf(parameter)); return true; } if (keyword.equals("cf")) { parserState.put(keyword, Integer.valueOf(parameter)); return true; } { RTFAttribute attr = straightforwardAttributes.get(keyword); if (attr != null) { boolean ok; switch(attr.domain()) { case RTFAttribute.D_CHARACTER: ok = attr.set(characterAttributes, parameter); break; case RTFAttribute.D_PARAGRAPH: ok = attr.set(paragraphAttributes, parameter); break; case RTFAttribute.D_SECTION: ok = attr.set(sectionAttributes, parameter); break; case RTFAttribute.D_META: mockery.backing = parserState; ok = attr.set(mockery, parameter); mockery.backing = null; break; case RTFAttribute.D_DOCUMENT: ok = attr.set(documentAttributes, parameter); break; default: /* should never happen */ ok = false; break; } if (ok) return true; } } if (keyword.equals("fs")) { StyleConstants.setFontSize(characterAttributes, (parameter / 2)); return true; } /* TODO: superscript/subscript */ if (keyword.equals("sl")) { if (parameter == 1000) { /* magic value! */ characterAttributes.removeAttribute(StyleConstants.LineSpacing); } else { /* TODO: The RTF sl attribute has special meaning if it's negative. Make sure that SwingText has the same special meaning, or find a way to imitate that. When SwingText handles this, also recognize the slmult keyword. */ StyleConstants.setLineSpacing(characterAttributes, parameter / 20f); } return true; } /* TODO: Other kinds of underlining */ if (keyword.equals("tx") || keyword.equals("tb")) { float tabPosition = parameter / 20f; int tabAlignment, tabLeader; Number item; tabAlignment = TabStop.ALIGN_LEFT; item = (Number)(parserState.get("tab_alignment")); if (item != null) tabAlignment = item.intValue(); tabLeader = TabStop.LEAD_NONE; item = (Number)(parserState.get("tab_leader")); if (item != null) tabLeader = item.intValue(); if (keyword.equals("tb")) tabAlignment = TabStop.ALIGN_BAR; parserState.remove("tab_alignment"); parserState.remove("tab_leader"); TabStop newStop = new TabStop(tabPosition, tabAlignment, tabLeader); Dictionary<Object, Object> tabs; Integer stopCount; tabs = (Dictionary<Object, Object>)parserState.get("_tabs"); if (tabs == null) { tabs = new Hashtable<Object, Object>(); parserState.put("_tabs", tabs); stopCount = Integer.valueOf(1); } else { stopCount = (Integer)tabs.get("stop count"); stopCount = Integer.valueOf(1 + stopCount.intValue()); } tabs.put(stopCount, newStop); tabs.put("stop count", stopCount); parserState.remove("_tabs_immutable"); return true; } if (keyword.equals("s") && paragraphStyles != null) { parserState.put("paragraphStyle", paragraphStyles[parameter]); return true; } if (keyword.equals("cs") && characterStyles != null) { parserState.put("characterStyle", characterStyles[parameter]); return true; } if (keyword.equals("ds") && sectionStyles != null) { parserState.put("sectionStyle", sectionStyles[parameter]); return true; } return false; } /** Returns a new MutableAttributeSet containing the * default character attributes */ protected MutableAttributeSet rootCharacterAttributes() { MutableAttributeSet set = new SimpleAttributeSet(); /* TODO: default font */ StyleConstants.setItalic(set, false); StyleConstants.setBold(set, false); StyleConstants.setUnderline(set, false); StyleConstants.setForeground(set, defaultColor()); return set; } /** Returns a new MutableAttributeSet containing the * default paragraph attributes */ protected MutableAttributeSet rootParagraphAttributes() { MutableAttributeSet set = new SimpleAttributeSet(); StyleConstants.setLeftIndent(set, 0f); StyleConstants.setRightIndent(set, 0f); StyleConstants.setFirstLineIndent(set, 0f); /* TODO: what should this be, really? */ set.setResolveParent(target.getStyle(StyleContext.DEFAULT_STYLE)); return set; } /** Returns a new MutableAttributeSet containing the * default section attributes */ protected MutableAttributeSet rootSectionAttributes() { MutableAttributeSet set = new SimpleAttributeSet(); return set; } /** * Calculates the current text (character) attributes in a form suitable * for SwingText from the current parser state. * * @returns a new MutableAttributeSet containing the text attributes. */ MutableAttributeSet currentTextAttributes() { MutableAttributeSet attributes = new SimpleAttributeSet(characterAttributes); Integer fontnum; Integer stateItem; /* figure out the font name */ /* TODO: catch exceptions for undefined attributes, bad font indices, etc.? (as it stands, it is the caller's job to clean up after corrupt RTF) */ fontnum = (Integer)parserState.get("f"); /* note setFontFamily() can not handle a null font */ String fontFamily; if (fontnum != null) fontFamily = fontTable.get(fontnum); else fontFamily = null; if (fontFamily != null) StyleConstants.setFontFamily(attributes, fontFamily); else attributes.removeAttribute(StyleConstants.FontFamily); if (colorTable != null) { stateItem = (Integer)parserState.get("cf"); if (stateItem != null) { Color fg = colorTable[stateItem.intValue()]; StyleConstants.setForeground(attributes, fg); } else { /* AttributeSet dies if you set a value to null */ attributes.removeAttribute(StyleConstants.Foreground); } } if (colorTable != null) { stateItem = (Integer)parserState.get("cb"); if (stateItem != null) { Color bg = colorTable[stateItem.intValue()]; attributes.addAttribute(StyleConstants.Background, bg); } else { /* AttributeSet dies if you set a value to null */ attributes.removeAttribute(StyleConstants.Background); } } Style characterStyle = (Style)parserState.get("characterStyle"); if (characterStyle != null) attributes.setResolveParent(characterStyle); /* Other attributes are maintained directly in "attributes" */ return attributes; } /** * Calculates the current paragraph attributes (with keys * as given in StyleConstants) from the current parser state. * * @returns a newly created MutableAttributeSet. * @see StyleConstants */ MutableAttributeSet currentParagraphAttributes() { /* NB if there were a mutableCopy() method we should use it */ MutableAttributeSet bld = new SimpleAttributeSet(paragraphAttributes); Integer stateItem; /*** Tab stops ***/ TabStop tabs[]; tabs = (TabStop[])parserState.get("_tabs_immutable"); if (tabs == null) { Dictionary workingTabs = (Dictionary)parserState.get("_tabs"); if (workingTabs != null) { int count = ((Integer)workingTabs.get("stop count")).intValue(); tabs = new TabStop[count]; for (int ix = 1; ix <= count; ix ++) tabs[ix-1] = (TabStop)workingTabs.get(Integer.valueOf(ix)); parserState.put("_tabs_immutable", tabs); } } if (tabs != null) bld.addAttribute(Constants.Tabs, tabs); Style paragraphStyle = (Style)parserState.get("paragraphStyle"); if (paragraphStyle != null) bld.setResolveParent(paragraphStyle); return bld; } /** * Calculates the current section attributes * from the current parser state. * * @returns a newly created MutableAttributeSet. */ public AttributeSet currentSectionAttributes() { MutableAttributeSet attributes = new SimpleAttributeSet(sectionAttributes); Style sectionStyle = (Style)parserState.get("sectionStyle"); if (sectionStyle != null) attributes.setResolveParent(sectionStyle); return attributes; } /** Resets the filter's internal notion of the current character * attributes to their default values. Invoked to handle the * \plain keyword. */ protected void resetCharacterAttributes() { handleKeyword("f", 0); handleKeyword("cf", 0); handleKeyword("fs", 24); /* 12 pt. */ Enumeration<RTFAttribute> attributes = straightforwardAttributes.elements(); while(attributes.hasMoreElements()) { RTFAttribute attr = attributes.nextElement(); if (attr.domain() == RTFAttribute.D_CHARACTER) attr.setDefault(characterAttributes); } handleKeyword("sl", 1000); parserState.remove("characterStyle"); } /** Resets the filter's internal notion of the current paragraph's * attributes to their default values. Invoked to handle the * \pard keyword. */ protected void resetParagraphAttributes() { parserState.remove("_tabs"); parserState.remove("_tabs_immutable"); parserState.remove("paragraphStyle"); StyleConstants.setAlignment(paragraphAttributes, StyleConstants.ALIGN_LEFT); Enumeration<RTFAttribute> attributes = straightforwardAttributes.elements(); while(attributes.hasMoreElements()) { RTFAttribute attr = attributes.nextElement(); if (attr.domain() == RTFAttribute.D_PARAGRAPH) attr.setDefault(characterAttributes); } } /** Resets the filter's internal notion of the current section's * attributes to their default values. Invoked to handle the * \sectd keyword. */ protected void resetSectionAttributes() { Enumeration<RTFAttribute> attributes = straightforwardAttributes.elements(); while(attributes.hasMoreElements()) { RTFAttribute attr = attributes.nextElement(); if (attr.domain() == RTFAttribute.D_SECTION) attr.setDefault(characterAttributes); } parserState.remove("sectionStyle"); } } /** RTFReader.TextHandlingDestination provides basic text handling * functionality. Subclasses must implement: <dl> * <dt>deliverText()<dd>to handle a run of text with the same * attributes * <dt>finishParagraph()<dd>to end the current paragraph and * set the paragraph's attributes * <dt>endSection()<dd>to end the current section * </dl> */ abstract class TextHandlingDestination extends AttributeTrackingDestination implements Destination { /** <code>true</code> if the reader has not just finished * a paragraph; false upon startup */ boolean inParagraph; public TextHandlingDestination() { super(); inParagraph = false; } public void handleText(String text) { if (! inParagraph) beginParagraph(); deliverText(text, currentTextAttributes()); } abstract void deliverText(String text, AttributeSet characterAttributes); public void close() { if (inParagraph) endParagraph(); super.close(); } public boolean handleKeyword(String keyword) { if (keyword.equals("\r") || keyword.equals("\n")) { keyword = "par"; } if (keyword.equals("par")) { // warnings.println("Ending paragraph."); endParagraph(); return true; } if (keyword.equals("sect")) { // warnings.println("Ending section."); endSection(); return true; } return super.handleKeyword(keyword); } protected void beginParagraph() { inParagraph = true; } protected void endParagraph() { AttributeSet pgfAttributes = currentParagraphAttributes(); AttributeSet chrAttributes = currentTextAttributes(); finishParagraph(pgfAttributes, chrAttributes); inParagraph = false; } abstract void finishParagraph(AttributeSet pgfA, AttributeSet chrA); abstract void endSection(); } /** RTFReader.DocumentDestination is a concrete subclass of * TextHandlingDestination which appends the text to the * StyledDocument given by the <code>target</code> ivar of the * containing RTFReader. */ class DocumentDestination extends TextHandlingDestination implements Destination { public void deliverText(String text, AttributeSet characterAttributes) { try { target.insertString(target.getLength(), text, currentTextAttributes()); } catch (BadLocationException ble) { /* This shouldn't be able to happen, of course */ /* TODO is InternalError the correct error to throw? */ throw new InternalError(ble.getMessage(), ble); } } public void finishParagraph(AttributeSet pgfAttributes, AttributeSet chrAttributes) { int pgfEndPosition = target.getLength(); try { target.insertString(pgfEndPosition, "\n", chrAttributes); target.setParagraphAttributes(pgfEndPosition, 1, pgfAttributes, true); } catch (BadLocationException ble) { /* This shouldn't be able to happen, of course */ /* TODO is InternalError the correct error to throw? */ throw new InternalError(ble.getMessage(), ble); } } public void endSection() { /* If we implemented sections, we'd end 'em here */ } } }