/*
* Copyright © 2007-2011 Rebecca G. Bettencourt / Kreative Software
* <p>
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* <a href="http://www.mozilla.org/MPL/">http://www.mozilla.org/MPL/</a>
* <p>
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
* License for the specific language governing rights and limitations
* under the License.
* <p>
* Alternatively, the contents of this file may be used under the terms
* of the GNU Lesser General Public License (the "LGPL License"), in which
* case the provisions of LGPL License are applicable instead of those
* above. If you wish to allow use of your version of this file only
* under the terms of the LGPL License and not to allow others to use
* your version of this file under the MPL, indicate your decision by
* deleting the provisions above and replace them with the notice and
* other provisions required by the LGPL License. If you do not delete
* the provisions above, a recipient may use your version of this file
* under either the MPL or the LGPL License.
* @since KSFL 1.0
* @author Rebecca G. Bettencourt, Kreative Software
*/
package com.kreative.rsrc;
import java.awt.*;
import java.awt.image.MemoryImageSource;
import java.io.UnsupportedEncodingException;
import com.kreative.ksfl.*;
/**
* The <code>RFont</code> class represents a Mac OS bitmap font resource.
* @since KSFL 1.0
* @author Rebecca G. Bettencourt, Kreative Software
*/
public class FontResource extends MacResource {
/**
* The resource type of a Mac OS bitmap font resource,
* the four-character constant <code>NFNT</code>.
*/
public static final int RESOURCE_TYPE = KSFLConstants.NFNT;
/**
* The resource type of an old-style Mac OS bitmap font resource,
* the four-character constant <code>FONT</code>.
*/
public static final int RESOURCE_TYPE_OLD = KSFLConstants.FONT;
/**
* Checks if a resource type is one this class knows how to handle.
* The default implementation is to always return true.
* It is recommended that subclasses override this.
* @param type A resource type to check.
* @return True if this class can handle this resource type, false otherwise.
*/
public static boolean isMyType(int type) {
return (type == RESOURCE_TYPE || type == RESOURCE_TYPE_OLD);
}
/**
* Constructs a new resource of type <code>NFNT</code> with the specified ID and data.
* The name is set to an empty string, and all attributes are cleared.
* @param id The resource ID.
* @param data The resource data.
*/
public FontResource(short id, byte[] data) {
super(RESOURCE_TYPE, id, data);
}
/**
* Constructs a new resource of type <code>NFNT</code> with the specified ID, name, and data.
* All attributes are cleared.
* @param id The resource ID.
* @param name The resource name.
* @param data The resource data.
*/
public FontResource(short id, String name, byte[] data) {
super(RESOURCE_TYPE, id, name, data);
}
/**
* Constructs a new resource of type <code>NFNT</code> with the specified ID, attributes, and data.
* The name is set to an empty string.
* @param id The resource ID.
* @param attr The resource attributes as a byte.
* @param data The resource data.
*/
public FontResource(short id, byte attr, byte[] data) {
super(RESOURCE_TYPE, id, attr, data);
}
/**
* Constructs a new resource of type <code>NFNT</code> with the specified ID, attributes, name, and data.
* @param id The resource ID.
* @param attr The resource attributes as a byte.
* @param name The resource name.
* @param data The resource data.
*/
public FontResource(short id, byte attr, String name, byte[] data) {
super(RESOURCE_TYPE, id, attr, name, data);
}
/**
* Constructs a new resource with the specified type, ID, and data.
* The name is set to an empty string, and all attributes are cleared.
* @param type The resource type as an integer.
* @param id The resource ID.
* @param data The resource data.
*/
public FontResource(int type, short id, byte[] data) {
super(type, id, data);
}
/**
* Constructs a new resource with the specified type, ID, name, and data.
* All attributes are cleared.
* @param type The resource type as an integer.
* @param id The resource ID.
* @param name The resource name.
* @param data The resource data.
*/
public FontResource(int type, short id, String name, byte[] data) {
super(type, id, name, data);
}
/**
* Constructs a new resource with the specified type, ID, attributes, and data.
* The name is set to an empty string.
* @param type The resource type as an integer.
* @param id The resource ID.
* @param attr The resource attributes as a byte.
* @param data The resource data.
*/
public FontResource(int type, short id, byte attr, byte[] data) {
super(type, id, attr, data);
}
/**
* Constructs a new resource with the specified type, ID, attributes, name, and data.
* @param type The resource type as an integer.
* @param id The resource ID.
* @param attr The resource attributes as a byte.
* @param name The resource name.
* @param data The resource data.
*/
public FontResource(int type, short id, byte attr, String name, byte[] data) {
super(type, id, attr, name, data);
}
/**
* Flag bit indicating that this font has an image height table.
*/
public static final short FLAG_HAS_IMAGE_HEIGHT_TABLE = 0x0001;
/**
* Flag bit indicating that this font has a glyph width table with fractional values.
*/
public static final short FLAG_HAS_GLYPH_WIDTH_TABLE = 0x0002;
/**
* Flag bits indicating the bit depth of the font.
*/
public static final short FLAG_BIT_DEPTH = 0x000C;
/**
* Value of the bit depth flags representing a 1-bit font.
*/
public static final short FLAG_BIT_DEPTH_1BIT = 0x0000;
/**
* Value of the bit depth flags representing a 2-bit font.
*/
public static final short FLAG_BIT_DEPTH_2BIT = 0x0004;
/**
* Value of the bit depth flags representing a 4-bit font.
*/
public static final short FLAG_BIT_DEPTH_4BIT = 0x0008;
/**
* Value of the bit depth flags representing an 8-bit font.
*/
public static final short FLAG_BIT_DEPTH_8BIT = 0x000C;
/**
* Flag bit indicating that this font has an accompanying <code>fctb</code>
* resource containing the font's color table.
*/
public static final short FLAG_HAS_FONT_COLOR_TABLE = 0x0080;
/**
* Flag bit indicating that this font is synthetic, or automatically created by QuickDraw.
*/
public static final short FLAG_IS_SYNTHETIC = 0x0100;
/**
* Flag bit indicating that this font is in color.
*/
public static final short FLAG_HAS_COLOR = 0x0200;
/**
* Reserved flag bit that must be set to one, as opposed to zero.
*/
public static final short FLAG_RESERVED_SET_BIT = 0x1000;
/**
* Flag bit indicating that this font is fixed-width.
*/
public static final short FLAG_FIXED_WIDTH = 0x2000;
/**
* Flag bit indicating that QuickDraw should not create synthetic fonts based on this font.
*/
public static final short FLAG_DONT_MAKE_SYNTHETIC = 0x4000;
/** Returns the flag bits describing the properties of this font. */
public short getFlags() { return KSFLUtilities.getShort(data, 0); }
/** Returns the character value of the first character in this font. This is in the font's encoding, which is not going to be Unicode in most cases. */
public char getFirstChar() { return (char)KSFLUtilities.getShort(data, 2); }
/** Returns the character value of the last character in this font. This is in the font's encoding, which is not going to be Unicode in most cases. */
public char getLastChar() { return (char)KSFLUtilities.getShort(data, 4); }
/** Returns the width of the widest character, in pixels. */
public short getMaxCharWidth() { return KSFLUtilities.getShort(data, 6); }
/** Returns the offset from the current X position at which all characters are drawn. This is either a negative number or zero. */
public short getKerning() { return KSFLUtilities.getShort(data, 8); }
/** Returns the negative descent of this font. If the font is properly formed, this should be equivalent to <code>-getDescent()</code>. */
public short getNDescent() { return KSFLUtilities.getShort(data, 10); }
/** Returns the maximum bounding box width, in pixels. This value actually varies widely and I'm not sure what exactly it means. */
public short getRectWidth() { return KSFLUtilities.getShort(data, 12); }
/** Returns the height of the font bitmap, in pixels. Same as <code>getBitmapHeight()</code>. */
public short getHeight() { return KSFLUtilities.getShort(data, 14); }
/** Returns the offset to the widths and offsets table, in number of short words from this field. Use <code>getOffsetToWidthOffsetTable()</code> to get the offset in number of bytes from the start of the font. I don't think Mac OS actually pays any attention to this field, and just assumes the table immediately follows the bitmap. */
public short getRawOffsetToWidthOffsetTable() { return KSFLUtilities.getShort(data, 16); }
/** Returns the offset to the widths and offsets table, in number of bytes from the beginning of the font. I don't think Mac OS actually pays any attention to this field, and just assumes the table immediately follows the bitmap. */
public int getOffsetToWidthOffsetTable() { return KSFLUtilities.getShort(data, 16)*2+16; }
/** Returns the ascent of this font. */
public short getAscent() { return KSFLUtilities.getShort(data, 18); }
/** Returns the descent of this font. If the font is properly formed, this should be equivalent to <code>-getNDescent()</code>. */
public short getDescent() { return KSFLUtilities.getShort(data, 20); }
/** Returns the leading, or number of pixels between each line of text, of this font. */
public short getLeading() { return KSFLUtilities.getShort(data, 22); }
/** Returns the number of short words in each scan line of the font bitmap. Use <code>getBitmapWidth()</code> to get the width in pixels (for 1-bit only). */
public short getRowBytes() { return KSFLUtilities.getShort(data, 24); }
/** Returns the offset where the font bitmap starts. */
public int getBitmapOffset() { return 26; }
/** Returns the length of the font bitmap in bytes. */
public int getBitmapLength() { return KSFLUtilities.getShort(data, 14)*(KSFLUtilities.getShort(data, 24)*2); }
/** Returns the height of the font bitmap, in pixels. Same as <code>getHeight()</code>. */
public int getBitmapHeight() { return KSFLUtilities.getShort(data, 14); }
/** Returns the width of the font bitmap, in pixels. This assumes the font is 1-bit. */
public int getBitmapWidth() { return KSFLUtilities.getShort(data, 24)*16; }
/** Returns the raw bitmap as binary data. */
public byte[] getBitmap() { return KSFLUtilities.copy(data, 26, KSFLUtilities.getShort(data, 14)*(KSFLUtilities.getShort(data, 24)*2)); }
/** Returns the font bitmap as an image, using black and white. This assumes the font is 1-bit. */
public Image getBitmapImage() {
return Toolkit.getDefaultToolkit().createImage(new BitmapImageSource(
getBitmapWidth(), getBitmapHeight(), getBitmap(), 0, getRowBytes()*2));
}
/** Returns the font bitmap as an image, using the specified background and foreground colors. This assumes the font is 1-bit. */
public Image getBitmapImage(Color bgColor, Color fgColor) {
return Toolkit.getDefaultToolkit().createImage(new BitmapImageSource(
getBitmapWidth(), getBitmapHeight(), getBitmap(), 0, getRowBytes()*2,
bgColor, fgColor));
}
/** Returns the offset where the bitmap offset table starts. */
public int getBOTOffset() { return getBitmapOffset()+getBitmapLength(); }
/** Returns the length of the bitmap offset table in bytes. */
public int getBOTLength() { return (KSFLUtilities.getShort(data, 4)-KSFLUtilities.getShort(data, 2)+3)*2; }
/** Returns the offset where the widths and offsets table starts. */
public int getWOTOffset() { return getBOTOffset()+getBOTLength(); }
/** Returns the length of the widths and offsets table in bytes. */
public int getWOTLength() { return (KSFLUtilities.getShort(data, 4)-KSFLUtilities.getShort(data, 2)+2)*2; }
/** Returns the offset where the glyph width table starts--or, if the font does not have one, where it would start if it did have one. */
public int getGWTOffset() { return getWOTOffset()+getWOTLength(); }
/** Returns the length of the glyph width table in bytes, or zero if the font does not have one. */
public int getGWTLength() { return ((KSFLUtilities.getShort(data, 0) & FLAG_HAS_GLYPH_WIDTH_TABLE) != 0) ? ((KSFLUtilities.getShort(data, 4)-KSFLUtilities.getShort(data, 2)+2)*2) : 0; }
/**
* Converts a character value to the equivalent offset in the font's tables.
* If the specified character value is not defined for this font, this maps
* to the index immediately after the last character, which is the index for
* the catchall character.
* <p>
* Character values are in the font's encoding, which is not going to be Unicode in most cases.
* @param ch the character value.
* @return the table index.
*/
public int charValueToIndex(int ch) {
char fc = getFirstChar();
char lc = getLastChar();
int b = getWOTOffset();
int offset = data[b+(ch-fc)*2] & 0xFF;
int width = data[b+(ch-fc)*2+1] & 0xFF;
if (ch < fc || ch > lc || width < 0 || offset < 0)
return (lc+1)-fc;
else
return ch-fc;
}
/**
* Converts an offset in the font's tables into the equivalent character value.
* Character values are in the font's encoding, which is not going to be Unicode in most cases.
* @param idx the table index.
* @return the character value.
*/
public int indexToCharValue(int idx) {
return getFirstChar()+idx;
}
/** Returns all of the above in a single object for efficient access. Also contains methods for drawing text with the font. */
public FontInfo getInfo() { return new FontInfo(); }
public class FontInfo {
public short flags;
public char firstChar;
public char lastChar;
public short maxCharWidth;
public short kerning;
public short nDescent;
public short rectWidth;
public short height;
public short rawOffsetToWidthOffsetTable;
public short ascent;
public short descent;
public short leading;
public short rowBytes;
public byte[] bitmap;
public Image bitmapImg;
public int[] bitmapLoc;
public int[] offset;
public int[] width;
public float[] widthx;
/**
* Creates a new font info record for this font.
*/
public FontInfo() {
flags = KSFLUtilities.getShort(data, 0);
firstChar = (char)KSFLUtilities.getShort(data, 2);
lastChar = (char)KSFLUtilities.getShort(data, 4);
maxCharWidth = KSFLUtilities.getShort(data, 6);
kerning = KSFLUtilities.getShort(data, 8);
nDescent = KSFLUtilities.getShort(data, 10);
rectWidth = KSFLUtilities.getShort(data, 12);
height = KSFLUtilities.getShort(data, 14);
rawOffsetToWidthOffsetTable = KSFLUtilities.getShort(data, 16);
ascent = KSFLUtilities.getShort(data, 18);
descent = KSFLUtilities.getShort(data, 20);
leading = KSFLUtilities.getShort(data, 22);
rowBytes = KSFLUtilities.getShort(data, 24);
int p = 26;
bitmap = KSFLUtilities.copy(data, p, height*rowBytes*2); p += height*rowBytes*2;
bitmapImg = Toolkit.getDefaultToolkit().createImage(new BitmapImageSource(rowBytes*16, height, bitmap, 0, rowBytes*2));
bitmapLoc = new int[lastChar-firstChar+3];
for (int i=0; i<bitmapLoc.length; i++) {
bitmapLoc[i] = (KSFLUtilities.getShort(data, p) & 0xFFFF); p+=2;
}
// p = ofstToWidthOffsetTable*2 + 16; // I don't think Mac OS actually pays attention to this field, so neither do I
offset = new int[lastChar-firstChar+2];
width = new int[lastChar-firstChar+2];
for (int i=0; i<offset.length && i<width.length; i++) {
offset[i] = ((data[p] == -1) ? (-1) : (data[p] & 0xFF)); p++;
width[i] = ((data[p] == -1) ? (-1) : (data[p] & 0xFF)); p++;
}
if ((flags & FLAG_HAS_GLYPH_WIDTH_TABLE) != 0) {
widthx = new float[lastChar-firstChar+2];
for (int i=0; i<widthx.length; i++) {
widthx[i] = (KSFLUtilities.getShort(data, p) & 0xFFFF)/256.0f; p+=2;
}
} else {
widthx = null;
}
}
/** Returns the font bitmap as an image, using black and white. This assumes the font is 1-bit. */
public Image getBitmapImage() {
return Toolkit.getDefaultToolkit().createImage(new BitmapImageSource(
rowBytes*16, height, bitmap, 0, rowBytes*2, 0xFFFFFFFF, 0xFF000000));
}
/** Returns the font bitmap as an image, using the foreground color of the specified graphics context and a transparent background. This assumes the font is 1-bit. */
public Image getBitmapImage(Graphics g) {
return Toolkit.getDefaultToolkit().createImage(new BitmapImageSource(
rowBytes*16, height, bitmap, 0, rowBytes*2, 0x00000000, g.getColor().getRGB()));
}
/** Returns the font bitmap as an image, using the specified background and foreground colors. This assumes the font is 1-bit. */
public Image getBitmapImage(int bgColor, int fgColor) {
return Toolkit.getDefaultToolkit().createImage(new BitmapImageSource(
rowBytes*16, height, bitmap, 0, rowBytes*2, bgColor, fgColor));
}
/** Returns the font bitmap as an image, using the specified background and foreground colors. This assumes the font is 1-bit. */
public Image getBitmapImage(Color bgColor, Color fgColor) {
return Toolkit.getDefaultToolkit().createImage(new BitmapImageSource(
rowBytes*16, height, bitmap, 0, rowBytes*2, bgColor, fgColor));
}
/**
* Converts a character value to the equivalent offset in the font's tables.
* If the specified character value is not defined for this font, this maps
* to the index immediately after the last character, which is the index for
* the catchall character.
* <p>
* Character values are in the font's encoding, which is not going to be Unicode in most cases.
* @param ch the character value.
* @return the table index.
*/
public int charValueToIndex(int ch) {
if (ch < firstChar || ch > lastChar || width[ch-firstChar] < 0 || offset[ch-firstChar] < 0)
return (lastChar+1)-firstChar;
else
return ch-firstChar;
}
/**
* Converts an offset in the font's tables into the equivalent character value.
* Character values are in the font's encoding, which is not going to be Unicode in most cases.
* @param idx the table index.
* @return the character value.
*/
public int indexToCharValue(int idx) {
return firstChar+idx;
}
public int drawCharacter(Graphics g, int x, int y, int ch) {
return drawCharacter(g, x, y, ch, getBitmapImage(g));
}
public int drawCharacter(Graphics g, int x, int y, int ch, int bg, int fg) {
return drawCharacter(g, x, y, ch, getBitmapImage(bg, fg));
}
public int drawCharacter(Graphics g, int x, int y, int ch, Color bg, Color fg) {
return drawCharacter(g, x, y, ch, getBitmapImage(bg, fg));
}
private int drawCharacter(Graphics g, int x, int y, int ch, Image bmp) {
// note: ch is in the font's encoding, not unicode
ch = charValueToIndex(ch);
int off = offset[ch];
int wid = width[ch];
int bx1 = bitmapLoc[ch];
int bx2 = bitmapLoc[ch+1];
g.drawImage(
bmp,
x+off+kerning, y-ascent,
x+off+kerning+(bx2-bx1), y-ascent+height,
bx1, 0, bx2, height,
null
);
return wid;
}
public void drawAlphabet(Graphics g, int x, int y, int w) {
drawAlphabet(g, x, y, w, getBitmapImage(g));
}
public void drawAlphabet(Graphics g, int x, int y, int w, int bg, int fg) {
drawAlphabet(g, x, y, w, getBitmapImage(bg, fg));
}
public void drawAlphabet(Graphics g, int x, int y, int w, Color bg, Color fg) {
drawAlphabet(g, x, y, w, getBitmapImage(bg, fg));
}
private void drawAlphabet(Graphics g, int x, int y, int w, Image bmp) {
int cx = x, cy = y;
for (int i = 0; i <= lastChar-firstChar+1; i++) {
if (offset[i] >= 0 && width[i] >= 0) {
int off = offset[i];
int wid = width[i];
int bx1 = bitmapLoc[i];
int bx2 = bitmapLoc[i+1];
if (cx+wid >= w) {
cx = x;
cy += ascent+descent+leading;
}
g.drawImage(
bmp,
cx+off+kerning, cy-ascent,
cx+off+kerning+(bx2-bx1), cy-ascent+height,
bx1, 0, bx2, height,
null
);
cx += wid;
}
}
}
public int getStringWidth(String s, String te) {
int w = 0;
byte[] b;
try {
b = s.getBytes(te);
} catch (UnsupportedEncodingException uee) {
b = s.getBytes();
}
for (int ch : b) {
ch = ch & 0xFF;
switch (ch) {
case '\t':
w -= w % 36;
w += 36;
break;
default:
ch = charValueToIndex(ch);
w += width[ch];
break;
}
}
return w;
}
public int getStringHeight(String s, String te, int w) {
int cx = 0, h = ascent+descent+leading;
byte[] b;
try {
b = s.getBytes(te);
} catch (UnsupportedEncodingException uee) {
b = s.getBytes();
}
for (int ch : b) {
ch = ch & 0xFF;
switch (ch) {
case '\t':
cx -= cx % 36;
cx += 36;
break;
case '\n':
case '\r':
cx = 0;
h += ascent+descent+leading;
break;
default:
{
ch = charValueToIndex(ch);
int wid = width[ch];
if (cx+wid >= w) {
cx = 0;
h += ascent+descent+leading;
}
cx += wid;
}
break;
}
}
return h;
}
public void drawString(Graphics g, int x, int y, String s, String te, int w) {
drawString(g, x, y, s, te, w, getBitmapImage(g));
}
public void drawString(Graphics g, int x, int y, String s, String te, int w, int bg, int fg) {
drawString(g, x, y, s, te, w, getBitmapImage(bg, fg));
}
public void drawString(Graphics g, int x, int y, String s, String te, int w, Color bg, Color fg) {
drawString(g, x, y, s, te, w, getBitmapImage(bg, fg));
}
private void drawString(Graphics g, int x, int y, String s, String te, int w, Image bmp) {
int cx = x, cy = y;
byte[] b;
try {
b = s.getBytes(te);
} catch (UnsupportedEncodingException uee) {
b = s.getBytes();
}
for (int ch : b) {
ch = ch & 0xFF;
switch (ch) {
case '\t':
cx -= (cx-x) % 36;
cx += 36;
break;
case '\n':
case '\r':
cx = x;
cy += ascent+descent+leading;
break;
default:
{
ch = charValueToIndex(ch);
int off = offset[ch];
int wid = width[ch];
int bx1 = bitmapLoc[ch];
int bx2 = bitmapLoc[ch+1];
if (cx+wid >= w) {
cx = x;
cy += ascent+descent+leading;
}
g.drawImage(
bmp,
cx+off+kerning, cy-ascent,
cx+off+kerning+(bx2-bx1), cy-ascent+height,
bx1, 0, bx2, height,
null
);
cx += wid;
}
break;
}
}
}
}
private static class BitmapImageSource extends MemoryImageSource {
private static int[] makePixels(byte[] bmp, int bgColor, int fgColor) {
int[] pix = new int[bmp.length*8];
for (int s=0, d=0; s < bmp.length && d < pix.length; s++, d+=8) {
for (int k=0, b=bmp[s] & 0xFF; k < 8; k++, b<<=1) {
pix[d+k] = ( ((b & 0x80) != 0) ? fgColor : bgColor );
}
}
return pix;
}
public BitmapImageSource(int w, int h, byte[] bmp, int off, int scan) {
super(w, h, makePixels(bmp, 0xFFFFFFFF, 0xFF000000), off*8, scan*8);
}
public BitmapImageSource(int w, int h, byte[] bmp, int off, int scan, int bgColor, int fgColor) {
super(w, h, makePixels(bmp, bgColor, fgColor), off*8, scan*8);
}
public BitmapImageSource(int w, int h, byte[] bmp, int off, int scan, Color bgColor, Color fgColor) {
super(w, h, makePixels(bmp, bgColor.getRGB(), fgColor.getRGB()), off*8, scan*8);
}
}
}