lib/filigree/src/font/raster.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const sys = @import("sys");
3 const coverage = @import("coverage.zig");
4 const face_table = @import("face.zig");
5 const outline = @import("outline.zig");
6
7 const cff_test_font_env = "FILIGREE_CFF_TEST_FONT";
8 const max_test_font_bytes = 128 * 1024 * 1024;
9
10 pub const Error = outline.Error || error{
11 GlyphIdTooLarge,
12 InvalidAtlas,
13 InvalidPixelSize,
14 };
15
16 pub const Rectangle = struct {
17 x: f32 = 0,
18 y: f32 = 0,
19 width: f32 = 0,
20 height: f32 = 0,
21 };
22
23 pub const GlyphBitmap = struct {
24 codepoint: i32 = 0,
25 glyph_id: u32 = 0,
26 width: i32 = 0,
27 height: i32 = 0,
28 offset_x: i32 = 0,
29 offset_y: i32 = 0,
30 advance_x: i32 = 0,
31 alpha: []u8 = &.{},
32
33 pub fn deinit(self: GlyphBitmap, allocator: std.mem.Allocator) void {
34 if (self.alpha.len > 0) allocator.free(self.alpha);
35 }
36 };
37
38 pub const AtlasGlyph = struct {
39 codepoint: i32 = 0,
40 glyph_id: u32 = 0,
41 width: i32 = 0,
42 height: i32 = 0,
43 offset_x: i32 = 0,
44 offset_y: i32 = 0,
45 advance_x: i32 = 0,
46 };
47
48 pub const Atlas = struct {
49 rgba: []u8 = &.{},
50 width: i32 = 0,
51 height: i32 = 0,
52 glyphs: []AtlasGlyph = &.{},
53 recs: []Rectangle = &.{},
54 base_size: i32 = 0,
55 glyph_padding: i32 = 0,
56
57 pub fn deinitPixels(self: *Atlas, allocator: std.mem.Allocator) void {
58 if (self.rgba.len > 0) allocator.free(self.rgba);
59 self.rgba = &.{};
60 }
61
62 pub fn deinit(self: Atlas, allocator: std.mem.Allocator) void {
63 if (self.rgba.len > 0) allocator.free(self.rgba);
64 if (self.glyphs.len > 0) allocator.free(self.glyphs);
65 if (self.recs.len > 0) allocator.free(self.recs);
66 }
67 };
68
69 const FloatPoint = struct {
70 x: f32,
71 y: f32,
72 on_curve: bool = true,
73 };
74
75 const ContourRange = struct {
76 start: usize,
77 end: usize,
78 };
79
80 const PixelBounds = struct {
81 left: i32,
82 top: i32,
83 right: i32,
84 bottom: i32,
85
86 fn width(self: PixelBounds) i32 {
87 return @max(0, self.right - self.left);
88 }
89
90 fn height(self: PixelBounds) i32 {
91 return @max(0, self.bottom - self.top);
92 }
93 };
94
95 /// Rasterizes one glyph. Scratch allocations end before return; `alpha` belongs to
96 /// `output_allocator` and must be released with `GlyphBitmap.deinit` and the same allocator.
97 pub fn glyphBitmapAlloc(
98 output_allocator: std.mem.Allocator,
99 scratch_allocator: std.mem.Allocator,
100 face: face_table.Face,
101 glyph_id: u32,
102 pixel_size: i32,
103 ) Error!GlyphBitmap {
104 if (pixel_size <= 0) return error.InvalidPixelSize;
105 const scale = fontScale(face, pixel_size);
106 const advance_x = scaleMetric(face.advanceWidth(glyph_id), scale);
107 var glyph = try outline.glyphAlloc(scratch_allocator, face, glyph_id);
108 defer glyph.deinit(scratch_allocator);
109
110 if (glyph.points.len == 0 or glyph.bounds.x_max <= glyph.bounds.x_min or glyph.bounds.y_max <= glyph.bounds.y_min) {
111 return blankGlyph(glyph_id, advance_x);
112 }
113
114 const pixel_bounds = glyphPixelBounds(glyph.bounds, face.ascender, scale);
115 const width = pixel_bounds.width();
116 const height = pixel_bounds.height();
117 if (width <= 0 or height <= 0) return blankGlyph(glyph_id, advance_x);
118
119 var points = std.ArrayListUnmanaged(FloatPoint).empty;
120 defer points.deinit(scratch_allocator);
121 var contours = std.ArrayListUnmanaged(ContourRange).empty;
122 defer contours.deinit(scratch_allocator);
123 try flattenGlyph(scratch_allocator, glyph, face.ascender, pixel_bounds, scale, &points, &contours);
124
125 const alpha = try output_allocator.alloc(u8, try pixelCount(width, height));
126 errdefer output_allocator.free(alpha);
127 @memset(alpha, 0);
128 if (points.items.len > 0) {
129 try coverage.rasterizeAlloc(scratch_allocator, alpha, width, height, points.items, contours.items);
130 }
131
132 return .{
133 .glyph_id = glyph_id,
134 .width = width,
135 .height = height,
136 .offset_x = pixel_bounds.left,
137 .offset_y = pixel_bounds.top,
138 .advance_x = advance_x,
139 .alpha = alpha,
140 };
141 }
142
143 pub fn loadAtlasAlloc(
144 output_allocator: std.mem.Allocator,
145 scratch_allocator: std.mem.Allocator,
146 font_bytes: []const u8,
147 font_size: i32,
148 glyph_ids: []const i32,
149 codepoints: []const i32,
150 padding: i32,
151 ) Error!Atlas {
152 if (font_size <= 0) return error.InvalidPixelSize;
153 if (padding < 0 or glyph_ids.len == 0 or glyph_ids.len != codepoints.len) return error.InvalidAtlas;
154 const face = face_table.Face.init(font_bytes) catch return error.InvalidFont;
155 if ((face.tableSlice("glyf") == null or face.tableSlice("loca") == null) and face.tableSlice("CFF ") == null) return error.UnsupportedOutline;
156
157 const bitmaps = try scratch_allocator.alloc(GlyphBitmap, glyph_ids.len);
158 var bitmap_count: usize = 0;
159 errdefer {
160 for (bitmaps[0..bitmap_count]) |bitmap| bitmap.deinit(scratch_allocator);
161 scratch_allocator.free(bitmaps);
162 }
163 var glyph_scratch = std.heap.ArenaAllocator.init(scratch_allocator);
164 defer glyph_scratch.deinit();
165 for (glyph_ids, codepoints) |raw_glyph_id, codepoint| {
166 const glyph_id = std.math.cast(u32, raw_glyph_id) orelse return error.GlyphIdTooLarge;
167 var bitmap = glyphBitmapAlloc(scratch_allocator, glyph_scratch.allocator(), face, glyph_id, font_size) catch |err| switch (err) {
168 error.UnsupportedOutline => blankGlyph(glyph_id, scaleMetric(face.advanceWidth(glyph_id), fontScale(face, font_size))),
169 else => return err,
170 };
171 bitmap.codepoint = codepoint;
172 bitmaps[bitmap_count] = bitmap;
173 bitmap_count += 1;
174 _ = glyph_scratch.reset(.retain_capacity);
175 }
176
177 const atlas = try packAtlasAlloc(output_allocator, bitmaps, font_size, padding);
178 for (bitmaps) |bitmap| bitmap.deinit(scratch_allocator);
179 scratch_allocator.free(bitmaps);
180 return atlas;
181 }
182
183 fn blankGlyph(glyph_id: u32, advance_x: i32) GlyphBitmap {
184 return .{
185 .glyph_id = glyph_id,
186 .advance_x = advance_x,
187 };
188 }
189
190 fn packAtlasAlloc(
191 allocator: std.mem.Allocator,
192 bitmaps: []const GlyphBitmap,
193 font_size: i32,
194 padding: i32,
195 ) Error!Atlas {
196 const glyphs = try allocator.alloc(AtlasGlyph, bitmaps.len);
197 errdefer allocator.free(glyphs);
198 const recs = try allocator.alloc(Rectangle, bitmaps.len);
199 errdefer allocator.free(recs);
200
201 const target_width = chooseAtlasWidth(bitmaps, padding);
202 var x: i32 = 0;
203 var y: i32 = 0;
204 var row_height: i32 = 0;
205 var used_width: i32 = 0;
206 for (bitmaps, 0..) |bitmap, index| {
207 const packed_width = @max(1, bitmap.width + padding * 2);
208 const packed_height = @max(1, bitmap.height + padding * 2);
209 if (x > 0 and x + packed_width > target_width) {
210 y += row_height;
211 x = 0;
212 row_height = 0;
213 }
214 recs[index] = .{
215 .x = @floatFromInt(x + padding),
216 .y = @floatFromInt(y + padding),
217 .width = @floatFromInt(bitmap.width),
218 .height = @floatFromInt(bitmap.height),
219 };
220 glyphs[index] = .{
221 .codepoint = bitmap.codepoint,
222 .glyph_id = bitmap.glyph_id,
223 .width = bitmap.width,
224 .height = bitmap.height,
225 .offset_x = bitmap.offset_x,
226 .offset_y = bitmap.offset_y,
227 .advance_x = bitmap.advance_x,
228 };
229 x += packed_width;
230 used_width = @max(used_width, x);
231 row_height = @max(row_height, packed_height);
232 }
233
234 const width = @max(1, used_width);
235 const height = @max(1, y + row_height);
236 const rgba_len = std.math.mul(usize, try pixelCount(width, height), 4) catch return error.InvalidAtlas;
237 const rgba = try allocator.alloc(u8, rgba_len);
238 errdefer allocator.free(rgba);
239 @memset(rgba, 0);
240 for (bitmaps, recs) |bitmap, rec| {
241 blitBitmap(rgba, width, height, bitmap, rec);
242 }
243
244 return .{
245 .rgba = rgba,
246 .width = width,
247 .height = height,
248 .glyphs = glyphs,
249 .recs = recs,
250 .base_size = font_size,
251 .glyph_padding = padding,
252 };
253 }
254
255 fn chooseAtlasWidth(bitmaps: []const GlyphBitmap, padding: i32) i32 {
256 var total_area: u64 = 0;
257 var min_width: i32 = 1;
258 for (bitmaps) |bitmap| {
259 const packed_width = @max(1, bitmap.width + padding * 2);
260 const packed_height = @max(1, bitmap.height + padding * 2);
261 min_width = @max(min_width, packed_width);
262 total_area += @as(u64, @intCast(packed_width)) * @as(u64, @intCast(packed_height));
263 }
264
265 var width: i32 = 64;
266 while (width < min_width) width *= 2;
267 while (@as(u64, @intCast(width)) * @as(u64, @intCast(width)) < total_area and width < 2048) width *= 2;
268 return width;
269 }
270
271 fn blitBitmap(rgba: []u8, atlas_width: i32, atlas_height: i32, bitmap: GlyphBitmap, rec: Rectangle) void {
272 if (bitmap.width <= 0 or bitmap.height <= 0 or bitmap.alpha.len == 0) return;
273 const dst_x0: i32 = @intFromFloat(rec.x);
274 const dst_y0: i32 = @intFromFloat(rec.y);
275 var row: i32 = 0;
276 while (row < bitmap.height) : (row += 1) {
277 var col: i32 = 0;
278 while (col < bitmap.width) : (col += 1) {
279 const dst_x = dst_x0 + col;
280 const dst_y = dst_y0 + row;
281 if (dst_x < 0 or dst_y < 0 or dst_x >= atlas_width or dst_y >= atlas_height) continue;
282 const src_index = @as(usize, @intCast(row)) * @as(usize, @intCast(bitmap.width)) + @as(usize, @intCast(col));
283 const dst_index = (@as(usize, @intCast(dst_y)) * @as(usize, @intCast(atlas_width)) + @as(usize, @intCast(dst_x))) * 4;
284 rgba[dst_index] = 255;
285 rgba[dst_index + 1] = 255;
286 rgba[dst_index + 2] = 255;
287 rgba[dst_index + 3] = bitmap.alpha[src_index];
288 }
289 }
290 }
291
292 fn flattenGlyph(
293 allocator: std.mem.Allocator,
294 glyph: outline.Glyph,
295 ascender: i32,
296 pixel_bounds: PixelBounds,
297 scale: f32,
298 points: *std.ArrayListUnmanaged(FloatPoint),
299 contours: *std.ArrayListUnmanaged(ContourRange),
300 ) Error!void {
301 for (glyph.contours) |contour| {
302 if (contour.start >= contour.end or contour.end > glyph.points.len) return error.InvalidFont;
303 try flattenContour(
304 allocator,
305 glyph.points[contour.start..contour.end],
306 ascender,
307 pixel_bounds,
308 scale,
309 points,
310 contours,
311 );
312 }
313 }
314
315 fn flattenContour(
316 allocator: std.mem.Allocator,
317 raw: []const outline.Point,
318 ascender: i32,
319 pixel_bounds: PixelBounds,
320 scale: f32,
321 points: *std.ArrayListUnmanaged(FloatPoint),
322 contours: *std.ArrayListUnmanaged(ContourRange),
323 ) Error!void {
324 if (raw.len == 0) return;
325 var expanded = std.ArrayListUnmanaged(FloatPoint).empty;
326 defer expanded.deinit(allocator);
327
328 for (raw, 0..) |point, index| {
329 const next = raw[(index + 1) % raw.len];
330 const converted = convertPoint(point, ascender, pixel_bounds, scale);
331 try expanded.append(allocator, converted);
332 if (!point.on_curve and !next.on_curve) {
333 try expanded.append(allocator, midpoint(converted, convertPoint(next, ascender, pixel_bounds, scale)));
334 }
335 }
336
337 const start_index = firstOnCurve(expanded.items) orelse return;
338 const range_start = points.items.len;
339 var current = expanded.items[start_index];
340 current.on_curve = true;
341 try points.append(allocator, current);
342
343 var relative: usize = 1;
344 while (relative < expanded.items.len) {
345 const next = expanded.items[(start_index + relative) % expanded.items.len];
346 if (next.on_curve) {
347 try points.append(allocator, next);
348 current = next;
349 relative += 1;
350 continue;
351 }
352
353 const end_index = (start_index + relative + 1) % expanded.items.len;
354 const end = expanded.items[end_index];
355 if (!end.on_curve) return error.InvalidFont;
356 try appendQuadratic(allocator, points, current, next, end);
357 current = end;
358 relative += 2;
359 }
360
361 if (points.items.len - range_start >= 3) {
362 try contours.append(allocator, .{ .start = range_start, .end = points.items.len });
363 } else {
364 points.shrinkRetainingCapacity(range_start);
365 }
366 }
367
368 fn convertPoint(point: outline.Point, ascender: i32, pixel_bounds: PixelBounds, scale: f32) FloatPoint {
369 return .{
370 .x = @as(f32, @floatFromInt(point.x)) * scale - @as(f32, @floatFromInt(pixel_bounds.left)),
371 .y = @as(f32, @floatFromInt(ascender - point.y)) * scale - @as(f32, @floatFromInt(pixel_bounds.top)),
372 .on_curve = point.on_curve,
373 };
374 }
375
376 fn midpoint(a: FloatPoint, b: FloatPoint) FloatPoint {
377 return .{
378 .x = (a.x + b.x) * 0.5,
379 .y = (a.y + b.y) * 0.5,
380 .on_curve = true,
381 };
382 }
383
384 fn firstOnCurve(points: []const FloatPoint) ?usize {
385 for (points, 0..) |point, index| {
386 if (point.on_curve) return index;
387 }
388 return null;
389 }
390
391 fn appendQuadratic(
392 allocator: std.mem.Allocator,
393 points: *std.ArrayListUnmanaged(FloatPoint),
394 p0: FloatPoint,
395 p1: FloatPoint,
396 p2: FloatPoint,
397 ) Error!void {
398 const steps: usize = 8;
399 for (1..(steps + 1)) |step| {
400 const t = @as(f32, @floatFromInt(step)) / @as(f32, @floatFromInt(steps));
401 const mt = 1.0 - t;
402 try points.append(allocator, .{
403 .x = mt * mt * p0.x + 2.0 * mt * t * p1.x + t * t * p2.x,
404 .y = mt * mt * p0.y + 2.0 * mt * t * p1.y + t * t * p2.y,
405 });
406 }
407 }
408
409 fn fontScale(face: face_table.Face, pixel_size: i32) f32 {
410 return @as(f32, @floatFromInt(pixel_size)) / @as(f32, @floatFromInt(face.units_per_em));
411 }
412
413 fn glyphPixelBounds(bounds: outline.Bounds, ascender: i32, scale: f32) PixelBounds {
414 return .{
415 .left = scaleFloor(bounds.x_min, scale),
416 .top = scaleFloor(ascender - bounds.y_max, scale),
417 .right = scaleCeil(bounds.x_max, scale),
418 .bottom = scaleCeil(ascender - bounds.y_min, scale),
419 };
420 }
421
422 fn scaleMetric(value: u16, scale: f32) i32 {
423 return scaleRound(value, scale);
424 }
425
426 fn scaleFloor(value: i32, scale: f32) i32 {
427 return @intFromFloat(@floor(@as(f32, @floatFromInt(value)) * scale));
428 }
429
430 fn scaleCeil(value: i32, scale: f32) i32 {
431 return @intFromFloat(@ceil(@as(f32, @floatFromInt(value)) * scale));
432 }
433
434 fn scaleRound(value: anytype, scale: f32) i32 {
435 return @intFromFloat(@round(@as(f32, @floatFromInt(value)) * scale));
436 }
437
438 fn pixelCount(width: i32, height: i32) Error!usize {
439 if (width <= 0 or height <= 0) return error.InvalidAtlas;
440 return std.math.mul(usize, @intCast(width), @intCast(height)) catch error.InvalidAtlas;
441 }
442
443 fn hasVisiblePixel(bytes: []const u8) bool {
444 for (bytes) |byte| {
445 if (byte != 0) return true;
446 }
447 return false;
448 }
449
450 fn expectGlyphCoverageMatchesReference(
451 allocator: std.mem.Allocator,
452 face: face_table.Face,
453 glyph_id: u32,
454 pixel_size: i32,
455 ) !void {
456 const scale = fontScale(face, pixel_size);
457 var glyph = try outline.glyphAlloc(allocator, face, glyph_id);
458 defer glyph.deinit(allocator);
459 if (glyph.points.len == 0 or glyph.bounds.x_max <= glyph.bounds.x_min or glyph.bounds.y_max <= glyph.bounds.y_min) return;
460
461 const pixel_bounds = glyphPixelBounds(glyph.bounds, face.ascender, scale);
462 const width = pixel_bounds.width();
463 const height = pixel_bounds.height();
464 if (width <= 0 or height <= 0) return;
465
466 var points = std.ArrayListUnmanaged(FloatPoint).empty;
467 defer points.deinit(allocator);
468 var contours = std.ArrayListUnmanaged(ContourRange).empty;
469 defer contours.deinit(allocator);
470 try flattenGlyph(allocator, glyph, face.ascender, pixel_bounds, scale, &points, &contours);
471
472 const expected = try allocator.alloc(u8, try pixelCount(width, height));
473 defer allocator.free(expected);
474 coverage.rasterizeReference(expected, width, height, points.items, contours.items);
475
476 const bitmap = try glyphBitmapAlloc(allocator, allocator, face, glyph_id, pixel_size);
477 defer bitmap.deinit(allocator);
478 try std.testing.expectEqualSlices(u8, expected, bitmap.alpha);
479 }
480
481 test "scanline rasterization matches reference fixture outlines" {
482 const fixtures = @import("../fixture/root.zig");
483 const allocator = std.testing.allocator;
484
485 const true_type_bytes = try fixtures.createWithOutlines(allocator);
486 defer allocator.free(true_type_bytes);
487 const true_type_face = try face_table.Face.init(true_type_bytes);
488 for ([_]i32{ 13, 20, 32 }) |pixel_size| {
489 for ([_]u21{ 'A', 'B', 'i' }) |codepoint| {
490 try expectGlyphCoverageMatchesReference(allocator, true_type_face, true_type_face.glyphId(codepoint), pixel_size);
491 }
492 }
493
494 const cff_bytes = try fixtures.createWithCffOutlines(allocator);
495 defer allocator.free(cff_bytes);
496 const cff_face = try face_table.Face.init(cff_bytes);
497 for ([_]i32{ 13, 20, 32 }) |pixel_size| {
498 try expectGlyphCoverageMatchesReference(allocator, cff_face, cff_face.glyphId('A'), pixel_size);
499 }
500 }
501
502 test "raster renders fixture outline alpha" {
503 const fixtures = @import("../fixture/root.zig");
504 const allocator = std.testing.allocator;
505 const bytes = try fixtures.createWithOutlines(allocator);
506 defer allocator.free(bytes);
507
508 const face = try face_table.Face.init(bytes);
509 const glyph_id = face.glyphId('A');
510 const bitmap = try glyphBitmapAlloc(allocator, allocator, face, glyph_id, 20);
511 defer bitmap.deinit(allocator);
512
513 try std.testing.expect(bitmap.width > 0);
514 try std.testing.expect(bitmap.height > 0);
515 try std.testing.expectEqual(@as(i32, 10), bitmap.advance_x);
516 try std.testing.expect(hasVisiblePixel(bitmap.alpha));
517 }
518
519 test "raster snaps glyph bitmap bounds from shared pixel edges" {
520 const bounds = outline.Bounds{ .x_min = 25, .y_min = 1, .x_max = 626, .y_max = 700 };
521 const pixel_bounds = glyphPixelBounds(bounds, 1000, 0.016);
522
523 try std.testing.expectEqual(@as(i32, 0), pixel_bounds.left);
524 try std.testing.expectEqual(@as(i32, 4), pixel_bounds.top);
525 try std.testing.expectEqual(@as(i32, 11), pixel_bounds.right);
526 try std.testing.expectEqual(@as(i32, 16), pixel_bounds.bottom);
527 try std.testing.expectEqual(@as(i32, 11), pixel_bounds.width());
528 try std.testing.expectEqual(@as(i32, 12), pixel_bounds.height());
529
530 const top_left = convertPoint(.{ .x = 25, .y = 700, .on_curve = true }, 1000, pixel_bounds, 0.016);
531 const bottom_right = convertPoint(.{ .x = 626, .y = 1, .on_curve = true }, 1000, pixel_bounds, 0.016);
532
533 try std.testing.expectApproxEqAbs(@as(f32, 0.4), top_left.x, 0.001);
534 try std.testing.expectApproxEqAbs(@as(f32, 0.8), top_left.y, 0.001);
535 try std.testing.expectApproxEqAbs(@as(f32, 10.016), bottom_right.x, 0.001);
536 try std.testing.expectApproxEqAbs(@as(f32, 11.984), bottom_right.y, 0.001);
537 }
538
539 test "raster renders composite fixture outline alpha" {
540 const fixtures = @import("../fixture/root.zig");
541 const allocator = std.testing.allocator;
542 const bytes = try fixtures.createWithOutlines(allocator);
543 defer allocator.free(bytes);
544
545 const face = try face_table.Face.init(bytes);
546 const bitmap = try glyphBitmapAlloc(allocator, allocator, face, face.glyphId('i'), 20);
547 defer bitmap.deinit(allocator);
548
549 try std.testing.expectEqual(@as(i32, 18), bitmap.width);
550 try std.testing.expectEqual(@as(i32, 14), bitmap.height);
551 try std.testing.expectEqual(@as(i32, 1), bitmap.offset_x);
552 try std.testing.expectEqual(@as(i32, 2), bitmap.offset_y);
553 try std.testing.expectEqual(@as(i32, 10), bitmap.advance_x);
554 try std.testing.expectEqual(@as(usize, 252), bitmap.alpha.len);
555
556 var row: usize = 0;
557 while (row < 14) : (row += 1) {
558 var col: usize = 0;
559 while (col < 18) : (col += 1) {
560 const expected: u8 = if (col < 8 or col >= 10) 255 else 0;
561 try std.testing.expectEqual(expected, bitmap.alpha[row * 18 + col]);
562 }
563 }
564 }
565
566 test "atlas packs fixture glyph bitmaps as rgba" {
567 const fixtures = @import("../fixture/root.zig");
568 const allocator = std.testing.allocator;
569 const bytes = try fixtures.createWithOutlines(allocator);
570 defer allocator.free(bytes);
571 const face = try face_table.Face.init(bytes);
572 var glyph_ids = [_]i32{
573 @intCast(face.glyphId('A')),
574 @intCast(face.glyphId('B')),
575 @intCast(face.glyphId(' ')),
576 };
577 const codepoints = [_]i32{ 'A', 'B', ' ' };
578
579 const atlas = try loadAtlasAlloc(allocator, allocator, bytes, 20, &glyph_ids, &codepoints, 2);
580 defer atlas.deinit(allocator);
581
582 try std.testing.expectEqual(@as(usize, 3), atlas.glyphs.len);
583 try std.testing.expectEqual(@as(i32, 'A'), atlas.glyphs[0].codepoint);
584 try std.testing.expect(atlas.width > 0);
585 try std.testing.expect(atlas.height > 0);
586 try std.testing.expect(atlas.rgba.len >= 4);
587 try std.testing.expect(hasVisiblePixel(atlas.rgba));
588 }
589
590 test "raster renders CFF fixture outline alpha" {
591 const fixtures = @import("../fixture/root.zig");
592 const allocator = std.testing.allocator;
593 const bytes = try fixtures.createWithCffOutlines(allocator);
594 defer allocator.free(bytes);
595
596 const face = try face_table.Face.init(bytes);
597 const glyph_id = face.glyphId('A');
598 const bitmap = try glyphBitmapAlloc(allocator, allocator, face, glyph_id, 20);
599 defer bitmap.deinit(allocator);
600
601 try std.testing.expect(bitmap.width > 0);
602 try std.testing.expect(bitmap.height > 0);
603 try std.testing.expectEqual(@as(i32, 10), bitmap.advance_x);
604 try std.testing.expect(hasVisiblePixel(bitmap.alpha));
605 }
606
607 test "raster renders configured CFF font alpha" {
608 const allocator = std.testing.allocator;
609 const path = sys.env.getOwned(allocator, cff_test_font_env) catch null orelse return error.SkipZigTest;
610 defer allocator.free(path);
611 const bytes = try sys.fs.readFileAlloc(allocator, path, max_test_font_bytes);
612 defer allocator.free(bytes);
613
614 const face = try face_table.Face.init(bytes);
615 if (face.tableSlice("CFF ") == null) return error.SkipZigTest;
616 const glyph_id = face.glyphId('A');
617 if (glyph_id == 0) return error.SkipZigTest;
618 const bitmap = try glyphBitmapAlloc(allocator, allocator, face, glyph_id, 32);
619 defer bitmap.deinit(allocator);
620
621 try std.testing.expect(bitmap.width > 0);
622 try std.testing.expect(bitmap.height > 0);
623 try std.testing.expect(hasVisiblePixel(bitmap.alpha));
624 }