Skip to content

Instantly share code, notes, and snippets.

@gszauer
Last active July 26, 2023 06:21
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gszauer/0add6695a4c1ccd617b3f4f9e1e9a3c6 to your computer and use it in GitHub Desktop.
Save gszauer/0add6695a4c1ccd617b3f4f9e1e9a3c6 to your computer and use it in GitHub Desktop.
Minimal ttf parser and rasterizer
#define _CRT_SECURE_NO_WARNINGS
#include "Font.h"
#include <tuple>
using std::vector;
using std::tuple;
using std::get;
void DrawPixel(i32 x, i32 y, u8 r, u8 g, u8 b);
static inline Point operator+(const Point& a, const Point& b) {
return Point(a.x + b.x, a.y + b.y);
}
static inline Point operator*(const Point& a, f32 b) {
return Point(a.x * b, a.y * b);
}
static inline Point operator-(const Point& a, const Point& b) {
return Point(a.x - b.x, a.y - b.y);
}
static inline i16 read_i16(u8** data) {
i16 result = (i16)(((*data)[0] << 8) | (*data)[1]);
*data += 2;
return result;
}
static inline u16 read_u16(u8** data) {
u16 result = (u16)(((*data)[0] << 8) | (*data)[1]);
*data += 2;
return result;
}
static inline i32 read_i32(u8** data) {
i32 result = (i32)(((*data)[0] << 24) | ((*data)[1] << 16) | ((*data)[2] << 8) | (*data)[3]);
*data += 4;
return result;
}
static inline u32 read_u32(u8** data) {
u32 result = (u32)(((*data)[0] << 24) | ((*data)[1] << 16) | ((*data)[2] << 8) | (*data)[3]);
*data += 4;
return result;
}
static inline f32 read_fixed(u8** data) {
u16 result = (i16)(((*data)[0] << 8) | (*data)[1]);
*data += 2;
return f32(result) / 16384.0f;
}
Font::Font(const char* file, bool flip) {
// Read in the actual file
FILE* fontFile = fopen(file, "rb");
fseek(fontFile, 0, SEEK_END);
u32 fontDataLength = ftell(fontFile);
fseek(fontFile, 0, SEEK_SET);
u8* data = (u8*)malloc(fontDataLength + 1);
fread(data, fontDataLength, 1, fontFile);
data[fontDataLength] = '\0';
fclose(fontFile);
u8* p = data + 4; // + sizeof(u32), skip the "scaler type" variable
u16 numTables = read_u16(&p);
u32 maxpTag = ('m' << 24) | ('a' << 16) | ('x' << 8) | 'p';
u32 headTag = ('h' << 24) | ('e' << 16) | ('a' << 8) | 'd';
u32 locaTag = ('l' << 24) | ('o' << 16) | ('c' << 8) | 'a';
u32 glyfTag = ('g' << 24) | ('l' << 16) | ('y' << 8) | 'f';
u32 cmapTag = ('c' << 24) | ('m' << 16) | ('a' << 8) | 'p';
u32 hheaTag = ('h' << 24) | ('h' << 16) | ('e' << 8) | 'a';
u32 hmtxTag = ('h' << 24) | ('m' << 16) | ('t' << 8) | 'x';
u32 kernTag = ('k' << 24) | ('e' << 16) | ('r' << 8) | 'n';
u8* maxp = 0;
u8* head = 0;
u8* loca = 0;
u8* glyf = 0;
u8* cmap = 0;
u8* hmtx = 0;
u8* hhea = 0;
u8* kern = 0;
u32 sizeofOffsetTable = 12; // 12: The table directory comes after the offset table, which is a u32 and 4 u16's
u32 tableDirectoryStride = 16; // 16: Size of each element in the table directory. The table directory struct is made up of 4 u32's.
u8* tableTag = data + sizeofOffsetTable;
for (u16 table = 0; table < numTables; ++table) {
u32 thisTag = (tableTag[0] << 24) | (tableTag[1] << 16) | (tableTag[2] << 8) | tableTag[3];
u8* pOffset = tableTag + 4 + 4; // Move past tag and skip checksum
u32 offset = (pOffset[0] << 24) | (pOffset[1] << 16) | (pOffset[2] << 8) | pOffset[3];
if (thisTag == maxpTag) {
maxp = data + offset;
}
else if (thisTag == headTag) {
head = data + offset;
}
else if (thisTag == locaTag) {
loca = data + offset;
}
else if (thisTag == glyfTag) {
glyf = data + offset;
}
else if (thisTag == cmapTag) {
cmap = data + offset;
}
else if (thisTag == hmtxTag) {
hmtx = data + offset;
}
else if (thisTag == hheaTag) {
hhea = data + offset;
} else if (thisTag == kernTag) {
kern = data + offset;
}
tableTag += tableDirectoryStride;
}
p = maxp + 4; // Skip version, which is a fixed u16.u16
u16 numGlyphs = read_u16(&p);
p = head + 50; // indexToLocFormat is the second to last variable in the head table.
i16 indexToLocFormat = read_i16(&p);
vector< u32 > locations;
locations.resize(numGlyphs + 1);
for (u16 i = 0; i < numGlyphs + 1; ++i) {
u32 offset = 0;
if (indexToLocFormat == 0) { // Short (16 bit)
u8* glyph = (u8*)((u16*)loca + i);
offset = read_u16(&glyph);
offset = offset * 2;
}
else { // Long (32 bit)
u8* glyph = (u8*)((u32*)loca + i);
offset = read_u32(&glyph);
}
locations[i] = offset;
}
vector< u32 > lengths;
lengths.resize(numGlyphs + 1);
for (u16 i = 0; i < numGlyphs; ++i) {
lengths[i] = locations[i + 1] - locations[i];
}
lengths[numGlyphs] = 0; // Because there is an extra
// Parse all glyphs in font file
allGlyphs.resize(numGlyphs);
for (u16 i = 0; i < numGlyphs; ++i) {
u8* glyph = glyf + locations[i];
i16 numberOfContours = read_i16(&glyph);
if (lengths[i] == 0) {
allGlyphs[i].min = allGlyphs[i].max = Point(0, 0);
}
else if (numberOfContours < 0) {
allGlyphs[i] = ParseCompoundGlyph(i, glyf, locations);
}
else {
allGlyphs[i] = ParseSimpleGlyph(i, glyf, locations);
}
allGlyphs[i].index = i;
if (flip) {
allGlyphs[i].FlipY();
}
}
p = cmap + 2; // Skip version
u16 numCMapSubTables = read_u16(&p);
u8* unicodeTable = 0;
for (u16 i = 0; i < numCMapSubTables; ++i) {
u16 platformID = read_u16(&p);
p += 2; // Skip platformSpecificID
u32 offset = read_u32(&p);
if (platformID == 0) { // Found UNICODE Platform
unicodeTable = cmap + offset;
}
}
if (unicodeTable != 0) {
std::vector< u16 > endCode;
std::vector< u16 > startCode;
std::vector< u16 > idDelta;
std::vector< u16 > idRangeOffset;
u16 segCount = 0;
p = unicodeTable;
u16 unicodeTableFormat = read_u16(&p);
if (unicodeTableFormat == 4) {
u16 unicodeTableLength = read_u16(&p);
u16 language = read_u16(&p);
u16 segCountX2 = read_u16(&p);
segCount = segCountX2 / 2;
p += 2 * 3; // Skip searchRange, entrySelector, rangeShift
endCode.resize(segCount);
startCode.resize(segCount);
idDelta.resize(segCount);
idRangeOffset.resize(segCount);
for (u16 i = 0; i < segCount; ++i) {
endCode[i] = read_u16(&p);
}
p += 2; // Skip padding
for (u16 i = 0; i < segCount; ++i) {
startCode[i] = read_u16(&p);
}
for (u16 i = 0; i < segCount; ++i) {
idDelta[i] = read_u16(&p);
}
for (u16 i = 0; i < segCount; ++i) {
idRangeOffset[i] = read_u16(&p);
}
// Next is the glyphIndexArray, but idRangeOffset is used to "index" into this data.
// The indexing scheme relies on this data being laid out contigously in memory
// let's read the contents of glyphIndexArray into idRangeOffset
u32 bytesLeft = unicodeTableLength - (p - (unicodeTable));
idRangeOffset.resize(segCount + bytesLeft / 2);
for (int i = 0; i < bytesLeft / 2; ++i) {
idRangeOffset[segCount + i] = read_u16(&p);
}
}
for (u16 s = 0; s < segCount; ++s) {
u16 start = startCode[s];
u16 end = endCode[s];
for (u16 c = start; c <= end; ++c) {
i32 index = 0;
if (idRangeOffset[s] != 0) {
index = *(&idRangeOffset[s] + (idRangeOffset[s] / 2) + (c - start));
}
else {
index = i32(idDelta[s] + c) % 65536;
}
if (index != 0) {
allGlyphs[index].codePoint = c;
}
unicodeMap[c] = index;
if (start == end) {
break;
}
}
}
} // End if (unicodeTable != 0)
p = hhea + 34; // numOfLongHorMetrics is the last entry in hhea
u16 numOfLongHorMetrics = (p[0] << 8) | p[1];
u16 numBearings = numGlyphs - numOfLongHorMetrics;
p = hmtx;
for (u16 i = 0; i < numOfLongHorMetrics; ++i) {
u16 advanceWidth = read_u16(&p);
i16 leftSideBearing = read_i16(&p);
allGlyphs[i].advance = advanceWidth;
allGlyphs[i].bearing = leftSideBearing;
}
for (u16 i = 0; i < numBearings; ++i) {
i16 leftSideBearing = read_i16(&p);
allGlyphs[i].bearing = leftSideBearing;
allGlyphs[i].advance = allGlyphs[numOfLongHorMetrics - 1].advance;
}
p = hhea + 4; // Skip version
ascent = read_i16(&p);
descent = read_i16(&p);
lineGap = read_i16(&p);
p = kern + 2; // "old" kern type with 16 bit version and num tables
u16 numKernTables = read_u16(&p);
for (u16 i = 0; i < numKernTables; ++i) {
u16 version = read_u16(&p);
u16 length = read_u16(&p);
u16 coverage = read_u16(&p);
bool horizontal = (coverage & 1) == 1; // KERN_COVERAGE_BIT
bool replace = (coverage & (1 << 3)) == (1 << 3); // KERN_OVERRIDE_BIT
bool format0 = (coverage >> 8) == 0;// Check coverage bit
if (!horizontal || !format0) {
p += length - 2 * 3;
continue; // Skip
}
u16 numKernPairs = read_u16(&p);
p += 6; // Skip search range, entry selector and range shift
for (int j = 0; j < numKernPairs; ++j) {
u16 left = read_u16(&p);
u16 right = read_u16(&p);
i16 value = read_i16(&p);
u32 key = ((u32)left << 16) | (u32)right;
if (!replace && kerning.find(key) != kerning.end()) {
kerning[key] += value;
}
else {
kerning[key] = value;
}
}
}
// units per em
p = head + 18; // Skip version, revision, checksum, magic number and flags
unitsPerEm = read_u16(&p);
free(data);
}
void Glyph::FlipY() {
for (i32 i = 0, s = edges.size(); i < s; ++i) {
edges[i].start.y = max.y - edges[i].start.y + min.y;
edges[i].end.y = max.y - edges[i].end.y + min.y;
edges[i].control.y = max.y - edges[i].control.y + min.y;
Point tmp = edges[i].start;
edges[i].start = edges[i].end;
edges[i].end = tmp;
}
isFlipped = !isFlipped;
}
Glyph Glyph::Transform(f32 a, f32 b, f32 c, f32 d, f32 e, f32 f, f32 m, f32 n) {
Glyph result = *this;
i32 numEdges = result.edges.size();
bool flip = a * d < 0.0f; // If the signs are different, flip
for (i32 edge = 0; edge < numEdges; ++edge) {
for (int p = 0; p < 3; ++p) {
f32 x = edges[edge].points[p].x;
f32 y = edges[edge].points[p].y;
result.edges[edge].points[p].x = m * ((a / m) * x + (c / m) * y + e);
result.edges[edge].points[p].y = n * ((b / n) * x + (d / n) * y + f);
}
if (flip) {
Point tmp = result.edges[edge].start;
result.edges[edge].start = result.edges[edge].end;
result.edges[edge].end = tmp;
}
}
f32 x = (m * ((a / m) * min.x + (c / m) * min.y + e));
f32 y = (n * ((b / n) * min.x + (d / n) * min.y + f));
result.min.x = x;
result.min.y = y;
x = (m * ((a / m) * max.x + (c / m) * max.y + e));
y = (n * ((b / n) * max.x + (d / n) * max.y + f));
result.max.x = x;
result.max.y = y;
return result;
}
Glyph Font::ParseSimpleGlyph(u16 index, u8* glyf, const vector< u32 >& loca) {
u8* glyph = glyf + loca[index];
Glyph result;
i16 numberOfContours = (glyph[0] << 8) | glyph[1];
if (numberOfContours < 0) {
return ParseCompoundGlyph(index, glyf, loca);
}
glyph += 2;
result.min.x = read_i16(&glyph);
result.min.y = read_i16(&glyph);
result.max.x = read_i16(&glyph);
result.max.y = read_i16(&glyph);
vector< u16 > endPtsOfContours;
endPtsOfContours.resize(numberOfContours);
for (i16 i = 0; i < numberOfContours; ++i) {
endPtsOfContours[i] = read_u16(&glyph);
}
u16 instructionsLength = read_u16(&glyph);
glyph += instructionsLength; // Skip grid fitting instructions
u32 lastIndex = endPtsOfContours[numberOfContours - 1];
u32 numFlags = lastIndex + 1;
vector< u8 > flags;
flags.resize(numFlags);
for (u32 i = 0; i < numFlags; ++i) {
flags[i] = *glyph++;
if (flags[i] & (1 << 3)) { // (1 << 3): REPEAT
u8 repeatCount = *glyph++;
while (repeatCount-- > 0) {
i += 1;
flags[i] = flags[i - 1];
}
}
}
std::vector< i16 > xCoordinates;
xCoordinates.resize(lastIndex + 1);
i16 prev_x = 0;
for (u32 i = 0, iSize = lastIndex + 1; i < iSize; ++i) {
u8 x_short = ((flags[i] & (1 << 1)) != 0) ? 1 : 0; // (1 << 1): X_SHORT
u8 x_short_pos = ((flags[i] & (1 << 4)) != 0) ? 1 : 0; // (1 << 4): X_SHORT_POS
i16 vector = 0;
u8 flag = (x_short << 1) | x_short_pos;
if (flag == 0) { // x_short off, x_short_pos off
vector = read_i16(&glyph);
}
else if (flag == 1) { // x_short off, x_short_pos on
vector = 0; // No delta, same as last one
}
else if (flag == 2) { // x_short on, x_short_pos of
vector = (*glyph++) * -1;
}
else if (flag == 3) { // x_short on, x_short_pos on
vector = *glyph++;
}
xCoordinates[i] = prev_x + vector;
prev_x = xCoordinates[i];
}
std::vector< i16 > yCoordinates;
yCoordinates.resize(lastIndex + 1);
i16 prev_y = 0;
for (u32 i = 0, iSize = lastIndex + 1; i < iSize; ++i) {
u8 y_short = ((flags[i] & (1 << 2)) != 0) ? 1 : 0; // (1 << 2): Y_SHORT
u8 y_short_pos = ((flags[i] & (1 << 5)) != 0) ? 1 : 0; // (1 << 5): Y_SHORT_POS
u8 flag = (y_short << 1) | (y_short_pos);
i16 vector = 0;
if (flag == 0) {
vector = read_i16(&glyph);
}
else if (flag == 1) {
vector = 0; // No delta, same as last one
}
else if (flag == 2) {
vector = (*glyph++) * -1;
}
else if (flag == 3) {
vector = *glyph++;
}
yCoordinates[i] = prev_y + vector;
prev_y = yCoordinates[i];
}
vector< vector< tuple< i16, i16, bool > > > contours;
contours.resize(numberOfContours);
for (i16 c = 0; c < numberOfContours; ++c) {
i32 startIndex = c <= 0 ? 0 : (endPtsOfContours[c - 1] + 1);
i32 endIndex = endPtsOfContours[c];
vector< tuple< i16, i16, bool > >& contour = contours[c];
bool first_on_curve = flags[startIndex] & 1;
if (first_on_curve) {
contour.push_back(tuple< i16, i16, bool >(xCoordinates[startIndex], yCoordinates[startIndex], true));
}
for (i32 index = startIndex + 1; index <= endIndex; ++index) {
bool on_curve = flags[index] & 1;
bool was_on_curve = flags[index - 1] & 1;
if (!on_curve && !was_on_curve) { // Not on curve, need to insert additional point
i16 gen_x = (xCoordinates[index] + xCoordinates[index - 1]) / 2;
i16 gen_y = (yCoordinates[index] + yCoordinates[index - 1]) / 2;
contour.push_back(tuple< i16, i16, bool >(gen_x, gen_y, true));
}
contour.push_back(tuple< i16, i16, bool >(xCoordinates[index], yCoordinates[index], on_curve));
}
contour.push_back(contour[0]);
}
// Convert contours to edge list
for (u32 c = 0, cSize = contours.size(); c < cSize; ++c) {
vector< tuple< i16, i16, bool > >& contour = contours[c];
for (u32 i = 1, iSize = contour.size(); i < iSize; ++i) {
if (!get<2>(contour[i])) { // If the point is off, do nothing
continue;
}
Edge edge;
edge.end = Point(get<0>(contour[i]), get<1>(contour[i]));
edge.quadratic = !get<2>(contour[i - 1]);
if (edge.quadratic) { // Curve
edge.control = Point(get<0>(contour[i - 1]), get<1>(contour[i - 1]));
edge.start = Point(get<0>(contour[i - 2]), get<1>(contour[i - 2]));
}
else { // Straight edge
edge.start = Point(get<0>(contour[i - 1]), get<1>(contour[i - 1]));
}
result.edges.push_back(edge);
}
}
return result;
}
Glyph Font::ParseCompoundGlyph(u16 index, u8* glyf, const vector< u32 >& loca) {
u8* glyph = glyf + loca[index];
Glyph result;
i16 numberOfContours = (glyph[0] << 8) | glyph[1];
if (numberOfContours >= 0) {
return ParseSimpleGlyph(index, glyf, loca);
}
glyph += 2;
result.min.x = read_i16(&glyph);
result.min.y = read_i16(&glyph);
result.max.x = read_i16(&glyph);
result.max.y = read_i16(&glyph);
u16 flags = 0;
do {
flags = read_u16(&glyph);
u16 glyphIndex = read_u16(&glyph);
f32 a, b, c, d, e, f, m, n;
if (flags & (1 << 1)) { // ARGS_ARE_XY_VALUES
if (flags & 1) { // ARG_1_AND_2_ARE_WORDS
e = read_i16(&glyph);
f = read_i16(&glyph);
}
else {
e = (i8)(*glyph++);
f = (i8)(*glyph++);
}
}
else {
e = f = 0.0f;
}
if (flags & (1 << 3)) { // WE_HAVE_A_SCALE
a = d = read_fixed(&glyph);
b = c = 0.0f;
}
else if (flags & (1 << 6)) { // WE_HAVE_AN_X_AND_Y_SCALE
a = read_fixed(&glyph);
d = read_fixed(&glyph);
b = c = 0.0f;
}
else if (flags & (1 << 7)) { // WE_HAVE_A_TWO_BY_TWO
a = read_fixed(&glyph);
b = read_fixed(&glyph);
c = read_fixed(&glyph);
d = read_fixed(&glyph);
}
else {
a = d = 1.0f;
b = c = 0.0f;
}
m = fmaxf(fabsf(a), fabsf(b));
n = fmaxf(fabsf(c), fabsf(d));
if (fabsf(fabsf(a) - fabsf(c)) < (33.0f / 65536.0f)) {
m = 2.0f * m;
}
if (fabsf(fabsf(b) - fabsf(d)) < (33.0f / 65536.0f)) {
n = 2.0f * n;
}
Glyph component = ParseSimpleGlyph(glyphIndex, glyf, loca);
component = component.Transform(a, b, c, d, e, f, m, n);
for (int c = 0; c < component.edges.size(); ++c) {
result.edges.push_back(component.edges[c]);
}
} while (flags & (1 << 5)); // MORE_COMPONENTS
return result;
}
bool Glyph::Contains(const Point& p) const {
i32 winding = 0;
i32 numEdges = edges.size();
for (i32 e = 0; e < numEdges; ++e) {
f32 startY = edges[e].start.y;
f32 endY = edges[e].end.y;
f32 startX = edges[e].start.x;
f32 endX = edges[e].end.x;
if (startY <= p.y && endY > p.y) { // Crossing from top to bottom
float dir = (startX - p.x) * (endY - p.y) - (endX - p.x) * (startY - p.y);
if (dir > 0) { // On Right
--winding;
}
}
else if (startY > p.y && endY <= p.y) { // Crossing from bottom to top
float dir = (startX - p.x) * (endY - p.y) - (endX - p.x) * (startY - p.y);
if (dir < 0) { // On Left
++winding;
}
}
if (edges[e].quadratic) { // Curved
f32 controlY = edges[e].control.y;
f32 controlX = edges[e].control.x;
float a = ((startY - endY) * (p.x - endX) + (endX - startX) * (p.y - endY)) / ((startY - endY) * (controlX - endX) + (endX - startX) * (controlY - endY));
float b = ((endY - controlY) * (p.x - endX) + (controlX - endX) * (p.y - endY)) / ((startY - endY) * (controlX - endX) + (endX - startX) * (controlY - endY));
float c = 1.0f - a - b;
if (a >= 0.0f && a <= 1.0f && b >= 0.0f && b <= 1.0f && c >= 0.0f && c <= 1.0f) { // Point in triangle
Point controlToStart = edges[e].start - edges[e].control;
Point controlToEnd = edges[e].end - edges[e].control;
float crossZ = controlToStart.x * controlToEnd.y - controlToStart.y * controlToEnd.x;
Point uv_point = Point(0.5f, 0.0f) * a + Point(0, 0) * b + Point(1, 1) * c;
float uv_value = uv_point.x * uv_point.x - uv_point.y;
if (crossZ < 0) {
uv_value = uv_point.y - uv_point.x * uv_point.x;
}
if (uv_value <= 0.0f) {
++winding;
}
else {
--winding;
}
}
}
}
return winding > 0;
}
void FillGlyph(const Glyph& glyph, i32 glyphX, i32 glyphY, f32 glyphScale, u8 r, u8 g, u8 b) {
i32 minX = (i32)(glyph.min.x * glyphScale) - 1;
i32 minY = (i32)(glyph.min.y * glyphScale) - 1;
i32 maxX = (i32)(glyph.max.x * glyphScale) + 1;
i32 maxY = (i32)(glyph.max.y * glyphScale) + 1;
for (i32 y = minY; y < maxY; ++y) {
for (i32 x = minX; x < maxX; ++x) {
if (glyph.Contains(Point(f32(x) / glyphScale, f32(y) / glyphScale))) {
DrawPixel(glyphX + x, glyphY + y, r, g, b);
}
}
}
}
void DrawString(Font& font, i32 x, i32 y, f32 scale, const char* string, u8 r, u8 g, u8 b) {
i32 xPos = x;
i32 yPos = y;
for (u8* iter = (u8*)string; *iter != '\0'; iter++) {
const Glyph& glyph = font.GetGlyphByCodePoint(*iter);
if (*iter == '\n') {
xPos = x;
yPos += font.GetLineSpace() * scale;
continue;
}
f32 xOffset = glyph.bearing * scale;
f32 yOffset = 0.0f;
if (glyph.IsFlipped()) { // Only need if flipped
yOffset = (glyph.max.y + glyph.min.y) * scale;
}
FillGlyph(glyph, xPos + xOffset, yPos - yOffset, scale, r, g, b);
xPos += glyph.advance * scale;
f32 kern = 0.0f;
if (*(iter + 1)) {
kern = font.GetKern(*(iter), *(iter + 1));
}
xPos += (i32)(kern * scale);
}
}
u32 Font::NumGlyphs() {
return allGlyphs.size();
}
const Glyph& Font::GetGlyphByIndex(u32 index) {
return allGlyphs[index];
}
const Glyph& Font::GetGlyphByCodePoint(u16 codePoint) {
u32 index = unicodeMap[codePoint];
return allGlyphs[index];
}
float Font::GetLineSpace() {
return ascent - descent + lineGap;
}
f32 Font::GetKern(u16 left, u16 right) {
left = GetGlyphByCodePoint(left).index;
right = GetGlyphByCodePoint(right).index;
u32 key = ((u32)left << 16) | (u32)right;
if (kerning.find(key) == kerning.end()) {
return 0.0f;
}
return kerning[key];
}
float Font::GetUnitsPerEm() {
return unitsPerEm;
}
float Font::GetScale(float pointSize, int screenDPI) {
return pointSize * float(screenDPI) / (72.0f * unitsPerEm);
}
bool Glyph::IsFlipped() const {
return isFlipped;
}
Edge::Edge() {
quadratic = false;
}
Glyph::Glyph() {
index = 0;
codePoint = 0;
isFlipped = false;
bearing = advance = 0.0f;
}
#pragma once
#include <vector>
#include <unordered_map>
typedef unsigned char u8;
typedef char i8;
typedef unsigned short u16;
typedef short i16;
typedef unsigned int u32;
typedef int i32;
typedef float f32;
struct Point {
f32 x;
f32 y;
inline Point() : x(0.0f), y(0.0f) { }
inline Point(f32 _x, f32 _y) : x(_x), y(_y) { }
};
struct Edge {
union {
struct {
Point start;
Point end;
Point control;
};
Point points[3];
};
bool quadratic;
Edge();
};
struct Glyph {
std::vector< Edge > edges; // In FUnits until transformed
Point min; // In FUnits until transformed
Point max; // In FUnits until transformed
u32 index;
u16 codePoint;
bool isFlipped;
f32 bearing;
f32 advance;
Glyph();
void FlipY();
Glyph Transform(f32 a, f32 b, f32 c, f32 d, f32 e, f32 f, f32 m, f32 n);
bool Contains(const Point& p) const;
bool IsFlipped() const;
};
class Font {
protected:
std::vector< Glyph > allGlyphs;
std::unordered_map< u16, u32 > unicodeMap;
std::unordered_map< u32, f32 > kerning;
i32 ascent;
i32 descent;
i32 lineGap;
float unitsPerEm;
protected:
Glyph ParseSimpleGlyph(u16 index, u8* glyf, const std::vector< u32 >& loca);
Glyph ParseCompoundGlyph(u16 index, u8* glyf, const std::vector< u32 >& loca);
public:
Font(const char* file, bool flip = true);
u32 NumGlyphs();
const Glyph& GetGlyphByIndex(u32 index);
const Glyph& GetGlyphByCodePoint(u16 codePoint);
float GetLineSpace();
f32 GetKern(u16 left, u16 right);
float GetUnitsPerEm();
float GetScale(float pointSize, int screenDPI);
};
void FillGlyph(const Glyph& glyph, i32 glyphX, i32 glyphY, f32 glyphScale, u8 r, u8 g, u8 b);
void DrawString(Font& font, i32 x, i32 y, f32 scale, const char* string, u8 r, u8 g, u8 b);
#pragma warning(disable : 28251)
#pragma warning(disable : 28159)
#define DPI_AWARE 0
#define WINDOW_TITLE L"Win32 Window"
#define WINDOW_CLASS L"FontDemo"
#pragma comment(lib, "Shcore.lib")
#pragma comment( linker, "/subsystem:console" )
#include <windows.h>
#include <iostream>
#include <ShellScalingAPI.h>
#include "Font.h"
#if _DEBUG
#include <crtdbg.h>
#endif
BITMAPINFO gFrameBufferInfo;
u8* gFrameBufferRGBA = 0;
u32 gFrameBufferWidth = 0;
u32 gFrameBufferHeight = 0;
u32 gDPIValue;
f32 gDPIScale;
Font gFont("arial.ttf");
void DrawFrame() {
const Glyph& g = gFont.GetGlyphByIndex(42);
FillGlyph(g, 10, 10, 0.05f, 255, 0, 0);
float scale = gFont.GetScale(24, gDPIValue);
std::cout << "Font scale: " << scale << ", Scaled EM: " << (scale * gFont.GetUnitsPerEm()) << "\n";
DrawString(gFont, 30, 200, scale, "Hello, World\nQuick brown fox", 0, 255, 0);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow);
LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam);
int main(int argc, const char** argv) {
#if _DEBUG
_CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_DEBUG);
#endif
WinMain(GetModuleHandle(NULL), NULL, GetCommandLineA(), SW_SHOWDEFAULT);
return 0;
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) {
HRESULT hr = SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
WNDCLASSEX wndclass;
wndclass.cbSize = sizeof(WNDCLASSEX);
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wndclass.lpszMenuName = 0;
wndclass.lpszClassName = WINDOW_CLASS;
RegisterClassEx(&wndclass);
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
int clientWidth = 800;
int clientHeight = 600;
gDPIValue = GetDpiForSystem();
gDPIScale = (float)gDPIValue / 96.0f;
std::cout << "DPI: " << gDPIValue << ", scale: " << gDPIScale << "\n";
clientWidth = (int)((float)clientWidth * gDPIScale);
clientHeight = (int)((float)clientHeight * gDPIScale);
RECT windowRect;
SetRect(&windowRect, (screenWidth / 2) - (clientWidth / 2), (screenHeight / 2) - (clientHeight / 2), (screenWidth / 2) + (clientWidth / 2), (screenHeight / 2) + (clientHeight / 2));
DWORD style = (WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU);
AdjustWindowRectEx(&windowRect, style, FALSE, 0);
HWND hwnd = CreateWindowEx(0, wndclass.lpszClassName, WINDOW_TITLE, style, windowRect.left, windowRect.top, windowRect.right - windowRect.left, windowRect.bottom - windowRect.top, NULL, NULL, hInstance, szCmdLine);
gFrameBufferInfo.bmiHeader.biSize = sizeof(gFrameBufferInfo.bmiHeader);
gFrameBufferInfo.bmiHeader.biWidth = clientWidth;
gFrameBufferInfo.bmiHeader.biHeight = -clientHeight;
gFrameBufferInfo.bmiHeader.biPlanes = 1;
gFrameBufferInfo.bmiHeader.biBitCount = 32;
gFrameBufferInfo.bmiHeader.biCompression = BI_RGB;
gFrameBufferWidth = clientWidth;
gFrameBufferHeight = clientHeight;
int bitmapMemorySize = (gFrameBufferWidth * gFrameBufferHeight) * 4;
gFrameBufferRGBA = (unsigned char*)VirtualAlloc(0, bitmapMemorySize, MEM_COMMIT, PAGE_READWRITE);
memset(gFrameBufferRGBA, (125) | (125 << 8) | (125 << 16) | (255 << 24), bitmapMemorySize);
ShowWindow(hwnd, SW_SHOW);
UpdateWindow(hwnd);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
VirtualFree(gFrameBufferRGBA, 0, MEM_RELEASE);
return (int)msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) {
switch (iMsg) {
case WM_CREATE:
break;
case WM_CLOSE:
DestroyWindow(hwnd);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_PAINT:
{
i32 bitmapMemorySize = (gFrameBufferWidth * gFrameBufferHeight) * 4;
memset(gFrameBufferRGBA, (125) | (125 << 8) | (125 << 16) | (255 << 24), bitmapMemorySize);
DrawFrame();
PAINTSTRUCT ps;
RECT clientRect;
HDC hdc = BeginPaint(hwnd, &ps);
GetClientRect(hwnd, &clientRect);
i32 clientWidth = clientRect.right - clientRect.left;
i32 clientHeight = clientRect.bottom - clientRect.top;
StretchDIBits(hdc,
0, 0, clientWidth, clientHeight,
0, 0, gFrameBufferWidth, gFrameBufferHeight,
gFrameBufferRGBA, &gFrameBufferInfo,
DIB_RGB_COLORS, SRCCOPY);
EndPaint(hwnd, &ps);
return 0;
}
break;
case WM_ERASEBKGND:
return 0;
}
return DefWindowProc(hwnd, iMsg, wParam, lParam);
}
void DrawPixel(i32 x, i32 y, u8 r, u8 g, u8 b) {
if (x < 0 || x >= gFrameBufferWidth || y < 0 || y >= gFrameBufferHeight) {
return;
}
i32 pixel = (y * gFrameBufferWidth + x) * 4;
gFrameBufferRGBA[pixel + 0] = b;
gFrameBufferRGBA[pixel + 1] = g;
gFrameBufferRGBA[pixel + 2] = r;
gFrameBufferRGBA[pixel + 3] = 255;
}
void DrawPoint(i32 x, i32 y, u8 r, u8 g, u8 b) {
DrawPixel(x + 0, y, r, g, b);
DrawPixel(x - 1, y, r, g, b);
DrawPixel(x + 1, y, r, g, b);
DrawPixel(x + 2, y, r, g, b);
DrawPixel(x + 0, y - 1, r, g, b);
DrawPixel(x - 1, y - 1, r, g, b);
DrawPixel(x + 1, y - 1, r, g, b);
DrawPixel(x + 2, y - 1, r, g, b);
DrawPixel(x + 0, y + 1, r, g, b);
DrawPixel(x + 1, y + 1, r, g, b);
DrawPixel(x + 0, y - 2, r, g, b);
DrawPixel(x + 1, y - 2, r, g, b);
}
void DrawLine(i32 x0, i32 y0, i32 x1, i32 y1, u8 r, u8 g, u8 b) {
i32 dx = abs(x1 - x0);
i32 dy = abs(y1 - y0);
i32 xStep = x0 < x1 ? 1 : -1;
i32 yStep = y0 < y1 ? 1 : -1;
i32 error = 0;
if (dx > dy) {
i32 m = 2 * dy;
i32 scale = 2 * dx;
for (i32 x = x0, y = y0; x != x1 + xStep; x += xStep) {
DrawPixel(x, y, r, g, b);
error += m;
if (error >= dx) {
y += yStep;
error -= scale;
}
}
}
else {
i32 m = 2 * dx;
i32 scale = 2 * dy;
for (i32 y = y0, x = x0; y != y1 + yStep; y += yStep) {
DrawPixel(x, y, r, g, b);
error += m;
if (error >= dy) {
x += xStep;
error -= scale;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment