This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

font: glyf outline decoder and rasterizer (#12893)

This adds a Glyf outline decoder and rasterizer.

So it turns out that FreeType and CoreText have very shitty APIs for raw
Glyf table rasterization. CoreText as far as I can find can't do it at
all. In both cases you have to create a synthetic font with just this
entry and rasterize the glyph. And the code to do all that was WAYYYYYY
complex such that this made way more sense.

We need this for the Glyph Protocol.

**AI disclosure:** Hand-written parser, rasterizer. AI assisted
validation and test writing. I read the spec myself.

cc @qwerasd205

+1584
+836
src/font/glyf_rasterize.zig
··· 1 + //! Rasterization for OpenType glyf outlines. 2 + //! 3 + //! This module intentionally lives in `font` rather than `font/opentype` 4 + //! because I wanted to keep `font/opentype` dependency free on the font 5 + //! package. 6 + 7 + const std = @import("std"); 8 + const assert = std.debug.assert; 9 + const Allocator = std.mem.Allocator; 10 + const z2d = @import("z2d"); 11 + 12 + const face = @import("face.zig"); 13 + const glyf = @import("opentype/glyf.zig"); 14 + 15 + /// Metrics describing the authored glyf coordinate space, since 16 + /// a glyf table doesn't contain this on its own. 17 + pub const DesignMetrics = struct { 18 + /// Units-per-em for outline/design coordinates. 19 + units_per_em: u32, 20 + 21 + /// Authored advance width in design units. 22 + advance_width: u32, 23 + 24 + /// Authored line height in design units. 25 + line_height: u32, 26 + }; 27 + 28 + /// An owned, tightly packed alpha8 bitmap. 29 + pub const Bitmap = struct { 30 + width: u32, 31 + height: u32, 32 + data: []u8, 33 + 34 + // An empty 0x0 bitmap. 35 + pub const empty: Bitmap = .{ .width = 0, .height = 0, .data = "" }; 36 + 37 + pub fn initEmpty(alloc: Allocator, width: u32, height: u32) Allocator.Error!Bitmap { 38 + const data = try alloc.alloc(u8, @as(usize, width) * @as(usize, height)); 39 + @memset(data, 0); 40 + return .{ .width = width, .height = height, .data = data }; 41 + } 42 + 43 + pub fn deinit(self: *Bitmap, alloc: Allocator) void { 44 + alloc.free(self.data); 45 + self.* = undefined; 46 + } 47 + }; 48 + 49 + pub const Error = Allocator.Error || z2d.Path.Error || z2d.painter.FillError; 50 + 51 + /// Rasterize a decoded glyf outline to a full-cell alpha bitmap. 52 + /// 53 + /// The returned bitmap is always `grid_metrics.cell_width * cell_width` by 54 + /// `grid_metrics.cell_height`. `opts.constraint` is applied using the same 55 + /// `face.RenderOptions.Constraint` machinery used by the platform font 56 + /// backends. 57 + /// 58 + /// The caller owns the returned bitmap. 59 + pub fn rasterize( 60 + alloc: Allocator, 61 + outline: glyf.Glyf.Outline, 62 + design: DesignMetrics, 63 + opts: face.RenderOptions, 64 + ) Error!Bitmap { 65 + assert(design.units_per_em > 0); 66 + assert(design.advance_width > 0); 67 + assert(design.line_height > 0); 68 + 69 + // Calculate our final width/height. 70 + const width: u32 = std.math.mul( 71 + u32, 72 + opts.grid_metrics.cell_width, 73 + opts.cell_width orelse 1, 74 + ) catch std.math.maxInt(u32); 75 + const height = opts.grid_metrics.cell_height; 76 + assert(width > 0 and height > 0); 77 + 78 + // If we have no contours or points then we have no drawable shape, but the 79 + // caller still asked for a cell-sized bitmap. Return that full bitmap with 80 + // zero coverage so downstream atlas/upload code doesn't need a separate 81 + // size contract for empty glyphs. 82 + if (outline.contours.len == 0 or outline.points.len == 0) return Bitmap.initEmpty(alloc, width, height); 83 + 84 + // Glyf entries have a header bounding box, but this rasterizer operates on 85 + // the decoded Outline only. Recompute bounds from the decoded coordinate 86 + // data so placement and scaling follow the geometry we actually draw. If a 87 + // source glyf header disagrees with its points, the point data is the safer 88 + // source of truth for rasterization; the header belongs in decode-time 89 + // validation/metadata, not this font-level drawing API. 90 + const bounds: Bounds = bounds: { 91 + var bounds: Bounds = .{ 92 + .x_min = @floatFromInt(outline.points[0].x), 93 + .y_min = @floatFromInt(outline.points[0].y), 94 + .x_max = @floatFromInt(outline.points[0].x), 95 + .y_max = @floatFromInt(outline.points[0].y), 96 + }; 97 + for (outline.points[1..]) |p| { 98 + const x: f64 = @floatFromInt(p.x); 99 + const y: f64 = @floatFromInt(p.y); 100 + bounds.x_min = @min(bounds.x_min, x); 101 + bounds.y_min = @min(bounds.y_min, y); 102 + bounds.x_max = @max(bounds.x_max, x); 103 + bounds.y_max = @max(bounds.y_max, y); 104 + } 105 + break :bounds bounds; 106 + }; 107 + 108 + // Degenerate point bounds can't produce filled area and would make the 109 + // point-to-bitmap transform divide by zero, so return a full transparent 110 + // bitmap just like an empty outline. 111 + if (bounds.width() == 0 or bounds.height() == 0) return Bitmap.initEmpty(alloc, width, height); 112 + 113 + // Build the surface we'll draw on. This is a simple alpha8 drawing. 114 + var sfc: z2d.Surface = try .init( 115 + .image_surface_alpha8, 116 + alloc, 117 + @intCast(width), 118 + @intCast(height), 119 + ); 120 + defer sfc.deinit(alloc); 121 + 122 + var path: z2d.Path = .empty; 123 + defer path.deinit(alloc); 124 + 125 + const placement: Placement = .init(bounds, design, opts); 126 + for (0..outline.contours.len) |i| try appendContourPath( 127 + alloc, 128 + &path, 129 + outline.contour(i), 130 + bounds, 131 + placement, 132 + ); 133 + 134 + try z2d.painter.fill( 135 + alloc, 136 + &sfc, 137 + &.{ .opaque_pattern = .{ 138 + .pixel = .{ .alpha8 = .{ .a = 255 } }, 139 + } }, 140 + path.nodes.items, 141 + .{}, 142 + ); 143 + 144 + return .{ 145 + .width = width, 146 + .height = height, 147 + .data = try alloc.dupe(u8, std.mem.sliceAsBytes(sfc.image_surface_alpha8.buf)), 148 + }; 149 + } 150 + 151 + const Bounds = struct { 152 + x_min: f64, 153 + y_min: f64, 154 + x_max: f64, 155 + y_max: f64, 156 + 157 + fn width(self: Bounds) f64 { 158 + return self.x_max - self.x_min; 159 + } 160 + 161 + fn height(self: Bounds) f64 { 162 + return self.y_max - self.y_min; 163 + } 164 + }; 165 + 166 + /// Cell-relative pixel rectangle where the decoded outline bounds should be 167 + /// rasterized within the output bitmap. 168 + /// 169 + /// This is deliberately the placement of the outline's computed point bounds, 170 + /// not the full declared advance/line-height box. `advance_width` and 171 + /// `line_height` describe the design-space layout box the outline was drawn 172 + /// within: they include intentional bearings and whitespace around the visible 173 + /// points. We use that declared box when applying `RenderOptions.Constraint` so 174 + /// sizing and alignment preserve those bearings consistently with other font 175 + /// backends; once that is resolved, we rasterize only the actual outline bounds 176 + /// into this rectangle. 177 + /// 178 + /// ```text 179 + /// output bitmap / terminal cell 180 + /// ╭────────────────────────────────────────────────────────────────────────╮ top 181 + /// │ │ 182 + /// │ declared advance/line-height box │ 183 + /// │ (outer layout box used for constraints) │ 184 + /// │ ╭────────────────────────────────────────────────────────────────╮ │ 185 + /// │ │ │ │ 186 + /// │◀──────── x ────────▶╭────────── width ──────────╮ │ │ 187 + /// │ │ │ Placement │ ▲ height │ │ 188 + /// │ │ │ outline point bounds │ │ │ │ 189 + /// │ │ │ pixels to draw │ │ │ │ 190 + /// │ │ │ │ │ │ │ 191 + /// │ │ ╰───────────────────────────╯ ▼ │ │ 192 + /// │ ╰─────────────────▲──────────────────────────────────────────────╯ │ 193 + /// │ │ y │ 194 + /// ╰────────────────────────────────────────────────────────────────────────╯ bottom 195 + /// x is measured from the bitmap left to the Placement left. 196 + /// y is measured from the bitmap bottom to the Placement bottom. 197 + /// bitmap_height is the full top-to-bottom bitmap height. 198 + /// ``` 199 + /// 200 + /// Constraints are applied to the outer box so the whitespace remains part of 201 + /// alignment decisions. `Placement` is the inner rectangle after that outer box 202 + /// has been constrained. 203 + const Placement = struct { 204 + /// Left edge of the rasterized outline bounds in bitmap pixels, measured 205 + /// from the bitmap's left edge. 206 + x: f64, 207 + 208 + /// Bottom edge of the rasterized outline bounds in bitmap pixels, measured 209 + /// from the bitmap's bottom edge. This matches the cell-relative y axis 210 + /// used by font.face.GlyphSize and is converted to z2d's y-down axis when 211 + /// points are transformed. 212 + y: f64, 213 + 214 + /// Width of the rasterized outline bounds in bitmap pixels after applying 215 + /// font.face.RenderOptions.Constraint. 216 + width: f64, 217 + 218 + /// Height of the rasterized outline bounds in bitmap pixels after applying 219 + /// font.face.RenderOptions.Constraint. 220 + height: f64, 221 + 222 + /// Full bitmap height in pixels, used to convert cell-relative y-up-ish 223 + /// placement into the y-down coordinate system used by z2d surfaces. 224 + bitmap_height: f64, 225 + 226 + /// Calculate where the decoded point bounds should land in the output 227 + /// bitmap. 228 + /// 229 + /// The glyf protocol supplies declared metrics (`units_per_em`, 230 + /// `advance_width`, and `line_height`) in design units, while Ghostty's 231 + /// font constraint code works in cell-relative pixels. We first map the em 232 + /// square to one cell height, matching the linked glyph rasterizer's 233 + /// baseline model where design-space `y=0` is the bottom/baseline of the em 234 + /// and `y=units_per_em` is its top. Then we describe the actual outline 235 + /// bounds as a relative sub-rectangle of the declared advance/line-height 236 + /// box. That declared box includes any intentional side bearings or 237 + /// vertical whitespace around the outline; constraints should apply to that 238 + /// layout box rather than to the tight point bounds alone. This returns the 239 + /// final pixel rectangle for only the outline bounds that we will rasterize. 240 + fn init( 241 + bounds: Bounds, 242 + design: DesignMetrics, 243 + opts: face.RenderOptions, 244 + ) Placement { 245 + // Start with protocol-like design units mapped so that the em square 246 + // occupies one cell. This makes units_per_em the scale reference and 247 + // preserves the linked rasterizer's y=0 baseline/bottom behavior. 248 + // Callers can then use RenderOptions.Constraint to fit/cover/stretch/ 249 + // align the declared advance/line-height box using existing font logic. 250 + const scale = @as(f64, @floatFromInt(opts.grid_metrics.cell_height)) / 251 + @as(f64, @floatFromInt(design.units_per_em)); 252 + 253 + // Convert the decoded point bounds into the same pixel coordinate space 254 + // expected by RenderOptions.Constraint. This rectangle is the visible 255 + // outline bounds, not the full advance/line-height layout box. 256 + const glyph: face.GlyphSize = .{ 257 + .width = bounds.width() * scale, 258 + .height = bounds.height() * scale, 259 + .x = bounds.x_min * scale, 260 + .y = bounds.y_min * scale, 261 + }; 262 + 263 + // Convert the declared layout box to pixels. This is the box that 264 + // carries intentional bearings/whitespace and should be constrained. 265 + const group_width = @as(f64, @floatFromInt(design.advance_width)) * scale; 266 + const group_height = @as(f64, @floatFromInt(design.line_height)) * scale; 267 + 268 + // Apply the same fit/cover/stretch/alignment/padding rules used by 269 + // normal font rendering. The result is still the outline bounds, but 270 + // placed as if its containing advance/line-height box was constrained. 271 + const constraint: face.RenderOptions.Constraint = constraint: { 272 + var constraint = opts.constraint; 273 + if (group_width > 0 and group_height > 0) { 274 + // Tell Constraint that `glyph` is a sub-rectangle of the 275 + // declared layout box. Constraint will size/align the outer box 276 + // and then return the corresponding transformed inner box. 277 + constraint.relative_width = glyph.width / group_width; 278 + constraint.relative_height = glyph.height / group_height; 279 + constraint.relative_x = glyph.x / group_width; 280 + constraint.relative_y = glyph.y / group_height; 281 + } 282 + break :constraint constraint; 283 + }; 284 + const constrained = constraint.constrain( 285 + glyph, 286 + opts.grid_metrics, 287 + opts.constraint_width, 288 + ); 289 + 290 + // Store the final outline placement plus the full bitmap height needed 291 + // later to flip from cell-relative y to z2d's y-down surface space. 292 + return .{ 293 + .x = constrained.x, 294 + .y = constrained.y, 295 + .width = constrained.width, 296 + .height = constrained.height, 297 + .bitmap_height = @floatFromInt(opts.grid_metrics.cell_height), 298 + }; 299 + } 300 + }; 301 + 302 + const Point = struct { 303 + x: f64, 304 + y: f64, 305 + }; 306 + 307 + /// Append one contour to a z2d path. 308 + /// 309 + /// Glyf contours are quadratic outlines with explicit on-curve points and 310 + /// off-curve control points. Consecutive off-curve points imply an on-curve 311 + /// point halfway between them, and a contour may begin with an off-curve point. 312 + /// This normalizes those cases while walking the closed contour and emits z2d 313 + /// line/cubic-curve operations in bitmap coordinates. 314 + fn appendContourPath( 315 + alloc: Allocator, 316 + path: *z2d.Path, 317 + contour: []const glyf.Glyf.Outline.Point, 318 + bounds: Bounds, 319 + placement: Placement, 320 + ) Error!void { 321 + if (contour.len == 0) return; 322 + 323 + const first = contour[0]; 324 + const last = contour[contour.len - 1]; 325 + 326 + var current: Point = undefined; 327 + var i: usize = 0; 328 + 329 + // Choose the starting on-curve point for this closed contour. If the first 330 + // point is off-curve then the contour logically starts either at the final 331 + // on-curve point, or at the implied midpoint between the final and first 332 + // off-curve points. 333 + if (first.on_curve) { 334 + i = 1; 335 + current = transformPoint( 336 + first, 337 + bounds, 338 + placement, 339 + ); 340 + } else if (last.on_curve) { 341 + current = transformPoint( 342 + last, 343 + bounds, 344 + placement, 345 + ); 346 + } else { 347 + current = midpoint( 348 + transformPoint(last, bounds, placement), 349 + transformPoint(first, bounds, placement), 350 + ); 351 + } 352 + 353 + // Move to the beginning 354 + try path.moveTo(alloc, current.x, current.y); 355 + 356 + // Go through the points and connect em! 357 + while (i < contour.len) { 358 + const p = contour[i]; 359 + 360 + // On-curve points connect to the current point with a straight line. 361 + if (p.on_curve) { 362 + current = transformPoint(p, bounds, placement); 363 + try path.lineTo(alloc, current.x, current.y); 364 + i += 1; 365 + continue; 366 + } 367 + 368 + // Off-curve points are quadratic control points. The following point is 369 + // either the curve endpoint or, if it is also off-curve, contributes an 370 + // implied on-curve endpoint halfway between the two controls. 371 + const control = transformPoint(p, bounds, placement); 372 + const next = contour[(i + 1) % contour.len]; 373 + const end = if (next.on_curve) transformPoint( 374 + next, 375 + bounds, 376 + placement, 377 + ) else midpoint( 378 + control, 379 + transformPoint(next, bounds, placement), 380 + ); 381 + 382 + // z2d paths only expose cubic curves, so convert the TrueType 383 + // quadratic segment to an equivalent cubic segment before appending it. 384 + const c1 = Point{ 385 + .x = current.x + ((2.0 / 3.0) * (control.x - current.x)), 386 + .y = current.y + ((2.0 / 3.0) * (control.y - current.y)), 387 + }; 388 + const c2 = Point{ 389 + .x = end.x + ((2.0 / 3.0) * (control.x - end.x)), 390 + .y = end.y + ((2.0 / 3.0) * (control.y - end.y)), 391 + }; 392 + try path.curveTo( 393 + alloc, 394 + c1.x, 395 + c1.y, 396 + c2.x, 397 + c2.y, 398 + end.x, 399 + end.y, 400 + ); 401 + 402 + current = end; 403 + 404 + // If we consumed an explicit on-curve endpoint then skip it; otherwise 405 + // the next off-curve point still needs to be used as the control point 406 + // for the following quadratic segment. 407 + i += if (next.on_curve) 2 else 1; 408 + } 409 + 410 + try path.close(alloc); 411 + } 412 + 413 + /// Convert a decoded glyf point from design-space coordinates to z2d bitmap 414 + /// coordinates. 415 + /// 416 + /// `bounds` describes the decoded outline's point/control bounds in glyf 417 + /// design units. `placement` describes where those bounds should land in the 418 + /// output bitmap after constraints are applied. Glyf coordinates are y-up; z2d 419 + /// surfaces are y-down, so this also flips the y axis using 420 + /// `placement.bitmap_height`. 421 + fn transformPoint( 422 + p: glyf.Glyf.Outline.Point, 423 + bounds: Bounds, 424 + placement: Placement, 425 + ) Point { 426 + const scale_x = placement.width / bounds.width(); 427 + const scale_y = placement.height / bounds.height(); 428 + const x_design: f64 = @floatFromInt(p.x); 429 + const y_design: f64 = @floatFromInt(p.y); 430 + return .{ 431 + .x = placement.x + ((x_design - bounds.x_min) * scale_x), 432 + .y = placement.bitmap_height - placement.y - 433 + ((y_design - bounds.y_min) * scale_y), 434 + }; 435 + } 436 + 437 + /// Return the implied on-curve point between two off-curve TrueType control 438 + /// points. 439 + fn midpoint(a: Point, b: Point) Point { 440 + return .{ 441 + .x = (a.x + b.x) / 2.0, 442 + .y = (a.y + b.y) / 2.0, 443 + }; 444 + } 445 + 446 + fn testMetrics(width: u32, height: u32) @import("Metrics.zig") { 447 + return .{ 448 + .cell_width = width, 449 + .cell_height = height, 450 + .cell_baseline = 0, 451 + .underline_position = height, 452 + .underline_thickness = 1, 453 + .strikethrough_position = height / 2, 454 + .strikethrough_thickness = 1, 455 + .overline_position = 0, 456 + .overline_thickness = 1, 457 + .box_thickness = 1, 458 + .cursor_thickness = 1, 459 + .cursor_height = height, 460 + .icon_height = @floatFromInt(height), 461 + .icon_height_single = @floatFromInt(height), 462 + .face_width = @floatFromInt(width), 463 + .face_height = @floatFromInt(height), 464 + .face_y = 0, 465 + }; 466 + } 467 + 468 + test { 469 + _ = @import("glyf_rasterize_png_test.zig"); 470 + } 471 + 472 + test "glyf_rasterize: empty outline returns empty bitmap" { 473 + const testing = std.testing; 474 + const alloc = testing.allocator; 475 + 476 + var bm = try rasterize(alloc, .{ .points = &.{}, .contours = &.{} }, .{ 477 + .units_per_em = 1000, 478 + .advance_width = 1000, 479 + .line_height = 1000, 480 + }, .{ 481 + .grid_metrics = testMetrics(20, 20), 482 + }); 483 + defer bm.deinit(alloc); 484 + 485 + try testing.expectEqual(@as(u32, 20), bm.width); 486 + try testing.expectEqual(@as(u32, 20), bm.height); 487 + try testing.expectEqual(@as(usize, 20 * 20), bm.data.len); 488 + for (bm.data) |v| try testing.expectEqual(@as(u8, 0), v); 489 + } 490 + 491 + test "glyf_rasterize: square fills bitmap center" { 492 + const testing = std.testing; 493 + const alloc = testing.allocator; 494 + 495 + const outline: glyf.Glyf.Outline = .{ 496 + .points = &.{ 497 + .{ .x = 0, .y = 0, .on_curve = true }, 498 + .{ .x = 1000, .y = 0, .on_curve = true }, 499 + .{ .x = 1000, .y = 1000, .on_curve = true }, 500 + .{ .x = 0, .y = 1000, .on_curve = true }, 501 + }, 502 + .contours = &.{3}, 503 + }; 504 + 505 + var bm = try rasterize(alloc, outline, .{ 506 + .units_per_em = 1000, 507 + .advance_width = 1000, 508 + .line_height = 1000, 509 + }, .{ 510 + .grid_metrics = testMetrics(20, 20), 511 + }); 512 + defer bm.deinit(alloc); 513 + 514 + try testing.expect(bm.data[10 * bm.width + 10] > 200); 515 + } 516 + 517 + test "glyf_rasterize: quadratic contour renders" { 518 + const testing = std.testing; 519 + const alloc = testing.allocator; 520 + 521 + const outline: glyf.Glyf.Outline = .{ 522 + .points = &.{ 523 + .{ .x = 0, .y = 0, .on_curve = true }, 524 + .{ .x = 500, .y = 1000, .on_curve = false }, 525 + .{ .x = 1000, .y = 0, .on_curve = true }, 526 + }, 527 + .contours = &.{2}, 528 + }; 529 + 530 + var bm = try rasterize(alloc, outline, .{ 531 + .units_per_em = 1000, 532 + .advance_width = 1000, 533 + .line_height = 1000, 534 + }, .{ 535 + .grid_metrics = testMetrics(20, 20), 536 + }); 537 + defer bm.deinit(alloc); 538 + 539 + var nonzero = false; 540 + for (bm.data) |v| nonzero = nonzero or v != 0; 541 + try testing.expect(nonzero); 542 + } 543 + 544 + test "glyf_rasterize: consecutive off-curve points render" { 545 + const testing = std.testing; 546 + const alloc = testing.allocator; 547 + 548 + const outline: glyf.Glyf.Outline = .{ 549 + .points = &.{ 550 + .{ .x = 0, .y = 0, .on_curve = true }, 551 + .{ .x = 250, .y = 1000, .on_curve = false }, 552 + .{ .x = 750, .y = 1000, .on_curve = false }, 553 + .{ .x = 1000, .y = 0, .on_curve = true }, 554 + }, 555 + .contours = &.{3}, 556 + }; 557 + 558 + var bm = try rasterize(alloc, outline, .{ 559 + .units_per_em = 1000, 560 + .advance_width = 1000, 561 + .line_height = 1000, 562 + }, .{ 563 + .grid_metrics = testMetrics(20, 20), 564 + }); 565 + defer bm.deinit(alloc); 566 + 567 + var nonzero = false; 568 + for (bm.data) |v| nonzero = nonzero or v != 0; 569 + try testing.expect(nonzero); 570 + } 571 + 572 + test "glyf_rasterize: units per em controls baseline scale" { 573 + const testing = std.testing; 574 + const alloc = testing.allocator; 575 + 576 + const outline: glyf.Glyf.Outline = .{ 577 + .points = &.{ 578 + .{ .x = 0, .y = 0, .on_curve = true }, 579 + .{ .x = 1000, .y = 0, .on_curve = true }, 580 + .{ .x = 1000, .y = 1000, .on_curve = true }, 581 + .{ .x = 0, .y = 1000, .on_curve = true }, 582 + }, 583 + .contours = &.{3}, 584 + }; 585 + 586 + var bm = try rasterize(alloc, outline, .{ 587 + .units_per_em = 2000, 588 + .advance_width = 1000, 589 + .line_height = 1000, 590 + }, .{ 591 + .grid_metrics = testMetrics(20, 20), 592 + }); 593 + defer bm.deinit(alloc); 594 + 595 + // With a 2000-unit em in a 20px cell, this 1000-unit square occupies the 596 + // bottom half of the cell. This matches the linked rasterizer's y=0 597 + // baseline/bottom behavior and proves units_per_em is the scale reference. 598 + try testing.expect(bm.data[15 * bm.width + 5] > 200); 599 + try testing.expectEqual(@as(u8, 0), bm.data[5 * bm.width + 5]); 600 + } 601 + 602 + test "glyf_rasterize: degenerate outline returns full empty bitmap" { 603 + const testing = std.testing; 604 + const alloc = testing.allocator; 605 + 606 + const outline: glyf.Glyf.Outline = .{ 607 + .points = &.{ 608 + .{ .x = 0, .y = 0, .on_curve = true }, 609 + .{ .x = 1000, .y = 0, .on_curve = true }, 610 + .{ .x = 500, .y = 0, .on_curve = true }, 611 + }, 612 + .contours = &.{2}, 613 + }; 614 + 615 + var bm = try rasterize(alloc, outline, .{ 616 + .units_per_em = 1000, 617 + .advance_width = 1000, 618 + .line_height = 1000, 619 + }, .{ 620 + .grid_metrics = testMetrics(20, 20), 621 + }); 622 + defer bm.deinit(alloc); 623 + 624 + try testing.expectEqual(@as(u32, 20), bm.width); 625 + try testing.expectEqual(@as(u32, 20), bm.height); 626 + try testing.expectEqual(@as(usize, 20 * 20), bm.data.len); 627 + for (bm.data) |v| try testing.expectEqual(@as(u8, 0), v); 628 + } 629 + 630 + test "glyf_rasterize: contour can start off curve with final on curve point" { 631 + const testing = std.testing; 632 + const alloc = testing.allocator; 633 + 634 + const outline: glyf.Glyf.Outline = .{ 635 + .points = &.{ 636 + .{ .x = 500, .y = 1000, .on_curve = false }, 637 + .{ .x = 1000, .y = 0, .on_curve = true }, 638 + .{ .x = 0, .y = 0, .on_curve = true }, 639 + }, 640 + .contours = &.{2}, 641 + }; 642 + 643 + var bm = try rasterize(alloc, outline, .{ 644 + .units_per_em = 1000, 645 + .advance_width = 1000, 646 + .line_height = 1000, 647 + }, .{ 648 + .grid_metrics = testMetrics(20, 20), 649 + }); 650 + defer bm.deinit(alloc); 651 + 652 + var nonzero = false; 653 + for (bm.data) |v| nonzero = nonzero or v != 0; 654 + try testing.expect(nonzero); 655 + } 656 + 657 + test "glyf_rasterize: contour can start with implied midpoint" { 658 + const testing = std.testing; 659 + const alloc = testing.allocator; 660 + 661 + const outline: glyf.Glyf.Outline = .{ 662 + .points = &.{ 663 + .{ .x = 250, .y = 1000, .on_curve = false }, 664 + .{ .x = 1000, .y = 0, .on_curve = true }, 665 + .{ .x = 750, .y = 1000, .on_curve = false }, 666 + .{ .x = 0, .y = 0, .on_curve = true }, 667 + }, 668 + .contours = &.{3}, 669 + }; 670 + 671 + var bm = try rasterize(alloc, outline, .{ 672 + .units_per_em = 1000, 673 + .advance_width = 1000, 674 + .line_height = 1000, 675 + }, .{ 676 + .grid_metrics = testMetrics(20, 20), 677 + }); 678 + defer bm.deinit(alloc); 679 + 680 + var nonzero = false; 681 + for (bm.data) |v| nonzero = nonzero or v != 0; 682 + try testing.expect(nonzero); 683 + } 684 + 685 + test "glyf_rasterize: multiple contours render independently" { 686 + const testing = std.testing; 687 + const alloc = testing.allocator; 688 + 689 + const outline: glyf.Glyf.Outline = .{ 690 + .points = &.{ 691 + .{ .x = 0, .y = 0, .on_curve = true }, 692 + .{ .x = 400, .y = 0, .on_curve = true }, 693 + .{ .x = 400, .y = 400, .on_curve = true }, 694 + .{ .x = 0, .y = 400, .on_curve = true }, 695 + .{ .x = 600, .y = 600, .on_curve = true }, 696 + .{ .x = 1000, .y = 600, .on_curve = true }, 697 + .{ .x = 1000, .y = 1000, .on_curve = true }, 698 + .{ .x = 600, .y = 1000, .on_curve = true }, 699 + }, 700 + .contours = &.{ 3, 7 }, 701 + }; 702 + 703 + var bm = try rasterize(alloc, outline, .{ 704 + .units_per_em = 1000, 705 + .advance_width = 1000, 706 + .line_height = 1000, 707 + }, .{ 708 + .grid_metrics = testMetrics(20, 20), 709 + }); 710 + defer bm.deinit(alloc); 711 + 712 + try testing.expect(bm.data[16 * bm.width + 4] > 200); 713 + try testing.expect(bm.data[4 * bm.width + 16] > 200); 714 + try testing.expectEqual(@as(u8, 0), bm.data[10 * bm.width + 10]); 715 + } 716 + 717 + test "glyf_rasterize: non-zero bearings preserve declared whitespace" { 718 + const testing = std.testing; 719 + const alloc = testing.allocator; 720 + 721 + const outline: glyf.Glyf.Outline = .{ 722 + .points = &.{ 723 + .{ .x = 250, .y = 0, .on_curve = true }, 724 + .{ .x = 750, .y = 0, .on_curve = true }, 725 + .{ .x = 750, .y = 1000, .on_curve = true }, 726 + .{ .x = 250, .y = 1000, .on_curve = true }, 727 + }, 728 + .contours = &.{3}, 729 + }; 730 + 731 + var bm = try rasterize(alloc, outline, .{ 732 + .units_per_em = 1000, 733 + .advance_width = 1000, 734 + .line_height = 1000, 735 + }, .{ 736 + .grid_metrics = testMetrics(20, 20), 737 + }); 738 + defer bm.deinit(alloc); 739 + 740 + try testing.expectEqual(@as(u8, 0), bm.data[10 * bm.width + 2]); 741 + try testing.expect(bm.data[10 * bm.width + 10] > 200); 742 + try testing.expectEqual(@as(u8, 0), bm.data[10 * bm.width + 17]); 743 + } 744 + 745 + test "glyf_rasterize: negative y coordinates descend below baseline" { 746 + const testing = std.testing; 747 + const alloc = testing.allocator; 748 + 749 + const outline: glyf.Glyf.Outline = .{ 750 + .points = &.{ 751 + .{ .x = 0, .y = -250, .on_curve = true }, 752 + .{ .x = 1000, .y = -250, .on_curve = true }, 753 + .{ .x = 1000, .y = 750, .on_curve = true }, 754 + .{ .x = 0, .y = 750, .on_curve = true }, 755 + }, 756 + .contours = &.{3}, 757 + }; 758 + 759 + var bm = try rasterize(alloc, outline, .{ 760 + .units_per_em = 1000, 761 + .advance_width = 1000, 762 + .line_height = 1000, 763 + }, .{ 764 + .grid_metrics = testMetrics(20, 20), 765 + }); 766 + defer bm.deinit(alloc); 767 + 768 + try testing.expectEqual(@as(u8, 0), bm.data[2 * bm.width + 10]); 769 + try testing.expect(bm.data[10 * bm.width + 10] > 200); 770 + try testing.expect(bm.data[18 * bm.width + 10] > 200); 771 + } 772 + 773 + test "glyf_rasterize: two-cell bitmap and constraint render within width" { 774 + const testing = std.testing; 775 + const alloc = testing.allocator; 776 + 777 + const outline: glyf.Glyf.Outline = .{ 778 + .points = &.{ 779 + .{ .x = 0, .y = 0, .on_curve = true }, 780 + .{ .x = 1000, .y = 0, .on_curve = true }, 781 + .{ .x = 1000, .y = 1000, .on_curve = true }, 782 + .{ .x = 0, .y = 1000, .on_curve = true }, 783 + }, 784 + .contours = &.{3}, 785 + }; 786 + 787 + var bm = try rasterize(alloc, outline, .{ 788 + .units_per_em = 1000, 789 + .advance_width = 1000, 790 + .line_height = 1000, 791 + }, .{ 792 + .grid_metrics = testMetrics(20, 20), 793 + .cell_width = 2, 794 + .constraint_width = 2, 795 + .constraint = .{ 796 + .size = .cover, 797 + .align_horizontal = .center, 798 + .align_vertical = .center, 799 + }, 800 + }); 801 + defer bm.deinit(alloc); 802 + 803 + try testing.expectEqual(@as(u32, 40), bm.width); 804 + try testing.expectEqual(@as(u32, 20), bm.height); 805 + try testing.expectEqual(@as(u8, 0), bm.data[10 * bm.width + 2]); 806 + try testing.expect(bm.data[10 * bm.width + 20] > 200); 807 + try testing.expectEqual(@as(u8, 0), bm.data[10 * bm.width + 37]); 808 + } 809 + 810 + test "glyf_rasterize: line height does not change unconstrained em scale" { 811 + const testing = std.testing; 812 + const alloc = testing.allocator; 813 + 814 + const outline: glyf.Glyf.Outline = .{ 815 + .points = &.{ 816 + .{ .x = 0, .y = 0, .on_curve = true }, 817 + .{ .x = 1000, .y = 0, .on_curve = true }, 818 + .{ .x = 1000, .y = 1000, .on_curve = true }, 819 + .{ .x = 0, .y = 1000, .on_curve = true }, 820 + }, 821 + .contours = &.{3}, 822 + }; 823 + 824 + var bm = try rasterize(alloc, outline, .{ 825 + .units_per_em = 1000, 826 + .advance_width = 1000, 827 + .line_height = 2000, 828 + }, .{ 829 + .grid_metrics = testMetrics(20, 20), 830 + }); 831 + defer bm.deinit(alloc); 832 + 833 + try testing.expect(bm.data[10 * bm.width + 10] > 200); 834 + try testing.expect(bm.data[2 * bm.width + 10] > 200); 835 + try testing.expect(bm.data[17 * bm.width + 10] > 200); 836 + }
+301
src/font/glyf_rasterize_png_test.zig
··· 1 + const std = @import("std"); 2 + const Allocator = std.mem.Allocator; 3 + const wuffs = @import("wuffs"); 4 + const z2d = @import("z2d"); 5 + 6 + const glyf_rasterize = @import("glyf_rasterize.zig"); 7 + const glyf = @import("opentype/glyf.zig"); 8 + 9 + const log = std.log.scoped(.glyf_rasterize); 10 + 11 + const test_glyf_payloads = [_][]const u8{ 12 + // Nerd Font branch, folder, home, heart, and Rust cog outlines from: 13 + // https://github.com/raphamorim/glyph-protocol-examples/blob/main/bubbletea/main.go 14 + "AAIARv8zAhIDnQAZAB0AABcjNTQ3Njc3Njc2NTUjNxcjFRQGBwcGBwYVEQcRM82HJxs3SyoUE2aPjmY0NCUvDxSHh83rVzgoIzAbKSZAoaenvF5kIhkfHSM9AVdXAn8=", 15 + "AAEAAP/UA5wC/AAVAAAXIiY1ETQ2MzMyFxcWMyEyFgcRFgYjcy9ERC/nOCQjER0BITBEAQFEMCxELwJBMEQvLhdEL/4yL0Q=", 16 + "AAEAAP+aBBEDNgA+AAABFAYjIxMUBxUUBisFIiY9AjQmIyMiBh0CFAYrAiIiJwYiIyMiJjc1MjQ1NSMiJjQ3ATYzMhcBFgQOJBY5AQEqHh0GBzsrHioiGHMYIioeLDkBBAMBBAIcHiwBAToYIhIBzg4aFw8BzBcBahgi/t4KBB4eKioeLHQYIiIYdCweKgICKh7KBAJ+IDISAZQODP5qFA==", 17 + "AAEAAP/dA5sC+QAZAAATJjU1NDY3NhYXFzc2NhcWFhUVFAcBBiMiJ1ZWel09eCwWFSx4PV16Vv66FB0eFAEhUHULXpAQCiYsFhYsJgoQkF4LdVD+zxMT", 18 + "AAoAAP/YAyEC+AENARcBWgFlAW8BfQGIAbsBxAHNAAAAMhYXFhYyNjc2MzIXFhcWFxY3NjMyFhcWFxY3NjMyFxcHBxcWNzYXFgcGFxYzMhcWBw4CFAcUFQcUFxYWFRQGFhcWFgcGFBcWFxYGBwYUFxYGBw4CFhYXFgcGBwYVFBcWBwYjIgYXFgYnJgcGFxcHBiInJgYHBiMiJyYHBgcGBiMiJyYiBwYiJyYmBwYGJyYmJyYHBiImJyYmBwYiJjc3JyYHBicmNzYmIwYmNzY2Nzc0JyYmNTQ3NiYnJiY3NjQnJiY0Njc2NjQnJicmJjY3NjYnJjc3Mjc2NScmJyYnJjYXMjY1NCYmJyc3NhcWNzcnJjYzMhcWNjc2NjMyFxY3Njc2NjMyFxYyNzY3FyIHBhYzMjYnJgczBwYHBhUUMzIXFhYHBgcOAhUGFQciFhcWFxYXFhcWFjc2NzY3NjMzNzYvAiYnJjU0NzcnJicmJicnBwYGJyYnBwYGFxYyNzYmJyYFIgcGFBcWNicmBRcWBwYPAgYXFzM1NRcVMzY3NjU0JyYjBxUzMhcWFQcjIhUGFjMyNzYyFxYXFhUWFhcWNzY/AjY2Fxc2NjQjIiYnJicmJyYnJiMGIgcGFjMyNiclIgcGFjc2JyYBjQQICgcECAgIEQMJBgUCAwUGEBMDBgQEAwUDFBIFAwQEAQEEBRIZBQUGBQMCFxUFBwwBAgICARgSChoEFBcEExEUDwQECBATERMEGAoGCAQEBhEHAxUZCAsGAxcYBAUGChoVAgMBAQQEBhQSCgMECgQRFAUEBgcGBAUQEAgLDQwOCwoODQgFBg4CBRUPDAQEBAgTFAYIAQEEBREaBAYGBQQYGQYKAQQCARgSCg0NBBQXBBMQERIEBg4KCAIDCg0ECBEUBA0QBwQFEBcBAQICAgsIGRgCAgIBBAMFGhIFBAEBCAMFEhMIBAQEBgUREQQGCAcEBgQQDwoLCQQFCQcMDBARCg0HPgEPQjEfoJ8NJzACAykCBAQCAQEEAgIDGAgEAwggCg0BAQMCDBABAgIBHRsDBw4PAx8xFkQTCRQSEAkHEvoMDgcHGAgEBAcFAjAHBA0OFBQTBf3wBAsIAh0cAQMKBFOENjcIERsLLzEkJAECAXl5ARgBBBQZEAYEBQYBRCEnKiYSBgYGECAcARg/NhQLDgkLBQkQBimQEAQPChESCA4BVRAGCigLChEFAvgEEQsIBgkREA8FCAIBDA0KERYCAgkIBAQWFwIDBQYFBBkVAwIDBhkDBAQEAQEBAQcEAwQHBSQICAgMEREICwoFBggKDAgQEQwJBAQCCAgHGAYDAwQIAxAXBwMGFBkKBgQCAxYWBAQJCAQVHAwOAwQQEwYREBMVFBMCEA0GAgQmAgQPDAoSFgQJCQgXFgICBAYEBhgVBgEMFgQKAwMGBAQEBgQTEQgJCQwQEAgLCwQMBAkHBAoDAwkLDAgGCAgSFwcEAwQHAgIGBQQXDAEEBQEGCgQTBAcGBAICFxYICAkEFhEKDA0BARcSBBEPEw8ERQcKHiAKBScEESgaBAIDCjghJh4BBAIBAQEBBAECAhUjEQMJAgcIGBMBAQURFgcNDAMHCgYgIQY0IxAcBAETEwgDBBObARgMCwwIFAQEAgIIHAYKKAsDAwkYDQQNDRAjLg9eXgE4AQQHDhQHA4g0AgQqKwICGgUECAkVHAEHFAMDCAcKAx4iCgcGARwCBAwPJjAHDgQCwgMJIiIJAg0UFBIRDgQ=", 19 + }; 20 + 21 + /// Return deterministic font metrics for the PNG reference test. 22 + /// 23 + /// These are intentionally minimal: the rasterizer only needs cell geometry, 24 + /// face geometry, and icon heights for constraint calculations. 25 + fn testMetrics(width: u32, height: u32) @import("Metrics.zig") { 26 + return .{ 27 + .cell_width = width, 28 + .cell_height = height, 29 + .cell_baseline = 0, 30 + .underline_position = height, 31 + .underline_thickness = 1, 32 + .strikethrough_position = height / 2, 33 + .strikethrough_thickness = 1, 34 + .overline_position = 0, 35 + .overline_thickness = 1, 36 + .box_thickness = 1, 37 + .cursor_thickness = 1, 38 + .cursor_height = height, 39 + .icon_height = @floatFromInt(height), 40 + .icon_height_single = @floatFromInt(height), 41 + .face_width = @floatFromInt(width), 42 + .face_height = @floatFromInt(height), 43 + .face_y = 0, 44 + }; 45 + } 46 + 47 + /// Decode a base64-encoded glyf protocol payload into an owned outline. 48 + /// 49 + /// The payload is a complete simple-glyph `glyf` table entry. The returned 50 + /// outline owns decoded point and contour storage and must be deinitialized by 51 + /// the caller. 52 + fn decodeGlyfPayload(alloc: Allocator, payload: []const u8) !glyf.Glyf.Outline { 53 + const decoder = std.base64.standard.Decoder; 54 + const size = try decoder.calcSizeForSlice(payload); 55 + const data = try alloc.alloc(u8, size); 56 + defer alloc.free(data); 57 + 58 + try decoder.decode(data, payload); 59 + const entry = try glyf.Glyf.Entry.init(data); 60 + return try entry.decode(alloc); 61 + } 62 + 63 + /// Copy a tightly packed alpha bitmap into the alpha atlas at `dst_x`, `dst_y`. 64 + /// 65 + /// The destination rectangle must fit inside the atlas. This is a test helper, 66 + /// so it trusts the hardcoded atlas layout rather than clipping. 67 + fn blitBitmap(atlas: *z2d.Surface, bm: glyf_rasterize.Bitmap, dst_x: usize, dst_y: usize) void { 68 + const dst_width: usize = @intCast(atlas.getWidth()); 69 + const dst = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf); 70 + for (0..bm.height) |y| { 71 + const src_start = y * bm.width; 72 + const src_end = src_start + bm.width; 73 + const dst_start = (dst_y + y) * dst_width + dst_x; 74 + @memcpy(dst[dst_start .. dst_start + bm.width], bm.data[src_start..src_end]); 75 + } 76 + } 77 + 78 + /// Draw faint terminal-cell outlines into one row of the alpha atlas. 79 + /// 80 + /// The boxes make cell advance and placement behavior visible in the reference 81 + /// PNG without overpowering the rendered glyph coverage. 82 + fn drawCellBoxes(atlas: *z2d.Surface, y: usize, cell_width: usize, cell_height: usize) void { 83 + const width: usize = @intCast(atlas.getWidth()); 84 + const dst = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf); 85 + const alpha = 64; 86 + 87 + var x: usize = 0; 88 + while (x < width) : (x += cell_width) { 89 + const right = @min(x + cell_width - 1, width - 1); 90 + const bottom = y + cell_height - 1; 91 + 92 + for (x..right + 1) |px| { 93 + dst[y * width + px] = @max(dst[y * width + px], alpha); 94 + dst[bottom * width + px] = @max(dst[bottom * width + px], alpha); 95 + } 96 + for (y..bottom + 1) |py| { 97 + dst[py * width + x] = @max(dst[py * width + x], alpha); 98 + dst[py * width + right] = @max(dst[py * width + right], alpha); 99 + } 100 + } 101 + } 102 + 103 + /// Compare a generated atlas PNG against the checked-in reference image. 104 + /// 105 + /// On missing reference or mismatch, copy the generated PNG into the workspace 106 + /// as `glyf_rasterize_test.png`. On pixel mismatch, also write 107 + /// `glyf_rasterize_diff.png`, where red is reference-only coverage and green is 108 + /// newly generated coverage. Returns true when a difference was found. 109 + fn diffAtlas( 110 + alloc: Allocator, 111 + atlas: *z2d.Surface, 112 + generated_path: []const u8, 113 + ) !bool { 114 + const ref_path = "src/font/testdata/glyf_rasterize.png"; 115 + 116 + const generated_file = try std.fs.openFileAbsolute(generated_path, .{ .mode = .read_only }); 117 + defer generated_file.close(); 118 + const generated_bytes = try generated_file.readToEndAlloc(alloc, std.math.maxInt(usize)); 119 + defer alloc.free(generated_bytes); 120 + 121 + const cwd_absolute = try std.fs.cwd().realpathAlloc(alloc, "."); 122 + defer alloc.free(cwd_absolute); 123 + 124 + const ref_file = std.fs.cwd().openFile(ref_path, .{ .mode = .read_only }) catch |err| { 125 + log.err("Can't open reference file {s}: {}", .{ ref_path, err }); 126 + 127 + const test_path = try std.fmt.allocPrint(alloc, "{s}/glyf_rasterize_test.png", .{cwd_absolute}); 128 + defer alloc.free(test_path); 129 + try std.fs.copyFileAbsolute(generated_path, test_path, .{}); 130 + return true; 131 + }; 132 + defer ref_file.close(); 133 + const ref_bytes = try ref_file.readToEndAlloc(alloc, std.math.maxInt(usize)); 134 + defer alloc.free(ref_bytes); 135 + 136 + if (std.mem.eql(u8, generated_bytes, ref_bytes)) return false; 137 + 138 + const test_path = try std.fmt.allocPrint(alloc, "{s}/glyf_rasterize_test.png", .{cwd_absolute}); 139 + defer alloc.free(test_path); 140 + try std.fs.copyFileAbsolute(generated_path, test_path, .{}); 141 + 142 + const ref_rgba = try wuffs.png.decode(alloc, ref_bytes); 143 + defer alloc.free(ref_rgba.data); 144 + 145 + if (ref_rgba.width != atlas.getWidth() or ref_rgba.height != atlas.getHeight()) { 146 + log.err( 147 + "glyf rasterize visual output dimensions differ from reference: " ++ 148 + "test={s} ({d}x{d}), reference={s} ({d}x{d})", 149 + .{ test_path, atlas.getWidth(), atlas.getHeight(), ref_path, ref_rgba.width, ref_rgba.height }, 150 + ); 151 + return true; 152 + } 153 + 154 + var diff = try z2d.Surface.init( 155 + .image_surface_rgb, 156 + alloc, 157 + atlas.getWidth(), 158 + atlas.getHeight(), 159 + ); 160 + defer diff.deinit(alloc); 161 + 162 + const test_gray = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf); 163 + const diff_pix = diff.image_surface_rgb.buf; 164 + var differs = false; 165 + for (test_gray, 0..) |t, i| { 166 + const r = ref_rgba.data[i * 4]; 167 + if (t == r) { 168 + diff_pix[i].r = t / 3; 169 + diff_pix[i].g = t / 3; 170 + diff_pix[i].b = t / 3; 171 + } else { 172 + differs = true; 173 + diff_pix[i].r = r; 174 + diff_pix[i].g = t; 175 + } 176 + } 177 + 178 + if (!differs) { 179 + log.err( 180 + "generated glyf rasterize PNG bytes differ from reference but pixels match; " ++ 181 + "test={s}, reference={s}", 182 + .{ test_path, ref_path }, 183 + ); 184 + return true; 185 + } 186 + 187 + const diff_path = "./glyf_rasterize_diff.png"; 188 + try z2d.png_exporter.writeToPNGFile(diff, diff_path, .{}); 189 + log.err( 190 + "glyf rasterize visual output differs from reference: test={s}, reference={s}, diff={s}", 191 + .{ test_path, ref_path, diff_path }, 192 + ); 193 + 194 + return true; 195 + } 196 + 197 + test "glyf_rasterize: bubbletea glyph protocol examples match reference image" { 198 + const testing = std.testing; 199 + const alloc = testing.allocator; 200 + 201 + // The generated PNG is a visual atlas for reading placement behavior. 202 + // Each column below is one terminal cell. The five payloads are rendered in 203 + // order: branch, folder, home, heart, rust. 204 + // 205 + // ```text 206 + // columns: 0 1 2 3 4 5 6 7 8 9 207 + // ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐ 208 + // row 0 │B│F│H│♥│R│ │ │ │ │ │ narrow/default: one-cell bitmap stride 209 + // ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤ 210 + // row 1 │B │F │H │♥ │R │ width=2: same glyphs, two-cell bitmaps 211 + // ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤ 212 + // row 2 │ B│ F│ H│ ♥│ R│ width=2 + horizontal center alignment 213 + // ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤ 214 + // row 3 │B │F │H │♥ │R │ advance_width=2000: wider design box 215 + // └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘ 216 + // ``` 217 + // 218 + // The faint grid lines in the PNG are these cell boundaries. Rows 2 and 3 219 + // are the design-metric/placement regression checks: row 2 centers a 220 + // one-cell-wide design box inside two cells, while row 3 centers a two-cell 221 + // design box so the visible glyph returns to the start of each span. 222 + const cell_width = 20; 223 + const cell_height = 20; 224 + const columns = test_glyf_payloads.len; 225 + const narrow_stride_x = cell_width; 226 + const wide_stride_x = cell_width * 2; 227 + const row_count = 4; 228 + 229 + var atlas = try z2d.Surface.init( 230 + .image_surface_alpha8, 231 + alloc, 232 + @intCast(wide_stride_x * columns), 233 + cell_height * row_count, 234 + ); 235 + defer atlas.deinit(alloc); 236 + 237 + for (test_glyf_payloads, 0..) |payload, i| { 238 + var outline = try decodeGlyfPayload(alloc, payload); 239 + defer outline.deinit(alloc); 240 + 241 + var narrow = try glyf_rasterize.rasterize(alloc, outline, .{ 242 + .units_per_em = 1000, 243 + .advance_width = 1000, 244 + .line_height = 1000, 245 + }, .{ 246 + .grid_metrics = testMetrics(cell_width, cell_height), 247 + }); 248 + defer narrow.deinit(alloc); 249 + blitBitmap(&atlas, narrow, i * narrow_stride_x, 0); 250 + 251 + var wide = try glyf_rasterize.rasterize(alloc, outline, .{ 252 + .units_per_em = 1000, 253 + .advance_width = 1000, 254 + .line_height = 1000, 255 + }, .{ 256 + .grid_metrics = testMetrics(cell_width, cell_height), 257 + .cell_width = 2, 258 + }); 259 + defer wide.deinit(alloc); 260 + blitBitmap(&atlas, wide, i * wide_stride_x, cell_height); 261 + 262 + var centered = try glyf_rasterize.rasterize(alloc, outline, .{ 263 + .units_per_em = 1000, 264 + .advance_width = 1000, 265 + .line_height = 1000, 266 + }, .{ 267 + .grid_metrics = testMetrics(cell_width, cell_height), 268 + .cell_width = 2, 269 + .constraint_width = 2, 270 + .constraint = .{ .align_horizontal = .center }, 271 + }); 272 + defer centered.deinit(alloc); 273 + blitBitmap(&atlas, centered, i * wide_stride_x, cell_height * 2); 274 + 275 + var designed_wide = try glyf_rasterize.rasterize(alloc, outline, .{ 276 + .units_per_em = 1000, 277 + .advance_width = 2000, 278 + .line_height = 1000, 279 + }, .{ 280 + .grid_metrics = testMetrics(cell_width, cell_height), 281 + .cell_width = 2, 282 + .constraint_width = 2, 283 + .constraint = .{ .align_horizontal = .center }, 284 + }); 285 + defer designed_wide.deinit(alloc); 286 + blitBitmap(&atlas, designed_wide, i * wide_stride_x, cell_height * 3); 287 + } 288 + 289 + for (0..row_count) |row| drawCellBoxes(&atlas, row * cell_height, cell_width, cell_height); 290 + 291 + var dir = testing.tmpDir(.{}); 292 + defer dir.cleanup(); 293 + const tmp_dir = try dir.dir.realpathAlloc(alloc, "."); 294 + defer alloc.free(tmp_dir); 295 + 296 + const generated_path = try std.fmt.allocPrint(alloc, "{s}/glyf_rasterize.png", .{tmp_dir}); 297 + defer alloc.free(generated_path); 298 + try z2d.png_exporter.writeToPNGFile(atlas, generated_path, .{}); 299 + 300 + try testing.expect(!try diffAtlas(alloc, &atlas, generated_path)); 301 + }
+1
src/font/main.zig
··· 15 15 pub const DeferredFace = @import("DeferredFace.zig"); 16 16 pub const Face = face.Face; 17 17 pub const Glyph = @import("Glyph.zig"); 18 + pub const glyf_rasterize = @import("glyf_rasterize.zig"); 18 19 pub const Metrics = @import("Metrics.zig"); 19 20 pub const opentype = @import("opentype.zig"); 20 21 pub const shape = @import("shape.zig");
+446
src/font/opentype/glyf.zig
··· 1 1 const std = @import("std"); 2 + const Allocator = std.mem.Allocator; 2 3 const sfnt = @import("sfnt.zig"); 3 4 4 5 /// Glyph Data Table ··· 14 15 /// Field names are in camelCase to match names in spec. 15 16 pub const Glyf = struct { 16 17 data: []const u8, 18 + 19 + /// A decoded glyph outline. 20 + /// 21 + /// The `contours` slice is the list of end point indices and 22 + /// `points` owns all the points. Glyf guarantees that contour 23 + /// points are sequential so we can just store the end and calculate 24 + /// the points that way. Use the helpers to make it ergonomic. 25 + pub const Outline = struct { 26 + /// List of contour end points. Calculate the full list of 27 + /// points using points[prev...this+1] 28 + contours: []const sfnt.uint16, 29 + 30 + /// The backing storage of all points in the entry. 31 + points: []const Point, 32 + 33 + /// A single decoded point in a simple glyph contour. 34 + pub const Point = struct { 35 + x: i32, 36 + y: i32, 37 + on_curve: bool, 38 + }; 39 + 40 + /// Return the point slice for the contour at `index`. 41 + /// 42 + /// The returned slice references `points` and is invalidated when 43 + /// this outline is deinitialized. 44 + pub fn contour(self: Outline, index: usize) []const Point { 45 + const start = if (index == 0) 46 + 0 47 + else 48 + @as(usize, self.contours[index - 1]) + 1; 49 + const end = @as(usize, self.contours[index]) + 1; 50 + return self.points[start..end]; 51 + } 52 + 53 + /// Free all memory owned by this outline. Pass in the same 54 + /// allocator used for decoding. 55 + pub fn deinit(self: *Outline, alloc: Allocator) void { 56 + alloc.free(self.contours); 57 + alloc.free(self.points); 58 + self.* = undefined; 59 + } 60 + }; 17 61 18 62 /// https://learn.microsoft.com/en-us/typography/opentype/spec/glyf#table-organization 19 63 pub const Entry = struct { ··· 241 285 TooManyPoints, 242 286 }; 243 287 288 + /// Errors that can be returned from `Entry.decode()`. 289 + pub const DecodeError = SizeError || Allocator.Error || error{ 290 + /// Coordinate delta accumulation overflowed. 291 + CoordinateOverflow, 292 + }; 293 + 244 294 /// Determines the size (in bytes) of this entry. 245 295 /// 246 296 /// If the entry is valid, returns the number of bytes ··· 393 443 // No issues found, the glyf entry is valid, return its length. 394 444 return @sizeOf(Header) + fbs.pos; 395 445 } 446 + 447 + /// Decode this simple glyph entry into an owned outline. 448 + /// 449 + /// NOTE: Currently produces errors when given composite glyphs 450 + /// or any glyphs that have hinting instructions included. 451 + pub fn decode(self: Entry, alloc: Allocator) DecodeError!Glyf.Outline { 452 + // We only support simple glyphs. 453 + switch (self.entryType()) { 454 + .simple => {}, 455 + .composite => return error.CompositeNotSupported, 456 + } 457 + 458 + var fbs = std.io.fixedBufferStream(self.data); 459 + const reader = fbs.reader(); 460 + 461 + // A zero-contour glyph may be header-only. See size for the 462 + // reason for the hardcoded 2 here. 463 + const num_contours: usize = @intCast(self.header.numberOfContours); 464 + if (num_contours == 0 and self.data.len < 2) return .{ 465 + .points = &.{}, 466 + .contours = &.{}, 467 + }; 468 + 469 + // We now know our full amount of contour ending points. 470 + const end_points = try alloc.alloc(sfnt.uint16, num_contours); 471 + errdefer alloc.free(end_points); 472 + 473 + // If we have no contours, then the only possible remaining 474 + // field is instructionLength. Instructions are not supported. 475 + if (num_contours == 0) { 476 + const instructions_length = try reader.readInt(sfnt.uint16, .big); 477 + if (instructions_length > 0) return error.InstructionsNotSupported; 478 + return .{ .points = &.{}, .contours = end_points }; 479 + } 480 + 481 + // The number of points is determined by the final end point 482 + // entry since the entries have to be monotonic (something 483 + // we verify below). 484 + const point_count: usize = point_count: { 485 + var prev_end_point: isize = -1; 486 + 487 + // Go through the end points array and update our end_points 488 + // with the valid index. The final endpoint tells us our point 489 + // count, since endpoints are stored as inclusive point indices. 490 + for (0..end_points.len) |i| { 491 + const index = try reader.readInt(sfnt.uint16, .big); 492 + if (index <= prev_end_point) return error.EndPointsOutOfOrder; 493 + prev_end_point = index; 494 + end_points[i] = index; 495 + } 496 + 497 + // The final point tells us our point count. 498 + break :point_count @as(usize, end_points[end_points.len - 1]) + 1; 499 + }; 500 + 501 + // Instructions are not supported. 502 + const instructions_length = try reader.readInt(sfnt.uint16, .big); 503 + if (instructions_length > 0) return error.InstructionsNotSupported; 504 + 505 + // Allocate our points right away even though the next entries 506 + // are flags. We want to do this so that if the allocator is 507 + // a bump allocator, the flags free will actually free it. 508 + const points = try alloc.alloc(Glyf.Outline.Point, point_count); 509 + errdefer alloc.free(points); 510 + 511 + // This is EXTREMELY annoying but all the flags are separate 512 + // from the points so we have to do some allocation here since 513 + // its a dynamic amount and we need to save the values for later. 514 + // 515 + // Typical glyphs have small point counts, so use stack storage 516 + // first while still falling back to the caller's allocator for 517 + // unusually large outlines. 518 + var flags_stack = std.heap.stackFallback(4096, alloc); 519 + const flags_alloc = flags_stack.get(); 520 + const flags = try flags_alloc.alloc(SimpleFlags, point_count); 521 + defer flags_alloc.free(flags); 522 + { 523 + var point_i: usize = 0; 524 + while (point_i < point_count) { 525 + const flag: SimpleFlags = @bitCast(try reader.readByte()); 526 + flags[point_i] = flag; 527 + point_i += 1; 528 + 529 + if (flag.repeat) { 530 + const repeat_count: usize = try reader.readByte(); 531 + if (point_i + repeat_count > point_count) return error.TooManyPoints; 532 + 533 + for (0..repeat_count) |_| { 534 + flags[point_i] = flag; 535 + point_i += 1; 536 + } 537 + } 538 + } 539 + } 540 + 541 + // Go through x coordinate deltas 542 + var x: i32 = 0; 543 + for (flags, points) |flag, *point| { 544 + const dx: i32 = if (flag.x_short) short: { 545 + break :short if (flag.x_repeat_or_sign) 546 + @as(i32, try reader.readByte()) 547 + else 548 + -@as(i32, try reader.readByte()); 549 + } else if (!flag.x_repeat_or_sign) 550 + @as(i32, try reader.readInt(sfnt.int16, .big)) 551 + else 552 + 0; 553 + 554 + x = std.math.add( 555 + i32, 556 + x, 557 + dx, 558 + ) catch return error.CoordinateOverflow; 559 + point.x = x; 560 + } 561 + 562 + // Go through y coordinate deltas 563 + var y: i32 = 0; 564 + for (flags, points) |flag, *point| { 565 + const dy: i32 = if (flag.y_short) short: { 566 + break :short if (flag.y_repeat_or_sign) 567 + @as(i32, try reader.readByte()) 568 + else 569 + -@as(i32, try reader.readByte()); 570 + } else if (!flag.y_repeat_or_sign) 571 + @as(i32, try reader.readInt(sfnt.int16, .big)) 572 + else 573 + 0; 574 + 575 + y = std.math.add( 576 + i32, 577 + y, 578 + dy, 579 + ) catch return error.CoordinateOverflow; 580 + point.y = y; 581 + point.on_curve = flag.on_curve; 582 + } 583 + 584 + return .{ 585 + .points = points, 586 + .contours = end_points, 587 + }; 588 + } 396 589 }; 397 590 398 591 /// Initialize the table from the provided data. ··· 451 644 return .{ end_offset - start_offset, try glyf.entry(start_offset) }; 452 645 } 453 646 647 + fn testAppendInt( 648 + buf: *std.ArrayList(u8), 649 + alloc: Allocator, 650 + comptime T: type, 651 + value: T, 652 + ) !void { 653 + var bytes: [@sizeOf(T)]u8 = undefined; 654 + std.mem.writeInt(T, &bytes, value, .big); 655 + try buf.appendSlice(alloc, &bytes); 656 + } 657 + 658 + fn testAppendHeader( 659 + buf: *std.ArrayList(u8), 660 + alloc: Allocator, 661 + number_of_contours: i16, 662 + x_min: i16, 663 + y_min: i16, 664 + x_max: i16, 665 + y_max: i16, 666 + ) !void { 667 + try testAppendInt(buf, alloc, i16, number_of_contours); 668 + try testAppendInt(buf, alloc, i16, x_min); 669 + try testAppendInt(buf, alloc, i16, y_min); 670 + try testAppendInt(buf, alloc, i16, x_max); 671 + try testAppendInt(buf, alloc, i16, y_max); 672 + } 673 + 454 674 test "glyf" { 455 675 const testing = std.testing; 456 676 const alloc = testing.allocator; ··· 475 695 try testing.expect(glyph_A.entryType() == .simple); 476 696 try testing.expect(len_A >= try glyph_A.size()); 477 697 698 + var outline_A = try glyph_A.decode(alloc); 699 + defer outline_A.deinit(alloc); 700 + try testing.expectEqual(@as(usize, @intCast(glyph_A.header.numberOfContours)), outline_A.contours.len); 701 + try testing.expect(outline_A.points.len > 0); 702 + 478 703 // Glyph "Ĩ" is at index 265. 479 704 const len_Itilde, const glyph_Itilde = try getGlyph(font, 265); 480 705 try testing.expect(glyph_Itilde.entryType() == .simple); 481 706 try testing.expect(len_Itilde >= try glyph_Itilde.size()); 482 707 } 483 708 709 + test "glyf: decode triangle" { 710 + const testing = std.testing; 711 + const alloc = testing.allocator; 712 + 713 + var buf: std.ArrayList(u8) = .empty; 714 + defer buf.deinit(alloc); 715 + 716 + try testAppendHeader(&buf, alloc, 1, 100, 100, 900, 900); 717 + try testAppendInt(&buf, alloc, u16, 2); // endPtsOfContours[0] 718 + try testAppendInt(&buf, alloc, u16, 0); // instructionLength 719 + try buf.append(alloc, 0x01); // on curve 720 + try buf.append(alloc, 0x01); // on curve 721 + try buf.append(alloc, 0x01); // on curve 722 + try testAppendInt(&buf, alloc, i16, 500); 723 + try testAppendInt(&buf, alloc, i16, -400); 724 + try testAppendInt(&buf, alloc, i16, 800); 725 + try testAppendInt(&buf, alloc, i16, 900); 726 + try testAppendInt(&buf, alloc, i16, -800); 727 + try testAppendInt(&buf, alloc, i16, 0); 728 + 729 + const glyph = try Glyf.Entry.init(buf.items); 730 + var outline = try glyph.decode(alloc); 731 + defer outline.deinit(alloc); 732 + 733 + try testing.expectEqual(@as(i16, 100), glyph.header.xMin); 734 + try testing.expectEqual(@as(i16, 900), glyph.header.xMax); 735 + try testing.expectEqual(@as(usize, 1), outline.contours.len); 736 + try testing.expectEqual(@as(usize, 3), outline.points.len); 737 + const contour = outline.contour(0); 738 + try testing.expectEqual(@as(usize, 3), contour.len); 739 + try testing.expectEqual(Glyf.Outline.Point{ .x = 500, .y = 900, .on_curve = true }, contour[0]); 740 + try testing.expectEqual(Glyf.Outline.Point{ .x = 100, .y = 100, .on_curve = true }, contour[1]); 741 + try testing.expectEqual(Glyf.Outline.Point{ .x = 900, .y = 100, .on_curve = true }, contour[2]); 742 + } 743 + 744 + test "glyf: decode multiple contours" { 745 + const testing = std.testing; 746 + const alloc = testing.allocator; 747 + 748 + var buf: std.ArrayList(u8) = .empty; 749 + defer buf.deinit(alloc); 750 + 751 + try testAppendHeader(&buf, alloc, 2, 0, 0, 30, 10); 752 + try testAppendInt(&buf, alloc, u16, 1); // first contour ends at point 1 753 + try testAppendInt(&buf, alloc, u16, 3); // second contour ends at point 3 754 + try testAppendInt(&buf, alloc, u16, 0); // instructionLength 755 + for (0..4) |_| try buf.append(alloc, 0x01); // on curve 756 + for ([_]i16{ 0, 10, 10, 10 }) |dx| try testAppendInt(&buf, alloc, i16, dx); 757 + for ([_]i16{ 0, 0, 10, 0 }) |dy| try testAppendInt(&buf, alloc, i16, dy); 758 + 759 + const glyph = try Glyf.Entry.init(buf.items); 760 + var outline = try glyph.decode(alloc); 761 + defer outline.deinit(alloc); 762 + 763 + try testing.expectEqual(@as(usize, 2), outline.contours.len); 764 + try testing.expectEqual(@as(usize, 4), outline.points.len); 765 + try testing.expectEqual(@as(u16, 1), outline.contours[0]); 766 + try testing.expectEqual(@as(u16, 3), outline.contours[1]); 767 + try testing.expectEqual(@as(usize, 2), outline.contour(0).len); 768 + try testing.expectEqual(@as(usize, 2), outline.contour(1).len); 769 + try testing.expectEqual(outline.points[0..2].ptr, outline.contour(0).ptr); 770 + try testing.expectEqual(outline.points[2..4].ptr, outline.contour(1).ptr); 771 + } 772 + 773 + test "glyf: decode repeat and short vector flags" { 774 + const testing = std.testing; 775 + const alloc = testing.allocator; 776 + 777 + var buf: std.ArrayList(u8) = .empty; 778 + defer buf.deinit(alloc); 779 + 780 + try testAppendHeader(&buf, alloc, 1, 0, -16, 16, 0); 781 + try testAppendInt(&buf, alloc, u16, 3); // four points 782 + try testAppendInt(&buf, alloc, u16, 0); // instructionLength 783 + try buf.append(alloc, 0x01 | 0x02 | 0x04 | 0x08 | 0x10); // on, x short positive, y short negative, repeat 784 + try buf.append(alloc, 3); // repeat for the next three points 785 + for ([_]u8{ 1, 2, 4, 8 }) |dx| try buf.append(alloc, dx); 786 + for ([_]u8{ 1, 2, 4, 8 }) |dy| try buf.append(alloc, dy); 787 + 788 + const glyph = try Glyf.Entry.init(buf.items); 789 + var outline = try glyph.decode(alloc); 790 + defer outline.deinit(alloc); 791 + 792 + try testing.expectEqual(@as(usize, 4), outline.points.len); 793 + try testing.expectEqual(Glyf.Outline.Point{ .x = 1, .y = -1, .on_curve = true }, outline.points[0]); 794 + try testing.expectEqual(Glyf.Outline.Point{ .x = 3, .y = -3, .on_curve = true }, outline.points[1]); 795 + try testing.expectEqual(Glyf.Outline.Point{ .x = 7, .y = -7, .on_curve = true }, outline.points[2]); 796 + try testing.expectEqual(Glyf.Outline.Point{ .x = 15, .y = -15, .on_curve = true }, outline.points[3]); 797 + } 798 + 799 + test "glyf: decode off curve and same coordinate flags" { 800 + const testing = std.testing; 801 + const alloc = testing.allocator; 802 + 803 + var buf: std.ArrayList(u8) = .empty; 804 + defer buf.deinit(alloc); 805 + 806 + try testAppendHeader(&buf, alloc, 1, 0, 0, 7, 9); 807 + try testAppendInt(&buf, alloc, u16, 1); // two points 808 + try testAppendInt(&buf, alloc, u16, 0); // instructionLength 809 + try buf.append(alloc, 0x10 | 0x20); // off curve, x same, y same 810 + try buf.append(alloc, 0x01 | 0x02 | 0x04 | 0x10 | 0x20); // on curve, short positive x/y 811 + try buf.append(alloc, 7); // x delta 812 + try buf.append(alloc, 9); // y delta 813 + 814 + const glyph = try Glyf.Entry.init(buf.items); 815 + var outline = try glyph.decode(alloc); 816 + defer outline.deinit(alloc); 817 + 818 + try testing.expectEqual(Glyf.Outline.Point{ .x = 0, .y = 0, .on_curve = false }, outline.points[0]); 819 + try testing.expectEqual(Glyf.Outline.Point{ .x = 7, .y = 9, .on_curve = true }, outline.points[1]); 820 + } 821 + 822 + test "glyf: decode one-point contour" { 823 + const testing = std.testing; 824 + const alloc = testing.allocator; 825 + 826 + var buf: std.ArrayList(u8) = .empty; 827 + defer buf.deinit(alloc); 828 + 829 + try testAppendHeader(&buf, alloc, 1, 0, 0, 0, 0); 830 + try testAppendInt(&buf, alloc, u16, 0); // endPtsOfContours[0] 831 + try testAppendInt(&buf, alloc, u16, 0); // instructionLength 832 + try buf.append(alloc, 0x01 | 0x10 | 0x20); // on curve, x same, y same 833 + 834 + const glyph = try Glyf.Entry.init(buf.items); 835 + var outline = try glyph.decode(alloc); 836 + defer outline.deinit(alloc); 837 + 838 + try testing.expectEqual(@as(usize, 1), outline.points.len); 839 + try testing.expectEqual(@as(usize, 1), outline.contours.len); 840 + try testing.expectEqual(@as(u16, 0), outline.contours[0]); 841 + try testing.expectEqual(Glyf.Outline.Point{ .x = 0, .y = 0, .on_curve = true }, outline.contour(0)[0]); 842 + } 843 + 844 + test "glyf: decode contour ending at max point index" { 845 + const testing = std.testing; 846 + const alloc = testing.allocator; 847 + 848 + var buf: std.ArrayList(u8) = .empty; 849 + defer buf.deinit(alloc); 850 + 851 + try testAppendHeader(&buf, alloc, 1, 0, 0, 0, 0); 852 + try testAppendInt(&buf, alloc, u16, std.math.maxInt(u16)); // 65536 points 853 + try testAppendInt(&buf, alloc, u16, 0); // instructionLength 854 + 855 + const flag = 0x01 | 0x10 | 0x20; // on curve, x same, y same 856 + var remaining: usize = @as(usize, std.math.maxInt(u16)) + 1; 857 + while (remaining > 0) { 858 + const run = @min(remaining, 256); 859 + if (run == 1) { 860 + try buf.append(alloc, flag); 861 + } else { 862 + try buf.append(alloc, flag | 0x08); // repeat 863 + try buf.append(alloc, @intCast(run - 1)); 864 + } 865 + remaining -= run; 866 + } 867 + 868 + const glyph = try Glyf.Entry.init(buf.items); 869 + var outline = try glyph.decode(alloc); 870 + defer outline.deinit(alloc); 871 + 872 + try testing.expectEqual(@as(usize, 65536), outline.points.len); 873 + try testing.expectEqual(@as(usize, 65536), outline.contour(0).len); 874 + } 875 + 484 876 test "glyf: reject glyphs with instructions and composite glyphs" { 485 877 const testing = std.testing; 486 878 const alloc = testing.allocator; ··· 496 888 Glyf.Entry.SizeError.InstructionsNotSupported, 497 889 glyph_notdef.size(), 498 890 ); 891 + try testing.expectError( 892 + Glyf.Entry.DecodeError.InstructionsNotSupported, 893 + glyph_notdef.decode(alloc), 894 + ); 499 895 500 896 // Glyph "Á" is at index 2. 501 897 const len_Aacute, const glyph_Aacute = try getGlyph(font, 2); ··· 505 901 Glyf.Entry.SizeError.CompositeNotSupported, 506 902 glyph_Aacute.size(), 507 903 ); 904 + try testing.expectError( 905 + Glyf.Entry.DecodeError.CompositeNotSupported, 906 + glyph_Aacute.decode(alloc), 907 + ); 508 908 } 509 909 510 910 test "glyf: reject truncated" { ··· 522 922 // it before the full length (which is 228 bytes). 523 923 glyph_nul.data = glyph_nul.data[0 .. 227 - @sizeOf(Glyf.Entry.Header)]; 524 924 try testing.expectError(Glyf.Entry.SizeError.EndOfStream, glyph_nul.size()); 925 + try testing.expectError(Glyf.Entry.DecodeError.EndOfStream, glyph_nul.decode(alloc)); 525 926 } 526 927 527 928 test "glyf: reject endpoints out of order" { ··· 544 945 // copied, we can just const cast it back to mutable lol. 545 946 std.mem.bytesAsSlice(u16, @as([]u8, @constCast(glyph_nul.data)))[3] = 0; 546 947 try testing.expectError(Glyf.Entry.SizeError.EndPointsOutOfOrder, glyph_nul.size()); 948 + try testing.expectError( 949 + Glyf.Entry.DecodeError.EndPointsOutOfOrder, 950 + glyph_nul.decode(alloc), 951 + ); 547 952 } 548 953 549 954 test "glyf: reject too many points" { ··· 568 973 @as([]u8, @constCast(glyph_nul.data))[107] |= 0x08; 569 974 @as([]u8, @constCast(glyph_nul.data))[108] = 0xFF; 570 975 try testing.expectError(Glyf.Entry.SizeError.TooManyPoints, glyph_nul.size()); 976 + try testing.expectError(Glyf.Entry.DecodeError.TooManyPoints, glyph_nul.decode(alloc)); 571 977 } 572 978 573 979 test "glyf: zero-contour glyph can be header-only" { 574 980 const testing = std.testing; 981 + const alloc = testing.allocator; 575 982 576 983 const header: Glyf.Entry.Header = .{ 577 984 .numberOfContours = 0, ··· 582 989 }; 583 990 const glyph = try Glyf.Entry.init(std.mem.asBytes(&header)); 584 991 try testing.expectEqual(@sizeOf(Glyf.Entry.Header), try glyph.size()); 992 + 993 + var outline = try glyph.decode(alloc); 994 + defer outline.deinit(alloc); 995 + try testing.expectEqual(@as(usize, 0), outline.points.len); 996 + try testing.expectEqual(@as(usize, 0), outline.contours.len); 997 + } 998 + 999 + test "glyf: zero-contour glyph can include instruction length" { 1000 + const testing = std.testing; 1001 + const alloc = testing.allocator; 1002 + 1003 + var buf: std.ArrayList(u8) = .empty; 1004 + defer buf.deinit(alloc); 1005 + 1006 + try testAppendHeader(&buf, alloc, 0, 0, 0, 0, 0); 1007 + try testAppendInt(&buf, alloc, u16, 0); // instructionLength 1008 + 1009 + const glyph = try Glyf.Entry.init(buf.items); 1010 + try testing.expectEqual(@sizeOf(Glyf.Entry.Header) + 2, try glyph.size()); 1011 + 1012 + var outline = try glyph.decode(alloc); 1013 + defer outline.deinit(alloc); 1014 + try testing.expectEqual(@as(usize, 0), outline.points.len); 1015 + try testing.expectEqual(@as(usize, 0), outline.contours.len); 1016 + } 1017 + 1018 + test "glyf: zero-contour glyph rejects instructions" { 1019 + const testing = std.testing; 1020 + const alloc = testing.allocator; 1021 + 1022 + var buf: std.ArrayList(u8) = .empty; 1023 + defer buf.deinit(alloc); 1024 + 1025 + try testAppendHeader(&buf, alloc, 0, 0, 0, 0, 0); 1026 + try testAppendInt(&buf, alloc, u16, 1); // instructionLength 1027 + 1028 + const glyph = try Glyf.Entry.init(buf.items); 1029 + try testing.expectError(Glyf.Entry.SizeError.InstructionsNotSupported, glyph.size()); 1030 + try testing.expectError(Glyf.Entry.DecodeError.InstructionsNotSupported, glyph.decode(alloc)); 585 1031 }
src/font/testdata/glyf_rasterize.png

This is a binary file and will not be displayed.