Add .gitignore for generated binary.
[ttf-to-distfield.git] / ttf-to-distfield.cpp
1 #include <ft2build.h>
2 #include FT_FREETYPE_H
3 #include FT_GLYPH_H
4
5 #include <err.h>
6 #include <stdint.h>
7
8 #include <algorithm>
9 #include <cmath>
10 #include <cstdlib>
11 #include <string>
12 #include <utility>
13 #include <vector>
14
15 const auto SCALE = 16u;
16 const auto PADDING = 4u;
17 const auto SCALED_PADDING = SCALE * PADDING;
18
19 struct Bitmap {
20 unsigned width;
21 unsigned height;
22 std::vector<uint8_t> buffer;
23
24 Bitmap(unsigned width, unsigned height)
25 : width(width)
26 , height(height)
27 , buffer(width * height)
28 {}
29
30 Bitmap(const FT_Bitmap &bitmap)
31 : width((unsigned)bitmap.width + 2 * SCALED_PADDING)
32 , height((unsigned)bitmap.rows + 2 * SCALED_PADDING)
33 , buffer(width * height)
34 {
35 for (auto y = 0u; y < (unsigned)bitmap.rows; ++y) {
36 for (auto x = 0u; x < (unsigned)bitmap.width; ++x) {
37 auto c = bitmap.buffer[y * (unsigned)bitmap.pitch + x / 8];
38 auto set = !!(c >> (7 - (x & 7)));
39 (*this)(x + SCALED_PADDING, y + SCALED_PADDING) = set;
40 }
41 }
42 }
43
44 inline uint8_t &operator()(unsigned x, unsigned y) {
45 auto idx = y * width + x;
46 return buffer[idx];
47 }
48
49 inline uint8_t operator()(unsigned x, unsigned y) const {
50 if (x >= width || y >= height)
51 return 0;
52 auto idx = y * width + x;
53 return buffer[idx];
54 }
55
56 void blit(const Bitmap &src, unsigned x, unsigned y) {
57 for (auto dy = 0u; dy < src.height; ++dy)
58 for (auto dx = 0u; dx < src.width; ++dx)
59 (*this)(x + dx, y + dy) = src(dx, dy);
60 }
61
62 uint8_t sdf(unsigned x, unsigned y, unsigned max_r) const {
63 auto dsq = max_r * max_r;
64 auto v = (*this)(x, y);
65
66 for (unsigned r = 1, rsq; r <= max_r && (rsq = r * r) < dsq; ++r) {
67 for (unsigned d = -r; (int)d <= (int)r; ++d) {
68 if ((*this)(x + d, y - r) != v)
69 dsq = std::min(d * d + rsq, dsq);
70 else if ((*this)(x + d, y + r) != v)
71 dsq = std::min(d * d + rsq, dsq);
72 else if ((*this)(x - r, y + d) != v)
73 dsq = std::min(rsq + d * d, dsq);
74 else if ((*this)(x + r, y + d) != v)
75 dsq = std::min(rsq + d * d, dsq);
76 }
77 }
78
79 // Map distance onto to [0, 255].
80 auto d = std::sqrt((float)dsq);
81 if (v == 0)
82 d = -d;
83 d *= (255.f / 2.f) / max_r;
84 d += (255.f / 2.f);
85 d = std::min(255.f, std::max(0.f, d));
86 return (uint8_t)(d + 0.5);
87 }
88
89 void save(const std::string &filename) const {
90 auto f = fopen(filename.c_str(), "wb");
91 if (!f)
92 err(1, "unable to write image: %s", filename.c_str());
93 const uint8_t header[] = {
94 0, // ID length
95 0, // color map
96 3, // image type = greyscale
97 0, 0, 0, 0, 0, // color map specification
98 0, 0, // X origin
99 0, 0, // Y origin
100 (uint8_t)(width & 0xFF),
101 (uint8_t)((width >> 8) & 0xFF),
102 (uint8_t)(height & 0xFF),
103 (uint8_t)((height >> 8) & 0xFF),
104 8, // bits per pixel
105 1 << 5, // scanlines are from top-left
106 };
107 fwrite(header, sizeof(header[0]), sizeof(header), f);
108 fwrite(&buffer[0], sizeof(buffer[0]), buffer.size(), f);
109 fclose(f);
110 }
111 };
112
113 struct Glyph {
114 Bitmap pixels;
115 unsigned codepoint;
116 unsigned width;
117 unsigned height;
118 unsigned x;
119 unsigned y;
120 float u0;
121 float v0;
122 float u1;
123 float v1;
124 float advance_x;
125 float advance_y;
126 float left;
127 float top;
128
129 Glyph(unsigned codepoint, const FT_BitmapGlyph &glyph)
130 : pixels(glyph->bitmap.width > 0
131 ? ((unsigned)glyph->bitmap.width / SCALE + 2 * PADDING)
132 : 0,
133 glyph->bitmap.rows > 0
134 ? ((unsigned)glyph->bitmap.rows / SCALE + 2 * PADDING)
135 : 0)
136 , codepoint(codepoint)
137 , width(pixels.width)
138 , height(pixels.height)
139 , x(0), y(0)
140 , u0(0), v0(0), u1(0), v1(0)
141 , advance_x((glyph->root.advance.x >> 16) / (float)SCALE)
142 , advance_y((glyph->root.advance.y >> 16) / (float)SCALE)
143 , left(glyph->left / (float)SCALE)
144 , top(glyph->top / (float)SCALE)
145 {
146 const Bitmap bmap(glyph->bitmap);
147
148 for (auto y = 0u; y < height; y++) {
149 for (auto x = 0u; x < width; x++) {
150 // Sample from center of high-res area.
151 auto xs = x * SCALE + SCALE / 2;
152 auto ys = y * SCALE + SCALE / 2;
153 pixels(x, y) = bmap.sdf(xs, ys, 2 * SCALE);
154 }
155 }
156 }
157 };
158
159 static bool glyph_codepoint_sort(const Glyph &a, const Glyph &b) {
160 return a.codepoint < b.codepoint;
161 }
162
163 static bool glyph_height_sort(const Glyph &a, const Glyph &b) {
164 // sort high to low, break ties by codepoint for stable output.
165 return a.height > b.height
166 || (a.height == b.height && glyph_codepoint_sort(a, b));
167 }
168
169 static std::vector<Glyph> get_glyphs(
170 FT_Face face, const std::vector<unsigned> &codepoints)
171 {
172 std::vector<Glyph> glyphs;
173 // A number of fonts provide two different glyph indices for
174 // "invalid" vs. "undisplayable" characters. Invalid characters
175 // should be ignored. Undisplayable characters should be ignored
176 // unless the character requested is the special undisplayable
177 // character.
178 const unsigned REPLACEMENT = 0xFFFD;
179 auto invalid = FT_Get_Char_Index(face, 0);
180 auto missing = FT_Get_Char_Index(face, REPLACEMENT);
181 auto warning = false;
182 glyphs.reserve(codepoints.size());
183
184 for (auto i : codepoints) {
185 FT_Glyph glyph;
186 if (FT_Get_Char_Index(face, i) != invalid
187 && (i == REPLACEMENT || FT_Get_Char_Index(face, i) != missing)
188 && !FT_Load_Char(face, i, FT_LOAD_MONOCHROME)
189 && !FT_Get_Glyph(face->glyph, &glyph)
190 && !FT_Glyph_To_Bitmap(&glyph, FT_RENDER_MODE_MONO, 0, 1)
191 && ((FT_BitmapGlyph)glyph)->bitmap.pixel_mode == FT_PIXEL_MODE_MONO
192 )
193 {
194 auto bmp_glyph = (FT_BitmapGlyph)glyph;
195 glyphs.push_back(Glyph(i, bmp_glyph));
196 if (warning)
197 warnx("Resuming generation at codepoint %d.", i);
198 warning = false;
199 } else if (!warning) {
200 warnx("Can't load codepoint %d and onwards.", i);
201 warning = true;
202 }
203 }
204
205 return glyphs;
206 }
207
208 static unsigned get_texture_size(unsigned &width, unsigned &height, std::vector<Glyph> &glyphs)
209 {
210 // Sort glyphs by height to minimize vertically wasted space.
211 std::sort(glyphs.begin(), glyphs.end(), glyph_height_sort);
212
213 // Get an estimated size by assuming all characters are the same
214 // height, and that we can lay them out exactly in a square. This
215 // drastically overestimates the space required unless the
216 // characters are actually all the max height, then it slightly
217 // underestimates it.
218 auto max_height = glyphs.front().height;
219 auto total_width = 0u;
220 for (auto &glyph : glyphs)
221 total_width += glyph.width;
222 auto side = ceil(sqrt(total_width * max_height));
223
224 // Round width up to the nearest power of 2.
225 for (width = 1; width < side; width *= 2);
226
227 // Now, lay glyphs out horizontally in fixed rows, to figure out
228 // how tall it *really* needs to be. While we're at it, figure out
229 // what the optimum space usage would be.
230 //
231 // This is a naive algorithm that leaves "a lot" of space empty,
232 // like 30-50% of the texture. The problem is unless the space is
233 // at least 50%, no amount of smart packing is going to improve it
234 // anyway, and will just make the texture harder to spot-check.
235
236 auto total_height = 0u;
237 auto width_accum = width + 1;
238 auto real_area = 0u;
239 auto row_start = 0u;
240 for (auto &glyph : glyphs) {
241 if (width_accum + glyph.width > width) {
242 width_accum = 0;
243 row_start = total_height;
244 total_height += glyph.height;
245 }
246 glyph.x = width_accum;
247 glyph.y = row_start;
248 width_accum += glyph.width;
249 real_area += glyph.width * glyph.height;
250 }
251
252 // Now round height up to nearest power of 2.
253 for (height = 1; height < total_height; height *= 2);
254
255 // With the height known, texture coordinates can be set.
256 for (auto &glyph : glyphs) {
257 if (glyph.width) {
258 glyph.u0 = (float)(glyph.x) / width;
259 glyph.v0 = (float)(glyph.y) / height;
260 glyph.u1 = (float)(glyph.x + glyph.width) / width;
261 glyph.v1 = (float)(glyph.y + glyph.height) / height;
262 } else {
263 glyph.u0 = glyph.v0 = glyph.u1 = glyph.v1 = 0.f;
264 }
265 }
266
267 return real_area;
268 }
269
270 static std::vector<unsigned> parse_ranges(const char *s) {
271 std::vector<unsigned> v;
272 for (; *s; s += !!*s) {
273 auto start = std::strtoul(s, (char **)&s, 0);
274 if (*s == '-') {
275 auto end = std::strtoul(++s, (char **)&s, 0);
276 if (start > end)
277 std::swap(start, end);
278 for (auto i = start; i <= end; ++i)
279 v.push_back((unsigned)i);
280 } else {
281 v.push_back((unsigned)start);
282 }
283 }
284 return v;
285 }
286
287 int main(int argc, char **argv) {
288 FT_Library library;
289 FT_Face face;
290 int error;
291
292 if (argc < 2 || argc > 4)
293 errx(1, "Usage: %s font size char-range", argv[0]);
294
295 auto size = argc > 2 ? (unsigned)atoi(argv[2]) : 64u;
296 auto codepoints = parse_ranges(argc > 3 ? argv[3] : "0-16777216");
297
298 if (codepoints.size() == 0)
299 errx(1, "Invalid codepoint range: %s", argv[3]);
300 if (size < 8)
301 errx(1, "Invalid size %s", argv[2]);
302
303 if ((error = FT_Init_FreeType(&library)))
304 errx(1, "Can't initialize FreeType: %d", error);
305 if ((error = FT_New_Face(library, argv[1], 0, &face)))
306 errx(1, "Can't load %s: %d", argv[1], error);
307 if ((error = FT_Set_Pixel_Sizes(face, 0, size * SCALE)))
308 errx(1, "Unable to set pixel size.");
309
310 auto glyphs = get_glyphs(face, codepoints);
311 if (glyphs.size() == 0)
312 errx(1, "No glyphs found.");
313
314 unsigned width, height;
315 auto real_area = get_texture_size(width, height, glyphs);
316 auto wasted = 1.f - float(real_area) / (width * height);
317 fprintf(stderr,
318 "%ux%u texture for %zi glyphs (%u%% wasted).\n", width, height,
319 glyphs.size(), (unsigned)(wasted * 100));
320
321 std::string name(argv[1]);
322 size_t sep;
323 name.erase(name.rfind("."));
324 if ((sep = name.rfind("/")) != std::string::npos)
325 name.erase(0, sep + 1);
326 if ((sep = name.rfind("\\")) != std::string::npos)
327 name.erase(0, sep + 1);
328
329 Bitmap total(width, height);
330 std::sort(glyphs.begin(), glyphs.end(), glyph_codepoint_sort);
331 auto json_filename = name + ".font.json";
332 auto f = fopen(json_filename.c_str(), "w");
333 if (!f)
334 errx(1, "unable to write JSON data: %s", json_filename.c_str());
335 fprintf(f, "{\n \"height\": %u,\n", size);
336 fprintf(f, " \"scale\": %u,\n", SCALE);
337 fprintf(f, " \"padding\": %u,\n", PADDING);
338 fprintf(f, " \"glyphs\": {");
339 auto first = true;
340 for (auto &glyph : glyphs) {
341 if (!first)
342 fprintf(f, ",");
343 first = false;
344 fprintf(f, "\n \"%u\": {\n", glyph.codepoint);
345 fprintf(f, " \"size\": [%u, %u],\n",
346 glyph.width, glyph.height);
347 fprintf(f, " \"advance\": [%g, %g],\n",
348 glyph.advance_x, glyph.advance_y);
349 fprintf(f, " \"offset\": [%g, %g],\n",
350 glyph.left, glyph.top);
351 fprintf(f, " \"tex_coords\": [%g, %g, %g, %g]\n",
352 glyph.u0, glyph.v0, glyph.u1, glyph.v1);
353 fprintf(f, " }");
354 total.blit(glyph.pixels, glyph.x, glyph.y);
355 }
356 fprintf(f, "\n }\n}\n");
357 total.save(name + ".tga");
358 }