1 /*
   2  * Copyright (c) 1997, 2016, 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     public static final Color defaultRTFColor = Color.black;
  80 
  81     public static final float defaultFontSize = 12f;
  82 
  83     public static final String defaultFontFamily = "Helvetica";
  84 
  85     /* constants so we can avoid allocating objects in inner loops */
  86     private static final 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     protected static CharacterKeywordPair[] textKeywords;
  95 
  96     static {
  97         MagicToken = new Object();
  98 
  99         Dictionary<String, String> textKeywordDictionary = RTFReader.textKeywords;
 100         Enumeration<String> keys = textKeywordDictionary.keys();
 101         Vector<CharacterKeywordPair> tempPairs = new Vector<CharacterKeywordPair>();
 102         while(keys.hasMoreElements()) {
 103             CharacterKeywordPair pair = new CharacterKeywordPair();
 104             pair.keyword = keys.nextElement();
 105             pair.character = 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 
 115 public static 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 
 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 
 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, colorCount);
 169             colorCount ++;
 170         }
 171 
 172         backgroundColor = a.getAttribute(StyleConstants.Background);
 173         if (backgroundColor != null &&
 174             colorTable.get(backgroundColor) == null) {
 175             colorTable.put(backgroundColor, 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, 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 
 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 = styleCount;
 204                 styleTable.put(a, aNum);
 205             }
 206         }
 207         a = a.getResolveParent();
 208     }
 209 }
 210 
 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 
 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 
 241 private static 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 
 268 private static 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 */
 295 public void writeLineBreak()
 296     throws IOException
 297 {
 298     writeRawString("\n");
 299     afterKeyword = false;
 300 }
 301 
 302 
 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<Object> 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 
 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 
 456 public void writeRTFTrailer()
 457     throws IOException
 458 {
 459     writeEndgroup();
 460     writeLineBreak();
 461 }
 462 
 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 
 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         if (!word.writeValue(parm, this, true) &&
 495                 word instanceof RTFAttributes.AssertiveAttribute ) {
 496             currentAttributes.removeAttribute(word.swingName());
 497         }
 498     }
 499 }
 500 
 501 protected void checkControlWords(MutableAttributeSet currentAttributes,
 502                                  AttributeSet newAttributes,
 503                                  RTFAttribute words[],
 504                                  int domain)
 505     throws IOException
 506 {
 507     int wordIndex;
 508     int wordCount = words.length;
 509     for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
 510         RTFAttribute attr = words[wordIndex];
 511         if (attr.domain() == domain)
 512             checkControlWord(currentAttributes, newAttributes, attr);
 513     }
 514 }
 515 
 516 void updateSectionAttributes(MutableAttributeSet current,
 517                              AttributeSet newAttributes,
 518                              boolean emitStyleChanges)
 519     throws IOException
 520 {
 521     if (emitStyleChanges) {
 522         Object oldStyle = current.getAttribute("sectionStyle");
 523         Object newStyle = findStyleNumber(newAttributes, Constants.STSection);
 524         if (oldStyle != newStyle) {
 525             if (oldStyle != null) {
 526                 resetSectionAttributes(current);
 527             }
 528             if (newStyle != null) {
 529                 writeControlWord("ds", ((Integer)newStyle).intValue());
 530                 current.addAttribute("sectionStyle", newStyle);
 531             } else {
 532                 current.removeAttribute("sectionStyle");
 533             }
 534         }
 535     }
 536 
 537     checkControlWords(current, newAttributes,
 538                       RTFAttributes.attributes, RTFAttribute.D_SECTION);
 539 }
 540 
 541 protected void resetSectionAttributes(MutableAttributeSet currentAttributes)
 542     throws IOException
 543 {
 544     writeControlWord("sectd");
 545 
 546     int wordIndex;
 547     int wordCount = RTFAttributes.attributes.length;
 548     for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
 549         RTFAttribute attr = RTFAttributes.attributes[wordIndex];
 550         if (attr.domain() == RTFAttribute.D_SECTION)
 551             attr.setDefault(currentAttributes);
 552     }
 553 
 554     currentAttributes.removeAttribute("sectionStyle");
 555 }
 556 
 557 void updateParagraphAttributes(MutableAttributeSet current,
 558                                AttributeSet newAttributes,
 559                                boolean emitStyleChanges)
 560     throws IOException
 561 {
 562     Object parm;
 563     Object oldStyle, newStyle;
 564 
 565     /* The only way to get rid of tabs or styles is with the \pard keyword,
 566        emitted by resetParagraphAttributes(). Ideally we should avoid
 567        emitting \pard if the new paragraph's tabs are a superset of the old
 568        paragraph's tabs. */
 569 
 570     if (emitStyleChanges) {
 571         oldStyle = current.getAttribute("paragraphStyle");
 572         newStyle = findStyleNumber(newAttributes, Constants.STParagraph);
 573         if (oldStyle != newStyle) {
 574             if (oldStyle != null) {
 575                 resetParagraphAttributes(current);
 576                 oldStyle = null;
 577             }
 578         }
 579     } else {
 580         oldStyle = null;
 581         newStyle = null;
 582     }
 583 
 584     Object oldTabs = current.getAttribute(Constants.Tabs);
 585     Object newTabs = newAttributes.getAttribute(Constants.Tabs);
 586     if (oldTabs != newTabs) {
 587         if (oldTabs != null) {
 588             resetParagraphAttributes(current);
 589             oldTabs = null;
 590             oldStyle = null;
 591         }
 592     }
 593 
 594     if (oldStyle != newStyle && newStyle != null) {
 595         writeControlWord("s", ((Integer)newStyle).intValue());
 596         current.addAttribute("paragraphStyle", newStyle);
 597     }
 598 
 599     checkControlWords(current, newAttributes,
 600                       RTFAttributes.attributes, RTFAttribute.D_PARAGRAPH);
 601 
 602     if (oldTabs != newTabs && newTabs != null) {
 603         TabStop tabs[] = (TabStop[])newTabs;
 604         int index;
 605         for(index = 0; index < tabs.length; index ++) {
 606             TabStop tab = tabs[index];
 607             switch (tab.getAlignment()) {
 608               case TabStop.ALIGN_LEFT:
 609               case TabStop.ALIGN_BAR:
 610                 break;
 611               case TabStop.ALIGN_RIGHT:
 612                 writeControlWord("tqr");
 613                 break;
 614               case TabStop.ALIGN_CENTER:
 615                 writeControlWord("tqc");
 616                 break;
 617               case TabStop.ALIGN_DECIMAL:
 618                 writeControlWord("tqdec");
 619                 break;
 620             }
 621             switch (tab.getLeader()) {
 622               case TabStop.LEAD_NONE:
 623                 break;
 624               case TabStop.LEAD_DOTS:
 625                 writeControlWord("tldot");
 626                 break;
 627               case TabStop.LEAD_HYPHENS:
 628                 writeControlWord("tlhyph");
 629                 break;
 630               case TabStop.LEAD_UNDERLINE:
 631                 writeControlWord("tlul");
 632                 break;
 633               case TabStop.LEAD_THICKLINE:
 634                 writeControlWord("tlth");
 635                 break;
 636               case TabStop.LEAD_EQUALS:
 637                 writeControlWord("tleq");
 638                 break;
 639             }
 640             int twips = Math.round(20f * tab.getPosition());
 641             if (tab.getAlignment() == TabStop.ALIGN_BAR) {
 642                 writeControlWord("tb", twips);
 643             } else {
 644                 writeControlWord("tx", twips);
 645             }
 646         }
 647         current.addAttribute(Constants.Tabs, tabs);
 648     }
 649 }
 650 
 651 public void writeParagraphElement(Element el)
 652     throws IOException
 653 {
 654     updateParagraphAttributes(outputAttributes, el.getAttributes(), true);
 655 
 656     int sub_count = el.getElementCount();
 657     for(int idx = 0; idx < sub_count; idx ++) {
 658         writeTextElement(el.getElement(idx));
 659     }
 660 
 661     writeControlWord("par");
 662     writeLineBreak();  /* makes the raw file more readable */
 663 }
 664 
 665 /* debugging. TODO: remove.
 666 private static String tabdump(Object tso)
 667 {
 668     String buf;
 669     int i;
 670 
 671     if (tso == null)
 672         return "[none]";
 673 
 674     TabStop[] ts = (TabStop[])tso;
 675 
 676     buf = "[";
 677     for(i = 0; i < ts.length; i++) {
 678         buf = buf + ts[i].toString();
 679         if ((i+1) < ts.length)
 680             buf = buf + ",";
 681     }
 682     return buf + "]";
 683 }
 684 */
 685 
 686 protected void resetParagraphAttributes(MutableAttributeSet currentAttributes)
 687     throws IOException
 688 {
 689     writeControlWord("pard");
 690 
 691     currentAttributes.addAttribute(StyleConstants.Alignment, Integer.valueOf(0));
 692 
 693     int wordIndex;
 694     int wordCount = RTFAttributes.attributes.length;
 695     for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
 696         RTFAttribute attr = RTFAttributes.attributes[wordIndex];
 697         if (attr.domain() == RTFAttribute.D_PARAGRAPH)
 698             attr.setDefault(currentAttributes);
 699     }
 700 
 701     currentAttributes.removeAttribute("paragraphStyle");
 702     currentAttributes.removeAttribute(Constants.Tabs);
 703 }
 704 
 705 void updateCharacterAttributes(MutableAttributeSet current,
 706                                AttributeSet newAttributes,
 707                                boolean updateStyleChanges)
 708     throws IOException
 709 {
 710     Object parm;
 711 
 712     if (updateStyleChanges) {
 713         Object oldStyle = current.getAttribute("characterStyle");
 714         Object newStyle = findStyleNumber(newAttributes,
 715                                           Constants.STCharacter);
 716         if (oldStyle != newStyle) {
 717             if (oldStyle != null) {
 718                 resetCharacterAttributes(current);
 719             }
 720             if (newStyle != null) {
 721                 writeControlWord("cs", ((Integer)newStyle).intValue());
 722                 current.addAttribute("characterStyle", newStyle);
 723             } else {
 724                 current.removeAttribute("characterStyle");
 725             }
 726         }
 727     }
 728 
 729     if ((parm = attrDiff(current, newAttributes,
 730                          StyleConstants.FontFamily, null)) != null) {
 731         Integer fontNum = fontTable.get(parm);
 732         writeControlWord("f", fontNum.intValue());
 733     }
 734 
 735     checkNumericControlWord(current, newAttributes,
 736                             StyleConstants.FontSize, "fs",
 737                             defaultFontSize, 2f);
 738 
 739     checkControlWords(current, newAttributes,
 740                       RTFAttributes.attributes, RTFAttribute.D_CHARACTER);
 741 
 742     checkNumericControlWord(current, newAttributes,
 743                             StyleConstants.LineSpacing, "sl",
 744                             0, 20f); /* TODO: sl wackiness */
 745 
 746     if ((parm = attrDiff(current, newAttributes,
 747                          StyleConstants.Background, MagicToken)) != null) {
 748         int colorNum;
 749         if (parm == MagicToken)
 750             colorNum = 0;
 751         else
 752             colorNum = colorTable.get(parm).intValue();
 753         writeControlWord("cb", colorNum);
 754     }
 755 
 756     if ((parm = attrDiff(current, newAttributes,
 757                          StyleConstants.Foreground, null)) != null) {
 758         int colorNum;
 759         if (parm == MagicToken)
 760             colorNum = 0;
 761         else
 762             colorNum = colorTable.get(parm).intValue();
 763         writeControlWord("cf", colorNum);
 764     }
 765 }
 766 
 767 protected void resetCharacterAttributes(MutableAttributeSet currentAttributes)
 768     throws IOException
 769 {
 770     writeControlWord("plain");
 771 
 772     int wordIndex;
 773     int wordCount = RTFAttributes.attributes.length;
 774     for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
 775         RTFAttribute attr = RTFAttributes.attributes[wordIndex];
 776         if (attr.domain() == RTFAttribute.D_CHARACTER)
 777             attr.setDefault(currentAttributes);
 778     }
 779 
 780     StyleConstants.setFontFamily(currentAttributes, defaultFontFamily);
 781     currentAttributes.removeAttribute(StyleConstants.FontSize); /* =default */
 782     currentAttributes.removeAttribute(StyleConstants.Background);
 783     currentAttributes.removeAttribute(StyleConstants.Foreground);
 784     currentAttributes.removeAttribute(StyleConstants.LineSpacing);
 785     currentAttributes.removeAttribute("characterStyle");
 786 }
 787 
 788 public void writeTextElement(Element el)
 789     throws IOException
 790 {
 791     updateCharacterAttributes(outputAttributes, el.getAttributes(), true);
 792 
 793     if (el.isLeaf()) {
 794         try {
 795             el.getDocument().getText(el.getStartOffset(),
 796                                      el.getEndOffset() - el.getStartOffset(),
 797                                      this.workingSegment);
 798         } catch (BadLocationException ble) {
 799             /* TODO is this the correct error to raise? */
 800             ble.printStackTrace();
 801             throw new InternalError(ble.getMessage());
 802         }
 803         writeText(this.workingSegment);
 804     } else {
 805         int sub_count = el.getElementCount();
 806         for(int idx = 0; idx < sub_count; idx ++)
 807             writeTextElement(el.getElement(idx));
 808     }
 809 }
 810 
 811 public void writeText(Segment s)
 812     throws IOException
 813 {
 814     int pos, end;
 815     char[] array;
 816 
 817     pos = s.offset;
 818     end = pos + s.count;
 819     array = s.array;
 820     for( ; pos < end; pos ++)
 821         writeCharacter(array[pos]);
 822 }
 823 
 824 public void writeText(String s)
 825     throws IOException
 826 {
 827     int pos, end;
 828 
 829     pos = 0;
 830     end = s.length();
 831     for( ; pos < end; pos ++)
 832         writeCharacter(s.charAt(pos));
 833 }
 834 
 835 public void writeRawString(String str)
 836     throws IOException
 837 {
 838     int strlen = str.length();
 839     for (int offset = 0; offset < strlen; offset ++)
 840         outputStream.write((int)str.charAt(offset));
 841 }
 842 
 843 public void writeControlWord(String keyword)
 844     throws IOException
 845 {
 846     outputStream.write('\\');
 847     writeRawString(keyword);
 848     afterKeyword = true;
 849 }
 850 
 851 public void writeControlWord(String keyword, int arg)
 852     throws IOException
 853 {
 854     outputStream.write('\\');
 855     writeRawString(keyword);
 856     writeRawString(String.valueOf(arg)); /* TODO: correct in all cases? */
 857     afterKeyword = true;
 858 }
 859 
 860 public void writeBegingroup()
 861     throws IOException
 862 {
 863     outputStream.write('{');
 864     afterKeyword = false;
 865 }
 866 
 867 public void writeEndgroup()
 868     throws IOException
 869 {
 870     outputStream.write('}');
 871     afterKeyword = false;
 872 }
 873 
 874 @SuppressWarnings("fallthrough")
 875 public void writeCharacter(char ch)
 876     throws IOException
 877 {
 878     /* Nonbreaking space is in most RTF encodings, but the keyword is
 879        preferable; same goes for tabs */
 880     if (ch == 0xA0) { /* nonbreaking space */
 881         outputStream.write(0x5C);  /* backslash */
 882         outputStream.write(0x7E);  /* tilde */
 883         afterKeyword = false; /* non-alpha keywords are self-terminating */
 884         return;
 885     }
 886 
 887     if (ch == 0x09) { /* horizontal tab */
 888         writeControlWord("tab");
 889         return;
 890     }
 891 
 892     if (ch == 10 || ch == 13) { /* newline / paragraph */
 893         /* ignore CRs, we'll write a paragraph element soon enough */
 894         return;
 895     }
 896 
 897     int b = convertCharacter(outputConversion, ch);
 898     if (b == 0) {
 899         /* Unicode characters which have corresponding RTF keywords */
 900         int i;
 901         for(i = 0; i < textKeywords.length; i++) {
 902             if (textKeywords[i].character == ch) {
 903                 writeControlWord(textKeywords[i].keyword);
 904                 return;
 905             }
 906         }
 907         /* In some cases it would be reasonable to check to see if the
 908            glyph being written out is in the Symbol encoding, and if so,
 909            to switch to the Symbol font for this character. TODO. */
 910         /* Currently all unrepresentable characters are written as
 911            Unicode escapes. */
 912         String approximation = approximationForUnicode(ch);
 913         if (approximation.length() != unicodeCount) {
 914             unicodeCount = approximation.length();
 915             writeControlWord("uc", unicodeCount);
 916         }
 917         writeControlWord("u", (int)ch);
 918         writeRawString(" ");
 919         writeRawString(approximation);
 920         afterKeyword = false;
 921         return;
 922     }
 923 
 924     if (b > 127) {
 925         int nybble;
 926         outputStream.write('\\');
 927         outputStream.write('\'');
 928         nybble = ( b & 0xF0 ) >>> 4;
 929         outputStream.write(hexdigits[nybble]);
 930         nybble = ( b & 0x0F );
 931         outputStream.write(hexdigits[nybble]);
 932         afterKeyword = false;
 933         return;
 934     }
 935 
 936     switch (b) {
 937     case '}':
 938     case '{':
 939     case '\\':
 940         outputStream.write(0x5C);  /* backslash */
 941         afterKeyword = false;  /* in a keyword, actually ... */
 942         /* fall through */
 943     default:
 944         if (afterKeyword) {
 945             outputStream.write(0x20);  /* space */
 946             afterKeyword = false;
 947         }
 948         outputStream.write(b);
 949         break;
 950     }
 951 }
 952 
 953 String approximationForUnicode(char ch)
 954 {
 955     /* TODO: Find reasonable approximations for all Unicode characters
 956        in all RTF code pages... heh, heh... */
 957     return "?";
 958 }
 959 
 960 /** Takes a translation table (a 256-element array of characters)
 961  * and creates an output conversion table for use by
 962  * convertCharacter(). */
 963     /* Not very efficient at all. Could be changed to sort the table
 964        for binary search. TODO. (Even though this is inefficient however,
 965        writing RTF is still much faster than reading it.) */
 966 static int[] outputConversionFromTranslationTable(char[] table)
 967 {
 968     int[] conversion = new int[2 * table.length];
 969 
 970     int index;
 971 
 972     for(index = 0; index < table.length; index ++) {
 973         conversion[index * 2] = table[index];
 974         conversion[(index * 2) + 1] = index;
 975     }
 976 
 977     return conversion;
 978 }
 979 
 980 static int[] outputConversionForName(String name)
 981     throws IOException
 982 {
 983     char[] table = (char[])RTFReader.getCharacterSet(name);
 984     return outputConversionFromTranslationTable(table);
 985 }
 986 
 987 /** Takes a char and a conversion table (an int[] in the current
 988  * implementation, but conversion tables should be treated as an opaque
 989  * type) and returns the
 990  * corresponding byte value (as an int, since bytes are signed).
 991  */
 992     /* Not very efficient. TODO. */
 993 protected static int convertCharacter(int[] conversion, char ch)
 994 {
 995    int index;
 996 
 997    for(index = 0; index < conversion.length; index += 2) {
 998        if(conversion[index] == ch)
 999            return conversion[index + 1];
1000    }
1001 
1002    return 0;  /* 0 indicates an unrepresentable character */
1003 }
1004 
1005 }