/**
* Copyright 2014 Will Knez
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wbknez.ktour.ui;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
/**
* A factory that can produce an image of a chess piece by rendering a Unicode
* character onto a pixel buffer.
*
* <p>
* This is, more or less, a cleaned up version of Andrew Thompson's answer from
* the following <i>StackOverflow</i> question:
* <a href="https://stackoverflow.com/questions/18686199/fill-unicode-characters-in-labels">Java: Fill Unicode Characters in Labels</a>
* </p>
*/
public class UnicodePieceFactory {
/**
* Represents a chess piece as a Unicode string.
*/
private static final class Piece {
/** The Unicode representation of this chess piece. */
private final String unicodeString;
/**
* Constructor.
*
* @param unicodeString
* The Unicode representation to use.
* @throws NullPointerException
* If <code>unicodeString</code> is <code>null</code>.
*/
public Piece(final String unicodeString) {
if(unicodeString == null) {
throw new NullPointerException();
}
this.unicodeString = unicodeString;
}
/**
* Returns the Unicode representation of this chess piece.
*
* @return The Unicode representation used.
*/
public String getUnicodeRepresentation() {
return this.unicodeString;
}
@Override
public String toString() {
return this.getUnicodeRepresentation();
}
}
/**
* Represents the class of chess pieces that are White.
*/
public static final class White {
/** A white king. */
public static final Piece King = new Piece("\u2654");
/** A white queen. */
public static final Piece Queen = new Piece("\u2655");
/** A white rook. */
public static final Piece Rook = new Piece("\u2656");
/** A white bishop. */
public static final Piece Bishop = new Piece("\u2657");
/** A white knight. */
public static final Piece Knight = new Piece("\u2658");
/** A white pawn. */
public static final Piece Pawn = new Piece("\u2659");
}
/**
* Represents the class of chess pieces that are Black.
*/
public static final class Black {
/** A black king. */
public static final Piece King = new Piece("\u265A");
/** A black queen. */
public static final Piece Queen = new Piece("\u265B");
/** A black rook. */
public static final Piece Rook = new Piece("\u265C");
/** A black bishop. */
public static final Piece Bishop = new Piece("\u265D");
/** A black pawn. */
public static final Piece Knight = new Piece("\u265E");
/** A black king. */
public static final Piece Pawn = new Piece("\u265F");
}
/**
* Default rendering hints.
*
* <p>
* For use when custom rendering hints are not desired.
* </p>
*/
public static final RenderingHints DefaultRenderingHints =
new RenderingHints(null);
static {
DefaultRenderingHints.put(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
DefaultRenderingHints.put(RenderingHints.KEY_DITHERING,
RenderingHints.VALUE_DITHER_ENABLE);
DefaultRenderingHints.put(RenderingHints.KEY_ALPHA_INTERPOLATION,
RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
}
/**
* Renders an image of the specified chess piece using Unicode symbols.
*
* @param piece
* The chess piece to create.
* @param outlineColor
* The color to use for the border, or outline, of the piece.
* @param fillColor
* The color to use for the "body", or fill, of the piece.
* @param font
* The font to use for creating the piece. Please note, this font
* <b>must</b> support the Unicode character(s) required by
* <code>piece</code>.
* @param hints
* The rendering hints to use when drawing the piece.
* @param stroke
* The stroke to use for the border and outline of the piece; this
* allows the use of dashed or other non-solid lines.
* @return The specified piece as an image.
* @throws NullPointerException
* If <code>piece</code>, <code>outlineColor</code>,
* <code>fillColor</code>, <code>font</code>, <code>hints</code>, or
* <code>stroke</code> are <code>null</code>.
*/
public BufferedImage createPiece(final Piece piece,
final Color outlineColor, final Color fillColor, final Font font,
final RenderingHints hints, final Stroke stroke) {
if (piece == null) {
throw new NullPointerException();
}
if (font == null) {
throw new NullPointerException();
}
if (hints == null) {
throw new NullPointerException();
}
if (outlineColor == null) {
throw new NullPointerException();
}
if (fillColor == null) {
throw new NullPointerException();
}
if (stroke == null) {
throw new NullPointerException();
}
// Set up the rendering canvas.
final int fontSize = font.getSize();
final BufferedImage resultantImage =
new BufferedImage(fontSize, fontSize,
BufferedImage.TYPE_INT_ARGB);
final Graphics2D graphics2D = (Graphics2D) resultantImage.getGraphics();
// Set the hints (if any) for font rendering.
graphics2D.setRenderingHints(hints);
final FontRenderContext fontContext = graphics2D.getFontRenderContext();
final GlyphVector glyphVector =
font.createGlyphVector(fontContext,
piece.getUnicodeRepresentation());
// Obtain a glyph outline from the font.
final Rectangle glyphBounds =
this.convertToIntegerBounds(glyphVector.getVisualBounds());
final Shape glyphShape = glyphVector.getOutline();
// Create the "real" glyph, centered on a new, transparent image.
final Shape centeredGlyphShape =
this.getCenteredShapeByBounds(fontSize, glyphBounds,
glyphShape);
final Area centeredGlyphArea = new Area(centeredGlyphShape);
final Area pieceShape =
this.getCroppedShapeByBounds(fontSize, centeredGlyphShape);
// Split into pieces to be redrawn (via an outline).
final ArrayList<Shape> splitRegions =
this.splitShapeIntoRegionsByBounds(pieceShape);
// Set the rendering options.
graphics2D.setStroke(stroke);
graphics2D.setColor(fillColor);
// Draw.
for (final Shape subRegion : splitRegions) {
final Rectangle subBounds = subRegion.getBounds();
if (Double.compare(subBounds.getX(), 0.0) != 0
|| Double.compare(subBounds.getY(), 0.0) != 0) {
graphics2D.fill(subRegion);
}
}
// Fill in the outline.
graphics2D.setColor(outlineColor);
graphics2D.fill(centeredGlyphArea);
// Clean up.
graphics2D.dispose();
return resultantImage;
}
/**
* Converts the specified rectangle with double coordinates to a rectangle
* that uses integer coordinates instead.
*
* @param rect
* The rectangle (that uses double coordinates) to convert.
* @return A rectangle whose coordinates are the integer equivalents of the
* specified (parameter) rectangle.
* @throws NullPointerException
* If <code>rect</code> is <code>null</code>.
*/
private Rectangle convertToIntegerBounds(final Rectangle2D rect) {
if (rect == null) {
throw new NullPointerException();
}
final Rectangle rectangle = new Rectangle();
rectangle.x = (int) rect.getX();
rectangle.y = (int) rect.getY();
rectangle.width = (int) rect.getWidth();
rectangle.height = (int) rect.getHeight();
return rectangle;
}
/**
* Crops the specified shape to the space occupied by the specified font
* size.
*
* @param fontSize
* The desired font size to crop to.
* @param origin
* The original shape to use (and crop).
* @return The specified shape reformatted to fit the specified font.
* @throws NullPointerException
* If <code>origin</code> is <code>null</code>.
*/
private Area getCroppedShapeByBounds(final int fontSize,
final Shape origin) {
if (origin == null) {
throw new NullPointerException();
}
final Area croppedArea =
new Area(new Rectangle2D.Double(0, 0, fontSize, fontSize));
final Area originArea = new Area(origin);
// Remove the original image from the cropped version, if necessary.
croppedArea.subtract(originArea);
return croppedArea;
}
/**
* Centers the specified glyph in the space occupied by the specified font
* size.
*
* <p>
* This prevents the glyph from being rendered askew because of any
* discrepancy between the desired font size and the size of the base glyph.
* </p>
*
* @param fontSize
* The size of the font to use.
* @param originBounds
* The bounds of the original shape.
* @param origin
* The original shape to use.
* @return A new shape that is "centered" on the region of space denoted by
* the font size.
*/
private Shape getCenteredShapeByBounds(final int fontSize,
final Rectangle originBounds, final Shape origin) {
if (originBounds == null) {
throw new NullPointerException();
}
if (origin == null) {
throw new NullPointerException();
}
// Get the empty space between the glyph and the font space.
final int xMargin = fontSize - (int) originBounds.getWidth();
final int yMargin = fontSize - (int) originBounds.getHeight();
// Create a transform to center the original shape in the new space.
final AffineTransform transform =
AffineTransform.getTranslateInstance((xMargin / 2)
+ (-1 * (int) originBounds.getX()), (yMargin / 2)
+ (-1 * (int) originBounds.getY()));
return transform.createTransformedShape(origin);
}
/**
* Split the specified shape into a list of sub-shapes, as defined by any
* geometric paths it may contain within.
*
* @param origin
* The shape to iterate over, path-wise.
* @return A list of shapes that denotes a path of sub-regions within the
* original shape.
* @throws NullPointerException
* If <code>origin</code> is <code>null</code>.
*/
private ArrayList<Shape> splitShapeIntoRegionsByBounds(final Shape origin) {
if (origin == null) {
throw new NullPointerException();
}
final ArrayList<Shape> splitRegions = new ArrayList<>();
final PathIterator pathIterator = origin.getPathIterator(null);
final GeneralPath generalPath = new GeneralPath();
while (!pathIterator.isDone()) {
final double[] coordinates = new double[6];
final int segmentType = pathIterator.currentSegment(coordinates);
final int windingRule = pathIterator.getWindingRule();
generalPath.setWindingRule(windingRule);
switch (segmentType) {
case PathIterator.SEG_MOVETO:
generalPath.reset();
generalPath.setWindingRule(windingRule);
generalPath.moveTo(coordinates[0], coordinates[1]);
break;
case PathIterator.SEG_LINETO:
generalPath.lineTo(coordinates[0], coordinates[1]);
break;
case PathIterator.SEG_QUADTO:
generalPath.quadTo(coordinates[0], coordinates[1],
coordinates[2], coordinates[3]);
break;
case PathIterator.SEG_CUBICTO:
generalPath.curveTo(coordinates[0], coordinates[1],
coordinates[2], coordinates[3], coordinates[4],
coordinates[5]);
break;
case PathIterator.SEG_CLOSE:
generalPath.closePath();
final Area pathArea = new Area(generalPath);
splitRegions.add(pathArea);
break;
default:
throw new RuntimeException("Unknown segment type.");
}
pathIterator.next();
}
return splitRegions;
}
}