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 }