/*
* 07/28/2008
*
* RtfGenerator.java - Generates RTF via a simple Java API.
* Copyright (C) 2008 Robert Futrell
* robert_futrell at users.sourceforge.net
* http://fifesoft.com/rsyntaxtextarea
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
*/
package org.fife.ui.rsyntaxtextarea;
import java.awt.Color;
import java.awt.Font;
import java.awt.GraphicsEnvironment;
import java.awt.Toolkit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Generates RTF text via a simple Java API.
* <p>
*
* The following RTF features are supported:
* <ul>
* <li>Fonts
* <li>Font sizes
* <li>Foreground and background colors
* <li>Bold, italic, and underline
* </ul>
*
* The RTF generated isn't really "optimized," but it will do, especially for small amounts of text, such as what's
* common when copy-and-pasting. It tries to be sufficient for the use case of copying syntax highlighted code:
* <ul>
* <li>It assumes that tokens changing foreground color often is fairly common.
* <li>It assumes that background highlighting is fairly uncommon.
* </ul>
*
* @author Robert Futrell
* @version 1.0
*/
public class RtfGenerator {
private List fontList;
private List colorList;
private StringBuffer document;
private boolean lastWasControlWord;
private int lastFontIndex;
private int lastFGIndex;
private boolean lastBold;
private boolean lastItalic;
private int lastFontSize;
private String monospacedFontName;
/**
* Java2D assumes a 72 dpi screen resolution, but on Windows the screen resolution is either 96 dpi or 120 dpi,
* depending on your font display settings. This is an attempt to make the RTF generated match the size of what's
* displayed in the RSyntaxTextArea.
*/
private int screenRes;
/**
* The default font size for RTF. This is point size, in half points.
*/
private static final int DEFAULT_FONT_SIZE = 12;// 24;
/**
* Constructor.
*/
public RtfGenerator() {
fontList = new ArrayList(1); // Usually only 1.
colorList = new ArrayList(1); // Usually only 1.
document = new StringBuffer();
reset();
}
/**
* Adds a newline to the RTF document.
*
* @see #appendToDoc(String, Font, Color, Color)
*/
public void appendNewline() {
document.append("\\par");
document.append('\n'); // Just for ease of reading RTF.
lastWasControlWord = false;
}
/**
* Appends styled text to the RTF document being generated.
*
* @param text
* The text to append.
* @param f
* The font of the text. If this is <code>null</code>, the default font is used.
* @param fg
* The foreground of the text. If this is <code>null</code>, the default foreground color is used.
* @param bg
* The background color of the text. If this is <code>null</code>, the default background color is used.
* @see #appendNewline()
*/
public void appendToDoc(String text, Font f, Color fg, Color bg) {
appendToDoc(text, f, fg, bg, false);
}
/**
* Appends styled text to the RTF document being generated.
*
* @param text
* The text to append.
* @param f
* The font of the text. If this is <code>null</code>, the default font is used.
* @param bg
* The background color of the text. If this is <code>null</code>, the default background color is used.
* @param underline
* Whether the text should be underlined.
* @see #appendNewline()
*/
public void appendToDocNoFG(String text, Font f, Color bg,
boolean underline) {
appendToDoc(text, f, null, bg, underline, false);
}
/**
* Appends styled text to the RTF document being generated.
*
* @param text
* The text to append.
* @param f
* The font of the text. If this is <code>null</code>, the default font is used.
* @param fg
* The foreground of the text. If this is <code>null</code>, the default foreground color is used.
* @param bg
* The background color of the text. If this is <code>null</code>, the default background color is used.
* @param underline
* Whether the text should be underlined.
* @see #appendNewline()
*/
public void appendToDoc(String text, Font f, Color fg, Color bg,
boolean underline) {
appendToDoc(text, f, fg, bg, underline, true);
}
/**
* Appends styled text to the RTF document being generated.
*
* @param text
* The text to append.
* @param f
* The font of the text. If this is <code>null</code>, the default font is used.
* @param fg
* The foreground of the text. If this is <code>null</code>, the default foreground color is used.
* @param bg
* The background color of the text. If this is <code>null</code>, the default background color is used.
* @param underline
* Whether the text should be underlined.
* @param setFG
* Whether the foreground specified by <code>fg</code> should be honored (if it is non-<code>null</code>
* ).
* @see #appendNewline()
*/
public void appendToDoc(String text, Font f, Color fg, Color bg,
boolean underline, boolean setFG) {
if (text != null) {
// Set font to use, if different from last addition.
int fontIndex = f == null ? 0 : (getFontIndex(fontList, f) + 1);
if (fontIndex != lastFontIndex) {
document.append("\\f").append(fontIndex);
lastFontIndex = fontIndex;
lastWasControlWord = true;
}
// Set styles to use.
if (f != null) {
int fontSize = fixFontSize(f.getSize2D()); // Half points
if (fontSize != lastFontSize) {
document.append("\\fs").append(fontSize);
lastFontSize = fontSize;
lastWasControlWord = true;
}
if (f.isBold() != lastBold) {
document.append(lastBold ? "\\b0" : "\\b");
lastBold = !lastBold;
lastWasControlWord = true;
}
if (f.isItalic() != lastItalic) {
document.append(lastItalic ? "\\i0" : "\\i");
lastItalic = !lastItalic;
lastWasControlWord = true;
}
}
else { // No font specified - assume neither bold nor italic.
if (lastFontSize != DEFAULT_FONT_SIZE) {
document.append("\\fs").append(DEFAULT_FONT_SIZE);
lastFontSize = DEFAULT_FONT_SIZE;
lastWasControlWord = true;
}
if (lastBold) {
document.append("\\b0");
lastBold = false;
lastWasControlWord = true;
}
if (lastItalic) {
document.append("\\i0");
lastItalic = false;
lastWasControlWord = true;
}
}
if (underline) {
document.append("\\ul");
lastWasControlWord = true;
}
// Set the foreground color.
if (setFG) {
int fgIndex = 0;
if (fg != null) { // null => fg color index 0
fgIndex = getIndex(colorList, fg) + 1;
}
if (fgIndex != lastFGIndex) {
document.append("\\cf").append(fgIndex);
lastFGIndex = fgIndex;
lastWasControlWord = true;
}
}
// Set the background color.
if (bg != null) {
int pos = getIndex(colorList, bg);
document.append("\\highlight").append(pos + 1);
lastWasControlWord = true;
}
if (lastWasControlWord) {
document.append(' '); // Delimiter
lastWasControlWord = false;
}
escapeAndAdd(document, text);
// Reset everything that was set for this text fragment.
if (bg != null) {
document.append("\\highlight0");
lastWasControlWord = true;
}
if (underline) {
document.append("\\ul0");
lastWasControlWord = true;
}
}
}
/**
* Appends some text to a buffer, with special care taken for special characters as defined by the RTF spec:
*
* <ul>
* <li>All tab characters are replaced with the string "<code>\tab</code>"
* <li>'\', '{' and '}' are changed to "\\", "\{" and "\}"
* </ul>
*
* @param text
* The text to append (with tab chars substituted).
* @param sb
* The buffer to append to.
*/
private final void escapeAndAdd(StringBuffer sb, String text) {
// TODO: On the move to 1.5 use StringBuffer append() overloads that
// can take a CharSequence and a range of that CharSequence to speed
// things up.
// int last = 0;
int count = text.length();
for (int i = 0; i < count; i++) {
char ch = text.charAt(i);
switch (ch) {
case '\t':
// Micro-optimization: for syntax highlighting with
// tab indentation, there are often multiple tabs
// back-to-back at the start of lines, so don't put
// spaces between each "\tab".
sb.append("\\tab");
while ((++i < count) && text.charAt(i) == '\t') {
sb.append("\\tab");
}
sb.append(' ');
i--; // We read one too far.
break;
case '\\':
case '{':
case '}':
sb.append('\\').append(ch);
break;
default:
sb.append(ch);
break;
}
}
}
/**
* Returns a font point size adjusted for the current screen resolution. Java2D assumes 72 dpi. On systems with
* larger dpi (Windows, GTK, etc.), font rendering will appear to small if we simply return a Java "Font" object's
* getSize() value. We need to adjust it for the screen resolution.
*
* @param pointSize
* A Java Font's point size, as returned from <code>getSize2D()</code>.
* @return The font point size, adjusted for the current screen resolution. This will allow other applications to
* render fonts the same size as they appear in the Java application.
*/
private int fixFontSize(float pointSize) {
if (screenRes != 72) { // Java2D assumes 72 dpi
pointSize = (int) Math.round(pointSize * screenRes / 72.0);
}
return (int) pointSize;
}
private String getColorTableRtf() {
// Example:
// "{\\colortbl ;\\red255\\green0\\blue0;\\red0\\green0\\blue255; }"
StringBuffer sb = new StringBuffer();
sb.append("{\\colortbl ;");
for (int i = 0; i < colorList.size(); i++) {
Color c = (Color) colorList.get(i);
sb.append("\\red").append(c.getRed());
sb.append("\\green").append(c.getGreen());
sb.append("\\blue").append(c.getBlue());
sb.append(';');
}
sb.append("}");
return sb.toString();
}
/**
* Returns the index of the specified font in a list of fonts. This method only checks for a font by its family
* name; its attributes such as bold and italic are ignored.
* <p>
*
* If the font is not in the list, it is added, and its new index is returned.
*
* @param list
* The list (possibly) containing the font.
* @param font
* The font to get the index of.
* @return The index of the font.
*/
private static int getFontIndex(List list, Font font) {
String fontName = font.getFamily();
for (int i = 0; i < list.size(); i++) {
Font font2 = (Font) list.get(i);
if (font2.getFamily().equals(fontName)) {
return i;
}
}
list.add(font);
return list.size() - 1;
}
private String getFontTableRtf() {
// Example:
// "{\\fonttbl{\\f0\\fmodern\\fcharset0 Courier;}}"
StringBuffer sb = new StringBuffer();
// Workaround for text areas using the Java logical font "Monospaced"
// by default. There's no way to know what it's mapped to, so we
// just search for a monospaced font on the system.
String monoFamilyName = getMonospacedFontName();
sb.append("{\\fonttbl{\\f0\\fnil\\fcharset0 " + monoFamilyName + ";}");
for (int i = 0; i < fontList.size(); i++) {
Font f = (Font) fontList.get(i);
String familyName = f.getFamily();
if (familyName.equals("Monospaced")) {
familyName = monoFamilyName;
}
sb.append("{\\f").append(i + 1).append("\\fnil\\fcharset0 ");
sb.append(familyName).append(";}");
}
sb.append('}');
return sb.toString();
}
/**
* Returns the index of the specified item in a list. If the item is not in the list, it is added, and its new index
* is returned.
*
* @param list
* The list (possibly) containing the item.
* @param item
* The item to get the index of.
* @return The index of the item.
*/
private static int getIndex(List list, Object item) {
int pos = list.indexOf(item);
if (pos == -1) {
list.add(item);
pos = list.size() - 1;
}
return pos;
}
/**
* Try to pick a monospaced font installed on this system. We try to check for monospaced fonts that are commonly
* installed on different OS's. This information was gleaned from
* http://www.codestyle.org/css/font-family/sampler-Monospace.shtml.
*
* @return The name of a monospaced font.
*/
private String getMonospacedFontName() {
if (monospacedFontName == null) {
GraphicsEnvironment ge = GraphicsEnvironment.
getLocalGraphicsEnvironment();
String[] familyNames = ge.getAvailableFontFamilyNames();
Arrays.sort(familyNames);
boolean windows = System.getProperty("os.name").toLowerCase().
indexOf("windows") >= 0;
// "Monaco" is the "standard" monospaced font on OS X. We'll
// check for it first so on Macs we don't get stuck with the
// uglier Courier New. It'll look funny on Windows though, so
// don't pick it if we're on Windows.
// It's found on Windows 1.76% of the time, OS X 96.73%
// of the time, and UNIX 00.00% (?) of the time.
if (!windows && Arrays.binarySearch(familyNames, "Monaco") >= 0) {
monospacedFontName = "Monaco";
}
// "Courier New" is found on Windows 96.48% of the time,
// OS X 92.38% of the time, and UNIX 61.95% of the time.
else if (Arrays.binarySearch(familyNames, "Courier New") >= 0) {
monospacedFontName = "Courier New";
}
// "Courier" is found on Windows ??.??% of the time,
// OS X 96.27% of the time, and UNIX 74.04% of the time.
else if (Arrays.binarySearch(familyNames, "Courier") >= 0) {
monospacedFontName = "Courier";
}
// "Nimbus Mono L" is on Windows 00.00% (?) of the time,
// OS X 00.00% (?) of the time, but on UNIX 88.79% of the time.
else if (Arrays.binarySearch(familyNames, "Nimbus Mono L") >= 0) {
monospacedFontName = "Nimbus Mono L";
}
// "Lucida Sans Typewriter" is on Windows 49.37% of the time,
// OS X 90.43% of the time, and UNIX 00.00% (?) of the time.
else if (Arrays.binarySearch(familyNames, "Lucida Sans Typewriter") >= 0) {
monospacedFontName = "Lucida Sans Typewriter";
}
// "Bitstream Vera Sans Mono" is on Windows 29.81% of the time,
// OS X 25.53% of the time, and UNIX 80.71% of the time.
else if (Arrays.binarySearch(familyNames, "Bitstream Vera Sans Mono") >= 0) {
monospacedFontName = "Bitstream Vera Sans Mono";
}
// Windows: 34.16% of the time, OS X: 00.00% (?) of the time,
// UNIX: 33.92% of the time.
if (monospacedFontName == null) {
monospacedFontName = "Terminal";
}
}
return monospacedFontName;
}
/**
* Returns the RTF document created by this generator.
*
* @return The RTF document, as a <code>String</code>.
*/
public String getRtf() {
StringBuffer sb = new StringBuffer();
sb.append("{");
// Header
sb.append("\\rtf1\\ansi\\ansicpg1252");
sb.append("\\deff0"); // First font in font table is the default
sb.append("\\deflang1033");
sb.append("\\viewkind4"); // "Normal" view
sb.append("\\uc\\pard\\f0");
sb.append("\\fs20"); // Font size in half-points (default 24)
sb.append(getFontTableRtf()).append('\n');
sb.append(getColorTableRtf()).append('\n');
// Content
sb.append(document);
sb.append("}");
// System.err.println("*** " + sb.length());
return sb.toString();
}
/**
* Resets this generator. All document information and content is cleared.
*/
public void reset() {
fontList.clear();
colorList.clear();
document.setLength(0);
lastWasControlWord = false;
lastFontIndex = 0;
lastFGIndex = 0;
lastBold = false;
lastItalic = false;
lastFontSize = DEFAULT_FONT_SIZE;
screenRes = Toolkit.getDefaultToolkit().getScreenResolution();
}
}