/* 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.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.StreamTokenizer; 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.MutableAttributeSet; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.Style; import javax.swing.text.StyleConstants; import javax.swing.text.StyleContext; import javax.swing.text.StyledDocument; import javax.swing.text.TabStop; /** * 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 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 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 straightforwardAttributes; static { straightforwardAttributes = RTFAttributes.attributesByKeyword(); } private final 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 textKeywords = null; static { textKeywords = new Hashtable(); textKeywords.put("\\", "\\"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("{", "{"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("}", "}"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put(" ", "\u00A0"); /* not in the spec... *///$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("~", "\u00A0"); /* nonbreaking space *///$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("_", "\u2011"); /* nonbreaking hyphen *///$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("bullet", "\u2022"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("emdash", "\u2014"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("emspace", "\u2003"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("endash", "\u2013"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("enspace", "\u2002"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("ldblquote", "\u201C"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("lquote", "\u2018"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("ltrmark", "\u200E"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("rdblquote", "\u201D"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("rquote", "\u2019"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("rtlmark", "\u200F"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("tab", "\u0009"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("zwj", "\u200D"); //$NON-NLS-1$ //$NON-NLS-2$ textKeywords.put("zwnj", "\u200C"); //$NON-NLS-1$ //$NON-NLS-2$ /* * There is no Unicode equivalent to an optional hyphen, as far as I can tell. */ textKeywords.put("-", "\u2027"); /* TODO: optional hyphen *///$NON-NLS-1$ //$NON-NLS-2$ } /* some entries in parserState */ static final String TabAlignmentKey = "tab_alignment"; //$NON-NLS-1$ static final String TabLeaderKey = "tab_leader"; //$NON-NLS-1$ static Dictionary characterSets; static boolean useNeXTForAnsi = false; static { characterSets = new Hashtable(); } /* 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(); fontTable = new Hashtable(); rtfversion = -1; mockery = new MockAttributeSet(); documentAttributes = new SimpleAttributeSet(); } /** * Called when the RTFParser encounters a bin keyword in the RTF stream. * * @see RTFParser */ @Override 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. */ @Override 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."); //$NON-NLS-1$ } /** 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 */ @Override 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"); //$NON-NLS-1$ if (oldSaveState != null) parserState.remove("_savedState"); //$NON-NLS-1$ Dictionary saveState = (Dictionary)((Hashtable)parserState).clone(); if (oldSaveState != null) saveState.put("_savedState", oldSaveState); //$NON-NLS-1$ parserState.put("_savedState", saveState); //$NON-NLS-1$ 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 */ @Override public void endgroup() { if (skippingCharacters > 0) { /* NB this indicates an error in the RTF. Log it? */ skippingCharacters = 0; } Dictionary restoredState = (Dictionary)parserState.get("_savedState"); //$NON-NLS-1$ Destination restoredDestination = (Destination)restoredState.get("dst"); //$NON-NLS-1$ 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"); //$NON-NLS-1$ if (previousState != null) { if (rtfDestination != previousState.get("dst")) { //$NON-NLS-1$ warning("Warning, RTF destination overridden, invalid RTF."); //$NON-NLS-1$ rtfDestination.close(); } } rtfDestination = newDestination; parserState.put("dst", rtfDestination); //$NON-NLS-1$ } /** * Called by the user when there is no more input (<i>i.e.</i>, at the end of the RTF file.) * * @see OutputStream#close */ @Override 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."); //$NON-NLS-1$ 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 */ @Override public boolean handleKeyword(String keyword) { Object item; boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword; if (skippingCharacters > 0) { skippingCharacters--; return true; } ignoreGroupIfUnknownKeyword = false; if ((item = textKeywords.get(keyword)) != null) { handleText((String)item); return true; } if (keyword.equals("fonttbl")) { //$NON-NLS-1$ setRTFDestination(new FonttblDestination()); return true; } if (keyword.equals("colortbl")) { //$NON-NLS-1$ setRTFDestination(new ColortblDestination()); return true; } if (keyword.equals("stylesheet")) { //$NON-NLS-1$ setRTFDestination(new StylesheetDestination()); return true; } if (keyword.equals("info")) { //$NON-NLS-1$ setRTFDestination(new InfoDestination()); return false; } if (keyword.equals("mac")) { //$NON-NLS-1$ setCharacterSet("mac"); //$NON-NLS-1$ return true; } if (keyword.equals("ansi")) { //$NON-NLS-1$ if (useNeXTForAnsi) setCharacterSet("NeXT"); //$NON-NLS-1$ else setCharacterSet("ansi"); //$NON-NLS-1$ return true; } if (keyword.equals("next")) { //$NON-NLS-1$ setCharacterSet("NeXT"); //$NON-NLS-1$ return true; } if (keyword.equals("pc")) { //$NON-NLS-1$ setCharacterSet("cpg437"); /* IBM Code Page 437 *///$NON-NLS-1$ return true; } if (keyword.equals("pca")) { //$NON-NLS-1$ setCharacterSet("cpg850"); /* IBM Code Page 850 *///$NON-NLS-1$ return true; } if (keyword.equals("*")) { //$NON-NLS-1$ 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") || //$NON-NLS-1$ keyword.equals("aftnsep") || //$NON-NLS-1$ keyword.equals("aftnsepc") || //$NON-NLS-1$ keyword.equals("annotation") || //$NON-NLS-1$ keyword.equals("atnauthor") || //$NON-NLS-1$ keyword.equals("atnicn") || //$NON-NLS-1$ keyword.equals("atnid") || //$NON-NLS-1$ keyword.equals("atnref") || //$NON-NLS-1$ keyword.equals("atntime") || //$NON-NLS-1$ keyword.equals("atrfend") || //$NON-NLS-1$ keyword.equals("atrfstart") || //$NON-NLS-1$ keyword.equals("bkmkend") || //$NON-NLS-1$ keyword.equals("bkmkstart") || //$NON-NLS-1$ keyword.equals("datafield") || //$NON-NLS-1$ keyword.equals("do") || //$NON-NLS-1$ keyword.equals("dptxbxtext") || //$NON-NLS-1$ keyword.equals("falt") || //$NON-NLS-1$ keyword.equals("field") || //$NON-NLS-1$ keyword.equals("file") || //$NON-NLS-1$ keyword.equals("filetbl") || //$NON-NLS-1$ keyword.equals("fname") || //$NON-NLS-1$ keyword.equals("fontemb") || //$NON-NLS-1$ keyword.equals("fontfile") || //$NON-NLS-1$ keyword.equals("footer") || //$NON-NLS-1$ keyword.equals("footerf") || //$NON-NLS-1$ keyword.equals("footerl") || //$NON-NLS-1$ keyword.equals("footerr") || //$NON-NLS-1$ keyword.equals("footnote") || //$NON-NLS-1$ keyword.equals("ftncn") || //$NON-NLS-1$ keyword.equals("ftnsep") || //$NON-NLS-1$ keyword.equals("ftnsepc") || //$NON-NLS-1$ keyword.equals("header") || //$NON-NLS-1$ keyword.equals("headerf") || //$NON-NLS-1$ keyword.equals("headerl") || //$NON-NLS-1$ keyword.equals("headerr") || //$NON-NLS-1$ keyword.equals("keycode") || //$NON-NLS-1$ keyword.equals("nextfile") || //$NON-NLS-1$ keyword.equals("object") || //$NON-NLS-1$ keyword.equals("pict") || //$NON-NLS-1$ keyword.equals("pn") || //$NON-NLS-1$ keyword.equals("pnseclvl") || //$NON-NLS-1$ keyword.equals("pntxtb") || //$NON-NLS-1$ keyword.equals("pntxta") || //$NON-NLS-1$ keyword.equals("revtbl") || //$NON-NLS-1$ keyword.equals("rxe") || //$NON-NLS-1$ keyword.equals("tc") || //$NON-NLS-1$ keyword.equals("template") || //$NON-NLS-1$ keyword.equals("txe") || //$NON-NLS-1$ keyword.equals("xe")) { //$NON-NLS-1$ 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 */ @Override public boolean handleKeyword(String keyword, int parameter) { boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword; if (skippingCharacters > 0) { skippingCharacters--; return true; } ignoreGroupIfUnknownKeyword = false; if (keyword.equals("cb")) //$NON-NLS-1$ { parserState.put(keyword, new Integer(parameter)); return true; } if (keyword.equals("uc")) //$NON-NLS-1$ { /* count of characters to skip after a unicode character */ parserState.put("UnicodeSkip", new Integer(parameter)); //$NON-NLS-1$ return true; } if (keyword.equals("u")) { //$NON-NLS-1$ if (parameter < 0) parameter = parameter + 65536; handleText((char)parameter); Number skip = (Number)(parserState.get("UnicodeSkip")); //$NON-NLS-1$ if (skip != null) { skippingCharacters = skip.intValue(); } else { skippingCharacters = 1; } return true; } if (keyword.equals("rtf")) { //$NON-NLS-1$ rtfversion = parameter; setRTFDestination(new DocumentDestination()); return true; } if (keyword.startsWith("NeXT") || //$NON-NLS-1$ keyword.equals("private")) //$NON-NLS-1$ 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); //$NON-NLS-1$ //$NON-NLS-2$ set = null; } if (set != null) { translationTable = (char[])set; } else { warning("Unknown RTF character set \"" + name + "\""); //$NON-NLS-1$ //$NON-NLS-2$ if (!name.equals("ansi")) { //$NON-NLS-1$ try { translationTable = (char[])getCharacterSet("ansi"); //$NON-NLS-1$ } catch (IOException e) { throw new InternalError("RTFReader: Unable to find character set resources (" + e + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } } } 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."); //$NON-NLS-1$ 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; set = (char[])characterSets.get(name); if (set == null) { InputStream charsetStream; charsetStream = (InputStream)java.security.AccessController.doPrivileged(new java.security.PrivilegedAction() { public Object run() { return RTFReader.class.getResourceAsStream("charsets/" + name + ".txt"); //$NON-NLS-1$ //$NON-NLS-2$ } }); 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))); 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 + ")"); //$NON-NLS-1$ //$NON-NLS-2$ } if (ttype != StreamTokenizer.TT_NUMBER) { // System.out.println("Bad token: type=" + ttype + " tok=" + in.sval); throw new IOException("Unexpected token in character set file"); //$NON-NLS-1$ // 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; String nextFontFamily; public void handleBinaryBlob(byte[] data) { /* Discard binary blobs. */ } /* * TODO do these routines work correctly if a write buffer divides a font name? (Probably not. Should allow for it as rare case) */ public void handleText(String text) { int semicolon = text.indexOf(';'); String fontName; Object fontNum; /* an Integer, but we don't care */ if (semicolon > 0) fontName = text.substring(0, semicolon); else fontName = text; /* TODO: do something with the font family. */ fontTable.put(new Integer(nextFontNumber), fontName); nextFontNumber = -1; nextFontFamily = null; return; } 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")) { //$NON-NLS-1$ 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 nums = fontTable.keys(); warning("Done reading font table."); //$NON-NLS-1$ while (nums.hasMoreElements()) { Integer num = (Integer)nums.nextElement(); warning("Number " + num + ": " + fontTable.get(num)); //$NON-NLS-1$ //$NON-NLS-2$ } } } /** * 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 proTemTable; public ColortblDestination() { red = 0; green = 0; blue = 0; proTemTable = new Vector(); } public void handleText(String text) { int index = 0; 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."); //$NON-NLS-1$ //$NON-NLS-2$ colorTable = new Color[count]; proTemTable.copyInto(colorTable); } public boolean handleKeyword(String keyword, int parameter) { if (keyword.equals("red")) //$NON-NLS-1$ red = parameter; else if (keyword.equals("green")) //$NON-NLS-1$ green = parameter; else if (keyword.equals("blue")) //$NON-NLS-1$ 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 definedStyles; public StylesheetDestination() { definedStyles = new Hashtable(); } @Override public void begingroup() { setRTFDestination(new StyleDefiningDestination()); } @Override public void close() { Vector chrStyles, pgfStyles, secStyles; chrStyles = new Vector(); pgfStyles = new Vector(); secStyles = new Vector(); Enumeration styles = definedStyles.elements(); while (styles.hasMoreElements()) { StyleDefiningDestination style; Style defined; style = (StyleDefiningDestination)styles.nextElement(); defined = style.realize(); warning("Style " + style.number + " (" + style.styleName + "): " + defined); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ String stype = (String)defined.getAttribute(Constants.StyleType); Vector 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; } @Override public void handleText(String text) { if (styleName != null) styleName = styleName + text; else styleName = text; } @Override public void close() { if (styleName != null) { int semicolon = styleName.indexOf(';'); if (semicolon > 0) { styleName = styleName.substring(0, semicolon); } } definedStyles.put(new Integer(number), this); super.close(); } @Override public boolean handleKeyword(String keyword) { if (keyword.equals("additive")) { //$NON-NLS-1$ additive = true; return true; } if (keyword.equals("shidden")) { //$NON-NLS-1$ hidden = true; return true; } return super.handleKeyword(keyword); } @Override public boolean handleKeyword(String keyword, int parameter) { if (keyword.equals("s")) { //$NON-NLS-1$ characterStyle = false; sectionStyle = false; number = parameter; } else if (keyword.equals("cs")) { //$NON-NLS-1$ characterStyle = true; sectionStyle = false; number = parameter; } else if (keyword.equals("ds")) { //$NON-NLS-1$ characterStyle = false; sectionStyle = true; number = parameter; } else if (keyword.equals("sbasedon")) { //$NON-NLS-1$ basedOn = parameter; } else if (keyword.equals("snext")) { //$NON-NLS-1$ 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 = (StyleDefiningDestination)definedStyles.get(new Integer(basedOn)); if (styleDest != null) { 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 = (StyleDefiningDestination)definedStyles.get(new Integer(nextStyle)); if (styleDest != null) { next = styleDest.realize(); } } if (next != null) realizedStyle.addAttribute(Constants.StyleNext, next); realizedStyle.addAttribute(Constants.StyleAdditive, new Boolean(additive)); realizedStyle.addAttribute(Constants.StyleHidden, new Boolean(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); //$NON-NLS-1$ paragraphAttributes = rootParagraphAttributes(); parserState.put("pgf", paragraphAttributes); //$NON-NLS-1$ sectionAttributes = rootSectionAttributes(); parserState.put("sec", sectionAttributes); //$NON-NLS-1$ } 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."); //$NON-NLS-1$ } 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); //$NON-NLS-1$ paragraphAttributes = new SimpleAttributeSet(); paragraphAttributes.addAttributes(paragraphParent); parserState.put("pgf", paragraphAttributes); //$NON-NLS-1$ sectionAttributes = new SimpleAttributeSet(); sectionAttributes.addAttributes(sectionParent); parserState.put("sec", sectionAttributes); //$NON-NLS-1$ } public void endgroup(Dictionary oldState) { characterAttributes = (MutableAttributeSet)parserState.get("chr"); //$NON-NLS-1$ paragraphAttributes = (MutableAttributeSet)parserState.get("pgf"); //$NON-NLS-1$ sectionAttributes = (MutableAttributeSet)parserState.get("sec"); //$NON-NLS-1$ } public void close() { } public boolean handleKeyword(String keyword) { if (keyword.equals("ulnone")) { //$NON-NLS-1$ return handleKeyword("ul", 0); //$NON-NLS-1$ } { Object item = straightforwardAttributes.get(keyword); if (item != null) { RTFAttribute attr = (RTFAttribute)item; 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")) { //$NON-NLS-1$ resetCharacterAttributes(); return true; } if (keyword.equals("pard")) { //$NON-NLS-1$ resetParagraphAttributes(); return true; } if (keyword.equals("sectd")) { //$NON-NLS-1$ resetSectionAttributes(); return true; } return false; } public boolean handleKeyword(String keyword, int parameter) { boolean booleanParameter = (parameter != 0); if (keyword.equals("fc")) //$NON-NLS-1$ keyword = "cf"; /* whatEVER, dude. *///$NON-NLS-1$ if (keyword.equals("f")) { //$NON-NLS-1$ parserState.put(keyword, new Integer(parameter)); return true; } if (keyword.equals("cf")) { //$NON-NLS-1$ parserState.put(keyword, new Integer(parameter)); return true; } { Object item = straightforwardAttributes.get(keyword); if (item != null) { RTFAttribute attr = (RTFAttribute)item; 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")) { //$NON-NLS-1$ StyleConstants.setFontSize(characterAttributes, (parameter / 2)); return true; } /* TODO: superscript/subscript */ if (keyword.equals("sl")) { //$NON-NLS-1$ 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")) { //$NON-NLS-1$ //$NON-NLS-2$ float tabPosition = parameter / 20f; int tabAlignment, tabLeader; Number item; tabAlignment = TabStop.ALIGN_LEFT; item = (Number)(parserState.get("tab_alignment")); //$NON-NLS-1$ if (item != null) tabAlignment = item.intValue(); tabLeader = TabStop.LEAD_NONE; item = (Number)(parserState.get("tab_leader")); //$NON-NLS-1$ if (item != null) tabLeader = item.intValue(); if (keyword.equals("tb")) //$NON-NLS-1$ tabAlignment = TabStop.ALIGN_BAR; parserState.remove("tab_alignment"); //$NON-NLS-1$ parserState.remove("tab_leader"); //$NON-NLS-1$ TabStop newStop = new TabStop(tabPosition, tabAlignment, tabLeader); Dictionary tabs; Integer stopCount; tabs = (Dictionary)parserState.get("_tabs"); //$NON-NLS-1$ if (tabs == null) { tabs = new Hashtable(); parserState.put("_tabs", tabs); //$NON-NLS-1$ stopCount = new Integer(1); } else { stopCount = (Integer)tabs.get("stop count"); //$NON-NLS-1$ stopCount = new Integer(1 + stopCount.intValue()); } tabs.put(stopCount, newStop); tabs.put("stop count", stopCount); //$NON-NLS-1$ parserState.remove("_tabs_immutable"); //$NON-NLS-1$ return true; } if (keyword.equals("s") && //$NON-NLS-1$ paragraphStyles != null) { parserState.put("paragraphStyle", paragraphStyles[parameter]); //$NON-NLS-1$ return true; } if (keyword.equals("cs") && //$NON-NLS-1$ characterStyles != null) { parserState.put("characterStyle", characterStyles[parameter]); //$NON-NLS-1$ return true; } if (keyword.equals("ds") && //$NON-NLS-1$ sectionStyles != null) { parserState.put("sectionStyle", sectionStyles[parameter]); //$NON-NLS-1$ 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"); //$NON-NLS-1$ /* note setFontFamily() can not handle a null font */ String fontFamily; if (fontnum != null) fontFamily = (String)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"); //$NON-NLS-1$ 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"); //$NON-NLS-1$ 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"); //$NON-NLS-1$ 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"); //$NON-NLS-1$ if (tabs == null) { Dictionary workingTabs = (Dictionary)parserState.get("_tabs"); //$NON-NLS-1$ if (workingTabs != null) { int count = ((Integer)workingTabs.get("stop count")).intValue(); //$NON-NLS-1$ tabs = new TabStop[count]; for (int ix = 1; ix <= count; ix++) tabs[ix - 1] = (TabStop)workingTabs.get(new Integer(ix)); parserState.put("_tabs_immutable", tabs); //$NON-NLS-1$ } } if (tabs != null) bld.addAttribute(Constants.Tabs, tabs); Style paragraphStyle = (Style)parserState.get("paragraphStyle"); //$NON-NLS-1$ 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"); //$NON-NLS-1$ 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); //$NON-NLS-1$ handleKeyword("cf", 0); //$NON-NLS-1$ handleKeyword("fs", 24); /* 12 pt. *///$NON-NLS-1$ Enumeration attributes = straightforwardAttributes.elements(); while (attributes.hasMoreElements()) { RTFAttribute attr = (RTFAttribute)attributes.nextElement(); if (attr.domain() == RTFAttribute.D_CHARACTER) attr.setDefault(characterAttributes); } handleKeyword("sl", 1000); //$NON-NLS-1$ parserState.remove("characterStyle"); //$NON-NLS-1$ } /** * 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"); //$NON-NLS-1$ parserState.remove("_tabs_immutable"); //$NON-NLS-1$ parserState.remove("paragraphStyle"); //$NON-NLS-1$ StyleConstants.setAlignment(paragraphAttributes, StyleConstants.ALIGN_LEFT); Enumeration attributes = straightforwardAttributes.elements(); while (attributes.hasMoreElements()) { RTFAttribute attr = (RTFAttribute)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 attributes = straightforwardAttributes.elements(); while (attributes.hasMoreElements()) { RTFAttribute attr = (RTFAttribute)attributes.nextElement(); if (attr.domain() == RTFAttribute.D_SECTION) attr.setDefault(characterAttributes); } parserState.remove("sectionStyle"); //$NON-NLS-1$ } } /** * 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; } @Override public void handleText(String text) { if (!inParagraph) beginParagraph(); deliverText(text, currentTextAttributes()); } abstract void deliverText(String text, AttributeSet characterAttributes); @Override public void close() { if (inParagraph) inParagraph = false; super.close(); } @Override public boolean handleKeyword(String keyword) { if (keyword.equals("\r") || keyword.equals("\n")) { //$NON-NLS-1$ //$NON-NLS-2$ keyword = "par"; //$NON-NLS-1$ } if (keyword.equals("par")) { //$NON-NLS-1$ // warnings.println("Ending paragraph."); endParagraph(); return true; } if (keyword.equals("sect")) { //$NON-NLS-1$ // 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 { @Override 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()); } } @Override public void finishParagraph(AttributeSet pgfAttributes, AttributeSet chrAttributes) { int pgfEndPosition = target.getLength(); try { target.insertString(pgfEndPosition, "\n", chrAttributes); //$NON-NLS-1$ 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()); } } @Override public void endSection() { /* If we implemented sections, we'd end 'em here */ } } }