/*
* Copyright (c) 2008 Patrick Wright
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the
* as published by the Free Software Foundation; either version 2.1
* of the License, or (at your option) any later version.
*
* This program 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 program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
import com.lowagie.text.DocumentException;
import com.lowagie.text.pdf.BaseFont;
import org.w3c.dom.Document;
import org.xhtmlrenderer.event.DefaultDocumentListener;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;
import org.xhtmlrenderer.resource.FSEntityResolver;
import org.xhtmlrenderer.resource.XMLResource;
import org.xhtmlrenderer.simple.FSScrollPane;
import org.xhtmlrenderer.simple.XHTMLPanel;
import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
import org.xhtmlrenderer.swing.FSMouseListener;
import org.xhtmlrenderer.swing.LinkListener;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.DefaultHandler2;
import org.xml.sax.helpers.XMLReaderFactory;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Opens a frame and displays, for a selected font, the glyphs for a range of Unicode code points. Can be used to
* identify which glyphs are supported by a font. Can export to PDF. Requires core-renderer and iText on classpath.
*
* @author Patrick Wright
*/
public class FontGlyphTableRender {
private static final int TO_SWING = 1;
private static final int TO_PDF = 2;
private static final String OUTPUT_ENTITIES = "entities";
private static final String OUTPUT_CODEPOINTS = "codepoints";
private static final int ENT_PER_PAGE = 399; // based on number we can fit on one printed (PDF) part
private int curFrom;
private String outputType = OUTPUT_CODEPOINTS;
private Font currentFont;
private JFrame frame;
private XHTMLPanel xpanel;
private JTextField fontPathTF;
private JTextField familyNameField;
private JButton prevBtn;
private JButton nextBtn;
public static void main(String[] args) throws Exception {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
new FontGlyphTableRender().run();
} catch (Throwable e) {
e.printStackTrace();
}
}
});
}
private void run() throws Exception {
frame = new JFrame("Flying Saucer: Show Font Glyphs");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel optionsPanel = new JPanel(new BorderLayout());
// TODO: don't know a good way to determine path where fonts are stored, per-os
// "/usr/share/fonts/truetype/freefont/FreeMono.ttf"
fontPathTF = new JTextField();
fontPathTF.setColumns(40);
familyNameField = new JTextField();
familyNameField.setEnabled(true);
familyNameField.setEditable(false);
familyNameField.setColumns(40);
JPanel top1 = new JPanel(new FlowLayout(FlowLayout.LEFT));
top1.add(new JLabel("Enter font path: "));
top1.add(fontPathTF);
JButton chooseFontFileBtn = new JButton("...");
chooseFontFileBtn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
String filename = File.separator + "tmp";
String famPath = fontPathTF.getText();
if (currentFont != null && famPath.length() > 0) {
filename = new File(famPath).getParent();
}
JFileChooser fc = new JFileChooser(new File(filename));
fc.showOpenDialog(frame);
File selFile = fc.getSelectedFile();
Font font = null;
String msg = "";
try {
font = loadFont(selFile.getPath());
} catch (IOException e1) {
// swallow, just allow font to be null
msg = e1.getMessage();
}
if (font == null) {
JOptionPane.showMessageDialog(frame, "Can't load file--is it a valid Font file? " + msg);
} else {
fontPathTF.setText(selFile.getPath());
familyNameField.setText(font.getFamily());
}
}
});
top1.add(chooseFontFileBtn);
ActionListener outputSelection = new ActionListener() {
public void actionPerformed(ActionEvent e) {
outputType = e.getActionCommand();
enableButtons();
if (currentFont != null) {
deferredChangePage(curFrom);
}
}
};
JRadioButton jrbCodePoint = new JRadioButton("Codepoints");
jrbCodePoint.setActionCommand(OUTPUT_CODEPOINTS);
jrbCodePoint.addActionListener(outputSelection);
jrbCodePoint.setSelected(true);
JRadioButton jrbEntities = new JRadioButton("Entities");
jrbEntities.setActionCommand(OUTPUT_ENTITIES);
jrbEntities.addActionListener(outputSelection);
ButtonGroup bg = new ButtonGroup();
bg.add(jrbCodePoint);
bg.add(jrbEntities);
top1.add(jrbCodePoint);
top1.add(jrbEntities);
JPanel top2 = new JPanel(new FlowLayout(FlowLayout.LEFT));
top2.add(new JLabel("Family: "));
top2.add(familyNameField);
JPanel top = new JPanel(new BorderLayout());
top.add(top1, BorderLayout.NORTH);
top.add(top2, BorderLayout.CENTER);
JPanel mid = new JPanel(new FlowLayout(FlowLayout.LEFT));
prevBtn = new JButton("Prev");
nextBtn = new JButton("Next");
JButton pdfBtn = new JButton("PDF");
JButton renderBtn = new JButton("Render");
prevBtn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
deferredChangePage(curFrom - ENT_PER_PAGE);
}
});
nextBtn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
deferredChangePage(curFrom + ENT_PER_PAGE);
}
});
renderBtn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
deferredChangePage(curFrom);
}
});
pdfBtn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent actionEvent) {
resolveCurrentFont();
if (currentFont == null) {
JOptionPane.showMessageDialog(frame, "Need a valid font file path");
fontPathTF.requestFocus();
return;
}
deferredLoadAndRender(curFrom, TO_PDF);
}
});
mid.add(prevBtn);
mid.add(nextBtn);
mid.add(renderBtn);
mid.add(pdfBtn);
optionsPanel.add(top, BorderLayout.NORTH);
optionsPanel.add(mid, BorderLayout.CENTER);
fontPathTF.addActionListener(new AbstractAction() {
public void actionPerformed(ActionEvent actionEvent) {
deferredChangePage(curFrom);
}
});
// Create a JPanel subclass to render the page
xpanel = new XHTMLPanel();
xpanel.addDocumentListener(new DefaultDocumentListener() {
public void documentLoaded() {
frame.setCursor(Cursor.getDefaultCursor());
}
});
resetMouseListeners();
// Put our xpanel in a scrolling pane. You can use
// a regular JScrollPane here, or our FSScrollPane.
// FSScrollPane is already set up to move the correct
// amount when scrolling 1 line or 1 page
FSScrollPane scroll = new FSScrollPane(xpanel);
scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
JPanel cont = new JPanel(new BorderLayout());
cont.add(optionsPanel, BorderLayout.NORTH);
cont.add(scroll, BorderLayout.CENTER);
frame.getContentPane().add(cont);
frame.pack();
frame.setSize(1024, 730);
enableButtons();
frame.setVisible(true);
}
private void resetMouseListeners() {
List l = xpanel.getMouseTrackingListeners();
for (Iterator i = l.iterator(); i.hasNext();) {
FSMouseListener listener = (FSMouseListener) i.next();
if (listener instanceof LinkListener) {
xpanel.removeMouseTrackingListener(listener);
}
}
}
private void enableButtons() {
prevBtn.setEnabled(outputType.equals(OUTPUT_CODEPOINTS) && curFrom > 0);
nextBtn.setEnabled(outputType.equals(OUTPUT_CODEPOINTS) && Math.pow(2, 16) - curFrom != 0);
}
private Font loadFont(String fontPath) throws IOException {
Font font;
try {
font = Font.createFont(Font.TRUETYPE_FONT, new File(fontPath).toURL().openStream());
return font.deriveFont(Font.PLAIN, 12);
} catch (FontFormatException e) {
System.err.println(fontPath + " INVALID FONT FORMAT " + e.getMessage());
return null;
}
}
private void deferredChangePage(final int startAt) {
deferredLoadAndRender(startAt, TO_SWING);
}
private void deferredLoadAndRender(final int startAt, final int renderTo) {
resolveCurrentFont();
if (currentFont == null) {
JOptionPane.showMessageDialog(frame, "Can't load font--check font file path.");
fontPathTF.requestFocus();
return;
}
frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
new Thread(new Runnable() {
public void run() {
final Document doc = loadDocument(startAt);
if (renderTo == TO_SWING) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
curFrom = startAt;
xpanel.setDocument(doc, null, new XhtmlNamespaceHandler());
xpanel.getSharedContext().getCss().getCascadedStyle(null, false);
} catch (Throwable e) {
JOptionPane.showMessageDialog(frame, "Can't load document (table of glyphs). Err: " + e.getMessage());
}
enableButtons();
}
});
} else {
frame.setCursor(Cursor.getDefaultCursor());
renderPDF(doc);
}
}
}).start();
}
private Document loadDocument(int startAt) {
curFrom = startAt;
String page;
if (outputType.equals(OUTPUT_CODEPOINTS)) {
Table table = buildGlyphTable(startAt, startAt + ENT_PER_PAGE);
page = new Page().toHtml(table.toHtml(getFontFamily(), curFrom), getFontFamily());
} else {
page = parseEnt(new Page().toHtml("", getFontFamily()));
}
// DEBUG
//System.out.println(page);
InputSource is = new InputSource(new BufferedReader(new StringReader(page)));
return XMLResource.load(is).getDocument();
}
private Table buildGlyphTable(int from, int to) {
Table table = new Table(16);
for (int j = from; j <= to; j++) {
if (isLegalInXml(j)) {
if (currentFont.canDisplay((char)j)) {
table.addColumn("&#" + j + ";");
table.addGlyph("&#" + j + ";");
} else {
table.addColumn("&#" + j + ";");
table.addGlyph(" ");
}
} else {
table.addColumn("&#" + j + ";");
table.addGlyph("!");
}
}
return table;
}
private void renderPDF(Document doc) {
String msgToUser = "";
File f;
try {
f = File.createTempFile("flying-saucer-glyph-test", ".pdf");
} catch (IOException e) {
//
JOptionPane.showMessageDialog(frame, "Can't create temp file for PDF output, err: " + e.getMessage());
return;
}
final ITextRenderer renderer = new ITextRenderer();
FileOutputStream fos = null;
try {
fos = new FileOutputStream(f);
BufferedOutputStream bos = new BufferedOutputStream(fos);
renderer.setDocument(doc, null, new XhtmlNamespaceHandler());
ITextFontResolver resolver = renderer.getFontResolver();
// TODO: encoding is hard-coded as IDENTITY_H; maybe give user option to override
resolver.addFont(
fontPathTF.getText(),
BaseFont.IDENTITY_H,
BaseFont.EMBEDDED
);
renderer.layout();
renderer.createPDF(bos);
msgToUser = "Rendered PDF: " + f.getCanonicalPath();
} catch (FileNotFoundException e) {
msgToUser = "Can't create PDF, err: " + e.getMessage();
} catch (DocumentException e) {
msgToUser = "Can't create PDF, err: " + e.getMessage();
} catch (IOException e) {
msgToUser = "Can't create PDF, err: " + e.getMessage();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
// swallow
}
}
}
if (msgToUser.length() != 0) {
final String finalMsg = msgToUser;
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JOptionPane.showMessageDialog(frame, finalMsg);
}
});
}
}
// attempts to load Font given font path; if succeeds, currentFont will have the reference, otherwise it will
// be set to null
private void resolveCurrentFont() {
String path = fontPathTF.getText();
if (path.length() == 0) {
currentFont = null;
return;
}
try {
currentFont = loadFont(path);
if (currentFont != null) {
familyNameField.setText(currentFont.getFamily());
}
} catch (IOException e) {
e.printStackTrace();
currentFont = null;
}
}
private String parseEnt(String html) {
try {
final Table table = new Table(15);
XMLReader parser = XMLReaderFactory.createXMLReader();
InputSource is = new InputSource(new BufferedReader(new StringReader(html)));
try {
parser.setFeature("http://xml.org/sax/features/validation", true);
} catch (SAXException e) {
System.err.println("Cannot activate validation.");
}
parser.setEntityResolver(FSEntityResolver.instance());
DefaultHandler2 dh2 = new DefaultHandler2() {
boolean isEnt;
// we'll get a callback for each DTD loaded; we're interested in the entity includes
public void externalEntityDecl(String name, String publicId, String systemId) throws SAXException {
super.externalEntityDecl(name, publicId, systemId);
isEnt = systemId.endsWith(".ent");
}
// we'll get a callback for each entity; those starting with % are elements, can skip
public void internalEntityDecl(String name, String value) throws SAXException {
super.internalEntityDecl(name, value);
if (isEnt) {
if (name.startsWith("%")) return;
int codePoint = (int)value.charAt(0);
// FIXME: codePointAt not available in 1.4
// Character.codePointAt(value.toCharArray(), 0);
table.addColumn("&" + name + ";");
table.addColumn("&#" + codePoint + ";");
table.addGlyph("&#" + codePoint + ";");
}
}
};
parser.setProperty("http://xml.org/sax/properties/declaration-handler", dh2);
parser.parse(is);
return new Page().toHtml(table.toHtml(getFontFamily(), 0), getFontFamily());
}
catch (SAXException e) {
e.printStackTrace();
return "";
}
catch (IOException e) {
e.printStackTrace();
return "";
}
}
private boolean isLegalInXml(int uccp) {
// see http://www.xml.com/2000/05/10/conformance/reports/report-xerces-nv.html
// and http://cse-mjmcl.cse.bris.ac.uk/blog/2007/02/14/1171465494443.html
return ((uccp == 0x9) ||
(uccp == 0xA) ||
(uccp == 0xD) ||
((uccp >= 0x20) && (uccp <= 0xD7FF)) ||
((uccp >= 0xE000) && (uccp <= 0xFFFD)) ||
((uccp >= 0x10000) && (uccp <= 0x10FFFF))) &&
uccp != 0x0DDD; // causes VM crash on ubuntu...
}
private String getFontFamily() {
return currentFont.getFamily();
}
private static class Page {
public String toHtml(String bodyContent, String fontFamily) {
StringBuffer sb = new StringBuffer();
sb.append(getHeadDecl(getStyleDecl(fontFamily)));
sb.append("<body>\n");
sb.append(bodyContent);
sb.append("</body>\n");
sb.append("</html>\n");
return sb.toString();
}
private String getHeadDecl(String style) {
return "<?xml version='1.0' encoding='utf-8'?>\n" +
"<!DOCTYPE html PUBLIC '-//W3C//DTD XHTML 1.0 Strict//EN' 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'>\n" +
"<html xmlns='http://www.w3.org/1999/xhtml'>\n" +
"<head>" +
"<title>Full Entity Chart</title>\n" +
style +
"</head>\n";
}
private String getStyleDecl(String fontFamily) {
String css = "* {font-size: 8pt; font-family: \"" + fontFamily + "\" } " +
"table {table-layout: fixed; width: 100%; border-collapse: collapse; border: 1px solid black;} " +
"col {} " +
".glyph {width: 1.35em; border-right-width: 2px;} " +
"td {border: 1px solid black; }" +
"td .glyph {}";
return "<style type=\"text/css\">\n" + css + "\n</style>\n";
}
}
private static class Table {
private int colCnt;
private List cols = new ArrayList();
private List headerLines = new ArrayList();
public Table(int colCnt) {
this.colCnt = colCnt;
}
public String toHtml(String fontFamily, int curFrom) {
StringBuffer sb = new StringBuffer();
for (Iterator it = headerLines.iterator(); it.hasNext();) {
String line = (String) it.next();
sb.append("<p>").append(line).append("</p>\n");
}
sb.append("<p>Table of Unicode Characters</p>\n");
sb.append("<p>Using font: ").append(fontFamily).append(", Unicode code points starting with ").append(curFrom).append("</p>\n");
sb.append("<p>Empty cell means no glyph available; ! means codepoint not allowed in XML, per spec.</p>\n");
sb.append("<table>\n");
for (int i = 0; i < colCnt; i++) {
sb.append("<col class=\"").append(((Col) cols.get(i)).cssClass).append("\"/>\n");
}
int cnt = 0;
sb.append("<tr>\n");
for (Iterator it = cols.iterator(); it.hasNext();) {
Col col = (Col) it.next();
sb.append("<td class=\"").append(((Col) cols.get(cnt)).cssClass).append("\">").append(col.content).append("</td>");
if (++cnt % colCnt == 0 && it.hasNext()) {
sb.append("\n</tr>\n");
sb.append("<tr>\n");
}
}
sb.append("\n</tr>\n");
sb.append("</table>\n");
return sb.toString();
}
public void addColumn(String content) {
cols.add(new Col("", content));
}
public void addGlyph(String content) {
cols.add(new Col("glyph", content));
}
public void addHeaderLine(String text) {
headerLines.add(text);
}
static class Col {
private final String cssClass;
private final String content;
public Col(String cssClass, String content) {
this.cssClass = cssClass;
this.content = content;
}
}
}
}