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 
 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 
 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, 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 
 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 
 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 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 
 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 */
 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 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         word.writeValue(parm, this, true);
 495     }
 496 }
 497 
 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 
 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 
 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 
 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 
 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 
 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 
 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 
 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 
 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 
 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 
 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 
 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 
 840 public void writeControlWord(String keyword)
 841     throws IOException
 842 {
 843     outputStream.write('\\');
 844     writeRawString(keyword);
 845     afterKeyword = true;
 846 }
 847 
 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 
 857 public void writeBegingroup()
 858     throws IOException
 859 {
 860     outputStream.write('{');
 861     afterKeyword = false;
 862 }
 863 
 864 public void writeEndgroup()
 865     throws IOException
 866 {
 867     outputStream.write('}');
 868     afterKeyword = false;
 869 }
 870 
 871 @SuppressWarnings("fallthrough")
 872 public void writeCharacter(char ch)
 873     throws IOException
 874 {
 875     /* Nonbreaking space is in most RTF encodings, but the keyword is
 876        preferable; same goes for tabs */
 877     if (ch == 0xA0) { /* nonbreaking space */
 878         outputStream.write(0x5C);  /* backslash */
 879         outputStream.write(0x7E);  /* tilde */
 880         afterKeyword = false; /* non-alpha keywords are self-terminating */
 881         return;
 882     }
 883 
 884     if (ch == 0x09) { /* horizontal tab */
 885         writeControlWord("tab");
 886         return;
 887     }
 888 
 889     if (ch == 10 || ch == 13) { /* newline / paragraph */
 890         /* ignore CRs, we'll write a paragraph element soon enough */
 891         return;
 892     }
 893 
 894     int b = convertCharacter(outputConversion, ch);
 895     if (b == 0) {
 896         /* Unicode characters which have corresponding RTF keywords */
 897         int i;
 898         for(i = 0; i < textKeywords.length; i++) {
 899             if (textKeywords[i].character == ch) {
 900                 writeControlWord(textKeywords[i].keyword);
 901                 return;
 902             }
 903         }
 904         /* In some cases it would be reasonable to check to see if the
 905            glyph being written out is in the Symbol encoding, and if so,
 906            to switch to the Symbol font for this character. TODO. */
 907         /* Currently all unrepresentable characters are written as
 908            Unicode escapes. */
 909         String approximation = approximationForUnicode(ch);
 910         if (approximation.length() != unicodeCount) {
 911             unicodeCount = approximation.length();
 912             writeControlWord("uc", unicodeCount);
 913         }
 914         writeControlWord("u", (int)ch);
 915         writeRawString(" ");
 916         writeRawString(approximation);
 917         afterKeyword = false;
 918         return;
 919     }
 920 
 921     if (b > 127) {
 922         int nybble;
 923         outputStream.write('\\');
 924         outputStream.write('\'');
 925         nybble = ( b & 0xF0 ) >>> 4;
 926         outputStream.write(hexdigits[nybble]);
 927         nybble = ( b & 0x0F );
 928         outputStream.write(hexdigits[nybble]);
 929         afterKeyword = false;
 930         return;
 931     }
 932 
 933     switch (b) {
 934     case '}':
 935     case '{':
 936     case '\\':
 937         outputStream.write(0x5C);  /* backslash */
 938         afterKeyword = false;  /* in a keyword, actually ... */
 939         /* fall through */
 940     default:
 941         if (afterKeyword) {
 942             outputStream.write(0x20);  /* space */
 943             afterKeyword = false;
 944         }
 945         outputStream.write(b);
 946         break;
 947     }
 948 }
 949 
 950 String approximationForUnicode(char ch)
 951 {
 952     /* TODO: Find reasonable approximations for all Unicode characters
 953        in all RTF code pages... heh, heh... */
 954     return "?";
 955 }
 956 
 957 /** Takes a translation table (a 256-element array of characters)
 958  * and creates an output conversion table for use by
 959  * convertCharacter(). */
 960     /* Not very efficient at all. Could be changed to sort the table
 961        for binary search. TODO. (Even though this is inefficient however,
 962        writing RTF is still much faster than reading it.) */
 963 static int[] outputConversionFromTranslationTable(char[] table)
 964 {
 965     int[] conversion = new int[2 * table.length];
 966 
 967     int index;
 968 
 969     for(index = 0; index < table.length; index ++) {
 970         conversion[index * 2] = table[index];
 971         conversion[(index * 2) + 1] = index;
 972     }
 973 
 974     return conversion;
 975 }
 976 
 977 static int[] outputConversionForName(String name)
 978     throws IOException
 979 {
 980     char[] table = (char[])RTFReader.getCharacterSet(name);
 981     return outputConversionFromTranslationTable(table);
 982 }
 983 
 984 /** Takes a char and a conversion table (an int[] in the current
 985  * implementation, but conversion tables should be treated as an opaque
 986  * type) and returns the
 987  * corresponding byte value (as an int, since bytes are signed).
 988  */
 989     /* Not very efficient. TODO. */
 990 static protected int convertCharacter(int[] conversion, char ch)
 991 {
 992    int index;
 993 
 994    for(index = 0; index < conversion.length; index += 2) {
 995        if(conversion[index] == ch)
 996            return conversion[index + 1];
 997    }
 998 
 999    return 0;  /* 0 indicates an unrepresentable character */
1000 }
1001 
1002 }