using System;
using System.Collections.Generic;
using System.Text;
namespace LunarLabs.Fonts
{
public class GlyphBitmap
{
public readonly int Width;
public readonly int Height;
public readonly byte[] Pixels;
public GlyphBitmap(int width, int height)
{
Width = width;
Height = height;
Pixels = new byte[width * height];
}
public void Draw(GlyphBitmap other, int x, int y)
{
for (int j = 0; j < other.Height; j++)
{
for (int i = 0; i < other.Width; i++)
{
var srcOfs = i + j * other.Width;
var destOfs = (x + i) + (y + j) * this.Width;
Pixels[destOfs] = other.Pixels[srcOfs];
}
}
}
}
public class FontGlyph
{
public GlyphBitmap Image { get; internal set; }
public int xOfs { get; internal set; }
public int yOfs { get; internal set; }
public int xAdvance { get; internal set; }
}
internal struct Edge
{
public float x0;
public float y0;
public float x1;
public float y1;
public bool invert;
}
internal struct Vertex
{
public short x;
public short y;
public short cx;
public short cy;
public byte vertexType;
public Vertex(byte vertexType, short x, short y, short cx, short cy)
{
this.vertexType = vertexType;
this.x = x;
this.y = y;
this.cx = cx;
this.cy = cy;
}
}
internal struct Point
{
public float x;
public float y;
public Point(float x, float y)
{
this.x = x;
this.y = y;
}
}
internal class ActiveEdge
{
public int x;
public int dx;
public float ey;
public ActiveEdge next;
public int valid;
}
public class Font
{
private int _glyphCount;
private byte[] _data; // pointer to .ttf file
private uint _loca;
private uint _head;
private uint _glyf;
private uint _hhea;
private uint _hmtx;
private uint _kern; // table locations as offset from start of .ttf
private uint _indexMap; // a cmap mapping for our chosen character encoding
private int _indexToLocFormat; // format needed to map from glyph index to glyph
private const byte PLATFORM_ID_UNICODE = 0;
private const byte PLATFORM_ID_MAC = 1;
private const byte PLATFORM_ID_ISO = 2;
private const byte PLATFORM_ID_MICROSOFT = 3;
private const byte MS_EID_SYMBOL = 0;
private const byte MS_EID_UNICODE_BMP = 1;
private const byte MS_EID_SHIFTJIS = 2;
private const byte MS_EID_UNICODE_FULL = 10;
private const int FIXSHIFT = 10;
private const int FIX = (1 << FIXSHIFT);
private const int FIXMASK = (FIX - 1);
private const byte VMOVE = 1;
private const byte VLINE = 2;
private const byte VCURVE = 3;
public Font(byte[] bytes)
{
_data = bytes;
var cmap = FindTable("cmap");
_loca = FindTable("loca");
_head = FindTable("head");
_glyf = FindTable("glyf");
_hhea = FindTable("hhea");
_hmtx = FindTable("hmtx");
_kern = FindTable("kern");
if (cmap == 0 || _loca == 0 || _head == 0 || _glyf == 0 || _hhea == 0 || _hmtx == 0)
{
throw new System.Exception("'Invalid font file");
}
var t = FindTable("maxp");
if (t != 0)
_glyphCount = ReadU16(t + 4);
else
_glyphCount = -1;
// find a cmap encoding table we understand *now* to avoid searching later. (todo: could make this installable)
var numTables = (int)(ReadU16(cmap + 2));
this._indexMap = 0;
for (int i = 0; i < numTables; i++)
{
uint encodingRecord = (uint)(cmap + 4 + 8 * i);
// find an encoding we understand:
switch (ReadU16(encodingRecord))
{
case PLATFORM_ID_MICROSOFT:
switch (ReadU16(encodingRecord + 2))
{
case MS_EID_UNICODE_BMP:
case MS_EID_UNICODE_FULL:
// MS/Unicode
_indexMap = (uint)(cmap + ReadU32(encodingRecord + 4));
break;
}
break;
}
}
if (_indexMap == 0)
{
throw new System.Exception("Could not find font index map");
}
_indexToLocFormat = ReadU16(_head + 50);
}
private byte Read8(uint offset)
{
if (offset >= _data.Length)
return 0;
else
return _data[offset];
}
private ushort ReadU16(uint offset)
{
if (offset >= _data.Length)
return 0;
else
return (ushort)((_data[offset] << 8) + _data[offset + 1]);
}
private short ReadS16(uint offset)
{
if (offset >= _data.Length)
return 0;
else
return (short)((_data[offset] << 8) + _data[offset + 1]);
}
private uint ReadU32(uint offset)
{
if (offset >= _data.Length)
return 0;
else
return (uint)((_data[offset] << 24) + (_data[offset + 1] << 16) + (_data[offset + 2] << 8) + _data[offset + 3]);
}
private int ReadS32(uint offset)
{
if (offset >= _data.Length)
return 0;
else
return (int)((_data[offset] << 24) + (_data[offset + 1] << 16) + (_data[offset + 2] << 8) + _data[offset + 3]);
}
private bool HasTag(uint offset, string tag)
{
if (offset >= _data.Length)
return false;
else
{
var bytes = Encoding.ASCII.GetBytes(tag);
return _data[offset + 0] == bytes[0] && _data[offset + 1] == bytes[1] && _data[offset + 2] == bytes[2] && _data[offset + 3] == bytes[3];
}
}
// TODO OPTIMIZE: binary search
private uint FindTable(string tag)
{
var num_tables = ReadU16(4);
if (num_tables <= 0)
{
return 0;
}
uint tabledir = 12;
for (uint i = 0; i < num_tables; i++)
{
uint loc = tabledir + 16 * i;
if (HasTag(loc, tag))
{
return ReadU32(loc + 8);
}
}
return 0;
}
private void GetGlyphHMetrics(int glyphIndex, out int advanceWidth, out int leftSideBearing)
{
uint numOfLongHorMetrics = ReadU16(_hhea + 34);
if (glyphIndex < numOfLongHorMetrics)
{
advanceWidth = ReadS16((uint)(_hmtx + 4 * glyphIndex));
leftSideBearing = ReadS16((uint)(_hmtx + 4 * glyphIndex + 2));
}
else
{
advanceWidth = ReadS16(_hmtx + 4 * (numOfLongHorMetrics - 1));
leftSideBearing = ReadS16((uint)(_hmtx + 4 * numOfLongHorMetrics + 2 * (glyphIndex - numOfLongHorMetrics)));
}
}
private int GetGlyphKernAdvance(int glyph1, int glyph2)
{
if (this._kern <= 0)
{
return 0;
}
// we only look at the first table. it must be 'horizontal' and format 0.
if (ReadU16(this._kern + 2) < 1) // number of tables
{
return 0;
}
if (ReadU16(this._kern + 8) != 1) // horizontal flag, format
{
return 0;
}
int l = 0;
int r = ReadU16(this._kern + 10) - 1;
uint needle = (uint)((glyph1 << 16) + glyph2);
while (l <= r)
{
var m = (l + r) >> 1;
var straw = ReadU32((uint)(this._kern + 18 + (m * 6))); // note: unaligned read
if (needle < straw)
r = m - 1;
else
if (needle > straw)
l = m + 1;
else
{
return ReadS16((uint)(this._kern + 22 + (m * 6)));
}
}
return 0;
}
private int GetCodepointKernAdvance(char ch1, char ch2)
{
if (_kern <= 0)
return 0;
else
return GetGlyphKernAdvance(FindGlyphIndex(ch1), FindGlyphIndex(ch2));
}
public void GetCodepointHMetrics(char codepoint, out int advanceWidth, out int leftSideBearing)
{
GetGlyphHMetrics(FindGlyphIndex(codepoint), out advanceWidth, out leftSideBearing);
}
// ascent is the coordinate above the baseline the font extends;
// descent is the coordinate below the baseline the font extends (i.e. it is typically negative)
// lineGap is the spacing between one row's descent and the next row's ascent...
// you should advance the vertical position by "*ascent - *descent + *lineGap"
// these are expressed in unscaled coordinates, so you must multiply by the scale factor for a given size
public void GetFontVMetrics(out int ascent, out int descent, out int lineGap)
{
ascent = ReadS16(_hhea + 4);
descent = ReadS16(_hhea + 6);
lineGap = ReadS16(_hhea + 8);
}
public float ScaleInEm(float ems)
{
return ScaleInPixels(ems * 16.0f);
}
public float ScaleInPixels(float pixelHeight)
{
var ascent = ReadS16(_hhea + 4);
var descent = ReadS16(_hhea + 6);
float fHeight = ascent - descent;
return pixelHeight / fHeight;
}
private GlyphBitmap GetCodepointBitmap(float scaleX, float scaleY, char codepoint, out int xoff, out int yoff)
{
return GetGlyphBitmap(scaleX, scaleY, 0, 0, FindGlyphIndex(codepoint), out xoff, out yoff);
}
private GlyphBitmap GetGlyphBitmap(float scale_x, float scale_y, float shift_x, float shift_y, int glyph, out int xoff, out int yoff)
{
var vertices = GetGlyphShape(glyph);
if (scale_x == 0)
scale_x = scale_y;
if (scale_y == 0)
{
if (scale_x == 0)
{
throw new Exception("invalid scale");
}
scale_y = scale_x;
}
int ix0, iy0, ix1, iy1;
GetGlyphBitmapBox(glyph, scale_x, scale_y, shift_x, shift_y, out ix0, out iy0, out ix1, out iy1);
int w = (ix1 - ix0);
int h = (iy1 - iy0);
if (w <= 0 || h <= 0)
{
throw new Exception("invalid glyph size");
}
// now we get the size
var result = new GlyphBitmap(w, h);
xoff = ix0;
yoff = iy0;
Rasterize(result, 0.35f, vertices, scale_x, scale_y, shift_x, shift_y, ix0, iy0, true);
return result;
}
private ushort FindGlyphIndex(char unicodeCodepoint)
{
var format = ReadU16(_indexMap);
switch (format)
{
// apple byte encoding
case 0:
{
var bytes = ReadU16(_indexMap + 2);
if (unicodeCodepoint < bytes - 6)
{
return _data[_indexMap + 6 + unicodeCodepoint];
}
else
{
return 0;
}
}
case 6:
{
var first = ReadU16(_indexMap + 6);
var count = ReadU16(_indexMap + 8);
if (unicodeCodepoint >= first && unicodeCodepoint < first + count)
{
return ReadU16((uint)(_indexMap + 10 + (unicodeCodepoint - first) * 2));
}
else
{
return 0;
}
}
// TODO: high-byte mapping for japanese/chinese/korean
case 2:
{
return 0;
}
// standard mapping for windows fonts: binary search collection of ranges
case 4:
{
var segcount = ReadU16(_indexMap + 6) >> 1;
var searchRange = ReadU16(_indexMap + 8) >> 1;
var entrySelector = ReadU16(_indexMap + 10);
var rangeShift = ReadU16(_indexMap + 12) >> 1;
// do a binary search of the segments
var endCount = _indexMap + 14;
var search = endCount;
if (unicodeCodepoint > 0xFFFF)
{
return 0;
}
// they lie from endCount .. endCount + segCount
// but searchRange is the nearest power of two, so...
if (unicodeCodepoint >= ReadU16((uint)(search + rangeShift * 2)))
{
search = (uint)(search + rangeShift * 2);
}
// now decrement to bias correctly to find smallest
search -= 2;
while (entrySelector != 0)
{
//stbtt_uint16 start, end;
searchRange = searchRange >> 1;
var startValue2 = ReadU16((uint)(search + 2 + segcount * 2 + 2));
var endValue2 = ReadU16(search + 2);
startValue2 = ReadU16((uint)(search + searchRange * 2 + segcount * 2 + 2));
endValue2 = ReadU16((uint)(search + searchRange * 2));
if (unicodeCodepoint > endValue2)
{
search = (uint)(search + searchRange * 2);
}
entrySelector--;
}
search += 2;
var item = (ushort)((search - endCount) >> 1);
//STBTT_assert(unicode_codepoint <= ttUSHORT(data + endCount + 2*item));
var startValue = ReadU16((uint)(_indexMap + 14 + segcount * 2 + 2 + 2 * item));
var endValue = ReadU16((uint)(_indexMap + 14 + 2 + 2 * item));
if (unicodeCodepoint < startValue)
{
//IntToString(unicode_codepoint); //BOO
return 0;
}
var offset = ReadU16((uint)(_indexMap + 14 + segcount * 6 + 2 + 2 * item));
if (offset == 0)
{
var n = ReadS16((uint)(_indexMap + 14 + segcount * 4 + 2 + 2 * item));
return (ushort)(unicodeCodepoint + n);
}
return ReadU16((uint)(offset + (unicodeCodepoint - startValue) * 2 + _indexMap + 14 + segcount * 6 + 2 + 2 * item));
}
case 12:
{
int ngroups = ReadU16(_indexMap + 6);
int low = 0;
int high = ngroups;
// Binary search the right group.
while (low <= high)
{
var mid = low + ((high - low) >> 1); // rounds down, so low <= mid < high
var start_char = ReadU32((uint)(_indexMap + 16 + mid * 12));
var end_char = ReadU32((uint)(_indexMap + 16 + mid * 12 + 4));
if (unicodeCodepoint < start_char)
high = mid - 1;
else
if (unicodeCodepoint > end_char)
low = mid + 1;
else
{
uint start_glyph = ReadU32((uint)(_indexMap + 16 + mid * 12 + 8));
return (ushort)(start_glyph + unicodeCodepoint - start_char);
}
}
return 0; // not found
}
// TODO
default:
{
return 0;
}
}
}
private int GetGlyfOffset(int glyphIndex)
{
if (glyphIndex >= _glyphCount)
{
return -1; // glyph index out of range
}
if (_indexToLocFormat >= 2)
{
return -1; // unknown index->glyph map format
}
int g1, g2;
if (_indexToLocFormat == 0)
{
g1 = (int)(_glyf + ReadU16((uint)(_loca + glyphIndex * 2)) * 2);
g2 = (int)(_glyf + ReadU16((uint)(_loca + glyphIndex * 2 + 2)) * 2);
}
else
{
g1 = (int)(_glyf + ReadU32((uint)(_loca + glyphIndex * 4)));
g2 = (int)(_glyf + ReadU32((uint)(_loca + glyphIndex * 4 + 4)));
}
if (g1 == g2)
{
return -1; // if length is 0, return -1
}
else
{
return g1;
}
}
private bool GetGlyphBox(int glyphIndex, out int x0, out int y0, out int x1, out int y1)
{
var g = GetGlyfOffset(glyphIndex);
if (g < 0)
{
x0 = 0;
x1 = 0;
y0 = 0;
y1 = 0;
return false;
}
x0 = ReadS16((uint)g + 2);
y0 = ReadS16((uint)g + 4);
x1 = ReadS16((uint)g + 6);
y1 = ReadS16((uint)g + 8);
return true;
}
private bool GetCodepointBox(char codepoint, out int x0, out int y0, out int x1, out int y1)
{
return GetGlyphBox(FindGlyphIndex(codepoint), out x0, out y0, out x1, out y1);
}
private List<Vertex> GetGlyphShape(int glyphIndex)
{
var g = GetGlyfOffset(glyphIndex);
if (g < 0)
{
return null;
}
var result = new List<Vertex>();
var numberOfContours = ReadS16((uint)g);
if (numberOfContours > 0)
{
byte flags = 0;
int j = 0;
bool was_off = false;
uint endPtsOfContours = (uint)(g + 10);
int ins = ReadU16((uint)(g + 10 + numberOfContours * 2));
uint pointOffset = (uint)(g + 10 + numberOfContours * 2 + 2 + ins);
int n = 1 + ReadU16((uint)(endPtsOfContours + numberOfContours * 2 - 2));
int m = n + numberOfContours; // a loose bound on how many vertices we might need
//Result.Count := M;
//SetLength(Result.List, Result.Count);
var vertices = new Vertex[m];
int next_move = 0;
byte flagCount = 0;
// in first pass, we load uninterpreted data into the allocated array
// above, shifted to the end of the array so we won't overwrite it when
// we create our final data starting from the front
int off = m - n; // starting offset for uninterpreted data, regardless of how m ends up being calculated
// first load flags
int pointIndex = 0;
for (var i = 0; i < n; i++)
{
if (flagCount == 0)
{
flags = _data[pointOffset + pointIndex];
pointIndex++;
if ((flags & 8) != 0)
{
flagCount = _data[pointOffset + pointIndex];
pointIndex++;
}
}
else
{
flagCount--;
}
vertices[off + i].vertexType = flags;
}
// now load x coordinates
short x = 0;
for (var i = 0; i < n; i++)
{
flags = vertices[off + i].vertexType;
if ((flags & 2) != 0)
{
short dx = _data[pointOffset + pointIndex];
pointIndex++;
if ((flags & 16) != 0)
{
x += dx;
}
else
{
x -= dx; // ???
}
}
else
{
if ((flags & 16) == 0)
{
x += ReadS16((uint)(pointOffset + pointIndex)); // PORT
pointIndex += 2;
}
}
vertices[off + i].x = x;
}
// now load y coordinates
short y = 0;
for (int i = 0; i < n; i++)
{
flags = vertices[off + i].vertexType;
if ((flags & 4) != 0)
{
short dy = _data[pointOffset + pointIndex];
pointIndex++;
if ((flags & 32) != 0)
{
y += dy;
}
else
{
y -= dy; // ???
}
}
else
{
if ((flags & 32) == 0)
{
y += ReadS16((uint)(pointOffset + pointIndex)); // PORT
pointIndex += 2;
}
}
vertices[off + i].y = y;
}
// now convert them to our format
short sx = 0;
short sy = 0;
short cx = 0;
short cy = 0;
var index = 0;
while (index < n)
{
flags = vertices[off + index].vertexType;
x = vertices[off + index].x;
y = vertices[off + index].y;
if (next_move == index)
{
// when we get to the end, we have to close the shape explicitly
if (index != 0)
{
if (was_off)
{
result.Add(new Vertex(VCURVE, sx, sy, cx, cy));
}
else
{
result.Add(new Vertex(VLINE, sx, sy, 0, 0));
}
}
// now start the new one
result.Add(new Vertex(VMOVE, x, y, 0, 0));
next_move = 1 + ReadU16((uint)(endPtsOfContours + j * 2));
j++;
was_off = false;
sx = x;
sy = y;
}
else
{
if ((flags & 1) == 0) // if it's a curve
{
if (was_off) // two off-curve control points in a row means interpolate an on-curve midpoint
{
result.Add(new Vertex(VCURVE, (short)((cx + x) >> 1), (short)((cy + y) >> 1), cx, cy));
}
cx = x;
cy = y;
was_off = true;
}
else
{
if (was_off)
{
result.Add(new Vertex(VCURVE, x, y, cx, cy));
}
else
{
result.Add(new Vertex(VLINE, x, y, 0, 0));
}
was_off = false;
}
}
index++;
}
if (index != 0)
{
if (was_off)
result.Add(new Vertex(VCURVE, sx, sy, cx, cy));
else
result.Add(new Vertex(VLINE, sx, sy, 0, 0));
}
}
else
if (numberOfContours == -1)
{
// Compound shapes.
bool more = true;
var comp2 = (uint)(g + 10);
var mtx = new float[6];
while (more)
{
mtx[0] = 1;
mtx[1] = 0;
mtx[2] = 0;
mtx[3] = 1;
mtx[4] = 0;
mtx[5] = 0;
byte flags = (byte)ReadS16(comp2);
comp2 += 2;
short gidx = ReadS16(comp2);
comp2 += 2;
if ((flags & 2) != 0)// XY values
{
if ((flags & 1) != 0)// shorts
{
mtx[4] = ReadS16(comp2);
comp2 += 2;
mtx[5] = ReadS16(comp2);
comp2 += 2;
}
else
{
mtx[4] = Read8(comp2);
comp2++;
mtx[5] = Read8(comp2);
comp2++;
}
}
else
{
// TODO handle matching point
throw new NotImplementedException("matching point");
}
if ((flags & (1 << 3)) != 0) // WE_HAVE_A_SCALE
{
mtx[0] = ReadS16(comp2) / 16384.0f;
mtx[1] = 0;
mtx[2] = 0;
mtx[3] = ReadS16(comp2) / 16384.0f;
comp2 += 2;
}
else
if ((flags & (1 << 6)) != 0)// WE_HAVE_AN_X_AND_YSCALE
{
mtx[0] = ReadS16(comp2) / 16384.0f;
comp2 += 2;
mtx[1] = 0;
mtx[2] = 0;
mtx[3] = ReadS16(comp2) / 16384.0f;
comp2 += 2;
}
else
if ((flags & (1 << 7)) != 0) // WE_HAVE_A_TWO_BY_TWO
{
mtx[0] = ReadS16(comp2) / 16384.0f;
comp2 += 2;
mtx[1] = ReadS16(comp2) / 16384.0f;
comp2 += 2;
mtx[2] = ReadS16(comp2) / 16384.0f;
comp2 += 2;
mtx[3] = ReadS16(comp2) / 16384.0f;
comp2 += 2;
}
// Find transformation scales.
var ms = (float)Math.Sqrt(mtx[0] * mtx[0] + mtx[1] * mtx[1]);
var ns = (float)Math.Sqrt(mtx[2] * mtx[2] + mtx[3] * mtx[3]);
// Get indexed glyph.
var comp_verts = GetGlyphShape(gidx);
if (comp_verts.Count > 0)
{
// Transform vertices.
for (int i = 0; i < comp_verts.Count; i++)
{
var vert = comp_verts[i];
var xx = vert.x;
var yy = vert.y;
vert.x = (short)(Math.Round(ms * (mtx[0] * xx + mtx[2] * yy + mtx[4])));
vert.y = (short)(Math.Round(ns * (mtx[1] * xx + mtx[3] * yy + mtx[5])));
xx = vert.cx;
yy = vert.cy;
vert.cx = (short)(Math.Round(ms * (mtx[0] * xx + mtx[2] * yy + mtx[4])));
vert.cy = (short)(Math.Round(ns * (mtx[1] * xx + mtx[3] * yy + mtx[5])));
// Append vertices.
result.Add(vert);
}
}
// More components ?
more = (flags & (1 << 5)) != 0;
}
}
else
if (numberOfContours < 0)
{
// TODO other compound variations?
throw new NotImplementedException("compound variation");
}
else
{
// numberOfCounters == 0, do nothing
}
return result;
}
// antialiasing software rasterizer
private void GetGlyphBitmapBox(int glyph, float scale_x, float scale_y, float shift_x, float shift_y, out int ix0, out int iy0, out int ix1, out int iy1)
{
int x0, y0, x1, y1;
if (!GetGlyphBox(glyph, out x0, out y0, out x1, out y1))
{
x0 = 0;
y0 = 0;
x1 = 0;
y1 = 0; // e.g. space character
}
// now move to integral bboxes (treating pixels as little squares, what pixels get touched)?
ix0 = (int)Math.Floor(x0 * scale_x + shift_x);
iy0 = -(int)Math.Ceiling(y1 * scale_y + shift_y);
ix1 = (int)Math.Ceiling(x1 * scale_x + shift_x);
iy1 = -(int)Math.Floor(y0 * scale_y + shift_y);
}
// tesselate until threshhold p is happy... TODO warped to compensate for non-linear stretching
private void TesselateCurve(List<Point> points, ref int numPoints, float x0, float y0, float x1, float y1, float x2, float y2, float objspaceFlatnessSquared, int n)
// mx, my, dx, dy: Single;
{
// midpoint
float mx = (x0 + 2 * x1 + x2) / 4.0f;
float my = (y0 + 2 * y1 + y2) / 4.0f;
// versus directly drawn line
float dx = (x0 + x2) / 2 - mx;
float dy = (y0 + y2) / 2 - my;
if (n > 16)// 65536 segments on one curve better be enough!
{
return;
}
if (dx * dx + dy * dy > objspaceFlatnessSquared)// half-pixel error allowed... need to be smaller if AA
{
TesselateCurve(points, ref numPoints, x0, y0, (x0 + x1) / 2.0f, (y0 + y1) / 2.0f, mx, my, objspaceFlatnessSquared, n + 1);
TesselateCurve(points, ref numPoints, mx, my, (x1 + x2) / 2.0f, (y1 + y2) / 2.0f, x2, y2, objspaceFlatnessSquared, n + 1);
}
else
{
if (points != null)
{
points.Add(new Point(x2, y2));
}
numPoints++;
}
}
// returns number of contours
private void FlattenCurves(List<Vertex> vertices, float objSpaceFlatness, out int[] contours, out List<Point> windings)
{
float objspace_flatness_squared = objSpaceFlatness * objSpaceFlatness;
int n = 0;
// count how many "moves" there are to get the contour count
for (int i = 0; i < vertices.Count; i++)
if (vertices[i].vertexType == VMOVE)
{
n++;
}
windings = null;
contours = null;
if (n == 0)
{
return;
}
int numPoints = 0;
int start = 0;
contours = new int[n];
// make two passes through the points so we don't need to realloc
for (int pass = 0; pass <= 1; pass++)
{
float x = 0;
float y = 0;
if (pass == 1)
{
contours = new int[numPoints * 2];
windings = new List<Point>(numPoints);
}
numPoints = 0;
n = -1;
for (int i = 0; i < vertices.Count; i++)
{
switch (vertices[i].vertexType)
{
case VMOVE:
{
// start the next contour
if (n >= 0)
{
contours[n] = numPoints - start;
}
n++;
start = numPoints;
x = vertices[i].x;
y = vertices[i].y;
if (windings != null)
{
windings.Add(new Point(x, y));
}
numPoints++;
break;
}
case VLINE:
{
x = vertices[i].x;
y = vertices[i].y;
if (windings != null)
{
windings.Add(new Point(x, y));
}
numPoints++;
break;
}
case VCURVE:
{
TesselateCurve(windings, ref numPoints, x, y,
vertices[i].cx, vertices[i].cy,
vertices[i].x, vertices[i].y,
objspace_flatness_squared, 0);
x = vertices[i].x;
y = vertices[i].y;
break;
}
}
}
contours[n] = numPoints - start;
}
}
private void Rasterize(GlyphBitmap bitmap, float flatnessInPixels, List<Vertex> vertices, float scaleX, float scaleY, float shiftX, float shiftY, int XOff, int YOff, bool Invert)
{
float scale = scaleX < scaleY ? scaleX : scaleY;
int[] windingLengths;
List<Point> windings;
FlattenCurves(vertices, flatnessInPixels / scale, out windingLengths, out windings);
if (windings.Count > 0)
{
Rasterize(bitmap, windings, windingLengths, scaleX, scaleY, shiftX, shiftY, XOff, YOff, Invert);
}
}
private void Rasterize(GlyphBitmap bitmap, List<Point> points, int[] windings, float scaleX, float scaleY, float shiftX, float shiftY, int XOff, int YOff, bool invert)
{
int ptOfs = 0;
float yScaleInv = invert ? -scaleY : scaleY;
// this value should divide 255 evenly; otherwise we won't reach full opacity
int vSubSamples = (bitmap.Height < 8) ? 15 : 5;
var edgeList = new List<Edge>(16);
int m = 0;
for (int i = 0; i < windings.Length; i++)
{
ptOfs = m;
m += windings[i];
int j = windings[i] - 1;
int k = 0;
while (k < windings[i])
{
int a = k;
int b = j;
var en = new Edge();
// skip the edge if horizontal
if (points[ptOfs + j].y != points[ptOfs + k].y)
{
// add edge from j to k to the list
en.invert = false;
if ((invert && points[ptOfs + j].y > points[ptOfs + k].y) || (!invert && points[ptOfs + j].y < points[ptOfs + k].y))
{
en.invert = true;
a = j;
b = k;
}
en.x0 = points[ptOfs + a].x * scaleX + shiftX;
en.y0 = points[ptOfs + a].y * yScaleInv * vSubSamples + shiftY;
en.x1 = points[ptOfs + b].x * scaleX + shiftX;
en.y1 = points[ptOfs + b].y * yScaleInv * vSubSamples + shiftY;
edgeList.Add(en);
}
j = k;
k++;
}
}
points.Clear();
// now sort the edges by their highest point (should snap to integer, and then by x)
edgeList.Sort((x, y) => EdgeCompare(x, y));
var temp = new Edge();
temp.y0 = 10000000;
edgeList.Add(temp);
// now, traverse the scanlines and find the intersections on each scanline, use xor winding rule
RasterizeSortedEdges(bitmap, edgeList, vSubSamples, XOff, YOff);
}
private int EdgeCompare(Edge pa, Edge pb)
{
if (pa.y0 < pb.y0)
return -1;
if (pa.y0 > pb.y0)
return 1;
return 0;
}
private ActiveEdge CreateActiveEdge(Edge edge, int offX, float startPoint)
{
var z = new ActiveEdge(); // TODO: make a pool of these!!!
float dxdy = (edge.x1 - edge.x0) / (edge.y1 - edge.y0);
//STBTT_assert(e->y0 <= start_point);
// round dx down to avoid going too far
if (dxdy < 0)
z.dx = -(int)Math.Floor(FIX * -dxdy);
else
z.dx = (int)Math.Floor(FIX * dxdy);
z.x = (int)Math.Floor(FIX * (edge.x0 + dxdy * (startPoint - edge.y0)));
z.x -= offX * FIX;
z.ey = edge.y1;
z.next = null;
if (edge.invert)
z.valid = 1;
else
z.valid = -1;
return z;
}
private void RasterizeSortedEdges(GlyphBitmap bitmap, List<Edge> e, int vSubSamples, int offX, int off_y)
{
int eIndex = 0;
ActiveEdge active = null;
int max_weight = 255 / vSubSamples; // weight per vertical scanline
int y = off_y * vSubSamples;
int n = e.Count - 1;
var tempEdge = e[n];
tempEdge.y0 = (off_y + bitmap.Height) * vSubSamples + 1;
e[n] = tempEdge;
var scanline = new byte[bitmap.Width];
float scanY = 0;
int j = 0;
while (j < bitmap.Height)
{
for (int iii = 0; iii < bitmap.Width; iii++)
{
scanline[iii] = 0;
}
for (int s = 0; s < vSubSamples; s++)
{
// find center of pixel for this scanline
scanY = y + 0.5f;
// update all active edges;
// remove all active edges that terminate before the center of this scanline
var curr = active;
ActiveEdge prev = null;
while (curr != null)
{
if (curr.ey <= scanY)
{
// delete from list
if (prev != null)
prev.next = curr.next;
else
active = curr.next;
curr = curr.next;
}
else
{
curr.x += curr.dx; // advance to position for current scanline
prev = curr;
curr = curr.next; // advance through list
}
}
// resort the list if needed
bool changed;
do
{
changed = false;
curr = active;
prev = null;
while (curr != null && curr.next != null)
{
var prox = curr.next;
if (curr.x > prox.x)
{
if (prev == null)
{
active = prox;
}
else
{
prev.next = prox;
}
curr.next = prox.next;
prox.next = curr;
Console.WriteLine("Sorted " + curr.ey + " with " + prox.ey);
changed = true;
}
prev = curr;
curr = curr.next; // advance through list
}
} while (changed);
// insert all edges that start before the center of this scanline -- omit ones that also end on this scanline
while (e[eIndex].y0 <= scanY)
{
if (e[eIndex].y1 > scanY)
{
var z = CreateActiveEdge(e[eIndex], offX, scanY);
// find insertion point
if (active == null)
active = z;
else
if (z.x < active.x) // insert at front
{
z.next = active;
active = z;
}
else
{
// find thing to insert AFTER
var p = active;
while (p.next != null && p.next.x < z.x)
{
p = p.next;
}
// at this point, p->next->x is NOT < z->x
z.next = p.next;
p.next = z;
}
}
eIndex++;
}
// now process all active edges in XOR fashion
if (active != null)
{
FillActiveEdges(scanline, bitmap.Width, active, max_weight);
}
y++;
}
for (int iii = 0; iii < bitmap.Width; iii++)
if (scanline[iii] > 0) // OPTIMIZATION?
{
int ofs = iii + j * bitmap.Width;
bitmap.Pixels[ofs] = scanline[iii];
}
j++;
}
}
// note: this routine clips fills that extend off the edges...
// ideally this wouldn't happen, but it could happen if the truetype glyph bounding boxes are wrong, or if the user supplies a too-small bitmap
private void FillActiveEdges(byte[] scanline, int len, ActiveEdge e, int max_weight)
{
// non-zero winding fill
int x0 = 0;
int w = 0;
int x1;
while (e != null)
{
if (w == 0)
{
// if we're currently at zero, we need to record the edge start point
x0 = e.x;
w += e.valid;
}
else
{
x1 = e.x;
w += e.valid;
// if we went to zero, we need to draw
if (w == 0)
{
int i = x0 >> FIXSHIFT;
int j = x1 >> FIXSHIFT;
if (i < len && j >= 0)
{
if (i == j)
{
// x0,x1 are the same pixel, so compute combined coverage
scanline[i] = (byte)(scanline[i] + (byte)(((x1 - x0) * max_weight) >> FIXSHIFT));
}
else
{
if (i >= 0)// add antialiasing for x0
scanline[i] = (byte)(scanline[i] + (byte)(((FIX - (x0 & FIXMASK)) * max_weight) >> FIXSHIFT));
else
i = -1; // clip
if (j < len) // add antialiasing for x1
scanline[j] = (byte)(scanline[j] + (byte)(((x1 & FIXMASK) * max_weight) >> FIXSHIFT));
else
j = len; // clip
i++;
while (i < j) // fill pixels between x0 and x1
{
scanline[i] = (byte)(scanline[i] + (byte)max_weight);
i++;
}
}
}
}
}
e = e.next;
}
}
public int GetKerning(char current, char next, float scale)
{
return (int)Math.Floor(GetCodepointKernAdvance(current, next) * scale);
}
public bool HasGlyph(char ID)
{
var P = FindGlyphIndex(ID);
return (P > 0);
}
public FontGlyph RenderGlyph(char ID, float scale)
{
if (!HasGlyph(ID))
{
return null;
}
var glyphTarget = new FontGlyph();
int xOfs, yOfs;
if (ID == ' ')
{
ID = '_';
GetCodepointBitmap(scale, scale, ID, out xOfs, out yOfs);
glyphTarget.Image = new GlyphBitmap(4, 4);
}
else
{
if (!HasGlyph(ID))
{
if (char.IsLetter(ID))
{
if (char.IsUpper(ID))
{
ID = char.ToLowerInvariant(ID);
}
else
{
ID = char.ToUpperInvariant(ID);
}
}
}
glyphTarget.Image = GetCodepointBitmap(scale, scale, ID, out xOfs, out yOfs);
}
glyphTarget.xOfs = xOfs;
glyphTarget.yOfs = yOfs;
int xAdv, lsb;
GetCodepointHMetrics(ID, out xAdv, out lsb);
glyphTarget.xAdvance = (int)Math.Floor(xAdv * scale);
return glyphTarget;
}
}
}
using System;
using System.Collections.Generic;
using System.Text;
namespace LunarLabs.Fonts
{
public class GlyphBitmap
{
public readonly int Width;
public readonly int Height;
public readonly byte[] Pixels;
public GlyphBitmap(int width, int height)
{
Width = width;
Height = height;
Pixels = new byte[width * height];
}
public void Draw(GlyphBitmap other, int x, int y)
{
for (int j = 0; j < other.Height; j++)
{
for (int i = 0; i < other.Width; i++)
{
var srcOfs = i + j * other.Width;
var destOfs = (x + i) + (y + j) * this.Width;
Pixels[destOfs] = other.Pixels[srcOfs];
}
}
}
}
public class FontGlyph
{
public GlyphBitmap Image { get; internal set; }
public int xOfs { get; internal set; }
public int yOfs { get; internal set; }
public int xAdvance { get; internal set; }
}
internal struct Edge
{
public float x0;
public float y0;
public float x1;
public float y1;
public bool invert;
}
internal struct Vertex
{
public short x;
public short y;
public short cx;
public short cy;
public byte vertexType;
public Vertex(byte vertexType, short x, short y, short cx, short cy)
{
this.vertexType = vertexType;
this.x = x;
this.y = y;
this.cx = cx;
this.cy = cy;
}
}
internal struct Point
{
public float x;
public float y;
public Point(float x, float y)
{
this.x = x;
this.y = y;
}
}
internal class ActiveEdge
{
public int x;
public int dx;
public float ey;
public ActiveEdge next;
public int valid;
}
public class Font
{
private int _glyphCount;
private byte[] _data; // pointer to .ttf file
private uint _loca;
private uint _head;
private uint _glyf;
private uint _hhea;
private uint _hmtx;
private uint _kern; // table locations as offset from start of .ttf
private uint _indexMap; // a cmap mapping for our chosen character encoding
private int _indexToLocFormat; // format needed to map from glyph index to glyph
private const byte PLATFORM_ID_UNICODE = 0;
private const byte PLATFORM_ID_MAC = 1;
private const byte PLATFORM_ID_ISO = 2;
private const byte PLATFORM_ID_MICROSOFT = 3;
private const byte MS_EID_SYMBOL = 0;
private const byte MS_EID_UNICODE_BMP = 1;
private const byte MS_EID_SHIFTJIS = 2;
private const byte MS_EID_UNICODE_FULL = 10;
private const int FIXSHIFT = 10;
private const int FIX = (1 << FIXSHIFT);
private const int FIXMASK = (FIX - 1);
private const byte VMOVE = 1;
private const byte VLINE = 2;
private const byte VCURVE = 3;
public Font(byte[] bytes)
{
_data = bytes;
var cmap = FindTable("cmap");
_loca = FindTable("loca");
_head = FindTable("head");
_glyf = FindTable("glyf");
_hhea = FindTable("hhea");
_hmtx = FindTable("hmtx");
_kern = FindTable("kern");
if (cmap == 0 || _loca == 0 || _head == 0 || _glyf == 0 || _hhea == 0 || _hmtx == 0)
{
throw new System.Exception("'Invalid font file");
}
var t = FindTable("maxp");
if (t != 0)
_glyphCount = ReadU16(t + 4);
else
_glyphCount = -1;
// find a cmap encoding table we understand *now* to avoid searching later. (todo: could make this installable)
var numTables = (int)(ReadU16(cmap + 2));
this._indexMap = 0;
for (int i = 0; i < numTables; i++)
{
uint encodingRecord = (uint)(cmap + 4 + 8 * i);
// find an encoding we understand:
switch (ReadU16(encodingRecord))
{
case PLATFORM_ID_MICROSOFT:
switch (ReadU16(encodingRecord + 2))
{
case MS_EID_UNICODE_BMP:
case MS_EID_UNICODE_FULL:
// MS/Unicode
_indexMap = (uint)(cmap + ReadU32(encodingRecord + 4));
break;
}
break;
}
}
if (_indexMap == 0)
{
throw new System.Exception("Could not find font index map");
}
_indexToLocFormat = ReadU16(_head + 50);
}
private byte Read8(uint offset)
{
if (offset >= _data.Length)
return 0;
else
return _data[offset];
}
private ushort ReadU16(uint offset)
{
if (offset >= _data.Length)
return 0;
else
return (ushort)((_data[offset] << 8) + _data[offset + 1]);
}
private short ReadS16(uint offset)
{
if (offset >= _data.Length)
return 0;
else
return (short)((_data[offset] << 8) + _data[offset + 1]);
}
private uint ReadU32(uint offset)
{
if (offset >= _data.Length)
return 0;
else
return (uint)((_data[offset] << 24) + (_data[offset + 1] << 16) + (_data[offset + 2] << 8) + _data[offset + 3]);
}
private int ReadS32(uint offset)
{
if (offset >= _data.Length)
return 0;
else
return (int)((_data[offset] << 24) + (_data[offset + 1] << 16) + (_data[offset + 2] << 8) + _data[offset + 3]);
}
private bool HasTag(uint offset, string tag)
{
if (offset >= _data.Length)
return false;
else
{
var bytes = Encoding.ASCII.GetBytes(tag);
return _data[offset + 0] == bytes[0] && _data[offset + 1] == bytes[1] && _data[offset + 2] == bytes[2] && _data[offset + 3] == bytes[3];
}
}
// TODO OPTIMIZE: binary search
private uint FindTable(string tag)
{
var num_tables = ReadU16(4);
if (num_tables <= 0)
{
return 0;
}
uint tabledir = 12;
for (uint i = 0; i < num_tables; i++)
{
uint loc = tabledir + 16 * i;
if (HasTag(loc, tag))
{
return ReadU32(loc + 8);
}
}
return 0;
}
private void GetGlyphHMetrics(int glyphIndex, out int advanceWidth, out int leftSideBearing)
{
uint numOfLongHorMetrics = ReadU16(_hhea + 34);
if (glyphIndex < numOfLongHorMetrics)
{
advanceWidth = ReadS16((uint)(_hmtx + 4 * glyphIndex));
leftSideBearing = ReadS16((uint)(_hmtx + 4 * glyphIndex + 2));
}
else
{
advanceWidth = ReadS16(_hmtx + 4 * (numOfLongHorMetrics - 1));
leftSideBearing = ReadS16((uint)(_hmtx + 4 * numOfLongHorMetrics + 2 * (glyphIndex - numOfLongHorMetrics)));
}
}
private int GetGlyphKernAdvance(int glyph1, int glyph2)
{
if (this._kern <= 0)
{
return 0;
}
// we only look at the first table. it must be 'horizontal' and format 0.
if (ReadU16(this._kern + 2) < 1) // number of tables
{
return 0;
}
if (ReadU16(this._kern + 8) != 1) // horizontal flag, format
{
return 0;
}
int l = 0;
int r = ReadU16(this._kern + 10) - 1;
uint needle = (uint)((glyph1 << 16) + glyph2);
while (l <= r)
{
var m = (l + r) >> 1;
var straw = ReadU32((uint)(this._kern + 18 + (m * 6))); // note: unaligned read
if (needle < straw)
r = m - 1;
else
if (needle > straw)
l = m + 1;
else
{
return ReadS16((uint)(this._kern + 22 + (m * 6)));
}
}
return 0;
}
private int GetCodepointKernAdvance(char ch1, char ch2)
{
if (_kern <= 0)
return 0;
else
return GetGlyphKernAdvance(FindGlyphIndex(ch1), FindGlyphIndex(ch2));
}
public void GetCodepointHMetrics(char codepoint, out int advanceWidth, out int leftSideBearing)
{
GetGlyphHMetrics(FindGlyphIndex(codepoint), out advanceWidth, out leftSideBearing);
}
// ascent is the coordinate above the baseline the font extends;
// descent is the coordinate below the baseline the font extends (i.e. it is typically negative)
// lineGap is the spacing between one row's descent and the next row's ascent...
// you should advance the vertical position by "*ascent - *descent + *lineGap"
// these are expressed in unscaled coordinates, so you must multiply by the scale factor for a given size
public void GetFontVMetrics(out int ascent, out int descent, out int lineGap)
{
ascent = ReadS16(_hhea + 4);
descent = ReadS16(_hhea + 6);
lineGap = ReadS16(_hhea + 8);
}
public float ScaleInEm(float ems)
{
return ScaleInPixels(ems * 16.0f);
}
public float ScaleInPixels(float pixelHeight)
{
var ascent = ReadS16(_hhea + 4);
var descent = ReadS16(_hhea + 6);
float fHeight = ascent - descent;
return pixelHeight / fHeight;
}
private GlyphBitmap GetCodepointBitmap(float scaleX, float scaleY, char codepoint, out int xoff, out int yoff)
{
return GetGlyphBitmap(scaleX, scaleY, 0, 0, FindGlyphIndex(codepoint), out xoff, out yoff);
}
private GlyphBitmap GetGlyphBitmap(float scale_x, float scale_y, float shift_x, float shift_y, int glyph, out int xoff, out int yoff)
{
var vertices = GetGlyphShape(glyph);
if (scale_x == 0)
scale_x = scale_y;
if (scale_y == 0)
{
if (scale_x == 0)
{
throw new Exception("invalid scale");
}
scale_y = scale_x;
}
int ix0, iy0, ix1, iy1;
GetGlyphBitmapBox(glyph, scale_x, scale_y, shift_x, shift_y, out ix0, out iy0, out ix1, out iy1);
int w = (ix1 - ix0);
int h = (iy1 - iy0);
if (w <= 0 || h <= 0)
{
throw new Exception("invalid glyph size");
}
// now we get the size
var result = new GlyphBitmap(w, h);
xoff = ix0;
yoff = iy0;
Rasterize(result, 0.35f, vertices, scale_x, scale_y, shift_x, shift_y, ix0, iy0, true);
return result;
}
private ushort FindGlyphIndex(char unicodeCodepoint)
{
var format = ReadU16(_indexMap);
switch (format)
{
// apple byte encoding
case 0:
{
var bytes = ReadU16(_indexMap + 2);
if (unicodeCodepoint < bytes - 6)
{
return _data[_indexMap + 6 + unicodeCodepoint];
}
else
{
return 0;
}
}
case 6:
{
var first = ReadU16(_indexMap + 6);
var count = ReadU16(_indexMap + 8);
if (unicodeCodepoint >= first && unicodeCodepoint < first + count)
{
return ReadU16((uint)(_indexMap + 10 + (unicodeCodepoint - first) * 2));
}
else
{
return 0;
}
}
// TODO: high-byte mapping for japanese/chinese/korean
case 2:
{
return 0;
}
// standard mapping for windows fonts: binary search collection of ranges
case 4:
{
var segcount = ReadU16(_indexMap + 6) >> 1;
var searchRange = ReadU16(_indexMap + 8) >> 1;
var entrySelector = ReadU16(_indexMap + 10);
var rangeShift = ReadU16(_indexMap + 12) >> 1;
// do a binary search of the segments
var endCount = _indexMap + 14;
var search = endCount;
if (unicodeCodepoint > 0xFFFF)
{
return 0;
}
// they lie from endCount .. endCount + segCount
// but searchRange is the nearest power of two, so...
if (unicodeCodepoint >= ReadU16((uint)(search + rangeShift * 2)))
{
search = (uint)(search + rangeShift * 2);
}
// now decrement to bias correctly to find smallest
search -= 2;
while (entrySelector != 0)
{
//stbtt_uint16 start, end;
searchRange = searchRange >> 1;
var startValue2 = ReadU16((uint)(search + 2 + segcount * 2 + 2));
var endValue2 = ReadU16(search + 2);
startValue2 = ReadU16((uint)(search + searchRange * 2 + segcount * 2 + 2));
endValue2 = ReadU16((uint)(search + searchRange * 2));
if (unicodeCodepoint > endValue2)
{
search = (uint)(search + searchRange * 2);
}
entrySelector--;
}
search += 2;
var item = (ushort)((search - endCount) >> 1);
//STBTT_assert(unicode_codepoint <= ttUSHORT(data + endCount + 2*item));
var startValue = ReadU16((uint)(_indexMap + 14 + segcount * 2 + 2 + 2 * item));
var endValue = ReadU16((uint)(_indexMap + 14 + 2 + 2 * item));
if (unicodeCodepoint < startValue)
{
//IntToString(unicode_codepoint); //BOO
return 0;
}
var offset = ReadU16((uint)(_indexMap + 14 + segcount * 6 + 2 + 2 * item));
if (offset == 0)
{
var n = ReadS16((uint)(_indexMap + 14 + segcount * 4 + 2 + 2 * item));
return (ushort)(unicodeCodepoint + n);
}
return ReadU16((uint)(offset + (unicodeCodepoint - startValue) * 2 + _indexMap + 14 + segcount * 6 + 2 + 2 * item));
}
case 12:
{
int ngroups = ReadU16(_indexMap + 6);
int low = 0;
int high = ngroups;
// Binary search the right group.
while (low <= high)
{
var mid = low + ((high - low) >> 1); // rounds down, so low <= mid < high
var start_char = ReadU32((uint)(_indexMap + 16 + mid * 12));
var end_char = ReadU32((uint)(_indexMap + 16 + mid * 12 + 4));
if (unicodeCodepoint < start_char)
high = mid - 1;
else
if (unicodeCodepoint > end_char)
low = mid + 1;
else
{
uint start_glyph = ReadU32((uint)(_indexMap + 16 + mid * 12 + 8));
return (ushort)(start_glyph + unicodeCodepoint - start_char);
}
}
return 0; // not found
}
// TODO
default:
{
return 0;
}
}
}
private int GetGlyfOffset(int glyphIndex)
{
if (glyphIndex >= _glyphCount)
{
return -1; // glyph index out of range
}
if (_indexToLocFormat >= 2)
{
return -1; // unknown index->glyph map format
}
int g1, g2;
if (_indexToLocFormat == 0)
{
g1 = (int)(_glyf + ReadU16((uint)(_loca + glyphIndex * 2)) * 2);
g2 = (int)(_glyf + ReadU16((uint)(_loca + glyphIndex * 2 + 2)) * 2);
}
else
{
g1 = (int)(_glyf + ReadU32((uint)(_loca + glyphIndex * 4)));
g2 = (int)(_glyf + ReadU32((uint)(_loca + glyphIndex * 4 + 4)));
}
if (g1 == g2)
{
return -1; // if length is 0, return -1
}
else
{
return g1;
}
}
private bool GetGlyphBox(int glyphIndex, out int x0, out int y0, out int x1, out int y1)
{
var g = GetGlyfOffset(glyphIndex);
if (g < 0)
{
x0 = 0;
x1 = 0;
y0 = 0;
y1 = 0;
return false;
}
x0 = ReadS16((uint)g + 2);
y0 = ReadS16((uint)g + 4);
x1 = ReadS16((uint)g + 6);
y1 = ReadS16((uint)g + 8);
return true;
}
private bool GetCodepointBox(char codepoint, out int x0, out int y0, out int x1, out int y1)
{
return GetGlyphBox(FindGlyphIndex(codepoint), out x0, out y0, out x1, out y1);
}
private List<Vertex> GetGlyphShape(int glyphIndex)
{
var g = GetGlyfOffset(glyphIndex);
if (g < 0)
{
return null;
}
var result = new List<Vertex>();
var numberOfContours = ReadS16((uint)g);
if (numberOfContours > 0)
{
byte flags = 0;
int j = 0;
bool was_off = false;
uint endPtsOfContours = (uint)(g + 10);
int ins = ReadU16((uint)(g + 10 + numberOfContours * 2));
uint pointOffset = (uint)(g + 10 + numberOfContours * 2 + 2 + ins);
int n = 1 + ReadU16((uint)(endPtsOfContours + numberOfContours * 2 - 2));
int m = n + numberOfContours; // a loose bound on how many vertices we might need
//Result.Count := M;
//SetLength(Result.List, Result.Count);
var vertices = new Vertex[m];
int next_move = 0;
byte flagCount = 0;
// in first pass, we load uninterpreted data into the allocated array
// above, shifted to the end of the array so we won't overwrite it when
// we create our final data starting from the front
int off = m - n; // starting offset for uninterpreted data, regardless of how m ends up being calculated
// first load flags
int pointIndex = 0;
for (var i = 0; i < n; i++)
{
if (flagCount == 0)
{
flags = _data[pointOffset + pointIndex];
pointIndex++;
if ((flags & 8) != 0)
{
flagCount = _data[pointOffset + pointIndex];
pointIndex++;
}
}
else
{
flagCount--;
}
vertices[off + i].vertexType = flags;
}
// now load x coordinates
short x = 0;
for (var i = 0; i < n; i++)
{
flags = vertices[off + i].vertexType;
if ((flags & 2) != 0)
{
short dx = _data[pointOffset + pointIndex];
pointIndex++;
if ((flags & 16) != 0)
{
x += dx;
}
else
{
x -= dx; // ???
}
}
else
{
if ((flags & 16) == 0)
{
x += ReadS16((uint)(pointOffset + pointIndex)); // PORT
pointIndex += 2;
}
}
vertices[off + i].x = x;
}
// now load y coordinates
short y = 0;
for (int i = 0; i < n; i++)
{
flags = vertices[off + i].vertexType;
if ((flags & 4) != 0)
{
short dy = _data[pointOffset + pointIndex];
pointIndex++;
if ((flags & 32) != 0)
{
y += dy;
}
else
{
y -= dy; // ???
}
}
else
{
if ((flags & 32) == 0)
{
y += ReadS16((uint)(pointOffset + pointIndex)); // PORT
pointIndex += 2;
}
}
vertices[off + i].y = y;
}
// now convert them to our format
short sx = 0;
short sy = 0;
short cx = 0;
short cy = 0;
var index = 0;
while (index < n)
{
flags = vertices[off + index].vertexType;
x = vertices[off + index].x;
y = vertices[off + index].y;
if (next_move == index)
{
// when we get to the end, we have to close the shape explicitly
if (index != 0)
{
if (was_off)
{
result.Add(new Vertex(VCURVE, sx, sy, cx, cy));
}
else
{
result.Add(new Vertex(VLINE, sx, sy, 0, 0));
}
}
// now start the new one
result.Add(new Vertex(VMOVE, x, y, 0, 0));
next_move = 1 + ReadU16((uint)(endPtsOfContours + j * 2));
j++;
was_off = false;
sx = x;
sy = y;
}
else
{
if ((flags & 1) == 0) // if it's a curve
{
if (was_off) // two off-curve control points in a row means interpolate an on-curve midpoint
{
result.Add(new Vertex(VCURVE, (short)((cx + x) >> 1), (short)((cy + y) >> 1), cx, cy));
}
cx = x;
cy = y;
was_off = true;
}
else
{
if (was_off)
{
result.Add(new Vertex(VCURVE, x, y, cx, cy));
}
else
{
result.Add(new Vertex(VLINE, x, y, 0, 0));
}
was_off = false;
}
}
index++;
}
if (index != 0)
{
if (was_off)
result.Add(new Vertex(VCURVE, sx, sy, cx, cy));
else
result.Add(new Vertex(VLINE, sx, sy, 0, 0));
}
}
else
if (numberOfContours == -1)
{
// Compound shapes.
bool more = true;
var comp2 = (uint)(g + 10);
var mtx = new float[6];
while (more)
{
mtx[0] = 1;
mtx[1] = 0;
mtx[2] = 0;
mtx[3] = 1;
mtx[4] = 0;
mtx[5] = 0;
byte flags = (byte)ReadS16(comp2);
comp2 += 2;
short gidx = ReadS16(comp2);
comp2 += 2;
if ((flags & 2) != 0)// XY values
{
if ((flags & 1) != 0)// shorts
{
mtx[4] = ReadS16(comp2);
comp2 += 2;
mtx[5] = ReadS16(comp2);
comp2 += 2;
}
else
{
mtx[4] = Read8(comp2);
comp2++;
mtx[5] = Read8(comp2);
comp2++;
}
}
else
{
// TODO handle matching point
throw new NotImplementedException("matching point");
}
if ((flags & (1 << 3)) != 0) // WE_HAVE_A_SCALE
{
mtx[0] = ReadS16(comp2) / 16384.0f;
mtx[1] = 0;
mtx[2] = 0;
mtx[3] = ReadS16(comp2) / 16384.0f;
comp2 += 2;
}
else
if ((flags & (1 << 6)) != 0)// WE_HAVE_AN_X_AND_YSCALE
{
mtx[0] = ReadS16(comp2) / 16384.0f;
comp2 += 2;
mtx[1] = 0;
mtx[2] = 0;
mtx[3] = ReadS16(comp2) / 16384.0f;
comp2 += 2;
}
else
if ((flags & (1 << 7)) != 0) // WE_HAVE_A_TWO_BY_TWO
{
mtx[0] = ReadS16(comp2) / 16384.0f;
comp2 += 2;
mtx[1] = ReadS16(comp2) / 16384.0f;
comp2 += 2;
mtx[2] = ReadS16(comp2) / 16384.0f;
comp2 += 2;
mtx[3] = ReadS16(comp2) / 16384.0f;
comp2 += 2;
}
// Find transformation scales.
var ms = (float)Math.Sqrt(mtx[0] * mtx[0] + mtx[1] * mtx[1]);
var ns = (float)Math.Sqrt(mtx[2] * mtx[2] + mtx[3] * mtx[3]);
// Get indexed glyph.
var comp_verts = GetGlyphShape(gidx);
if (comp_verts.Count > 0)
{
// Transform vertices.
for (int i = 0; i < comp_verts.Count; i++)
{
var vert = comp_verts[i];
var xx = vert.x;
var yy = vert.y;
vert.x = (short)(Math.Round(ms * (mtx[0] * xx + mtx[2] * yy + mtx[4])));
vert.y = (short)(Math.Round(ns * (mtx[1] * xx + mtx[3] * yy + mtx[5])));
xx = vert.cx;
yy = vert.cy;
vert.cx = (short)(Math.Round(ms * (mtx[0] * xx + mtx[2] * yy + mtx[4])));
vert.cy = (short)(Math.Round(ns * (mtx[1] * xx + mtx[3] * yy + mtx[5])));
// Append vertices.
result.Add(vert);
}
}
// More components ?
more = (flags & (1 << 5)) != 0;
}
}
else
if (numberOfContours < 0)
{
// TODO other compound variations?
throw new NotImplementedException("compound variation");
}
else
{
// numberOfCounters == 0, do nothing
}
return result;
}
// antialiasing software rasterizer
private void GetGlyphBitmapBox(int glyph, float scale_x, float scale_y, float shift_x, float shift_y, out int ix0, out int iy0, out int ix1, out int iy1)
{
int x0, y0, x1, y1;
if (!GetGlyphBox(glyph, out x0, out y0, out x1, out y1))
{
x0 = 0;
y0 = 0;
x1 = 0;
y1 = 0; // e.g. space character
}
// now move to integral bboxes (treating pixels as little squares, what pixels get touched)?
ix0 = (int)Math.Floor(x0 * scale_x + shift_x);
iy0 = -(int)Math.Ceiling(y1 * scale_y + shift_y);
ix1 = (int)Math.Ceiling(x1 * scale_x + shift_x);
iy1 = -(int)Math.Floor(y0 * scale_y + shift_y);
}
// tesselate until threshhold p is happy... TODO warped to compensate for non-linear stretching
private void TesselateCurve(List<Point> points, ref int numPoints, float x0, float y0, float x1, float y1, float x2, float y2, float objspaceFlatnessSquared, int n)
// mx, my, dx, dy: Single;
{
// midpoint
float mx = (x0 + 2 * x1 + x2) / 4.0f;
float my = (y0 + 2 * y1 + y2) / 4.0f;
// versus directly drawn line
float dx = (x0 + x2) / 2 - mx;
float dy = (y0 + y2) / 2 - my;
if (n > 16)// 65536 segments on one curve better be enough!
{
return;
}
if (dx * dx + dy * dy > objspaceFlatnessSquared)// half-pixel error allowed... need to be smaller if AA
{
TesselateCurve(points, ref numPoints, x0, y0, (x0 + x1) / 2.0f, (y0 + y1) / 2.0f, mx, my, objspaceFlatnessSquared, n + 1);
TesselateCurve(points, ref numPoints, mx, my, (x1 + x2) / 2.0f, (y1 + y2) / 2.0f, x2, y2, objspaceFlatnessSquared, n + 1);
}
else
{
if (points != null)
{
points.Add(new Point(x2, y2));
}
numPoints++;
}
}
// returns number of contours
private void FlattenCurves(List<Vertex> vertices, float objSpaceFlatness, out int[] contours, out List<Point> windings)
{
float objspace_flatness_squared = objSpaceFlatness * objSpaceFlatness;
int n = 0;
// count how many "moves" there are to get the contour count
for (int i = 0; i < vertices.Count; i++)
if (vertices[i].vertexType == VMOVE)
{
n++;
}
windings = null;
contours = null;
if (n == 0)
{
return;
}
int numPoints = 0;
int start = 0;
contours = new int[n];
// make two passes through the points so we don't need to realloc
for (int pass = 0; pass <= 1; pass++)
{
float x = 0;
float y = 0;
if (pass == 1)
{
contours = new int[numPoints * 2];
windings = new List<Point>(numPoints);
}
numPoints = 0;
n = -1;
for (int i = 0; i < vertices.Count; i++)
{
switch (vertices[i].vertexType)
{
case VMOVE:
{
// start the next contour
if (n >= 0)
{
contours[n] = numPoints - start;
}
n++;
start = numPoints;
x = vertices[i].x;
y = vertices[i].y;
if (windings != null)
{
windings.Add(new Point(x, y));
}
numPoints++;
break;
}
case VLINE:
{
x = vertices[i].x;
y = vertices[i].y;
if (windings != null)
{
windings.Add(new Point(x, y));
}
numPoints++;
break;
}
case VCURVE:
{
TesselateCurve(windings, ref numPoints, x, y,
vertices[i].cx, vertices[i].cy,
vertices[i].x, vertices[i].y,
objspace_flatness_squared, 0);
x = vertices[i].x;
y = vertices[i].y;
break;
}
}
}
contours[n] = numPoints - start;
}
}
private void Rasterize(GlyphBitmap bitmap, float flatnessInPixels, List<Vertex> vertices, float scaleX, float scaleY, float shiftX, float shiftY, int XOff, int YOff, bool Invert)
{
float scale = scaleX < scaleY ? scaleX : scaleY;
int[] windingLengths;
List<Point> windings;
FlattenCurves(vertices, flatnessInPixels / scale, out windingLengths, out windings);
if (windings.Count > 0)
{
Rasterize(bitmap, windings, windingLengths, scaleX, scaleY, shiftX, shiftY, XOff, YOff, Invert);
}
}
private void Rasterize(GlyphBitmap bitmap, List<Point> points, int[] windings, float scaleX, float scaleY, float shiftX, float shiftY, int XOff, int YOff, bool invert)
{
int ptOfs = 0;
float yScaleInv = invert ? -scaleY : scaleY;
// this value should divide 255 evenly; otherwise we won't reach full opacity
int vSubSamples = (bitmap.Height < 8) ? 15 : 5;
var edgeList = new List<Edge>(16);
int m = 0;
for (int i = 0; i < windings.Length; i++)
{
ptOfs = m;
m += windings[i];
int j = windings[i] - 1;
int k = 0;
while (k < windings[i])
{
int a = k;
int b = j;
var en = new Edge();
// skip the edge if horizontal
if (points[ptOfs + j].y != points[ptOfs + k].y)
{
// add edge from j to k to the list
en.invert = false;
if ((invert && points[ptOfs + j].y > points[ptOfs + k].y) || (!invert && points[ptOfs + j].y < points[ptOfs + k].y))
{
en.invert = true;
a = j;
b = k;
}
en.x0 = points[ptOfs + a].x * scaleX + shiftX;
en.y0 = points[ptOfs + a].y * yScaleInv * vSubSamples + shiftY;
en.x1 = points[ptOfs + b].x * scaleX + shiftX;
en.y1 = points[ptOfs + b].y * yScaleInv * vSubSamples + shiftY;
edgeList.Add(en);
}
j = k;
k++;
}
}
points.Clear();
// now sort the edges by their highest point (should snap to integer, and then by x)
edgeList.Sort((x, y) => EdgeCompare(x, y));
var temp = new Edge();
temp.y0 = 10000000;
edgeList.Add(temp);
// now, traverse the scanlines and find the intersections on each scanline, use xor winding rule
RasterizeSortedEdges(bitmap, edgeList, vSubSamples, XOff, YOff);
}
private int EdgeCompare(Edge pa, Edge pb)
{
if (pa.y0 < pb.y0)
return -1;
if (pa.y0 > pb.y0)
return 1;
return 0;
}
private ActiveEdge CreateActiveEdge(Edge edge, int offX, float startPoint)
{
var z = new ActiveEdge(); // TODO: make a pool of these!!!
float dxdy = (edge.x1 - edge.x0) / (edge.y1 - edge.y0);
//STBTT_assert(e->y0 <= start_point);
// round dx down to avoid going too far
if (dxdy < 0)
z.dx = -(int)Math.Floor(FIX * -dxdy);
else
z.dx = (int)Math.Floor(FIX * dxdy);
z.x = (int)Math.Floor(FIX * (edge.x0 + dxdy * (startPoint - edge.y0)));
z.x -= offX * FIX;
z.ey = edge.y1;
z.next = null;
if (edge.invert)
z.valid = 1;
else
z.valid = -1;
return z;
}
private void RasterizeSortedEdges(GlyphBitmap bitmap, List<Edge> e, int vSubSamples, int offX, int off_y)
{
int eIndex = 0;
ActiveEdge active = null;
int max_weight = 255 / vSubSamples; // weight per vertical scanline
int y = off_y * vSubSamples;
int n = e.Count - 1;
var tempEdge = e[n];
tempEdge.y0 = (off_y + bitmap.Height) * vSubSamples + 1;
e[n] = tempEdge;
var scanline = new byte[bitmap.Width];
float scanY = 0;
int j = 0;
while (j < bitmap.Height)
{
for (int iii = 0; iii < bitmap.Width; iii++)
{
scanline[iii] = 0;
}
for (int s = 0; s < vSubSamples; s++)
{
// find center of pixel for this scanline
scanY = y + 0.5f;
// update all active edges;
// remove all active edges that terminate before the center of this scanline
var curr = active;
ActiveEdge prev = null;
while (curr != null)
{
if (curr.ey <= scanY)
{
// delete from list
if (prev != null)
prev.next = curr.next;
else
active = curr.next;
curr = curr.next;
}
else
{
curr.x += curr.dx; // advance to position for current scanline
prev = curr;
curr = curr.next; // advance through list
}
}
// resort the list if needed
bool changed;
do
{
changed = false;
curr = active;
prev = null;
while (curr != null && curr.next != null)
{
var prox = curr.next;
if (curr.x > prox.x)
{
if (prev == null)
{
active = prox;
}
else
{
prev.next = prox;
}
curr.next = prox.next;
prox.next = curr;
Console.WriteLine("Sorted " + curr.ey + " with " + prox.ey);
changed = true;
}
prev = curr;
curr = curr.next; // advance through list
}
} while (changed);
// insert all edges that start before the center of this scanline -- omit ones that also end on this scanline
while (e[eIndex].y0 <= scanY)
{
if (e[eIndex].y1 > scanY)
{
var z = CreateActiveEdge(e[eIndex], offX, scanY);
// find insertion point
if (active == null)
active = z;
else
if (z.x < active.x) // insert at front
{
z.next = active;
active = z;
}
else
{
// find thing to insert AFTER
var p = active;
while (p.next != null && p.next.x < z.x)
{
p = p.next;
}
// at this point, p->next->x is NOT < z->x
z.next = p.next;
p.next = z;
}
}
eIndex++;
}
// now process all active edges in XOR fashion
if (active != null)
{
FillActiveEdges(scanline, bitmap.Width, active, max_weight);
}
y++;
}
for (int iii = 0; iii < bitmap.Width; iii++)
if (scanline[iii] > 0) // OPTIMIZATION?
{
int ofs = iii + j * bitmap.Width;
bitmap.Pixels[ofs] = scanline[iii];
}
j++;
}
}
// note: this routine clips fills that extend off the edges...
// ideally this wouldn't happen, but it could happen if the truetype glyph bounding boxes are wrong, or if the user supplies a too-small bitmap
private void FillActiveEdges(byte[] scanline, int len, ActiveEdge e, int max_weight)
{
// non-zero winding fill
int x0 = 0;
int w = 0;
int x1;
while (e != null)
{
if (w == 0)
{
// if we're currently at zero, we need to record the edge start point
x0 = e.x;
w += e.valid;
}
else
{
x1 = e.x;
w += e.valid;
// if we went to zero, we need to draw
if (w == 0)
{
int i = x0 >> FIXSHIFT;
int j = x1 >> FIXSHIFT;
if (i < len && j >= 0)
{
if (i == j)
{
// x0,x1 are the same pixel, so compute combined coverage
scanline[i] = (byte)(scanline[i] + (byte)(((x1 - x0) * max_weight) >> FIXSHIFT));
}
else
{
if (i >= 0)// add antialiasing for x0
scanline[i] = (byte)(scanline[i] + (byte)(((FIX - (x0 & FIXMASK)) * max_weight) >> FIXSHIFT));
else
i = -1; // clip
if (j < len) // add antialiasing for x1
scanline[j] = (byte)(scanline[j] + (byte)(((x1 & FIXMASK) * max_weight) >> FIXSHIFT));
else
j = len; // clip
i++;
while (i < j) // fill pixels between x0 and x1
{
scanline[i] = (byte)(scanline[i] + (byte)max_weight);
i++;
}
}
}
}
}
e = e.next;
}
}
public int GetKerning(char current, char next, float scale)
{
return (int)Math.Floor(GetCodepointKernAdvance(current, next) * scale);
}
public bool HasGlyph(char ID)
{
var P = FindGlyphIndex(ID);
return (P > 0);
}
public FontGlyph RenderGlyph(char ID, float scale)
{
if (!HasGlyph(ID))
{
return null;
}
var glyphTarget = new FontGlyph();
int xOfs, yOfs;
if (ID == ' ')
{
ID = '_';
GetCodepointBitmap(scale, scale, ID, out xOfs, out yOfs);
glyphTarget.Image = new GlyphBitmap(4, 4);
}
else
{
if (!HasGlyph(ID))
{
if (char.IsLetter(ID))
{
if (char.IsUpper(ID))
{
ID = char.ToLowerInvariant(ID);
}
else
{
ID = char.ToUpperInvariant(ID);
}
}
}
glyphTarget.Image = GetCodepointBitmap(scale, scale, ID, out xOfs, out yOfs);
}
glyphTarget.xOfs = xOfs;
glyphTarget.yOfs = yOfs;
int xAdv, lsb;
GetCodepointHMetrics(ID, out xAdv, out lsb);
glyphTarget.xAdvance = (int)Math.Floor(xAdv * scale);
return glyphTarget;
}
}
}