1 /*
2  * Copyright (c) 1997, 2008, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 package javax.swing.text.rtf;
26 
27 import java.lang.*;
28 import java.util.*;
29 import java.awt.Color;
30 import java.awt.Font;
31 import java.io.OutputStream;
32 import java.io.IOException;
33 
34 import javax.swing.text.*;
35 
36 /**
37  * Generates an RTF output stream (java.io.OutputStream) from rich text
38  * (handed off through a series of LTTextAcceptor calls).  Can be used to
39  * generate RTF from any object which knows how to write to a text acceptor
40  * (e.g., LTAttributedText and LTRTFFilter).
41  *
42  * <p>Note that this is a lossy conversion since RTF's model of
43  * text does not exactly correspond with LightText's.
44  *
45  * @see LTAttributedText
46  * @see LTRTFFilter
47  * @see LTTextAcceptor
48  * @see java.io.OutputStream
49  */
50 
51 class RTFGenerator extends Object
52 {
53     /* These dictionaries map Colors, font names, or Style objects
54        to Integers */
55     Dictionary<Object, Integer> colorTable;
56     int colorCount;
57     Dictionary<String, Integer> fontTable;
58     int fontCount;
59     Dictionary<AttributeSet, Integer> styleTable;
60     int styleCount;
61 
62     /* where all the text is going */
63     OutputStream outputStream;
64 
65     boolean afterKeyword;
66 
67     MutableAttributeSet outputAttributes;
68 
69     /* the value of the last \\ucN keyword emitted */
70     int unicodeCount;
71 
72     /* for efficiency's sake (ha) */
73     private Segment workingSegment;
74 
75     int[] outputConversion;
76 
77     /** The default color, used for text without an explicit color
78      *  attribute. */
79     static public final Color defaultRTFColor = Color.black;
80 
81     static public final float defaultFontSize = 12f;
82 
83     static public final String defaultFontFamily = "Helvetica";
84 
85     /* constants so we can avoid allocating objects in inner loops */
86     final static private Object MagicToken;
87 
88     /* An array of character-keyword pairs. This could be done
89        as a dictionary (and lookup would be quicker), but that
90        would require allocating an object for every character
91        written (slow!). */
92     static class CharacterKeywordPair
93       { public char character; public String keyword; }
94     static protected CharacterKeywordPair[] textKeywords;
95 
96     static {
97         MagicToken = new Object();
98 
99         Dictionary textKeywordDictionary = RTFReader.textKeywords;
100         Enumeration keys = textKeywordDictionary.keys();
101         Vector<CharacterKeywordPair> tempPairs = new Vector<CharacterKeywordPair>();
102         while(keys.hasMoreElements()) {
103             CharacterKeywordPair pair = new CharacterKeywordPair();
104             pair.keyword = (String)keys.nextElement();
105             pair.character = ((String)textKeywordDictionary.get(pair.keyword)).charAt(0);
106             tempPairs.addElement(pair);
107         }
108         textKeywords = new CharacterKeywordPair[tempPairs.size()];
109         tempPairs.copyInto(textKeywords);
110     }
111 
112     static final char[] hexdigits = { '0', '1', '2', '3', '4', '5', '6', '7',
113                                       '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
114 
writeDocument(Document d, OutputStream to)115 static public void writeDocument(Document d, OutputStream to)
116     throws IOException
117 {
118     RTFGenerator gen = new RTFGenerator(to);
119     Element root = d.getDefaultRootElement();
120 
121     gen.examineElement(root);
122     gen.writeRTFHeader();
123     gen.writeDocumentProperties(d);
124 
125     /* TODO this assumes a particular element structure; is there
126        a way to iterate more generically ? */
127     int max = root.getElementCount();
128     for(int idx = 0; idx < max; idx++)
129         gen.writeParagraphElement(root.getElement(idx));
130 
131     gen.writeRTFTrailer();
132 }
133 
RTFGenerator(OutputStream to)134 public RTFGenerator(OutputStream to)
135 {
136     colorTable = new Hashtable<Object, Integer>();
137     colorTable.put(defaultRTFColor, Integer.valueOf(0));
138     colorCount = 1;
139 
140     fontTable = new Hashtable<String, Integer>();
141     fontCount = 0;
142 
143     styleTable = new Hashtable<AttributeSet, Integer>();
144     /* TODO: put default style in style table */
145     styleCount = 0;
146 
147     workingSegment = new Segment();
148 
149     outputStream = to;
150 
151     unicodeCount = 1;
152 }
153 
examineElement(Element el)154 public void examineElement(Element el)
155 {
156     AttributeSet a = el.getAttributes();
157     String fontName;
158     Object foregroundColor, backgroundColor;
159 
160     tallyStyles(a);
161 
162     if (a != null) {
163         /* TODO: default color must be color 0! */
164 
165         foregroundColor = StyleConstants.getForeground(a);
166         if (foregroundColor != null &&
167             colorTable.get(foregroundColor) == null) {
168             colorTable.put(foregroundColor, new Integer(colorCount));
169             colorCount ++;
170         }
171 
172         backgroundColor = a.getAttribute(StyleConstants.Background);
173         if (backgroundColor != null &&
174             colorTable.get(backgroundColor) == null) {
175             colorTable.put(backgroundColor, new Integer(colorCount));
176             colorCount ++;
177         }
178 
179         fontName = StyleConstants.getFontFamily(a);
180 
181         if (fontName == null)
182             fontName = defaultFontFamily;
183 
184         if (fontName != null &&
185             fontTable.get(fontName) == null) {
186             fontTable.put(fontName, new Integer(fontCount));
187             fontCount ++;
188         }
189     }
190 
191     int el_count = el.getElementCount();
192     for(int el_idx = 0; el_idx < el_count; el_idx ++) {
193         examineElement(el.getElement(el_idx));
194     }
195 }
196 
tallyStyles(AttributeSet a)197 private void tallyStyles(AttributeSet a) {
198     while (a != null) {
199         if (a instanceof Style) {
200             Integer aNum = styleTable.get(a);
201             if (aNum == null) {
202                 styleCount = styleCount + 1;
203                 aNum = new Integer(styleCount);
204                 styleTable.put(a, aNum);
205             }
206         }
207         a = a.getResolveParent();
208     }
209 }
210 
findStyle(AttributeSet a)211 private Style findStyle(AttributeSet a)
212 {
213     while(a != null) {
214         if (a instanceof Style) {
215             Object aNum = styleTable.get(a);
216             if (aNum != null)
217                 return (Style)a;
218         }
219         a = a.getResolveParent();
220     }
221     return null;
222 }
223 
findStyleNumber(AttributeSet a, String domain)224 private Integer findStyleNumber(AttributeSet a, String domain)
225 {
226     while(a != null) {
227         if (a instanceof Style) {
228             Integer aNum = styleTable.get(a);
229             if (aNum != null) {
230                 if (domain == null ||
231                     domain.equals(a.getAttribute(Constants.StyleType)))
232                     return aNum;
233             }
234 
235         }
236         a = a.getResolveParent();
237     }
238     return null;
239 }
240 
attrDiff(MutableAttributeSet oldAttrs, AttributeSet newAttrs, Object key, Object dfl)241 static private Object attrDiff(MutableAttributeSet oldAttrs,
242                                AttributeSet newAttrs,
243                                Object key,
244                                Object dfl)
245 {
246     Object oldValue, newValue;
247 
248     oldValue = oldAttrs.getAttribute(key);
249     newValue = newAttrs.getAttribute(key);
250 
251     if (newValue == oldValue)
252         return null;
253     if (newValue == null) {
254         oldAttrs.removeAttribute(key);
255         if (dfl != null && !dfl.equals(oldValue))
256             return dfl;
257         else
258             return null;
259     }
260     if (oldValue == null ||
261         !equalArraysOK(oldValue, newValue)) {
262         oldAttrs.addAttribute(key, newValue);
263         return newValue;
264     }
265     return null;
266 }
267 
equalArraysOK(Object a, Object b)268 static private boolean equalArraysOK(Object a, Object b)
269 {
270     Object[] aa, bb;
271     if (a == b)
272         return true;
273     if (a == null || b == null)
274         return false;
275     if (a.equals(b))
276         return true;
277     if (!(a.getClass().isArray() && b.getClass().isArray()))
278         return false;
279     aa = (Object[])a;
280     bb = (Object[])b;
281     if (aa.length != bb.length)
282         return false;
283 
284     int i;
285     int l = aa.length;
286     for(i = 0; i < l; i++) {
287         if (!equalArraysOK(aa[i], bb[i]))
288             return false;
289     }
290 
291     return true;
292 }
293 
294 /* Writes a line break to the output file, for ease in debugging */
writeLineBreak()295 public void writeLineBreak()
296     throws IOException
297 {
298     writeRawString("\n");
299     afterKeyword = false;
300 }
301 
302 
writeRTFHeader()303 public void writeRTFHeader()
304     throws IOException
305 {
306     int index;
307 
308     /* TODO: Should the writer attempt to examine the text it's writing
309        and pick a character set which will most compactly represent the
310        document? (currently the writer always uses the ansi character
311        set, which is roughly ISO-8859 Latin-1, and uses Unicode escapes
312        for all other characters. However Unicode is a relatively
313        recent addition to RTF, and not all readers will understand it.) */
314     writeBegingroup();
315     writeControlWord("rtf", 1);
316     writeControlWord("ansi");
317     outputConversion = outputConversionForName("ansi");
318     writeLineBreak();
319 
320     /* write font table */
321     String[] sortedFontTable = new String[fontCount];
322     Enumeration<String> fonts = fontTable.keys();
323     String font;
324     while(fonts.hasMoreElements()) {
325         font = fonts.nextElement();
326         Integer num = fontTable.get(font);
327         sortedFontTable[num.intValue()] = font;
328     }
329     writeBegingroup();
330     writeControlWord("fonttbl");
331     for(index = 0; index < fontCount; index ++) {
332         writeControlWord("f", index);
333         writeControlWord("fnil");  /* TODO: supply correct font style */
334         writeText(sortedFontTable[index]);
335         writeText(";");
336     }
337     writeEndgroup();
338     writeLineBreak();
339 
340     /* write color table */
341     if (colorCount > 1) {
342         Color[] sortedColorTable = new Color[colorCount];
343         Enumeration colors = colorTable.keys();
344         Color color;
345         while(colors.hasMoreElements()) {
346             color = (Color)colors.nextElement();
347             Integer num = colorTable.get(color);
348             sortedColorTable[num.intValue()] = color;
349         }
350         writeBegingroup();
351         writeControlWord("colortbl");
352         for(index = 0; index < colorCount; index ++) {
353             color = sortedColorTable[index];
354             if (color != null) {
355                 writeControlWord("red", color.getRed());
356                 writeControlWord("green", color.getGreen());
357                 writeControlWord("blue", color.getBlue());
358             }
359             writeRawString(";");
360         }
361         writeEndgroup();
362         writeLineBreak();
363     }
364 
365     /* write the style sheet */
366     if (styleCount > 1) {
367         writeBegingroup();
368         writeControlWord("stylesheet");
369         Enumeration<AttributeSet> styles = styleTable.keys();
370         while(styles.hasMoreElements()) {
371             Style style = (Style)styles.nextElement();
372             int styleNumber = styleTable.get(style).intValue();
373             writeBegingroup();
374             String styleType = (String)style.getAttribute(Constants.StyleType);
375             if (styleType == null)
376                 styleType = Constants.STParagraph;
377             if (styleType.equals(Constants.STCharacter)) {
378                 writeControlWord("*");
379                 writeControlWord("cs", styleNumber);
380             } else if(styleType.equals(Constants.STSection)) {
381                 writeControlWord("*");
382                 writeControlWord("ds", styleNumber);
383             } else {
384                 writeControlWord("s", styleNumber);
385             }
386 
387             AttributeSet basis = style.getResolveParent();
388             MutableAttributeSet goat;
389             if (basis == null) {
390                 goat = new SimpleAttributeSet();
391             } else {
392                 goat = new SimpleAttributeSet(basis);
393             }
394 
395             updateSectionAttributes(goat, style, false);
396             updateParagraphAttributes(goat, style, false);
397             updateCharacterAttributes(goat, style, false);
398 
399             basis = style.getResolveParent();
400             if (basis != null && basis instanceof Style) {
401                 Integer basedOn = styleTable.get(basis);
402                 if (basedOn != null) {
403                     writeControlWord("sbasedon", basedOn.intValue());
404                 }
405             }
406 
407             Style nextStyle = (Style)style.getAttribute(Constants.StyleNext);
408             if (nextStyle != null) {
409                 Integer nextNum = styleTable.get(nextStyle);
410                 if (nextNum != null) {
411                     writeControlWord("snext", nextNum.intValue());
412                 }
413             }
414 
415             Boolean hidden = (Boolean)style.getAttribute(Constants.StyleHidden);
416             if (hidden != null && hidden.booleanValue())
417                 writeControlWord("shidden");
418 
419             Boolean additive = (Boolean)style.getAttribute(Constants.StyleAdditive);
420             if (additive != null && additive.booleanValue())
421                 writeControlWord("additive");
422 
423 
424             writeText(style.getName());
425             writeText(";");
426             writeEndgroup();
427         }
428         writeEndgroup();
429         writeLineBreak();
430     }
431 
432     outputAttributes = new SimpleAttributeSet();
433 }
434 
writeDocumentProperties(Document doc)435 void writeDocumentProperties(Document doc)
436     throws IOException
437 {
438     /* Write the document properties */
439     int i;
440     boolean wroteSomething = false;
441 
442     for(i = 0; i < RTFAttributes.attributes.length; i++) {
443         RTFAttribute attr = RTFAttributes.attributes[i];
444         if (attr.domain() != RTFAttribute.D_DOCUMENT)
445             continue;
446         Object prop = doc.getProperty(attr.swingName());
447         boolean ok = attr.writeValue(prop, this, false);
448         if (ok)
449             wroteSomething = true;
450     }
451 
452     if (wroteSomething)
453         writeLineBreak();
454 }
455 
writeRTFTrailer()456 public void writeRTFTrailer()
457     throws IOException
458 {
459     writeEndgroup();
460     writeLineBreak();
461 }
462 
checkNumericControlWord(MutableAttributeSet currentAttributes, AttributeSet newAttributes, Object attrName, String controlWord, float dflt, float scale)463 protected void checkNumericControlWord(MutableAttributeSet currentAttributes,
464                                        AttributeSet newAttributes,
465                                        Object attrName,
466                                        String controlWord,
467                                        float dflt, float scale)
468     throws IOException
469 {
470     Object parm;
471 
472     if ((parm = attrDiff(currentAttributes, newAttributes,
473                          attrName, MagicToken)) != null) {
474         float targ;
475         if (parm == MagicToken)
476             targ = dflt;
477         else
478             targ = ((Number)parm).floatValue();
479         writeControlWord(controlWord, Math.round(targ * scale));
480     }
481 }
482 
checkControlWord(MutableAttributeSet currentAttributes, AttributeSet newAttributes, RTFAttribute word)483 protected void checkControlWord(MutableAttributeSet currentAttributes,
484                                 AttributeSet newAttributes,
485                                 RTFAttribute word)
486     throws IOException
487 {
488     Object parm;
489 
490     if ((parm = attrDiff(currentAttributes, newAttributes,
491                          word.swingName(), MagicToken)) != null) {
492         if (parm == MagicToken)
493             parm = null;
494         word.writeValue(parm, this, true);
495     }
496 }
497 
checkControlWords(MutableAttributeSet currentAttributes, AttributeSet newAttributes, RTFAttribute words[], int domain)498 protected void checkControlWords(MutableAttributeSet currentAttributes,
499                                  AttributeSet newAttributes,
500                                  RTFAttribute words[],
501                                  int domain)
502     throws IOException
503 {
504     int wordIndex;
505     int wordCount = words.length;
506     for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
507         RTFAttribute attr = words[wordIndex];
508         if (attr.domain() == domain)
509             checkControlWord(currentAttributes, newAttributes, attr);
510     }
511 }
512 
updateSectionAttributes(MutableAttributeSet current, AttributeSet newAttributes, boolean emitStyleChanges)513 void updateSectionAttributes(MutableAttributeSet current,
514                              AttributeSet newAttributes,
515                              boolean emitStyleChanges)
516     throws IOException
517 {
518     if (emitStyleChanges) {
519         Object oldStyle = current.getAttribute("sectionStyle");
520         Object newStyle = findStyleNumber(newAttributes, Constants.STSection);
521         if (oldStyle != newStyle) {
522             if (oldStyle != null) {
523                 resetSectionAttributes(current);
524             }
525             if (newStyle != null) {
526                 writeControlWord("ds", ((Integer)newStyle).intValue());
527                 current.addAttribute("sectionStyle", newStyle);
528             } else {
529                 current.removeAttribute("sectionStyle");
530             }
531         }
532     }
533 
534     checkControlWords(current, newAttributes,
535                       RTFAttributes.attributes, RTFAttribute.D_SECTION);
536 }
537 
resetSectionAttributes(MutableAttributeSet currentAttributes)538 protected void resetSectionAttributes(MutableAttributeSet currentAttributes)
539     throws IOException
540 {
541     writeControlWord("sectd");
542 
543     int wordIndex;
544     int wordCount = RTFAttributes.attributes.length;
545     for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
546         RTFAttribute attr = RTFAttributes.attributes[wordIndex];
547         if (attr.domain() == RTFAttribute.D_SECTION)
548             attr.setDefault(currentAttributes);
549     }
550 
551     currentAttributes.removeAttribute("sectionStyle");
552 }
553 
updateParagraphAttributes(MutableAttributeSet current, AttributeSet newAttributes, boolean emitStyleChanges)554 void updateParagraphAttributes(MutableAttributeSet current,
555                                AttributeSet newAttributes,
556                                boolean emitStyleChanges)
557     throws IOException
558 {
559     Object parm;
560     Object oldStyle, newStyle;
561 
562     /* The only way to get rid of tabs or styles is with the \pard keyword,
563        emitted by resetParagraphAttributes(). Ideally we should avoid
564        emitting \pard if the new paragraph's tabs are a superset of the old
565        paragraph's tabs. */
566 
567     if (emitStyleChanges) {
568         oldStyle = current.getAttribute("paragraphStyle");
569         newStyle = findStyleNumber(newAttributes, Constants.STParagraph);
570         if (oldStyle != newStyle) {
571             if (oldStyle != null) {
572                 resetParagraphAttributes(current);
573                 oldStyle = null;
574             }
575         }
576     } else {
577         oldStyle = null;
578         newStyle = null;
579     }
580 
581     Object oldTabs = current.getAttribute(Constants.Tabs);
582     Object newTabs = newAttributes.getAttribute(Constants.Tabs);
583     if (oldTabs != newTabs) {
584         if (oldTabs != null) {
585             resetParagraphAttributes(current);
586             oldTabs = null;
587             oldStyle = null;
588         }
589     }
590 
591     if (oldStyle != newStyle && newStyle != null) {
592         writeControlWord("s", ((Integer)newStyle).intValue());
593         current.addAttribute("paragraphStyle", newStyle);
594     }
595 
596     checkControlWords(current, newAttributes,
597                       RTFAttributes.attributes, RTFAttribute.D_PARAGRAPH);
598 
599     if (oldTabs != newTabs && newTabs != null) {
600         TabStop tabs[] = (TabStop[])newTabs;
601         int index;
602         for(index = 0; index < tabs.length; index ++) {
603             TabStop tab = tabs[index];
604             switch (tab.getAlignment()) {
605               case TabStop.ALIGN_LEFT:
606               case TabStop.ALIGN_BAR:
607                 break;
608               case TabStop.ALIGN_RIGHT:
609                 writeControlWord("tqr");
610                 break;
611               case TabStop.ALIGN_CENTER:
612                 writeControlWord("tqc");
613                 break;
614               case TabStop.ALIGN_DECIMAL:
615                 writeControlWord("tqdec");
616                 break;
617             }
618             switch (tab.getLeader()) {
619               case TabStop.LEAD_NONE:
620                 break;
621               case TabStop.LEAD_DOTS:
622                 writeControlWord("tldot");
623                 break;
624               case TabStop.LEAD_HYPHENS:
625                 writeControlWord("tlhyph");
626                 break;
627               case TabStop.LEAD_UNDERLINE:
628                 writeControlWord("tlul");
629                 break;
630               case TabStop.LEAD_THICKLINE:
631                 writeControlWord("tlth");
632                 break;
633               case TabStop.LEAD_EQUALS:
634                 writeControlWord("tleq");
635                 break;
636             }
637             int twips = Math.round(20f * tab.getPosition());
638             if (tab.getAlignment() == TabStop.ALIGN_BAR) {
639                 writeControlWord("tb", twips);
640             } else {
641                 writeControlWord("tx", twips);
642             }
643         }
644         current.addAttribute(Constants.Tabs, tabs);
645     }
646 }
647 
writeParagraphElement(Element el)648 public void writeParagraphElement(Element el)
649     throws IOException
650 {
651     updateParagraphAttributes(outputAttributes, el.getAttributes(), true);
652 
653     int sub_count = el.getElementCount();
654     for(int idx = 0; idx < sub_count; idx ++) {
655         writeTextElement(el.getElement(idx));
656     }
657 
658     writeControlWord("par");
659     writeLineBreak();  /* makes the raw file more readable */
660 }
661 
662 /* debugging. TODO: remove.
663 private static String tabdump(Object tso)
664 {
665     String buf;
666     int i;
667 
668     if (tso == null)
669         return "[none]";
670 
671     TabStop[] ts = (TabStop[])tso;
672 
673     buf = "[";
674     for(i = 0; i < ts.length; i++) {
675         buf = buf + ts[i].toString();
676         if ((i+1) < ts.length)
677             buf = buf + ",";
678     }
679     return buf + "]";
680 }
681 */
682 
resetParagraphAttributes(MutableAttributeSet currentAttributes)683 protected void resetParagraphAttributes(MutableAttributeSet currentAttributes)
684     throws IOException
685 {
686     writeControlWord("pard");
687 
688     currentAttributes.addAttribute(StyleConstants.Alignment, Integer.valueOf(0));
689 
690     int wordIndex;
691     int wordCount = RTFAttributes.attributes.length;
692     for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
693         RTFAttribute attr = RTFAttributes.attributes[wordIndex];
694         if (attr.domain() == RTFAttribute.D_PARAGRAPH)
695             attr.setDefault(currentAttributes);
696     }
697 
698     currentAttributes.removeAttribute("paragraphStyle");
699     currentAttributes.removeAttribute(Constants.Tabs);
700 }
701 
updateCharacterAttributes(MutableAttributeSet current, AttributeSet newAttributes, boolean updateStyleChanges)702 void updateCharacterAttributes(MutableAttributeSet current,
703                                AttributeSet newAttributes,
704                                boolean updateStyleChanges)
705     throws IOException
706 {
707     Object parm;
708 
709     if (updateStyleChanges) {
710         Object oldStyle = current.getAttribute("characterStyle");
711         Object newStyle = findStyleNumber(newAttributes,
712                                           Constants.STCharacter);
713         if (oldStyle != newStyle) {
714             if (oldStyle != null) {
715                 resetCharacterAttributes(current);
716             }
717             if (newStyle != null) {
718                 writeControlWord("cs", ((Integer)newStyle).intValue());
719                 current.addAttribute("characterStyle", newStyle);
720             } else {
721                 current.removeAttribute("characterStyle");
722             }
723         }
724     }
725 
726     if ((parm = attrDiff(current, newAttributes,
727                          StyleConstants.FontFamily, null)) != null) {
728         Integer fontNum = fontTable.get(parm);
729         writeControlWord("f", fontNum.intValue());
730     }
731 
732     checkNumericControlWord(current, newAttributes,
733                             StyleConstants.FontSize, "fs",
734                             defaultFontSize, 2f);
735 
736     checkControlWords(current, newAttributes,
737                       RTFAttributes.attributes, RTFAttribute.D_CHARACTER);
738 
739     checkNumericControlWord(current, newAttributes,
740                             StyleConstants.LineSpacing, "sl",
741                             0, 20f); /* TODO: sl wackiness */
742 
743     if ((parm = attrDiff(current, newAttributes,
744                          StyleConstants.Background, MagicToken)) != null) {
745         int colorNum;
746         if (parm == MagicToken)
747             colorNum = 0;
748         else
749             colorNum = colorTable.get(parm).intValue();
750         writeControlWord("cb", colorNum);
751     }
752 
753     if ((parm = attrDiff(current, newAttributes,
754                          StyleConstants.Foreground, null)) != null) {
755         int colorNum;
756         if (parm == MagicToken)
757             colorNum = 0;
758         else
759             colorNum = colorTable.get(parm).intValue();
760         writeControlWord("cf", colorNum);
761     }
762 }
763 
resetCharacterAttributes(MutableAttributeSet currentAttributes)764 protected void resetCharacterAttributes(MutableAttributeSet currentAttributes)
765     throws IOException
766 {
767     writeControlWord("plain");
768 
769     int wordIndex;
770     int wordCount = RTFAttributes.attributes.length;
771     for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
772         RTFAttribute attr = RTFAttributes.attributes[wordIndex];
773         if (attr.domain() == RTFAttribute.D_CHARACTER)
774             attr.setDefault(currentAttributes);
775     }
776 
777     StyleConstants.setFontFamily(currentAttributes, defaultFontFamily);
778     currentAttributes.removeAttribute(StyleConstants.FontSize); /* =default */
779     currentAttributes.removeAttribute(StyleConstants.Background);
780     currentAttributes.removeAttribute(StyleConstants.Foreground);
781     currentAttributes.removeAttribute(StyleConstants.LineSpacing);
782     currentAttributes.removeAttribute("characterStyle");
783 }
784 
writeTextElement(Element el)785 public void writeTextElement(Element el)
786     throws IOException
787 {
788     updateCharacterAttributes(outputAttributes, el.getAttributes(), true);
789 
790     if (el.isLeaf()) {
791         try {
792             el.getDocument().getText(el.getStartOffset(),
793                                      el.getEndOffset() - el.getStartOffset(),
794                                      this.workingSegment);
795         } catch (BadLocationException ble) {
796             /* TODO is this the correct error to raise? */
797             ble.printStackTrace();
798             throw new InternalError(ble.getMessage());
799         }
800         writeText(this.workingSegment);
801     } else {
802         int sub_count = el.getElementCount();
803         for(int idx = 0; idx < sub_count; idx ++)
804             writeTextElement(el.getElement(idx));
805     }
806 }
807 
writeText(Segment s)808 public void writeText(Segment s)
809     throws IOException
810 {
811     int pos, end;
812     char[] array;
813 
814     pos = s.offset;
815     end = pos + s.count;
816     array = s.array;
817     for( ; pos < end; pos ++)
818         writeCharacter(array[pos]);
819 }
820 
writeText(String s)821 public void writeText(String s)
822     throws IOException
823 {
824     int pos, end;
825 
826     pos = 0;
827     end = s.length();
828     for( ; pos < end; pos ++)
829         writeCharacter(s.charAt(pos));
830 }
831 
writeRawString(String str)832 public void writeRawString(String str)
833     throws IOException
834 {
835     int strlen = str.length();
836     for (int offset = 0; offset < strlen; offset ++)
837         outputStream.write((int)str.charAt(offset));
838 }
839 
writeControlWord(String keyword)840 public void writeControlWord(String keyword)
841     throws IOException
842 {
843     outputStream.write('\\');
844     writeRawString(keyword);
845     afterKeyword = true;
846 }
847 
writeControlWord(String keyword, int arg)848 public void writeControlWord(String keyword, int arg)
849     throws IOException
850 {
851     outputStream.write('\\');
852     writeRawString(keyword);
853     writeRawString(String.valueOf(arg)); /* TODO: correct in all cases? */
854     afterKeyword = true;
855 }
856 
writeBegingroup()857 public void writeBegingroup()
858     throws IOException
859 {
860     outputStream.write('{');
861     afterKeyword = false;
862 }
863 
writeEndgroup()864 public void writeEndgroup()
865     throws IOException
866 {
867     outputStream.write('}');
868     afterKeyword = false;
869 }
870 
writeCharacter(char ch)871 public void writeCharacter(char ch)
872     throws IOException
873 {
874     /* Nonbreaking space is in most RTF encodings, but the keyword is
875        preferable; same goes for tabs */
876     if (ch == 0xA0) { /* nonbreaking space */
877         outputStream.write(0x5C);  /* backslash */
878         outputStream.write(0x7E);  /* tilde */
879         afterKeyword = false; /* non-alpha keywords are self-terminating */
880         return;
881     }
882 
883     if (ch == 0x09) { /* horizontal tab */
884         writeControlWord("tab");
885         return;
886     }
887 
888     if (ch == 10 || ch == 13) { /* newline / paragraph */
889         /* ignore CRs, we'll write a paragraph element soon enough */
890         return;
891     }
892 
893     int b = convertCharacter(outputConversion, ch);
894     if (b == 0) {
895         /* Unicode characters which have corresponding RTF keywords */
896         int i;
897         for(i = 0; i < textKeywords.length; i++) {
898             if (textKeywords[i].character == ch) {
899                 writeControlWord(textKeywords[i].keyword);
900                 return;
901             }
902         }
903         /* In some cases it would be reasonable to check to see if the
904            glyph being written out is in the Symbol encoding, and if so,
905            to switch to the Symbol font for this character. TODO. */
906         /* Currently all unrepresentable characters are written as
907            Unicode escapes. */
908         String approximation = approximationForUnicode(ch);
909         if (approximation.length() != unicodeCount) {
910             unicodeCount = approximation.length();
911             writeControlWord("uc", unicodeCount);
912         }
913         writeControlWord("u", (int)ch);
914         writeRawString(" ");
915         writeRawString(approximation);
916         afterKeyword = false;
917         return;
918     }
919 
920     if (b > 127) {
921         int nybble;
922         outputStream.write('\\');
923         outputStream.write('\'');
924         nybble = ( b & 0xF0 ) >>> 4;
925         outputStream.write(hexdigits[nybble]);
926         nybble = ( b & 0x0F );
927         outputStream.write(hexdigits[nybble]);
928         afterKeyword = false;
929         return;
930     }
931 
932     switch (b) {
933     case '}':
934     case '{':
935     case '\\':
936         outputStream.write(0x5C);  /* backslash */
937         afterKeyword = false;  /* in a keyword, actually ... */
938         /* fall through */
939     default:
940         if (afterKeyword) {
941             outputStream.write(0x20);  /* space */
942             afterKeyword = false;
943         }
944         outputStream.write(b);
945         break;
946     }
947 }
948 
approximationForUnicode(char ch)949 String approximationForUnicode(char ch)
950 {
951     /* TODO: Find reasonable approximations for all Unicode characters
952        in all RTF code pages... heh, heh... */
953     return "?";
954 }
955 
956 /** Takes a translation table (a 256-element array of characters)
957  * and creates an output conversion table for use by
958  * convertCharacter(). */
959     /* Not very efficient at all. Could be changed to sort the table
960        for binary search. TODO. (Even though this is inefficient however,
961        writing RTF is still much faster than reading it.) */
outputConversionFromTranslationTable(char[] table)962 static int[] outputConversionFromTranslationTable(char[] table)
963 {
964     int[] conversion = new int[2 * table.length];
965 
966     int index;
967 
968     for(index = 0; index < table.length; index ++) {
969         conversion[index * 2] = table[index];
970         conversion[(index * 2) + 1] = index;
971     }
972 
973     return conversion;
974 }
975 
outputConversionForName(String name)976 static int[] outputConversionForName(String name)
977     throws IOException
978 {
979     char[] table = (char[])RTFReader.getCharacterSet(name);
980     return outputConversionFromTranslationTable(table);
981 }
982 
983 /** Takes a char and a conversion table (an int[] in the current
984  * implementation, but conversion tables should be treated as an opaque
985  * type) and returns the
986  * corresponding byte value (as an int, since bytes are signed).
987  */
988     /* Not very efficient. TODO. */
convertCharacter(int[] conversion, char ch)989 static protected int convertCharacter(int[] conversion, char ch)
990 {
991    int index;
992 
993    for(index = 0; index < conversion.length; index += 2) {
994        if(conversion[index] == ch)
995            return conversion[index + 1];
996    }
997 
998    return 0;  /* 0 indicates an unrepresentable character */
999 }
1000 
1001 }
1002