package com.srbenoit.font;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.Iterator;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageOutputStream;
import com.srbenoit.log.LoggedPanel;
/**
* A panel that renders the glyphs of a font.
*/
public class GlyphPanel extends LoggedPanel {
/** version number for serialization */
private static final long serialVersionUID = -3511066856984634391L;
/** the owning frame */
private final transient ViewerInt ownerFrame;
/** the font to be rendered */
private transient Font font = null;
/** an offscreen image that is rendered to */
private transient BufferedImage offscreen = null;
/** the width of the box for each glyph */
private transient int boxWidth;
/** the height of the box for each glyph */
private transient int boxHeight;
/** flag to control whether bounds are drawn around characters */
private transient boolean drawBoxes = false;
/** flag indicating offscreen image needs to be repainted */
private transient boolean dirty = false;
/**
* Constructs a new GlyphPanel
.
*
* @param owner the viewer that owns this panel
*/
public GlyphPanel(final ViewerInt owner) {
super();
this.ownerFrame = owner;
setPreferredSize(new Dimension(640, 480));
setBackground(Color.white);
}
/**
* Sets the font that the panel will render.
*
* @param theFont the font
*/
public void setTheFont(final Font theFont) {
this.font = theFont;
this.dirty = true;
}
/**
* Sets the flag controlling whether or not bounds are drawn.
*
* @param drawBoundsBoxes true
to draw bounds boxes; false
* otherwise
*/
public void setDrawBoundsBoxes(final boolean drawBoundsBoxes) {
this.drawBoxes = drawBoundsBoxes;
this.dirty = true;
}
/**
* Gets the state of the flag controlling whether or not bounds are drawn.
*
* @return true
if bounding boxes are being drawn; false
otherwise
*/
public boolean isDrawingBoundsBoxes() {
return this.drawBoxes;
}
/**
* Redraws the panel. If this is the first time paint has been called since the font was
* changed, the offscreen glyph image is created.
*
* @param grx the Graphics
object to render to
*/
@Override public void paint(final Graphics grx) {
grx.setColor(Color.white);
grx.fillRect(0, 0, getWidth(), getHeight());
if (this.dirty) {
if (this.offscreen != null) {
this.offscreen.flush();
}
buildOffscreen((Graphics2D) grx);
this.dirty = false;
}
grx.drawImage(this.offscreen, 0, 0, this);
}
/**
* Creates an offscreen image with the glyph renderings.
*
* @param grx the Graphics2D
object to render to
*/
private void buildOffscreen(final Graphics2D grx) {
char[] chars;
Font labelFont;
FontMetrics fmLabel;
FontMetrics fmFont;
int columns;
int imgW;
int imgH;
int rows;
Graphics2D g2d;
// Get the characters the font supports
chars = getFontSupportedChars(this.font);
// Create a font to be used for labeling
labelFont = new Font("Dialog", Font.PLAIN, 9);
fmLabel = grx.getFontMetrics(labelFont);
fmFont = grx.getFontMetrics(this.font);
// Find box width based on max size of label or glyph
this.boxWidth = fmLabel.stringWidth("9999");
if (this.boxWidth < (int) (fmFont.getMaxCharBounds(grx).getWidth() + 0.9)) {
this.boxWidth = (int) (fmFont.getMaxCharBounds(grx).getWidth() + 0.9);
}
this.boxWidth++; // add a pixel per box for left border
// Using box width, and assuming right border, find boxes per row
columns = 639 / this.boxWidth;
// Compute total width of image, including borders
imgW = (this.boxWidth * columns) + 1; // add a pixel for right border
// Determine the number of rows
rows = chars.length / columns; // full rows
if ((rows * columns) < chars.length) {
rows++; // partial row
}
// Compute box height. Note we don't need descent on label since
// digits don't extend below baseline, but we add 1 pixel below,
// along with 1 pixel for an interior border line.
this.boxHeight = fmFont.getHeight() + fmLabel.getAscent();
this.boxHeight += 2; // Add top border and border between glyph and
// label
imgH = (this.boxHeight * rows) + 1; // add a pixel for bottom border
// Add space for a line for the font name
imgH += fmFont.getHeight() + fmFont.getLeading();
g2d = createOffscreen(imgW, imgH);
drawGrid(g2d, fmFont);
g2d.setFont(labelFont);
drawLabels(g2d, chars, fmFont, fmLabel);
makeImage(g2d, fmFont, chars);
setPreferredSize(new Dimension(imgW, imgH));
this.ownerFrame.updateScroller(this.boxHeight);
}
/**
* Creates the offscreen image and build its graphics object.
*
* @param imgW the image width
* @param imgH the image height
* @return the Graphics2D
for the image
*/
private Graphics2D createOffscreen(final int imgW, final int imgH) {
Graphics2D g2d;
this.offscreen = new BufferedImage(imgW, imgH, BufferedImage.TYPE_INT_RGB);
g2d = (Graphics2D) (this.offscreen.getGraphics());
g2d.setBackground(Color.white);
g2d.clearRect(0, 0, imgW, imgH);
return g2d;
}
/**
* Draws the grid on the image.
*
* @param g2d the Graphics2D
to which to draw
* @param fmFont the metrics of the font being rendered
*/
private void drawGrid(final Graphics2D g2d, final FontMetrics fmFont) {
int inx;
int yPos;
int rows;
int columns;
rows = this.offscreen.getHeight() / this.boxHeight;
columns = this.offscreen.getWidth() / this.boxWidth;
yPos = fmFont.getHeight();
// Draw grid
for (inx = 0; inx <= rows; inx++) {
g2d.setColor(Color.lightGray);
g2d.fillRect(0, yPos + (inx * this.boxHeight) + fmFont.getHeight(),
this.offscreen.getWidth(), this.boxHeight - fmFont.getHeight());
g2d.setColor(Color.black);
g2d.drawLine(0, yPos + (inx * this.boxHeight), this.offscreen.getWidth(),
yPos + (inx * this.boxHeight));
g2d.setColor(Color.gray);
g2d.drawLine(0, yPos + (inx * this.boxHeight) + fmFont.getHeight(),
this.offscreen.getWidth(), yPos + (inx * this.boxHeight) + fmFont.getHeight());
}
g2d.setColor(Color.BLACK);
for (inx = 0; inx <= columns; inx++) {
g2d.drawLine(inx * this.boxWidth, yPos, inx * this.boxWidth,
yPos + this.offscreen.getHeight() - fmFont.getHeight() - fmFont.getLeading() - 1);
}
}
/**
* Draws the labels on the image.
*
* @param g2d the Graphics2D
to which to draw
* @param chars the characters supported by the font
* @param fmFont the metrics of the font being rendered
* @param fmLabel the metrics of the label font
*/
private void drawLabels(final Graphics2D g2d, final char[] chars, final FontMetrics fmFont,
final FontMetrics fmLabel) {
int xPos;
int yPos;
int inx;
String str;
yPos = fmFont.getHeight();
xPos = 0;
for (inx = 0; inx < chars.length; inx++) {
str = Integer.toHexString(chars[inx]);
g2d.drawString(str, xPos + 1 + ((this.boxWidth - fmLabel.stringWidth(str)) / 2),
yPos + fmFont.getHeight() + fmLabel.getAscent());
xPos += this.boxWidth;
if (xPos >= (this.offscreen.getWidth() - 1)) {
xPos = 0;
yPos += this.boxHeight;
}
}
}
/**
* Draws the image with a grid with labels and all the glyphs.
*
* @param g2d the Graphics
to which to draw
* @param fmFont the metrics for the font being displayed
* @param chars the characters supported by the font
*/
private void makeImage(final Graphics2D g2d, final FontMetrics fmFont, final char[] chars) {
FontRenderContext frc;
int inx;
int xPos;
int yPos;
String str;
int pixX;
int pixY;
char[] chr;
try {
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
} catch (NoSuchFieldError e) {
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
}
// Draw the line of example text
g2d.setFont(this.font);
g2d.setColor(Color.black);
str = this.font.getName() + ", " + this.font.getSize() + " point ";
g2d.drawString(str, 5, fmFont.getAscent() + (fmFont.getLeading() / 2));
xPos = 5 + fmFont.stringWidth(str);
g2d.setFont(this.font.deriveFont(Font.BOLD));
str = " Bold, ";
g2d.drawString(str, xPos, fmFont.getAscent() + (fmFont.getLeading() / 2));
xPos += g2d.getFontMetrics().stringWidth(str);
g2d.setFont(this.font.deriveFont(Font.ITALIC));
str = " Italic";
g2d.drawString(str, xPos, fmFont.getAscent() + (fmFont.getLeading() / 2));
xPos += g2d.getFontMetrics().stringWidth(str);
g2d.setFont(this.font);
// Draw glyphs
xPos = 0;
yPos = fmFont.getHeight();
g2d.setFont(this.font);
chr = new char[1];
for (inx = 0; inx < chars.length; inx++) {
chr[0] = chars[inx];
pixX = xPos + 1 + ((this.boxWidth - fmFont.charWidth(chr[0])) / 2);
pixY = yPos + fmFont.getAscent();
// Draw a glyph bounds box around the character, if configured
if (this.drawBoxes) {
g2d.setColor(Color.LIGHT_GRAY);
frc = g2d.getFontRenderContext();
g2d.draw(this.font.createGlyphVector(frc, chr).getPixelBounds(frc, pixX, pixY));
g2d.setColor(Color.BLACK);
}
// Draw the character
g2d.drawChars(chr, 0, 1, pixX, pixY);
xPos += this.boxWidth;
if (xPos >= (this.offscreen.getWidth() - 1)) {
xPos = 0;
yPos += this.boxHeight;
}
}
}
/**
* Gets the list of characters that a font supports.
*
* @param fnt the font
* @return the list of supported characters
*/
private char[] getFontSupportedChars(final Font fnt) {
StringBuilder builder;
char[] chars;
builder = new StringBuilder(200);
for (char c = 0; c < 0xFFFF; c++) {
if (fnt.canDisplay(c)) {
builder.append(c);
}
}
chars = builder.toString().toCharArray();
return chars;
}
/**
* Exports the image as a JPEG file.
*
* @param target the file to write to
*/
public void export(final File target) {
Iterator iter;
ImageWriter writer;
FileImageOutputStream fios;
iter = ImageIO.getImageWritersByFormatName("png");
if (iter.hasNext()) {
writer = iter.next();
try {
fios = new FileImageOutputStream(target);
writer.setOutput(fios);
writer.write(this.offscreen);
fios.close();
} catch (Exception exc) {
LOG.log(Level.WARNING, "Failed to write {0}: {1}",
new Object[] { target.getAbsolutePath(), exc.getLocalizedMessage() });
}
}
}
}