1 /*
   2  * Copyright (c) 1997, 2017, 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 java.io.OutputStream
  46  */
  47 class RTFGenerator extends Object
  48 {
  49     /* These dictionaries map Colors, font names, or Style objects
  50        to Integers */
  51     Dictionary<Object, Integer> colorTable;
  52     int colorCount;
  53     Dictionary<String, Integer> fontTable;
  54     int fontCount;
  55     Dictionary<AttributeSet, Integer> styleTable;
  56     int styleCount;
  57 
  58     /* where all the text is going */
  59     OutputStream outputStream;
  60 
  61     boolean afterKeyword;
  62 
  63     MutableAttributeSet outputAttributes;
  64 
  65     /* the value of the last \\ucN keyword emitted */
  66     int unicodeCount;
  67 
  68     /* for efficiency's sake (ha) */
  69     private Segment workingSegment;
  70 
  71     int[] outputConversion;
  72 
  73     /** The default color, used for text without an explicit color
  74      *  attribute. */
  75     public static final Color defaultRTFColor = Color.black;
  76 
  77     public static final float defaultFontSize = 12f;
  78 
  79     public static final String defaultFontFamily = "Helvetica";
  80 
  81     /* constants so we can avoid allocating objects in inner loops */
  82     private static final Object MagicToken;
  83 
  84     /* An array of character-keyword pairs. This could be done
  85        as a dictionary (and lookup would be quicker), but that
  86        would require allocating an object for every character
  87        written (slow!). */
  88     static class CharacterKeywordPair
  89       { public char character; public String keyword; }
  90     protected static CharacterKeywordPair[] textKeywords;
  91 
  92     static {
  93         MagicToken = new Object();
  94 
  95         Dictionary<String, String> textKeywordDictionary = RTFReader.textKeywords;
  96         Enumeration<String> keys = textKeywordDictionary.keys();
  97         Vector<CharacterKeywordPair> tempPairs = new Vector<CharacterKeywordPair>();
  98         while(keys.hasMoreElements()) {
  99             CharacterKeywordPair pair = new CharacterKeywordPair();
 100             pair.keyword = keys.nextElement();
 101             pair.character = textKeywordDictionary.get(pair.keyword).charAt(0);
 102             tempPairs.addElement(pair);
 103         }
 104         textKeywords = new CharacterKeywordPair[tempPairs.size()];
 105         tempPairs.copyInto(textKeywords);
 106     }
 107 
 108     static final char[] hexdigits = { '0', '1', '2', '3', '4', '5', '6', '7',
 109                                       '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
 110 
 111 public static void writeDocument(Document d, OutputStream to)
 112     throws IOException
 113 {
 114     RTFGenerator gen = new RTFGenerator(to);
 115     Element root = d.getDefaultRootElement();
 116 
 117     gen.examineElement(root);
 118     gen.writeRTFHeader();
 119     gen.writeDocumentProperties(d);
 120 
 121     /* TODO this assumes a particular element structure; is there
 122        a way to iterate more generically ? */
 123     int max = root.getElementCount();
 124     for(int idx = 0; idx < max; idx++)
 125         gen.writeParagraphElement(root.getElement(idx));
 126 
 127     gen.writeRTFTrailer();
 128 }
 129 
 130 public RTFGenerator(OutputStream to)
 131 {
 132     colorTable = new Hashtable<Object, Integer>();
 133     colorTable.put(defaultRTFColor, Integer.valueOf(0));
 134     colorCount = 1;
 135 
 136     fontTable = new Hashtable<String, Integer>();
 137     fontCount = 0;
 138 
 139     styleTable = new Hashtable<AttributeSet, Integer>();
 140     /* TODO: put default style in style table */
 141     styleCount = 0;
 142 
 143     workingSegment = new Segment();
 144 
 145     outputStream = to;
 146 
 147     unicodeCount = 1;
 148 }
 149 
 150 public void examineElement(Element el)
 151 {
 152     AttributeSet a = el.getAttributes();
 153     String fontName;
 154     Object foregroundColor, backgroundColor;
 155 
 156     tallyStyles(a);
 157 
 158     if (a != null) {
 159         /* TODO: default color must be color 0! */
 160 
 161         foregroundColor = StyleConstants.getForeground(a);
 162         if (foregroundColor != null &&
 163             colorTable.get(foregroundColor) == null) {
 164             colorTable.put(foregroundColor, colorCount);
 165             colorCount ++;
 166         }
 167 
 168         backgroundColor = a.getAttribute(StyleConstants.Background);
 169         if (backgroundColor != null &&
 170             colorTable.get(backgroundColor) == null) {
 171             colorTable.put(backgroundColor, colorCount);
 172             colorCount ++;
 173         }
 174 
 175         fontName = StyleConstants.getFontFamily(a);
 176 
 177         if (fontName == null)
 178             fontName = defaultFontFamily;
 179 
 180         if (fontName != null &&
 181             fontTable.get(fontName) == null) {
 182             fontTable.put(fontName, fontCount);
 183             fontCount ++;
 184         }
 185     }
 186 
 187     int el_count = el.getElementCount();
 188     for(int el_idx = 0; el_idx < el_count; el_idx ++) {
 189         examineElement(el.getElement(el_idx));
 190     }
 191 }
 192 
 193 private void tallyStyles(AttributeSet a) {
 194     while (a != null) {
 195         if (a instanceof Style) {
 196             Integer aNum = styleTable.get(a);
 197             if (aNum == null) {
 198                 styleCount = styleCount + 1;
 199                 aNum = styleCount;
 200                 styleTable.put(a, aNum);
 201             }
 202         }
 203         a = a.getResolveParent();
 204     }
 205 }
 206 
 207 private Style findStyle(AttributeSet a)
 208 {
 209     while(a != null) {
 210         if (a instanceof Style) {
 211             Object aNum = styleTable.get(a);
 212             if (aNum != null)
 213                 return (Style)a;
 214         }
 215         a = a.getResolveParent();
 216     }
 217     return null;
 218 }
 219 
 220 private Integer findStyleNumber(AttributeSet a, String domain)
 221 {
 222     while(a != null) {
 223         if (a instanceof Style) {
 224             Integer aNum = styleTable.get(a);
 225             if (aNum != null) {
 226                 if (domain == null ||
 227                     domain.equals(a.getAttribute(Constants.StyleType)))
 228                     return aNum;
 229             }
 230 
 231         }
 232         a = a.getResolveParent();
 233     }
 234     return null;
 235 }
 236 
 237 private static Object attrDiff(MutableAttributeSet oldAttrs,
 238                                AttributeSet newAttrs,
 239                                Object key,
 240                                Object dfl)
 241 {
 242     Object oldValue, newValue;
 243 
 244     oldValue = oldAttrs.getAttribute(key);
 245     newValue = newAttrs.getAttribute(key);
 246 
 247     if (newValue == oldValue)
 248         return null;
 249     if (newValue == null) {
 250         oldAttrs.removeAttribute(key);
 251         if (dfl != null && !dfl.equals(oldValue))
 252             return dfl;
 253         else
 254             return null;
 255     }
 256     if (oldValue == null ||
 257         !equalArraysOK(oldValue, newValue)) {
 258         oldAttrs.addAttribute(key, newValue);
 259         return newValue;
 260     }
 261     return null;
 262 }
 263 
 264 private static boolean equalArraysOK(Object a, Object b)
 265 {
 266     Object[] aa, bb;
 267     if (a == b)
 268         return true;
 269     if (a == null || b == null)
 270         return false;
 271     if (a.equals(b))
 272         return true;
 273     if (!(a.getClass().isArray() && b.getClass().isArray()))
 274         return false;
 275     aa = (Object[])a;
 276     bb = (Object[])b;
 277     if (aa.length != bb.length)
 278         return false;
 279 
 280     int i;
 281     int l = aa.length;
 282     for(i = 0; i < l; i++) {
 283         if (!equalArraysOK(aa[i], bb[i]))
 284             return false;
 285     }
 286 
 287     return true;
 288 }
 289 
 290 /* Writes a line break to the output file, for ease in debugging */
 291 public void writeLineBreak()
 292     throws IOException
 293 {
 294     writeRawString("\n");
 295     afterKeyword = false;
 296 }
 297 
 298 
 299 public void writeRTFHeader()
 300     throws IOException
 301 {
 302     int index;
 303 
 304     /* TODO: Should the writer attempt to examine the text it's writing
 305        and pick a character set which will most compactly represent the
 306        document? (currently the writer always uses the ansi character
 307        set, which is roughly ISO-8859 Latin-1, and uses Unicode escapes
 308        for all other characters. However Unicode is a relatively
 309        recent addition to RTF, and not all readers will understand it.) */
 310     writeBegingroup();
 311     writeControlWord("rtf", 1);
 312     writeControlWord("ansi");
 313     outputConversion = outputConversionForName("ansi");
 314     writeLineBreak();
 315 
 316     /* write font table */
 317     String[] sortedFontTable = new String[fontCount];
 318     Enumeration<String> fonts = fontTable.keys();
 319     String font;
 320     while(fonts.hasMoreElements()) {
 321         font = fonts.nextElement();
 322         Integer num = fontTable.get(font);
 323         sortedFontTable[num.intValue()] = font;
 324     }
 325     writeBegingroup();
 326     writeControlWord("fonttbl");
 327     for(index = 0; index < fontCount; index ++) {
 328         writeControlWord("f", index);
 329         writeControlWord("fnil");  /* TODO: supply correct font style */
 330         writeText(sortedFontTable[index]);
 331         writeText(";");
 332     }
 333     writeEndgroup();
 334     writeLineBreak();
 335 
 336     /* write color table */
 337     if (colorCount > 1) {
 338         Color[] sortedColorTable = new Color[colorCount];
 339         Enumeration<Object> colors = colorTable.keys();
 340         Color color;
 341         while(colors.hasMoreElements()) {
 342             color = (Color)colors.nextElement();
 343             Integer num = colorTable.get(color);
 344             sortedColorTable[num.intValue()] = color;
 345         }
 346         writeBegingroup();
 347         writeControlWord("colortbl");
 348         for(index = 0; index < colorCount; index ++) {
 349             color = sortedColorTable[index];
 350             if (color != null) {
 351                 writeControlWord("red", color.getRed());
 352                 writeControlWord("green", color.getGreen());
 353                 writeControlWord("blue", color.getBlue());
 354             }
 355             writeRawString(";");
 356         }
 357         writeEndgroup();
 358         writeLineBreak();
 359     }
 360 
 361     /* write the style sheet */
 362     if (styleCount > 1) {
 363         writeBegingroup();
 364         writeControlWord("stylesheet");
 365         Enumeration<AttributeSet> styles = styleTable.keys();
 366         while(styles.hasMoreElements()) {
 367             Style style = (Style)styles.nextElement();
 368             int styleNumber = styleTable.get(style).intValue();
 369             writeBegingroup();
 370             String styleType = (String)style.getAttribute(Constants.StyleType);
 371             if (styleType == null)
 372                 styleType = Constants.STParagraph;
 373             if (styleType.equals(Constants.STCharacter)) {
 374                 writeControlWord("*");
 375                 writeControlWord("cs", styleNumber);
 376             } else if(styleType.equals(Constants.STSection)) {
 377                 writeControlWord("*");
 378                 writeControlWord("ds", styleNumber);
 379             } else {
 380                 writeControlWord("s", styleNumber);
 381             }
 382 
 383             AttributeSet basis = style.getResolveParent();
 384             MutableAttributeSet goat;
 385             if (basis == null) {
 386                 goat = new SimpleAttributeSet();
 387             } else {
 388                 goat = new SimpleAttributeSet(basis);
 389             }
 390 
 391             updateSectionAttributes(goat, style, false);
 392             updateParagraphAttributes(goat, style, false);
 393             updateCharacterAttributes(goat, style, false);
 394 
 395             basis = style.getResolveParent();
 396             if (basis != null && basis instanceof Style) {
 397                 Integer basedOn = styleTable.get(basis);
 398                 if (basedOn != null) {
 399                     writeControlWord("sbasedon", basedOn.intValue());
 400                 }
 401             }
 402 
 403             Style nextStyle = (Style)style.getAttribute(Constants.StyleNext);
 404             if (nextStyle != null) {
 405                 Integer nextNum = styleTable.get(nextStyle);
 406                 if (nextNum != null) {
 407                     writeControlWord("snext", nextNum.intValue());
 408                 }
 409             }
 410 
 411             Boolean hidden = (Boolean)style.getAttribute(Constants.StyleHidden);
 412             if (hidden != null && hidden.booleanValue())
 413                 writeControlWord("shidden");
 414 
 415             Boolean additive = (Boolean)style.getAttribute(Constants.StyleAdditive);
 416             if (additive != null && additive.booleanValue())
 417                 writeControlWord("additive");
 418 
 419 
 420             writeText(style.getName());
 421             writeText(";");
 422             writeEndgroup();
 423         }
 424         writeEndgroup();
 425         writeLineBreak();
 426     }
 427 
 428     outputAttributes = new SimpleAttributeSet();
 429 }
 430 
 431 void writeDocumentProperties(Document doc)
 432     throws IOException
 433 {
 434     /* Write the document properties */
 435     int i;
 436     boolean wroteSomething = false;
 437 
 438     for(i = 0; i < RTFAttributes.attributes.length; i++) {
 439         RTFAttribute attr = RTFAttributes.attributes[i];
 440         if (attr.domain() != RTFAttribute.D_DOCUMENT)
 441             continue;
 442         Object prop = doc.getProperty(attr.swingName());
 443         boolean ok = attr.writeValue(prop, this, false);
 444         if (ok)
 445             wroteSomething = true;
 446     }
 447 
 448     if (wroteSomething)
 449         writeLineBreak();
 450 }
 451 
 452 public void writeRTFTrailer()
 453     throws IOException
 454 {
 455     writeEndgroup();
 456     writeLineBreak();
 457 }
 458 
 459 protected void checkNumericControlWord(MutableAttributeSet currentAttributes,
 460                                        AttributeSet newAttributes,
 461                                        Object attrName,
 462                                        String controlWord,
 463                                        float dflt, float scale)
 464     throws IOException
 465 {
 466     Object parm;
 467 
 468     if ((parm = attrDiff(currentAttributes, newAttributes,
 469                          attrName, MagicToken)) != null) {
 470         float targ;
 471         if (parm == MagicToken)
 472             targ = dflt;
 473         else
 474             targ = ((Number)parm).floatValue();
 475         writeControlWord(controlWord, Math.round(targ * scale));
 476     }
 477 }
 478 
 479 protected void checkControlWord(MutableAttributeSet currentAttributes,
 480                                 AttributeSet newAttributes,
 481                                 RTFAttribute word)
 482     throws IOException
 483 {
 484     Object parm;
 485 
 486     if ((parm = attrDiff(currentAttributes, newAttributes,
 487                          word.swingName(), MagicToken)) != null) {
 488         if (parm == MagicToken)
 489             parm = null;
 490         if (!word.writeValue(parm, this, true) &&
 491                 word instanceof RTFAttributes.AssertiveAttribute ) {
 492             currentAttributes.removeAttribute(word.swingName());
 493         }
 494     }
 495 }
 496 
 497 protected void checkControlWords(MutableAttributeSet currentAttributes,
 498                                  AttributeSet newAttributes,
 499                                  RTFAttribute words[],
 500                                  int domain)
 501     throws IOException
 502 {
 503     int wordIndex;
 504     int wordCount = words.length;
 505     for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
 506         RTFAttribute attr = words[wordIndex];
 507         if (attr.domain() == domain)
 508             checkControlWord(currentAttributes, newAttributes, attr);
 509     }
 510 }
 511 
 512 void updateSectionAttributes(MutableAttributeSet current,
 513                              AttributeSet newAttributes,
 514                              boolean emitStyleChanges)
 515     throws IOException
 516 {
 517     if (emitStyleChanges) {
 518         Object oldStyle = current.getAttribute("sectionStyle");
 519         Object newStyle = findStyleNumber(newAttributes, Constants.STSection);
 520         if (oldStyle != newStyle) {
 521             if (oldStyle != null) {
 522                 resetSectionAttributes(current);
 523             }
 524             if (newStyle != null) {
 525                 writeControlWord("ds", ((Integer)newStyle).intValue());
 526                 current.addAttribute("sectionStyle", newStyle);
 527             } else {
 528                 current.removeAttribute("sectionStyle");
 529             }
 530         }
 531     }
 532 
 533     checkControlWords(current, newAttributes,
 534                       RTFAttributes.attributes, RTFAttribute.D_SECTION);
 535 }
 536 
 537 protected void resetSectionAttributes(MutableAttributeSet currentAttributes)
 538     throws IOException
 539 {
 540     writeControlWord("sectd");
 541 
 542     int wordIndex;
 543     int wordCount = RTFAttributes.attributes.length;
 544     for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
 545         RTFAttribute attr = RTFAttributes.attributes[wordIndex];
 546         if (attr.domain() == RTFAttribute.D_SECTION)
 547             attr.setDefault(currentAttributes);
 548     }
 549 
 550     currentAttributes.removeAttribute("sectionStyle");
 551 }
 552 
 553 void updateParagraphAttributes(MutableAttributeSet current,
 554                                AttributeSet newAttributes,
 555                                boolean emitStyleChanges)
 556     throws IOException
 557 {
 558     Object parm;
 559     Object oldStyle, newStyle;
 560 
 561     /* The only way to get rid of tabs or styles is with the \pard keyword,
 562        emitted by resetParagraphAttributes(). Ideally we should avoid
 563        emitting \pard if the new paragraph's tabs are a superset of the old
 564        paragraph's tabs. */
 565 
 566     if (emitStyleChanges) {
 567         oldStyle = current.getAttribute("paragraphStyle");
 568         newStyle = findStyleNumber(newAttributes, Constants.STParagraph);
 569         if (oldStyle != newStyle) {
 570             if (oldStyle != null) {
 571                 resetParagraphAttributes(current);
 572                 oldStyle = null;
 573             }
 574         }
 575     } else {
 576         oldStyle = null;
 577         newStyle = null;
 578     }
 579 
 580     Object oldTabs = current.getAttribute(Constants.Tabs);
 581     Object newTabs = newAttributes.getAttribute(Constants.Tabs);
 582     if (oldTabs != newTabs) {
 583         if (oldTabs != null) {
 584             resetParagraphAttributes(current);
 585             oldTabs = null;
 586             oldStyle = null;
 587         }
 588     }
 589 
 590     if (oldStyle != newStyle && newStyle != null) {
 591         writeControlWord("s", ((Integer)newStyle).intValue());
 592         current.addAttribute("paragraphStyle", newStyle);
 593     }
 594 
 595     checkControlWords(current, newAttributes,
 596                       RTFAttributes.attributes, RTFAttribute.D_PARAGRAPH);
 597 
 598     if (oldTabs != newTabs && newTabs != null) {
 599         TabStop tabs[] = (TabStop[])newTabs;
 600         int index;
 601         for(index = 0; index < tabs.length; index ++) {
 602             TabStop tab = tabs[index];
 603             switch (tab.getAlignment()) {
 604               case TabStop.ALIGN_LEFT:
 605               case TabStop.ALIGN_BAR:
 606                 break;
 607               case TabStop.ALIGN_RIGHT:
 608                 writeControlWord("tqr");
 609                 break;
 610               case TabStop.ALIGN_CENTER:
 611                 writeControlWord("tqc");
 612                 break;
 613               case TabStop.ALIGN_DECIMAL:
 614                 writeControlWord("tqdec");
 615                 break;
 616             }
 617             switch (tab.getLeader()) {
 618               case TabStop.LEAD_NONE:
 619                 break;
 620               case TabStop.LEAD_DOTS:
 621                 writeControlWord("tldot");
 622                 break;
 623               case TabStop.LEAD_HYPHENS:
 624                 writeControlWord("tlhyph");
 625                 break;
 626               case TabStop.LEAD_UNDERLINE:
 627                 writeControlWord("tlul");
 628                 break;
 629               case TabStop.LEAD_THICKLINE:
 630                 writeControlWord("tlth");
 631                 break;
 632               case TabStop.LEAD_EQUALS:
 633                 writeControlWord("tleq");
 634                 break;
 635             }
 636             int twips = Math.round(20f * tab.getPosition());
 637             if (tab.getAlignment() == TabStop.ALIGN_BAR) {
 638                 writeControlWord("tb", twips);
 639             } else {
 640                 writeControlWord("tx", twips);
 641             }
 642         }
 643         current.addAttribute(Constants.Tabs, tabs);
 644     }
 645 }
 646 
 647 public void writeParagraphElement(Element el)
 648     throws IOException
 649 {
 650     updateParagraphAttributes(outputAttributes, el.getAttributes(), true);
 651 
 652     int sub_count = el.getElementCount();
 653     for(int idx = 0; idx < sub_count; idx ++) {
 654         writeTextElement(el.getElement(idx));
 655     }
 656 
 657     writeControlWord("par");
 658     writeLineBreak();  /* makes the raw file more readable */
 659 }
 660 
 661 /* debugging. TODO: remove.
 662 private static String tabdump(Object tso)
 663 {
 664     String buf;
 665     int i;
 666 
 667     if (tso == null)
 668         return "[none]";
 669 
 670     TabStop[] ts = (TabStop[])tso;
 671 
 672     buf = "[";
 673     for(i = 0; i < ts.length; i++) {
 674         buf = buf + ts[i].toString();
 675         if ((i+1) < ts.length)
 676             buf = buf + ",";
 677     }
 678     return buf + "]";
 679 }
 680 */
 681 
 682 protected void resetParagraphAttributes(MutableAttributeSet currentAttributes)
 683     throws IOException
 684 {
 685     writeControlWord("pard");
 686 
 687     currentAttributes.addAttribute(StyleConstants.Alignment, Integer.valueOf(0));
 688 
 689     int wordIndex;
 690     int wordCount = RTFAttributes.attributes.length;
 691     for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
 692         RTFAttribute attr = RTFAttributes.attributes[wordIndex];
 693         if (attr.domain() == RTFAttribute.D_PARAGRAPH)
 694             attr.setDefault(currentAttributes);
 695     }
 696 
 697     currentAttributes.removeAttribute("paragraphStyle");
 698     currentAttributes.removeAttribute(Constants.Tabs);
 699 }
 700 
 701 void updateCharacterAttributes(MutableAttributeSet current,
 702                                AttributeSet newAttributes,
 703                                boolean updateStyleChanges)
 704     throws IOException
 705 {
 706     Object parm;
 707 
 708     if (updateStyleChanges) {
 709         Object oldStyle = current.getAttribute("characterStyle");
 710         Object newStyle = findStyleNumber(newAttributes,
 711                                           Constants.STCharacter);
 712         if (oldStyle != newStyle) {
 713             if (oldStyle != null) {
 714                 resetCharacterAttributes(current);
 715             }
 716             if (newStyle != null) {
 717                 writeControlWord("cs", ((Integer)newStyle).intValue());
 718                 current.addAttribute("characterStyle", newStyle);
 719             } else {
 720                 current.removeAttribute("characterStyle");
 721             }
 722         }
 723     }
 724 
 725     if ((parm = attrDiff(current, newAttributes,
 726                          StyleConstants.FontFamily, null)) != null) {
 727         Integer fontNum = fontTable.get(parm);
 728         writeControlWord("f", fontNum.intValue());
 729     }
 730 
 731     checkNumericControlWord(current, newAttributes,
 732                             StyleConstants.FontSize, "fs",
 733                             defaultFontSize, 2f);
 734 
 735     checkControlWords(current, newAttributes,
 736                       RTFAttributes.attributes, RTFAttribute.D_CHARACTER);
 737 
 738     checkNumericControlWord(current, newAttributes,
 739                             StyleConstants.LineSpacing, "sl",
 740                             0, 20f); /* TODO: sl wackiness */
 741 
 742     if ((parm = attrDiff(current, newAttributes,
 743                          StyleConstants.Background, MagicToken)) != null) {
 744         int colorNum;
 745         if (parm == MagicToken)
 746             colorNum = 0;
 747         else
 748             colorNum = colorTable.get(parm).intValue();
 749         writeControlWord("cb", colorNum);
 750     }
 751 
 752     if ((parm = attrDiff(current, newAttributes,
 753                          StyleConstants.Foreground, null)) != null) {
 754         int colorNum;
 755         if (parm == MagicToken)
 756             colorNum = 0;
 757         else
 758             colorNum = colorTable.get(parm).intValue();
 759         writeControlWord("cf", colorNum);
 760     }
 761 }
 762 
 763 protected void resetCharacterAttributes(MutableAttributeSet currentAttributes)
 764     throws IOException
 765 {
 766     writeControlWord("plain");
 767 
 768     int wordIndex;
 769     int wordCount = RTFAttributes.attributes.length;
 770     for(wordIndex = 0; wordIndex < wordCount; wordIndex++) {
 771         RTFAttribute attr = RTFAttributes.attributes[wordIndex];
 772         if (attr.domain() == RTFAttribute.D_CHARACTER)
 773             attr.setDefault(currentAttributes);
 774     }
 775 
 776     StyleConstants.setFontFamily(currentAttributes, defaultFontFamily);
 777     currentAttributes.removeAttribute(StyleConstants.FontSize); /* =default */
 778     currentAttributes.removeAttribute(StyleConstants.Background);
 779     currentAttributes.removeAttribute(StyleConstants.Foreground);
 780     currentAttributes.removeAttribute(StyleConstants.LineSpacing);
 781     currentAttributes.removeAttribute("characterStyle");
 782 }
 783 
 784 public void writeTextElement(Element el)
 785     throws IOException
 786 {
 787     updateCharacterAttributes(outputAttributes, el.getAttributes(), true);
 788 
 789     if (el.isLeaf()) {
 790         try {
 791             el.getDocument().getText(el.getStartOffset(),
 792                                      el.getEndOffset() - el.getStartOffset(),
 793                                      this.workingSegment);
 794         } catch (BadLocationException ble) {
 795             /* TODO is this the correct error to raise? */
 796             ble.printStackTrace();
 797             throw new InternalError(ble.getMessage());
 798         }
 799         writeText(this.workingSegment);
 800     } else {
 801         int sub_count = el.getElementCount();
 802         for(int idx = 0; idx < sub_count; idx ++)
 803             writeTextElement(el.getElement(idx));
 804     }
 805 }
 806 
 807 public void writeText(Segment s)
 808     throws IOException
 809 {
 810     int pos, end;
 811     char[] array;
 812 
 813     pos = s.offset;
 814     end = pos + s.count;
 815     array = s.array;
 816     for( ; pos < end; pos ++)
 817         writeCharacter(array[pos]);
 818 }
 819 
 820 public void writeText(String s)
 821     throws IOException
 822 {
 823     int pos, end;
 824 
 825     pos = 0;
 826     end = s.length();
 827     for( ; pos < end; pos ++)
 828         writeCharacter(s.charAt(pos));
 829 }
 830 
 831 public void writeRawString(String str)
 832     throws IOException
 833 {
 834     int strlen = str.length();
 835     for (int offset = 0; offset < strlen; offset ++)
 836         outputStream.write((int)str.charAt(offset));
 837 }
 838 
 839 public void writeControlWord(String keyword)
 840     throws IOException
 841 {
 842     outputStream.write('\\');
 843     writeRawString(keyword);
 844     afterKeyword = true;
 845 }
 846 
 847 public void writeControlWord(String keyword, int arg)
 848     throws IOException
 849 {
 850     outputStream.write('\\');
 851     writeRawString(keyword);
 852     writeRawString(String.valueOf(arg)); /* TODO: correct in all cases? */
 853     afterKeyword = true;
 854 }
 855 
 856 public void writeBegingroup()
 857     throws IOException
 858 {
 859     outputStream.write('{');
 860     afterKeyword = false;
 861 }
 862 
 863 public void writeEndgroup()
 864     throws IOException
 865 {
 866     outputStream.write('}');
 867     afterKeyword = false;
 868 }
 869 
 870 @SuppressWarnings("fallthrough")
 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 
 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.) */
 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 
 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. */
 989 protected static 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 }