Last active
July 26, 2023 06:21
-
-
Save gszauer/0add6695a4c1ccd617b3f4f9e1e9a3c6 to your computer and use it in GitHub Desktop.
Minimal ttf parser and rasterizer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#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