lib/termtex/src/image.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

   1 const std = @import("std");
   2 const filigree = @import("filigree");
   3 const ast = @import("ast.zig");
   4 const operator = @import("operator.zig");
   5 const parse = @import("parse.zig");
   6 
   7 pub const Color = filigree.render.Color;
   8 
   9 pub const Options = struct {
  10     font_bytes: []const u8,
  11     fallback_font_bytes: []const []const u8 = &.{},
  12     font_size: i32 = 24,
  13     /// Output storage acquired once per image and reused for every text run.
  14     shaping_output: filigree.Output.Limits = .{
  15         .max_glyphs = 4096,
  16         .max_ligature_carets = 4096,
  17     },
  18     padding_x: u32 = 4,
  19     padding_y: u32 = 4,
  20     foreground: Color = .{},
  21     background: Color = .{ .a = 0 },
  22 };
  23 
  24 pub const Image = struct {
  25     allocator: std.mem.Allocator,
  26     pixels: []u8,
  27     width: u32,
  28     height: u32,
  29     stride: u32,
  30     baseline: u32,
  31 
  32     pub fn deinit(self: *Image) void {
  33         self.allocator.free(self.pixels);
  34         self.* = undefined;
  35     }
  36 };
  37 
  38 const Style = struct {
  39     font_size: i32,
  40 };
  41 
  42 const Metrics = struct {
  43     ascender: i32,
  44     descender: i32,
  45     line_gap: i32,
  46     line_height: i32,
  47 };
  48 
  49 const Box = struct {
  50     width: i32,
  51     height: i32,
  52     baseline: i32,
  53     commands: []const Command,
  54     class: MathClass = .ordinary,
  55 };
  56 
  57 const MathClass = enum {
  58     ordinary,
  59     operator,
  60     binary,
  61     relation,
  62     open,
  63     close,
  64     punctuation,
  65     inner,
  66     spacing,
  67 };
  68 
  69 const Command = union(enum) {
  70     text: Text,
  71     rect: Rect,
  72     line: Line,
  73     glyph: Glyph,
  74 };
  75 
  76 const Text = struct {
  77     x: i32,
  78     y: i32,
  79     value: []const u8,
  80     font_size: i32,
  81 };
  82 
  83 const Rect = struct {
  84     x: i32,
  85     y: i32,
  86     width: i32,
  87     height: i32,
  88 };
  89 
  90 const Line = struct {
  91     x0: i32,
  92     y0: i32,
  93     x1: i32,
  94     y1: i32,
  95     width: i32,
  96 };
  97 
  98 const Glyph = struct {
  99     x: i32,
 100     y: i32,
 101     glyph_id: u32,
 102     font_size: i32,
 103 };
 104 
 105 const GlyphInkBounds = struct {
 106     left: i32,
 107     top: i32,
 108     right: i32,
 109     bottom: i32,
 110 };
 111 
 112 const MathRadicalSign = struct {
 113     width: i32,
 114     height: i32,
 115     commands: []const Command,
 116 };
 117 
 118 const Context = struct {
 119     allocator: std.mem.Allocator,
 120     scratch: std.mem.Allocator,
 121     options: Options,
 122     font: filigree.Font,
 123     fallback_fonts: []const filigree.Font,
 124     shaping_output: *filigree.Output,
 125 
 126     const layoutExpr = layoutExprImpl;
 127     const layoutText = layoutTextImpl;
 128     const layoutOperator = layoutOperatorImpl;
 129     const layoutSpace = layoutSpaceImpl;
 130     const layoutRow = layoutRowImpl;
 131     const layoutFraction = layoutFractionImpl;
 132     const layoutSqrt = layoutSqrtImpl;
 133     const layoutScripts = layoutScriptsImpl;
 134     const layoutLimits = layoutLimitsImpl;
 135     const layoutMathHorizontalArrow = layoutMathHorizontalArrowImpl;
 136     const layoutStretchArrow = layoutStretchArrowImpl;
 137     const layoutAccent = layoutAccentImpl;
 138     const layoutNativeAccent = layoutNativeAccentImpl;
 139     const layoutAnnotation = layoutAnnotationImpl;
 140     const layoutStack = layoutStackImpl;
 141     const layoutBrace = layoutBraceImpl;
 142     const layoutBoxed = layoutBoxedImpl;
 143     const layoutDelimited = layoutDelimitedImpl;
 144     const layoutDelimiter = layoutDelimiterImpl;
 145     const layoutDelimiterShape = layoutDelimiterShapeImpl;
 146     const layoutGrid = layoutGridImpl;
 147     const paint = paintImpl;
 148     const fontMetrics = fontMetricsImpl;
 149     const fallbackCandidates = fallbackCandidatesImpl;
 150     const makeBox = makeBoxImpl;
 151     const boxFromList = boxFromListImpl;
 152 };
 153 
 154 pub fn render(allocator: std.mem.Allocator, source: []const u8, options: Options) !Image {
 155     var arena = std.heap.ArenaAllocator.init(allocator);
 156     defer arena.deinit();
 157     const expr = try parse.parse(arena.allocator(), source);
 158     return renderExpr(allocator, arena.allocator(), expr, options);
 159 }
 160 
 161 pub fn renderExpr(
 162     allocator: std.mem.Allocator,
 163     scratch: std.mem.Allocator,
 164     expr: *const ast.Expr,
 165     options: Options,
 166 ) !Image {
 167     try validateOptions(options);
 168     var font = filigree.Font.initFromBytes(options.font_bytes.ptr, options.font_bytes.len) orelse return error.InvalidFont;
 169     defer font.deinit();
 170     font.setScale(@floatFromInt(options.font_size), 72);
 171     const fallback_fonts = try loadFallbackFonts(allocator, options.fallback_font_bytes);
 172     defer unloadFallbackFonts(allocator, fallback_fonts);
 173     var shaping_output = try filigree.Output.init(allocator, options.shaping_output);
 174     defer shaping_output.deinit(allocator);
 175     var context = Context{
 176         .allocator = allocator,
 177         .scratch = scratch,
 178         .options = options,
 179         .font = font,
 180         .fallback_fonts = fallback_fonts,
 181         .shaping_output = &shaping_output,
 182     };
 183     const box = try context.layoutExpr(expr, .{ .font_size = options.font_size });
 184     const width = try imageDimension(box.width, options.padding_x);
 185     const height = try imageDimension(box.height, options.padding_y);
 186     const stride = try std.math.mul(u32, width, 4);
 187     const pixels = try allocator.alloc(u8, try std.math.mul(usize, stride, height));
 188     errdefer allocator.free(pixels);
 189     fill(pixels, width, height, stride, options.background);
 190 
 191     const canvas = try filigree.render.Canvas.init(pixels, width, height, stride);
 192     try context.paint(canvas, box.commands, @intCast(options.padding_x), @intCast(options.padding_y));
 193 
 194     return .{
 195         .allocator = allocator,
 196         .pixels = pixels,
 197         .width = width,
 198         .height = height,
 199         .stride = stride,
 200         .baseline = @intCast(@max(0, box.baseline + @as(i32, @intCast(options.padding_y)))),
 201     };
 202 }
 203 
 204 fn layoutExprImpl(context: *Context, expr: *const ast.Expr, style: Style) anyerror!Box {
 205     return switch (expr.*) {
 206         .row => |items| try context.layoutRow(items, style),
 207         .text => |value| try layoutTextWithDisplayOperator(context, value, style),
 208         .operator => |value| try context.layoutOperator(value, style),
 209         .space => |value| try context.layoutSpace(value, style),
 210         .fraction => |value| try context.layoutFraction(value, style),
 211         .sqrt => |value| try context.layoutSqrt(value, style),
 212         .scripts => |value| try context.layoutScripts(value, style),
 213         .accent => |value| try context.layoutAccent(value, style),
 214         .annotation => |value| try context.layoutAnnotation(value, style),
 215         .delimited => |value| try context.layoutDelimited(value, style),
 216         .grid => |value| try context.layoutGrid(value, style),
 217     };
 218 }
 219 
 220 fn layoutTextWithDisplayOperator(context: *Context, value: []const u8, style: Style) anyerror!Box {
 221     if (!operator.displayOperatorText(value)) return context.layoutText(value, style);
 222     const min_height = displayOperatorMinHeight(context, style) orelse return context.layoutText(value, style);
 223     const nominal = try context.layoutText(value, style);
 224     if (nominal.height >= min_height) return nominal;
 225     var out = try context.layoutText(value, delimiterStyle(context, min_height, style));
 226     out.class = .operator;
 227     return out;
 228 }
 229 
 230 fn layoutTextImpl(context: *Context, value: []const u8, style: Style) anyerror!Box {
 231     const metrics = context.fontMetrics(style.font_size);
 232     if (value.len == 0) return context.makeBox(0, metrics.line_height, metrics.ascender, &.{});
 233     var shaped_font = context.font;
 234     shaped_font.setScale(@floatFromInt(style.font_size), 72);
 235     const fallback_candidates = try context.fallbackCandidates(style.font_size);
 236     const shaped = if (fallback_candidates.len == 0)
 237         try filigree.measureUtf8(context.allocator, context.shaping_output, &shaped_font, value)
 238     else
 239         try filigree.measureFallbackUtf8(
 240             context.allocator,
 241             context.shaping_output,
 242             &shaped_font,
 243             fallback_candidates,
 244             value,
 245         );
 246     var command = [_]Command{.{ .text = .{
 247         .x = 0,
 248         .y = 0,
 249         .value = value,
 250         .font_size = style.font_size,
 251     } }};
 252     var box = try context.makeBox(@max(1, shaped.advance_x), metrics.line_height, metrics.ascender, &command);
 253     box.class = mathClassForText(value);
 254     return box;
 255 }
 256 
 257 fn layoutOperatorImpl(context: *Context, value: ast.Operator, style: Style) anyerror!Box {
 258     var out = try context.layoutExpr(value.body, style);
 259     out.class = .operator;
 260     return out;
 261 }
 262 
 263 fn layoutSpaceImpl(context: *Context, value: ast.Space, style: Style) anyerror!Box {
 264     const metrics = context.fontMetrics(style.font_size);
 265     var box = try context.makeBox(spacePixels(value, style), metrics.line_height, metrics.ascender, &.{});
 266     box.class = .spacing;
 267     return box;
 268 }
 269 
 270 fn layoutRowImpl(context: *Context, items: []const *ast.Expr, style: Style) anyerror!Box {
 271     if (items.len == 0) {
 272         const metrics = context.fontMetrics(style.font_size);
 273         return context.makeBox(0, metrics.line_height, metrics.ascender, &.{});
 274     }
 275     const children = try context.scratch.alloc(Box, items.len);
 276     var baseline: i32 = 0;
 277     var descent: i32 = 0;
 278     for (items, 0..) |item, index| {
 279         children[index] = try context.layoutExpr(item, style);
 280         baseline = @max(baseline, children[index].baseline);
 281         descent = @max(descent, children[index].height - children[index].baseline);
 282     }
 283     var width_cursor: i32 = 0;
 284     var min_x: i32 = 0;
 285     var max_x: i32 = 0;
 286     for (children, 0..) |child, index| {
 287         if (index != 0) width_cursor += mathClassSpace(children, index, style);
 288         min_x = @min(min_x, width_cursor);
 289         max_x = @max(max_x, width_cursor + child.width);
 290         width_cursor += child.width;
 291         min_x = @min(min_x, width_cursor);
 292         max_x = @max(max_x, width_cursor);
 293     }
 294     var commands: std.ArrayListUnmanaged(Command) = .empty;
 295     var x: i32 = -min_x;
 296     for (children, 0..) |child, index| {
 297         if (index != 0) x += mathClassSpace(children, index, style);
 298         const y = baseline - child.baseline;
 299         try appendCommands(context.scratch, &commands, child.commands, x, y);
 300         x += child.width;
 301     }
 302     return context.boxFromList(max_x - min_x, baseline + descent, baseline, &commands);
 303 }
 304 
 305 fn layoutFractionImpl(context: *Context, value: ast.Fraction, style: Style) anyerror!Box {
 306     const numerator = try context.layoutExpr(value.numerator, style);
 307     const denominator = try context.layoutExpr(value.denominator, style);
 308     const side = @max(2, @divTrunc(style.font_size, 4));
 309     const numerator_gap = fractionNumeratorGap(context, style, value.style);
 310     const denominator_gap = fractionDenominatorGap(context, style, value.style);
 311     const rule = fractionRuleWidth(context, style);
 312     const inner = @max(numerator.width, denominator.width);
 313     const width = inner + side * 2;
 314     if (mathConstants(context) != null) return try layoutMathFraction(context, numerator, denominator, width, rule, numerator_gap, denominator_gap, style, value.style);
 315     const rule_y = numerator.height + numerator_gap;
 316     const denominator_y = rule_y + rule + denominator_gap;
 317     var commands: std.ArrayListUnmanaged(Command) = .empty;
 318     try appendCommands(context.scratch, &commands, numerator.commands, centered(width, numerator.width), 0);
 319     try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = rule_y, .width = width, .height = rule } });
 320     try appendCommands(context.scratch, &commands, denominator.commands, centered(width, denominator.width), denominator_y);
 321     return context.boxFromList(width, denominator_y + denominator.height, rule_y + rule, &commands);
 322 }
 323 
 324 fn layoutMathFraction(context: *Context, numerator: Box, denominator: Box, width: i32, rule: i32, numerator_gap: i32, denominator_gap: i32, style: Style, fraction_style: ast.FractionStyle) anyerror!Box {
 325     const rule_y = -mathAxisHeight(context, style) - @divTrunc(rule, 2);
 326     var numerator_y = -fractionNumeratorShiftUp(context, style, fraction_style) - numerator.baseline;
 327     const numerator_current_gap = rule_y - (numerator_y + numerator.height);
 328     if (numerator_current_gap < numerator_gap) numerator_y -= numerator_gap - numerator_current_gap;
 329     var denominator_y = fractionDenominatorShiftDown(context, style, fraction_style) - denominator.baseline;
 330     const denominator_current_gap = denominator_y - (rule_y + rule);
 331     if (denominator_current_gap < denominator_gap) denominator_y += denominator_gap - denominator_current_gap;
 332     const min_y = @min(numerator_y, rule_y);
 333     const max_y = @max(denominator_y + denominator.height, rule_y + rule);
 334     const y_shift = if (min_y < 0) -min_y else 0;
 335     var commands: std.ArrayListUnmanaged(Command) = .empty;
 336     try appendCommands(context.scratch, &commands, numerator.commands, centered(width, numerator.width), numerator_y + y_shift);
 337     try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = rule_y + y_shift, .width = width, .height = rule } });
 338     try appendCommands(context.scratch, &commands, denominator.commands, centered(width, denominator.width), denominator_y + y_shift);
 339     return context.boxFromList(width, max_y - min_y, y_shift, &commands);
 340 }
 341 
 342 fn layoutSqrtImpl(context: *Context, value: ast.Radical, style: Style) anyerror!Box {
 343     const body = try context.layoutExpr(value.body, style);
 344     const rooted = try layoutSqrtBody(context, body, style);
 345     const index_expr = value.index orelse return rooted;
 346     const index = try context.layoutExpr(index_expr, scriptScriptStyle(context, style));
 347     return try layoutRootDegree(context, rooted, index, style);
 348 }
 349 
 350 fn layoutSqrtBody(context: *Context, body: Box, style: Style) anyerror!Box {
 351     const rule = radicalRuleWidth(context, style);
 352     const clearance = radicalClearance(context, style);
 353     const extra_ascender = radicalExtraAscender(context, style);
 354     const rule_y = extra_ascender;
 355     const body_y = rule_y + rule + clearance;
 356     const target_height = body.height + clearance + rule;
 357     if (try layoutMathRadicalSign(context, target_height, style)) |radical| {
 358         const body_x = radical.width;
 359         const height = @max(body_y + body.height, rule_y + radical.height);
 360         var commands: std.ArrayListUnmanaged(Command) = .empty;
 361         try appendCommands(context.scratch, &commands, radical.commands, 0, rule_y);
 362         try commands.append(context.scratch, .{ .rect = .{
 363             .x = body_x,
 364             .y = rule_y,
 365             .width = body.width,
 366             .height = rule,
 367         } });
 368         try appendCommands(context.scratch, &commands, body.commands, body_x, body_y);
 369         return context.boxFromList(body_x + body.width, height, body_y + body.baseline, &commands);
 370     }
 371     const radical_width = radicalWidth(context, target_height, style);
 372     const body_gap = @max(2, @divTrunc(style.font_size, 8));
 373     const body_x = radical_width + body_gap;
 374     const radical_x0 = 0;
 375     const radical_x1 = @max(rule * 2, @divTrunc(radical_width, 3));
 376     const radical_x2 = body_x - body_gap;
 377     const radical_y0 = body_y + @divTrunc(body.height * 3, 5);
 378     const radical_y1 = body_y + body.height - rule;
 379     const radical_y2 = rule_y + rule;
 380     var commands: std.ArrayListUnmanaged(Command) = .empty;
 381     try commands.append(context.scratch, .{ .line = .{ .x0 = radical_x0, .y0 = radical_y0, .x1 = radical_x1, .y1 = radical_y1, .width = rule } });
 382     try commands.append(context.scratch, .{ .line = .{ .x0 = radical_x1, .y0 = radical_y1, .x1 = radical_x2, .y1 = radical_y2, .width = rule } });
 383     try commands.append(context.scratch, .{ .rect = .{
 384         .x = radical_x2,
 385         .y = rule_y,
 386         .width = body.width + body_gap,
 387         .height = rule,
 388     } });
 389     try appendCommands(context.scratch, &commands, body.commands, body_x, body_y);
 390     return context.boxFromList(body_x + body.width, body_y + body.height, body_y + body.baseline, &commands);
 391 }
 392 
 393 fn layoutRootDegree(context: *Context, rooted: Box, degree: Box, style: Style) anyerror!Box {
 394     const before = @max(0, radicalKernBeforeDegree(context, style));
 395     const after = @max(-degree.width, radicalKernAfterDegree(context, style));
 396     const radical_height = @max(1, rooted.height);
 397     const raise = @divTrunc(@as(i64, radical_height) * radicalDegreeBottomRaisePercent(context) + 50, 100);
 398     const degree_y = radical_height - @as(i32, @intCast(raise)) - degree.height;
 399     const min_y = @min(0, degree_y);
 400     const max_y = @max(rooted.height, degree_y + degree.height);
 401     const rooted_x = before + degree.width + after;
 402     const width = @max(rooted_x + rooted.width, before + degree.width);
 403     var commands: std.ArrayListUnmanaged(Command) = .empty;
 404     try appendCommands(context.scratch, &commands, degree.commands, before, degree_y - min_y);
 405     try appendCommands(context.scratch, &commands, rooted.commands, rooted_x, -min_y);
 406     var out = try context.boxFromList(width, max_y - min_y, rooted.baseline - min_y, &commands);
 407     out.class = rooted.class;
 408     return out;
 409 }
 410 
 411 fn layoutMathRadicalSign(context: *Context, target_height: i32, style: Style) anyerror!?MathRadicalSign {
 412     const glyph_id = context.font.face.glyphId(0x221a);
 413     if (glyph_id == 0) return null;
 414     const target = pixelHeightToDesignUnits(context, target_height, style);
 415     if (context.font.face.mathVerticalVariant(glyph_id, target)) |variant| {
 416         if (variant.advance_measurement >= target) return try layoutMathRadicalGlyph(context, variant.glyph_id, variant.advance_measurement, target_height, style);
 417     }
 418     return try layoutMathRadicalAssembly(context, glyph_id, target, target_height, style);
 419 }
 420 
 421 fn layoutMathRadicalGlyph(context: *Context, glyph_id: u32, advance_measurement: u16, target_height: i32, style: Style) anyerror!?MathRadicalSign {
 422     const bounds = (try glyphInkBounds(context, glyph_id, style)) orelse return null;
 423     const ink_width = @max(1, bounds.right - bounds.left);
 424     const ink_height = @max(1, bounds.bottom - bounds.top);
 425     const advance_height = designUnitsToPixelsCeil(context, advance_measurement, style);
 426     const height = @max(target_height, @max(ink_height, advance_height));
 427     var commands = [_]Command{.{ .glyph = .{
 428         .x = -bounds.left,
 429         .y = -bounds.top,
 430         .glyph_id = glyph_id,
 431         .font_size = style.font_size,
 432     } }};
 433     const owned = try context.scratch.dupe(Command, &commands);
 434     return .{ .width = ink_width, .height = height, .commands = owned };
 435 }
 436 
 437 fn layoutMathRadicalAssembly(context: *Context, glyph_id: u32, target: u16, target_height: i32, style: Style) anyerror!?MathRadicalSign {
 438     const assembly = (try context.font.face.mathVerticalAssemblyAlloc(context.scratch, glyph_id, target)) orelse return null;
 439     if (assembly.parts.len == 0 or assembly.advance_measurement < target) return null;
 440     const assembly_height = @max(1, designUnitsToPixelsCeil(context, assembly.advance_measurement, style));
 441     const bounds = try context.scratch.alloc(GlyphInkBounds, assembly.parts.len);
 442     var left_bound: i32 = std.math.maxInt(i32);
 443     var right_bound: i32 = std.math.minInt(i32);
 444     var height = @max(target_height, assembly_height);
 445     for (assembly.parts, 0..) |part, index| {
 446         bounds[index] = (try glyphInkBounds(context, part.glyph_id, style)) orelse return null;
 447         left_bound = @min(left_bound, bounds[index].left);
 448         right_bound = @max(right_bound, bounds[index].right);
 449         const part_end = @as(u32, part.advance_offset) + @as(u32, part.full_advance);
 450         if (part_end > assembly.advance_measurement) return null;
 451         const part_top_design: u16 = @intCast(@as(u32, assembly.advance_measurement) - part_end);
 452         const part_top = designUnitsToPixelsFloor(context, part_top_design, style);
 453         height = @max(height, part_top + @max(1, bounds[index].bottom - bounds[index].top));
 454     }
 455     if (left_bound >= right_bound) return null;
 456 
 457     var commands: std.ArrayListUnmanaged(Command) = .empty;
 458     for (assembly.parts, 0..) |part, index| {
 459         const part_end = @as(u32, part.advance_offset) + @as(u32, part.full_advance);
 460         const part_top_design: u16 = @intCast(@as(u32, assembly.advance_measurement) - part_end);
 461         const part_top = designUnitsToPixelsFloor(context, part_top_design, style);
 462         try commands.append(context.scratch, .{ .glyph = .{
 463             .x = -left_bound,
 464             .y = part_top - bounds[index].top,
 465             .glyph_id = part.glyph_id,
 466             .font_size = style.font_size,
 467         } });
 468     }
 469     return .{
 470         .width = right_bound - left_bound,
 471         .height = height,
 472         .commands = try commands.toOwnedSlice(context.scratch),
 473     };
 474 }
 475 
 476 fn layoutScriptsImpl(context: *Context, value: ast.Scripts, style: Style) anyerror!Box {
 477     if (operator.limitsBase(value.base)) return context.layoutLimits(value, style);
 478     const base = try context.layoutExpr(value.base, style);
 479     const script = scriptStyle(context, style);
 480     const sup = if (value.sup) |expr| try context.layoutExpr(expr, script) else null;
 481     const sub = if (value.sub) |expr| try context.layoutExpr(expr, script) else null;
 482     if (mathConstants(context) != null) return try layoutMathScripts(context, base, sup, sub, style);
 483     const gap = @max(1, @divTrunc(style.font_size, 8));
 484     const script_width = @max(if (sup) |box| box.width else 0, if (sub) |box| box.width else 0);
 485     const base_y = if (sup) |box| @max(0, box.height - @divTrunc(base.baseline, 2)) else 0;
 486     const baseline = base_y + base.baseline;
 487     const sub_y = baseline + gap;
 488     var height = base_y + base.height;
 489     if (sub) |box| height = @max(height, sub_y + box.height);
 490     var commands: std.ArrayListUnmanaged(Command) = .empty;
 491     try appendCommands(context.scratch, &commands, base.commands, 0, base_y);
 492     if (sup) |box| try appendCommands(context.scratch, &commands, box.commands, base.width + gap, 0);
 493     if (sub) |box| try appendCommands(context.scratch, &commands, box.commands, base.width + gap, sub_y);
 494     var out = try context.boxFromList(base.width + if (script_width == 0) 0 else gap + script_width, height, baseline, &commands);
 495     out.class = base.class;
 496     return out;
 497 }
 498 
 499 fn layoutMathScripts(context: *Context, base: Box, sup: ?Box, sub: ?Box, style: Style) anyerror!Box {
 500     const script_gap = scriptHorizontalGap(context, style);
 501     const script_width = @max(if (sup) |box| box.width else 0, if (sub) |box| box.width else 0);
 502     const script_x = base.width + if (script_width == 0) 0 else script_gap;
 503     const metrics = context.fontMetrics(style.font_size);
 504     const extended_base = base.height > metrics.line_height;
 505     const base_y = -base.baseline;
 506     var sup_y: ?i32 = null;
 507     var sub_y: ?i32 = null;
 508     if (sup) |box| {
 509         var shift = @max(
 510             superscriptShiftUp(context, style),
 511             box.height - box.baseline + superscriptBottomMin(context, style),
 512         );
 513         if (extended_base) shift = @max(shift, base.baseline - superscriptBaselineDropMax(context, style));
 514         sup_y = -shift - box.baseline;
 515     }
 516     if (sub) |box| {
 517         var shift = @max(
 518             subscriptShiftDown(context, style),
 519             box.baseline - subscriptTopMax(context, style),
 520         );
 521         if (extended_base) shift = @max(shift, base.height - base.baseline + subscriptBaselineDropMin(context, style));
 522         sub_y = shift - box.baseline;
 523     }
 524     if (sup != null and sub != null) {
 525         const sup_box = sup.?;
 526         var above_y = sup_y.?;
 527         var below_y = sub_y.?;
 528         const needed_gap = subSuperscriptGap(context, style);
 529         const current_gap = below_y - (above_y + sup_box.height);
 530         if (current_gap < needed_gap) {
 531             var remaining = needed_gap - current_gap;
 532             const lowest_sup_bottom = -superscriptBottomMaxWithSubscript(context, style);
 533             const sup_bottom = above_y + sup_box.height;
 534             if (sup_bottom > lowest_sup_bottom) {
 535                 const lift = @min(remaining, sup_bottom - lowest_sup_bottom);
 536                 above_y -= lift;
 537                 remaining -= lift;
 538             }
 539             below_y += remaining;
 540         }
 541         sup_y = above_y;
 542         sub_y = below_y;
 543     }
 544     var min_y = base_y;
 545     var max_y = base_y + base.height;
 546     if (sup) |box| {
 547         min_y = @min(min_y, sup_y.?);
 548         max_y = @max(max_y, sup_y.? + box.height);
 549     }
 550     if (sub) |box| {
 551         min_y = @min(min_y, sub_y.?);
 552         max_y = @max(max_y, sub_y.? + box.height);
 553     }
 554     const y_shift = if (min_y < 0) -min_y else 0;
 555     const width = base.width + if (script_width == 0) 0 else script_gap + script_width;
 556     var commands: std.ArrayListUnmanaged(Command) = .empty;
 557     try appendCommands(context.scratch, &commands, base.commands, 0, base_y + y_shift);
 558     if (sup) |box| try appendCommands(context.scratch, &commands, box.commands, script_x, sup_y.? + y_shift);
 559     if (sub) |box| try appendCommands(context.scratch, &commands, box.commands, script_x, sub_y.? + y_shift);
 560     var out = try context.boxFromList(width, max_y - min_y, y_shift, &commands);
 561     out.class = base.class;
 562     return out;
 563 }
 564 
 565 fn layoutLimitsImpl(context: *Context, value: ast.Scripts, style: Style) anyerror!Box {
 566     var base = try context.layoutExpr(value.base, style);
 567     const script = scriptStyle(context, style);
 568     const sup = if (value.sup) |expr| try context.layoutExpr(expr, script) else null;
 569     const sub = if (value.sub) |expr| try context.layoutExpr(expr, script) else null;
 570     const stretch_arrow = operator.stretchArrowBase(value.base);
 571     if (stretch_arrow) |arrow| {
 572         const label_width = @max(if (sup) |box| box.width else 0, if (sub) |box| box.width else 0);
 573         const target_width = @max(base.width, label_width + stretchArrowLabelPadding(style) * 2);
 574         base = try context.layoutStretchArrow(arrow, target_width, base, value.base, style);
 575     }
 576     const stretch_stack = stretch_arrow != null;
 577     const top_shift = if (stretch_stack) stretchStackTopShiftUp(context, style) else upperLimitBaselineRise(context, style);
 578     const top_gap = if (stretch_stack) stretchStackGapAbove(context, style) else upperLimitGap(context, style);
 579     const bottom_shift = if (stretch_stack) stretchStackBottomShiftDown(context, style) else lowerLimitBaselineDrop(context, style);
 580     const bottom_gap = if (stretch_stack) stretchStackGapBelow(context, style) else lowerLimitGap(context, style);
 581     const width = @max(base.width, @max(if (sup) |box| box.width else 0, if (sub) |box| box.width else 0));
 582     const base_y = if (sup) |box| @max(
 583         top_shift + box.baseline,
 584         top_gap + box.height,
 585     ) else 0;
 586     const base_bottom = base_y + base.height;
 587     const sub_y = if (sub) |box| base_bottom + @max(
 588         bottom_gap,
 589         bottom_shift - box.baseline,
 590     ) else base_bottom;
 591     var height = base_bottom;
 592     if (sub) |box| height = sub_y + box.height;
 593 
 594     var commands: std.ArrayListUnmanaged(Command) = .empty;
 595     if (sup) |box| try appendCommands(context.scratch, &commands, box.commands, centered(width, box.width), 0);
 596     try appendCommands(context.scratch, &commands, base.commands, centered(width, base.width), base_y);
 597     if (sub) |box| try appendCommands(context.scratch, &commands, box.commands, centered(width, box.width), sub_y);
 598     var out = try context.boxFromList(width, height, base_y + base.baseline, &commands);
 599     out.class = base.class;
 600     return out;
 601 }
 602 
 603 fn layoutStretchArrowImpl(context: *Context, arrow: operator.StretchArrow, target_width: i32, nominal: Box, base_expr: *const ast.Expr, style: Style) anyerror!Box {
 604     if (try context.layoutMathHorizontalArrow(base_expr, target_width, nominal, style)) |box| return box;
 605     const left_head = if (arrow.left_head) |text| try context.layoutText(text, style) else null;
 606     const right_head = if (arrow.right_head) |text| try context.layoutText(text, style) else null;
 607     const width = @max(target_width, stretchArrowMinimumWidth(left_head, right_head, style));
 608     const metrics = context.fontMetrics(style.font_size);
 609     var ascender = @max(nominal.baseline, metrics.ascender);
 610     var descender = @max(nominal.height - nominal.baseline, metrics.line_height - metrics.ascender);
 611     if (left_head) |box| {
 612         ascender = @max(ascender, box.baseline);
 613         descender = @max(descender, box.height - box.baseline);
 614     }
 615     if (right_head) |box| {
 616         ascender = @max(ascender, box.baseline);
 617         descender = @max(descender, box.height - box.baseline);
 618     }
 619     const baseline = ascender;
 620     const height = ascender + descender;
 621     const rule = strokeWidth(context, style);
 622     const axis = stretchArrowAxis(context, style, baseline, height, rule);
 623     const overlap = stretchArrowHeadOverlap(style);
 624     var shaft_start: i32 = 0;
 625     if (left_head) |box| shaft_start = @max(0, box.width - overlap);
 626     var shaft_end = width;
 627     if (right_head) |box| shaft_end = width - @max(0, box.width - overlap);
 628     if (shaft_end <= shaft_start) {
 629         shaft_start = 0;
 630         shaft_end = width;
 631     }
 632 
 633     var commands: std.ArrayListUnmanaged(Command) = .empty;
 634     switch (arrow.shaft) {
 635         .single => try appendStretchArrowShaft(context.scratch, &commands, shaft_start, shaft_end, axis, rule),
 636         .double => try appendDoubleStretchArrowShaft(context.scratch, &commands, shaft_start, shaft_end, axis, rule, style),
 637         .squiggle => try appendSquiggleStretchArrowShaft(context.scratch, &commands, shaft_start, shaft_end, axis, rule, style),
 638     }
 639     if (arrow.left_bar) try appendStretchArrowBar(context.scratch, &commands, axis, rule, style);
 640     if (left_head) |box| try appendCommands(context.scratch, &commands, box.commands, 0, baseline - box.baseline);
 641     if (right_head) |box| try appendCommands(context.scratch, &commands, box.commands, width - box.width, baseline - box.baseline);
 642     var out = try context.boxFromList(width, height, baseline, &commands);
 643     out.class = nominal.class;
 644     return out;
 645 }
 646 
 647 fn layoutMathHorizontalArrowImpl(context: *Context, base_expr: *const ast.Expr, target_width: i32, nominal: Box, style: Style) anyerror!?Box {
 648     const glyph_id = singleGlyphId(context, base_expr) orelse return null;
 649     const target = pixelWidthToDesignUnits(context, target_width, style);
 650     if (context.font.face.mathHorizontalVariant(glyph_id, target)) |variant| {
 651         if (variant.advance_measurement >= target) return try layoutMathHorizontalGlyphArrow(context, variant.glyph_id, variant.advance_measurement, target_width, nominal, style);
 652     }
 653     return try layoutMathHorizontalAssemblyArrow(context, glyph_id, target, target_width, nominal, style);
 654 }
 655 
 656 fn layoutMathHorizontalGlyphArrow(context: *Context, glyph_id: u32, advance_measurement: u16, target_width: i32, nominal: Box, style: Style) anyerror!?Box {
 657     const bounds = (try glyphInkBounds(context, glyph_id, style)) orelse return null;
 658     const metrics = context.fontMetrics(style.font_size);
 659     const baseline = @max(nominal.baseline, metrics.ascender);
 660     const height = @max(nominal.height, metrics.line_height);
 661     const x_shift = @max(0, -bounds.left);
 662     const advance_width = designUnitsToPixelsCeil(context, advance_measurement, style);
 663     const width = @max(target_width, @max(advance_width + x_shift, bounds.right + x_shift));
 664     var commands = [_]Command{.{ .glyph = .{
 665         .x = x_shift,
 666         .y = baseline - metrics.ascender,
 667         .glyph_id = glyph_id,
 668         .font_size = style.font_size,
 669     } }};
 670     var out = try context.makeBox(width, height, baseline, &commands);
 671     out.class = nominal.class;
 672     return out;
 673 }
 674 
 675 fn layoutMathHorizontalAssemblyArrow(context: *Context, glyph_id: u32, target: u16, target_width: i32, nominal: Box, style: Style) anyerror!?Box {
 676     const assembly = (try context.font.face.mathHorizontalAssemblyAlloc(context.scratch, glyph_id, target)) orelse return null;
 677     if (assembly.parts.len == 0 or assembly.advance_measurement < target) return null;
 678     const metrics = context.fontMetrics(style.font_size);
 679     const baseline = @max(nominal.baseline, metrics.ascender);
 680     const height = @max(nominal.height, metrics.line_height);
 681     const bounds = try context.scratch.alloc(GlyphInkBounds, assembly.parts.len);
 682     var left_bound: i32 = std.math.maxInt(i32);
 683     var right_bound: i32 = std.math.minInt(i32);
 684     for (assembly.parts, 0..) |part, index| {
 685         const part_end = @as(u32, part.advance_offset) + @as(u32, part.full_advance);
 686         if (part_end > assembly.advance_measurement) return null;
 687         const part_x = designUnitsToPixelsFloor(context, part.advance_offset, style);
 688         bounds[index] = (try glyphInkBounds(context, part.glyph_id, style)) orelse return null;
 689         left_bound = @min(left_bound, part_x + bounds[index].left);
 690         right_bound = @max(right_bound, part_x + bounds[index].right);
 691     }
 692     if (left_bound >= right_bound) return null;
 693 
 694     const x_shift = @max(0, -left_bound);
 695     const assembly_width = designUnitsToPixelsCeil(context, assembly.advance_measurement, style);
 696     const width = @max(target_width, @max(assembly_width + x_shift, right_bound + x_shift));
 697     var commands: std.ArrayListUnmanaged(Command) = .empty;
 698     for (assembly.parts) |part| {
 699         try commands.append(context.scratch, .{ .glyph = .{
 700             .x = x_shift + designUnitsToPixelsFloor(context, part.advance_offset, style),
 701             .y = baseline - metrics.ascender,
 702             .glyph_id = part.glyph_id,
 703             .font_size = style.font_size,
 704         } });
 705     }
 706     var out = try context.boxFromList(width, height, baseline, &commands);
 707     out.class = nominal.class;
 708     return out;
 709 }
 710 
 711 fn layoutAccentImpl(context: *Context, value: ast.Accent, style: Style) anyerror!Box {
 712     const body = try context.layoutExpr(value.body, style);
 713     var commands: std.ArrayListUnmanaged(Command) = .empty;
 714     switch (value.mark) {
 715         .bar => {
 716             const rule = overbarRuleWidth(context, style);
 717             const gap = overbarGap(context, style);
 718             const extra = overbarExtraAscender(context, style);
 719             const body_y = extra + rule + gap;
 720             try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = extra, .width = body.width, .height = rule } });
 721             try appendCommands(context.scratch, &commands, body.commands, 0, body_y);
 722             var out = try context.boxFromList(body.width, body.height + body_y, body.baseline + body_y, &commands);
 723             out.class = body.class;
 724             return out;
 725         },
 726         .underline => {
 727             const rule = underbarRuleWidth(context, style);
 728             const gap = underbarGap(context, style);
 729             const extra = underbarExtraDescender(context, style);
 730             try appendCommands(context.scratch, &commands, body.commands, 0, 0);
 731             try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = body.height + gap, .width = body.width, .height = rule } });
 732             var out = try context.boxFromList(body.width, body.height + gap + rule + extra, body.baseline, &commands);
 733             out.class = body.class;
 734             return out;
 735         },
 736         else => {
 737             if (try context.layoutNativeAccent(value.mark, body, style)) |box| return box;
 738             const mark_text = accentText(value.mark);
 739             const mark_style = scriptStyle(context, style);
 740             const mark_box = try context.layoutText(mark_text, mark_style);
 741             const mark_x = accentMarkX(context, value.body, body, mark_text, mark_box, style, mark_style);
 742             const body_y = @divTrunc(mark_box.height, 2);
 743             const min_x = @min(@as(i32, 0), mark_x);
 744             const max_x = @max(body.width, mark_x + mark_box.width);
 745             try appendCommands(context.scratch, &commands, mark_box.commands, mark_x - min_x, 0);
 746             try appendCommands(context.scratch, &commands, body.commands, -min_x, body_y);
 747             var out = try context.boxFromList(max_x - min_x, body.height + body_y, body.baseline + body_y, &commands);
 748             out.class = body.class;
 749             return out;
 750         },
 751     }
 752 }
 753 
 754 fn layoutNativeAccentImpl(context: *Context, mark: ast.AccentMark, body: Box, style: Style) anyerror!?Box {
 755     const rule = strokeWidth(context, style);
 756     const accent_height = wideAccentHeight(style, rule);
 757     const accent_width = @max(body.width, wideAccentMinimumWidth(style, rule));
 758     var accent_commands: std.ArrayListUnmanaged(Command) = .empty;
 759     switch (mark) {
 760         .hat => try appendWideHat(context.scratch, &accent_commands, accent_width, 0, accent_height, rule),
 761         .tilde => try appendWideTilde(context.scratch, &accent_commands, accent_width, 0, accent_height, rule),
 762         .check => try appendWideCheck(context.scratch, &accent_commands, accent_width, 0, accent_height, rule),
 763         .breve => try appendWideBreve(context.scratch, &accent_commands, accent_width, 0, accent_height, rule),
 764         .vec => try appendWideArrowAccent(context.scratch, &accent_commands, accent_width, 0, accent_height, rule, false, true),
 765         .overleft => try appendWideArrowAccent(context.scratch, &accent_commands, accent_width, 0, accent_height, rule, true, false),
 766         .overleftright => try appendWideArrowAccent(context.scratch, &accent_commands, accent_width, 0, accent_height, rule, true, true),
 767         else => return null,
 768     }
 769 
 770     const gap = @max(1, @divTrunc(style.font_size, 12));
 771     const width = @max(body.width, accent_width);
 772     const body_y = accent_height + gap;
 773     var commands: std.ArrayListUnmanaged(Command) = .empty;
 774     try appendCommands(context.scratch, &commands, accent_commands.items, centered(width, accent_width), 0);
 775     try appendCommands(context.scratch, &commands, body.commands, centered(width, body.width), body_y);
 776     var out = try context.boxFromList(width, body_y + body.height, body_y + body.baseline, &commands);
 777     out.class = body.class;
 778     return out;
 779 }
 780 
 781 fn layoutAnnotationImpl(context: *Context, value: ast.Annotation, style: Style) anyerror!Box {
 782     return switch (value.kind) {
 783         .plain => try context.layoutStack(value.base, value.over, value.under, style),
 784         .overbrace => try context.layoutBrace(value.base, style, true),
 785         .underbrace => try context.layoutBrace(value.base, style, false),
 786         .boxed => try context.layoutBoxed(value.base, style),
 787     };
 788 }
 789 
 790 fn layoutStackImpl(context: *Context, base_expr: *ast.Expr, over_expr: ?*ast.Expr, under_expr: ?*ast.Expr, style: Style) anyerror!Box {
 791     const base = try context.layoutExpr(base_expr, style);
 792     const script = scriptStyle(context, style);
 793     const over = if (over_expr) |expr| try context.layoutExpr(expr, script) else null;
 794     const under = if (under_expr) |expr| try context.layoutExpr(expr, script) else null;
 795     const gap = stackGap(context, style);
 796     const top_shift = stackTopShiftUp(context, style);
 797     const bottom_shift = stackBottomShiftDown(context, style);
 798     const width = @max(base.width, @max(if (over) |box| box.width else 0, if (under) |box| box.width else 0));
 799     const base_y = if (over) |box| @max(
 800         top_shift + box.baseline - base.baseline,
 801         gap + box.height,
 802     ) else 0;
 803     const base_baseline = base_y + base.baseline;
 804     const under_y = if (under) |box| @max(
 805         base_y + base.height + gap,
 806         base_baseline + bottom_shift - box.baseline,
 807     ) else base_y + base.height;
 808     var height = base_y + base.height;
 809     if (under) |box| height = under_y + box.height;
 810     var commands: std.ArrayListUnmanaged(Command) = .empty;
 811     if (over) |box| try appendCommands(context.scratch, &commands, box.commands, centered(width, box.width), 0);
 812     try appendCommands(context.scratch, &commands, base.commands, centered(width, base.width), base_y);
 813     if (under) |box| try appendCommands(context.scratch, &commands, box.commands, centered(width, box.width), under_y);
 814     return context.boxFromList(width, height, base_y + base.baseline, &commands);
 815 }
 816 
 817 fn layoutBraceImpl(context: *Context, body_expr: *ast.Expr, style: Style, over: bool) anyerror!Box {
 818     const body = try context.layoutExpr(body_expr, style);
 819     const rule = strokeWidth(context, style);
 820     const gap = @max(2, @divTrunc(style.font_size, 8));
 821     const brace_height = horizontalBraceHeight(style, rule);
 822     var commands: std.ArrayListUnmanaged(Command) = .empty;
 823     if (over) {
 824         try appendHorizontalBrace(context.scratch, &commands, body.width, 0, brace_height, rule, true);
 825         try appendCommands(context.scratch, &commands, body.commands, 0, brace_height + gap);
 826         return context.boxFromList(body.width, body.height + brace_height + gap, body.baseline + brace_height + gap, &commands);
 827     }
 828     try appendCommands(context.scratch, &commands, body.commands, 0, 0);
 829     try appendHorizontalBrace(context.scratch, &commands, body.width, body.height + gap, brace_height, rule, false);
 830     return context.boxFromList(body.width, body.height + brace_height + gap, body.baseline, &commands);
 831 }
 832 
 833 fn layoutBoxedImpl(context: *Context, body_expr: *ast.Expr, style: Style) anyerror!Box {
 834     const body = try context.layoutExpr(body_expr, style);
 835     const rule = strokeWidth(context, style);
 836     const pad = @max(3, @divTrunc(style.font_size, 5));
 837     const width = body.width + pad * 2;
 838     const height = body.height + pad * 2;
 839     var commands: std.ArrayListUnmanaged(Command) = .empty;
 840     try appendFrame(context.scratch, &commands, width, height, rule);
 841     try appendCommands(context.scratch, &commands, body.commands, pad, pad);
 842     return context.boxFromList(width, height, body.baseline + pad, &commands);
 843 }
 844 
 845 fn layoutDelimitedImpl(context: *Context, value: ast.Delimited, style: Style) anyerror!Box {
 846     const body = try context.layoutExpr(value.body, style);
 847     const gap = @max(1, @divTrunc(style.font_size, 10));
 848     const target_height = @max(body.height, delimitedSubFormulaMinHeight(context, style));
 849     const target_baseline = body.baseline + centered(target_height, body.height);
 850     const left = try context.layoutDelimiter(value.left, target_height, target_baseline, style, true);
 851     const right = try context.layoutDelimiter(value.right, target_height, target_baseline, style, false);
 852     const baseline = @max(body.baseline, @max(left.baseline, right.baseline));
 853     const descent = @max(body.height - body.baseline, @max(left.height - left.baseline, right.height - right.baseline));
 854     const height = baseline + descent;
 855     var commands: std.ArrayListUnmanaged(Command) = .empty;
 856     try appendCommands(context.scratch, &commands, left.commands, 0, baseline - left.baseline);
 857     try appendCommands(context.scratch, &commands, body.commands, left.width + gap, baseline - body.baseline);
 858     try appendCommands(context.scratch, &commands, right.commands, left.width + gap + body.width + gap, baseline - right.baseline);
 859     var out = try context.boxFromList(left.width + body.width + right.width + gap * 2, height, baseline, &commands);
 860     out.class = .inner;
 861     return out;
 862 }
 863 
 864 fn layoutDelimiterImpl(context: *Context, delimiter: ast.Delimiter, height: i32, baseline: i32, style: Style, left: bool) anyerror!Box {
 865     return switch (delimiter) {
 866         .none => context.makeBox(0, height, baseline, &.{}),
 867         .text => |value| context.layoutText(value, style),
 868         .shape => |shape| try context.layoutDelimiterShape(shape, height, baseline, style, left),
 869     };
 870 }
 871 
 872 fn layoutDelimiterShapeImpl(context: *Context, shape: ast.DelimiterShape, height: i32, baseline: i32, style: Style, left: bool) anyerror!Box {
 873     if (try layoutMathVariantDelimiter(context, shape, height, baseline, style)) |box| return box;
 874     const rule = strokeWidth(context, style);
 875     const width = @max(rule, @divTrunc(style.font_size, 3));
 876     if (shape == .bar or shape == .double_bar) {
 877         var commands: std.ArrayListUnmanaged(Command) = .empty;
 878         const x = if (left) 0 else width - rule;
 879         try commands.append(context.scratch, .{ .rect = .{ .x = x, .y = 0, .width = rule, .height = height } });
 880         if (shape == .double_bar) try commands.append(context.scratch, .{ .rect = .{ .x = x + rule * 2, .y = 0, .width = rule, .height = height } });
 881         return context.boxFromList(if (shape == .double_bar) width + rule * 2 else width, height, baseline, &commands);
 882     }
 883     if (shape == .left_bracket or shape == .right_bracket or shape == .left_floor or shape == .right_floor or shape == .left_ceil or shape == .right_ceil) {
 884         var commands: std.ArrayListUnmanaged(Command) = .empty;
 885         const x = if (left) 0 else width - rule;
 886         try commands.append(context.scratch, .{ .rect = .{ .x = x, .y = 0, .width = rule, .height = height } });
 887         if (shape == .left_bracket or shape == .right_bracket or shape == .left_ceil or shape == .right_ceil) {
 888             try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = 0, .width = width, .height = rule } });
 889         }
 890         if (shape == .left_bracket or shape == .right_bracket or shape == .left_floor or shape == .right_floor) {
 891             try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = height - rule, .width = width, .height = rule } });
 892         }
 893         return context.boxFromList(width, height, baseline, &commands);
 894     }
 895     if (shape == .left_double_bracket or shape == .right_double_bracket) {
 896         const double_width = @max(rule * 5, @max(@divTrunc(style.font_size, 2), width));
 897         var commands: std.ArrayListUnmanaged(Command) = .empty;
 898         const outer = if (left) 0 else double_width - rule;
 899         const inner = if (left) rule * 2 else double_width - rule * 3;
 900         try commands.append(context.scratch, .{ .rect = .{ .x = outer, .y = 0, .width = rule, .height = height } });
 901         try commands.append(context.scratch, .{ .rect = .{ .x = inner, .y = 0, .width = rule, .height = height } });
 902         try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = 0, .width = double_width, .height = rule } });
 903         try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = height - rule, .width = double_width, .height = rule } });
 904         return context.boxFromList(double_width, height, baseline, &commands);
 905     }
 906     if (try layoutStrokedDelimiter(context, shape, height, baseline, style)) |box| return box;
 907     const box = try context.layoutText(delimiterShapeText(shape), delimiterStyle(context, height, style));
 908     const out_height = @max(height, box.height);
 909     const out_baseline = baseline + centered(out_height, height);
 910     var commands: std.ArrayListUnmanaged(Command) = .empty;
 911     try appendCommands(context.scratch, &commands, box.commands, 0, centered(out_height, box.height));
 912     return context.boxFromList(box.width, out_height, out_baseline, &commands);
 913 }
 914 
 915 fn layoutMathVariantDelimiter(context: *Context, shape: ast.DelimiterShape, height: i32, baseline: i32, style: Style) anyerror!?Box {
 916     const codepoint = delimiterShapeCodepoint(shape) orelse return null;
 917     const glyph_id = context.font.face.glyphId(codepoint);
 918     if (glyph_id == 0) return null;
 919     const target = pixelHeightToDesignUnits(context, height, style);
 920     if (context.font.face.mathVerticalVariant(glyph_id, target)) |variant| {
 921         if (variant.advance_measurement >= target) return try layoutMathGlyphDelimiter(context, variant.glyph_id, height, baseline, style);
 922     }
 923     return try layoutMathAssemblyDelimiter(context, glyph_id, target, height, baseline, style);
 924 }
 925 
 926 fn layoutMathGlyphDelimiter(context: *Context, glyph_id: u32, height: i32, baseline: i32, style: Style) anyerror!Box {
 927     const bounds = (try glyphInkBounds(context, glyph_id, style)) orelse return error.InvalidFont;
 928     const ink_width = @max(1, bounds.right - bounds.left);
 929     const ink_height = @max(1, bounds.bottom - bounds.top);
 930     const out_height = @max(height, ink_height);
 931     const out_baseline = baseline + centered(out_height, height);
 932     var commands = [_]Command{.{ .glyph = .{
 933         .x = -bounds.left,
 934         .y = centered(out_height, ink_height) - bounds.top,
 935         .glyph_id = glyph_id,
 936         .font_size = style.font_size,
 937     } }};
 938     return try context.makeBox(ink_width, out_height, out_baseline, &commands);
 939 }
 940 
 941 fn layoutMathAssemblyDelimiter(context: *Context, glyph_id: u32, target: u16, height: i32, baseline: i32, style: Style) anyerror!?Box {
 942     const assembly = (try context.font.face.mathVerticalAssemblyAlloc(context.scratch, glyph_id, target)) orelse return null;
 943     if (assembly.parts.len == 0 or assembly.advance_measurement < target) return null;
 944     const assembly_height = @max(1, designUnitsToPixelsCeil(context, assembly.advance_measurement, style));
 945     const out_height = @max(height, assembly_height);
 946     const out_baseline = baseline + centered(out_height, height);
 947     const assembly_y = centered(out_height, assembly_height);
 948     const bounds = try context.scratch.alloc(GlyphInkBounds, assembly.parts.len);
 949     var left_bound: i32 = std.math.maxInt(i32);
 950     var right_bound: i32 = std.math.minInt(i32);
 951     for (assembly.parts, 0..) |part, index| {
 952         bounds[index] = (try glyphInkBounds(context, part.glyph_id, style)) orelse return null;
 953         left_bound = @min(left_bound, bounds[index].left);
 954         right_bound = @max(right_bound, bounds[index].right);
 955     }
 956     if (left_bound >= right_bound) return null;
 957 
 958     var commands: std.ArrayListUnmanaged(Command) = .empty;
 959     for (assembly.parts, 0..) |part, index| {
 960         const part_end = @as(u32, part.advance_offset) + @as(u32, part.full_advance);
 961         if (part_end > assembly.advance_measurement) return null;
 962         const part_top_design: u16 = @intCast(@as(u32, assembly.advance_measurement) - part_end);
 963         const part_top = assembly_y + designUnitsToPixelsFloor(context, part_top_design, style);
 964         try commands.append(context.scratch, .{ .glyph = .{
 965             .x = -left_bound,
 966             .y = part_top - bounds[index].top,
 967             .glyph_id = part.glyph_id,
 968             .font_size = style.font_size,
 969         } });
 970     }
 971     return try context.boxFromList(right_bound - left_bound, out_height, out_baseline, &commands);
 972 }
 973 
 974 fn layoutStrokedDelimiter(context: *Context, shape: ast.DelimiterShape, height: i32, baseline: i32, style: Style) anyerror!?Box {
 975     return switch (shape) {
 976         .left_paren => try layoutParenStroke(context, height, baseline, style, true),
 977         .right_paren => try layoutParenStroke(context, height, baseline, style, false),
 978         .left_angle => try layoutAngleStroke(context, height, baseline, style, true, false),
 979         .right_angle => try layoutAngleStroke(context, height, baseline, style, false, false),
 980         .left_double_angle => try layoutAngleStroke(context, height, baseline, style, true, true),
 981         .right_double_angle => try layoutAngleStroke(context, height, baseline, style, false, true),
 982         .left_brace => try layoutBraceStroke(context, height, baseline, style, true),
 983         .right_brace => try layoutBraceStroke(context, height, baseline, style, false),
 984         else => null,
 985     };
 986 }
 987 
 988 fn layoutParenStroke(context: *Context, height: i32, baseline: i32, style: Style, left: bool) !Box {
 989     const rule = strokeWidth(context, style);
 990     const out_height = strokedDelimiterHeight(height, style);
 991     const out_baseline = baseline + centered(out_height, height);
 992     const width = @max(rule * 4, @max(@divTrunc(style.font_size, 2), @divTrunc(out_height, 8)));
 993     var commands: std.ArrayListUnmanaged(Command) = .empty;
 994     const outer = if (left) width - rule else 0;
 995     const inner = if (left) 0 else width - rule;
 996     const shoulder = if (left) @divTrunc(width, 3) else width - rule - @divTrunc(width, 3);
 997     try commands.append(context.scratch, .{ .line = .{ .x0 = outer, .y0 = 0, .x1 = shoulder, .y1 = @divTrunc(out_height, 5), .width = rule } });
 998     try commands.append(context.scratch, .{ .line = .{ .x0 = shoulder, .y0 = @divTrunc(out_height, 5), .x1 = inner, .y1 = @divTrunc(out_height, 2), .width = rule } });
 999     try commands.append(context.scratch, .{ .line = .{ .x0 = inner, .y0 = @divTrunc(out_height, 2), .x1 = shoulder, .y1 = @divTrunc(out_height * 4, 5), .width = rule } });
1000     try commands.append(context.scratch, .{ .line = .{ .x0 = shoulder, .y0 = @divTrunc(out_height * 4, 5), .x1 = outer, .y1 = out_height - rule, .width = rule } });
1001     return context.boxFromList(width, out_height, out_baseline, &commands);
1002 }
1003 
1004 fn layoutAngleStroke(context: *Context, height: i32, baseline: i32, style: Style, left: bool, double: bool) !Box {
1005     const rule = strokeWidth(context, style);
1006     const out_height = strokedDelimiterHeight(height, style);
1007     const out_baseline = baseline + centered(out_height, height);
1008     const single_width = @max(rule * 4, @max(@divTrunc(style.font_size, 2), @divTrunc(out_height, 7)));
1009     const gap = if (double) @max(rule * 2, @divTrunc(single_width, 4)) else 0;
1010     const width = if (double) single_width + gap else single_width;
1011     var commands: std.ArrayListUnmanaged(Command) = .empty;
1012     try appendAngleStroke(context.scratch, &commands, single_width, out_height, rule, left, if (double and left) 0 else gap);
1013     if (double) try appendAngleStroke(context.scratch, &commands, single_width, out_height, rule, left, if (left) gap else 0);
1014     return context.boxFromList(width, out_height, out_baseline, &commands);
1015 }
1016 
1017 fn layoutBraceStroke(context: *Context, height: i32, baseline: i32, style: Style, left: bool) !Box {
1018     const rule = strokeWidth(context, style);
1019     const out_height = strokedDelimiterHeight(height, style);
1020     const out_baseline = baseline + centered(out_height, height);
1021     const width = @max(rule * 5, @max(@divTrunc(style.font_size, 2), @divTrunc(out_height, 9)));
1022     var commands: std.ArrayListUnmanaged(Command) = .empty;
1023     try appendVerticalBrace(context.scratch, &commands, width, out_height, rule, left, 0);
1024     return context.boxFromList(width, out_height, out_baseline, &commands);
1025 }
1026 
1027 fn appendAngleStroke(allocator: std.mem.Allocator, commands: *std.ArrayListUnmanaged(Command), width: i32, height: i32, rule: i32, left: bool, dx: i32) !void {
1028     const outer = if (left) width - rule else 0;
1029     const inner = if (left) 0 else width - rule;
1030     const middle_y = @divTrunc(height, 2);
1031     try commands.append(allocator, .{ .line = .{ .x0 = dx + outer, .y0 = 0, .x1 = dx + inner, .y1 = middle_y, .width = rule } });
1032     try commands.append(allocator, .{ .line = .{ .x0 = dx + inner, .y0 = middle_y, .x1 = dx + outer, .y1 = height - rule, .width = rule } });
1033 }
1034 
1035 fn strokedDelimiterHeight(height: i32, style: Style) i32 {
1036     return height + @max(2, @divTrunc(style.font_size, 6));
1037 }
1038 
1039 fn layoutGridImpl(context: *Context, value: ast.Grid, style: Style) anyerror!Box {
1040     if (value.rows.len == 0) return context.layoutText("", style);
1041     var columns: usize = 0;
1042     for (value.rows) |row| columns = @max(columns, row.cells.len);
1043     if (columns == 0) return context.layoutText("", style);
1044 
1045     const column_widths = try context.scratch.alloc(i32, columns);
1046     @memset(column_widths, 0);
1047     const row_baselines = try context.scratch.alloc(i32, value.rows.len);
1048     const row_heights = try context.scratch.alloc(i32, value.rows.len);
1049     const boxes = try context.scratch.alloc([]Box, value.rows.len);
1050     for (value.rows, 0..) |row, row_index| {
1051         boxes[row_index] = try context.scratch.alloc(Box, row.cells.len);
1052         var baseline: i32 = 0;
1053         var descent: i32 = 0;
1054         for (row.cells, 0..) |cell, column_index| {
1055             const box = try context.layoutExpr(cell, style);
1056             boxes[row_index][column_index] = box;
1057             column_widths[column_index] = @max(column_widths[column_index], box.width);
1058             baseline = @max(baseline, box.baseline);
1059             descent = @max(descent, box.height - box.baseline);
1060         }
1061         row_baselines[row_index] = baseline;
1062         row_heights[row_index] = baseline + descent;
1063     }
1064 
1065     const gap = @max(4, @divTrunc(style.font_size, 2));
1066     var body_width: i32 = gap * @as(i32, @intCast(columns - 1));
1067     for (column_widths) |width| body_width += width;
1068     var body_height: i32 = 0;
1069     for (row_heights) |height| body_height += height;
1070     var commands: std.ArrayListUnmanaged(Command) = .empty;
1071     var y: i32 = 0;
1072     for (value.rows, 0..) |row, row_index| {
1073         var x: i32 = 0;
1074         for (0..columns) |column_index| {
1075             if (column_index < row.cells.len) {
1076                 const box = boxes[row_index][column_index];
1077                 const offset = switch (value.alignment) {
1078                     .center => centered(column_widths[column_index], box.width),
1079                     .left => 0,
1080                 };
1081                 try appendCommands(context.scratch, &commands, box.commands, x + offset, y + row_baselines[row_index] - box.baseline);
1082             }
1083             x += column_widths[column_index] + gap;
1084         }
1085         y += row_heights[row_index];
1086     }
1087     const body = try context.boxFromList(body_width, body_height, @divTrunc(body_height, 2), &commands);
1088     const left = try context.layoutDelimiter(gridLeftDelimiter(value.fence), body.height, body.baseline, style, true);
1089     const right = try context.layoutDelimiter(gridRightDelimiter(value.fence), body.height, body.baseline, style, false);
1090     if (left.width == 0 and right.width == 0) return body;
1091     const fence_gap = @max(1, @divTrunc(style.font_size, 10));
1092     var framed: std.ArrayListUnmanaged(Command) = .empty;
1093     try appendCommands(context.scratch, &framed, left.commands, 0, 0);
1094     try appendCommands(context.scratch, &framed, body.commands, left.width + fence_gap, 0);
1095     try appendCommands(context.scratch, &framed, right.commands, left.width + fence_gap + body.width + fence_gap, 0);
1096     return context.boxFromList(left.width + body.width + right.width + fence_gap * 2, body.height, body.baseline, &framed);
1097 }
1098 
1099 fn paintImpl(context: *Context, canvas: filigree.render.Canvas, commands: []const Command, origin_x: i32, origin_y: i32) !void {
1100     for (commands) |command| {
1101         switch (command) {
1102             .text => |text| {
1103                 var shaped_font = context.font;
1104                 shaped_font.setScale(@floatFromInt(text.font_size), 72);
1105                 const fallback_candidates = try context.fallbackCandidates(text.font_size);
1106                 if (fallback_candidates.len == 0) {
1107                     _ = try filigree.drawUtf8(
1108                         context.allocator,
1109                         context.shaping_output,
1110                         canvas,
1111                         &shaped_font,
1112                         text.value,
1113                         text.font_size,
1114                         origin_x + text.x,
1115                         origin_y + text.y,
1116                         .{ .color = context.options.foreground },
1117                     );
1118                 } else {
1119                     _ = try filigree.drawFallbackUtf8(
1120                         context.allocator,
1121                         context.shaping_output,
1122                         canvas,
1123                         &shaped_font,
1124                         fallback_candidates,
1125                         text.value,
1126                         text.font_size,
1127                         origin_x + text.x,
1128                         origin_y + text.y,
1129                         .{ .color = context.options.foreground },
1130                     );
1131                 }
1132             },
1133             .rect => |rect| drawRect(canvas, origin_x + rect.x, origin_y + rect.y, rect.width, rect.height, context.options.foreground),
1134             .line => |line| drawLine(canvas, origin_x + line.x0, origin_y + line.y0, origin_x + line.x1, origin_y + line.y1, line.width, context.options.foreground),
1135             .glyph => |glyph| try drawGlyphCommand(context, canvas, glyph, origin_x, origin_y),
1136         }
1137     }
1138 }
1139 
1140 fn drawGlyphCommand(context: *Context, canvas: filigree.render.Canvas, glyph: Glyph, origin_x: i32, origin_y: i32) !void {
1141     var glyphs = [_]filigree.ShapedGlyph{.{
1142         .glyph_id = glyph.glyph_id,
1143         .cluster = 0,
1144         .x_advance = 0,
1145         .y_advance = 0,
1146         .x_offset = 0,
1147         .y_offset = 0,
1148     }};
1149     const clusters = [_]filigree.Cluster{};
1150     const carets = [_]filigree.LigatureCaret{};
1151     const run = filigree.GlyphRun{
1152         .glyphs = &glyphs,
1153         .clusters = &clusters,
1154         .ligature_carets = &carets,
1155         .total_x_advance = 0,
1156         .total_y_advance = 0,
1157         .direction = .ltr,
1158         .writing_mode = .horizontal,
1159         .output_order = .logical,
1160     };
1161     _ = try filigree.drawGlyphRun(
1162         context.allocator,
1163         canvas,
1164         context.font.face,
1165         run,
1166         glyph.font_size,
1167         origin_x + glyph.x,
1168         origin_y + glyph.y,
1169         .{ .color = context.options.foreground },
1170     );
1171 }
1172 
1173 fn fontMetricsImpl(context: *const Context, font_size: i32) Metrics {
1174     const ascender = @max(1, scaleSigned(context.font.face.ascender, context.font.face.units_per_em, font_size));
1175     const descender = @max(1, -scaleSigned(context.font.face.descender, context.font.face.units_per_em, font_size));
1176     const line_gap = @max(0, scaleSigned(context.font.face.line_gap, context.font.face.units_per_em, font_size));
1177     return .{
1178         .ascender = ascender,
1179         .descender = descender,
1180         .line_gap = line_gap,
1181         .line_height = @max(1, ascender + descender + line_gap),
1182     };
1183 }
1184 
1185 fn fallbackCandidatesImpl(context: *Context, font_size: i32) ![]const filigree.FallbackCandidate {
1186     if (context.fallback_fonts.len == 0) return &.{};
1187     const fonts = try context.scratch.alloc(filigree.Font, context.fallback_fonts.len);
1188     const candidates = try context.scratch.alloc(filigree.FallbackCandidate, context.fallback_fonts.len);
1189     for (context.fallback_fonts, 0..) |font, index| {
1190         fonts[index] = font;
1191         fonts[index].setScale(@floatFromInt(font_size), 72);
1192         candidates[index] = .{ .font = &fonts[index] };
1193     }
1194     return candidates;
1195 }
1196 
1197 fn makeBoxImpl(context: *Context, width: i32, height: i32, baseline: i32, commands: []const Command) !Box {
1198     const owned = try context.scratch.dupe(Command, commands);
1199     return .{ .width = width, .height = height, .baseline = baseline, .commands = owned };
1200 }
1201 
1202 fn boxFromListImpl(context: *Context, width: i32, height: i32, baseline: i32, commands: *std.ArrayListUnmanaged(Command)) !Box {
1203     return .{
1204         .width = width,
1205         .height = height,
1206         .baseline = baseline,
1207         .commands = try commands.toOwnedSlice(context.scratch),
1208     };
1209 }
1210 
1211 fn appendCommands(
1212     allocator: std.mem.Allocator,
1213     out: *std.ArrayListUnmanaged(Command),
1214     commands: []const Command,
1215     dx: i32,
1216     dy: i32,
1217 ) !void {
1218     for (commands) |command| {
1219         try out.append(allocator, offsetCommand(command, dx, dy));
1220     }
1221 }
1222 
1223 fn offsetCommand(command: Command, dx: i32, dy: i32) Command {
1224     return switch (command) {
1225         .text => |text| .{ .text = .{
1226             .x = text.x + dx,
1227             .y = text.y + dy,
1228             .value = text.value,
1229             .font_size = text.font_size,
1230         } },
1231         .rect => |rect| .{ .rect = .{
1232             .x = rect.x + dx,
1233             .y = rect.y + dy,
1234             .width = rect.width,
1235             .height = rect.height,
1236         } },
1237         .line => |line| .{ .line = .{
1238             .x0 = line.x0 + dx,
1239             .y0 = line.y0 + dy,
1240             .x1 = line.x1 + dx,
1241             .y1 = line.y1 + dy,
1242             .width = line.width,
1243         } },
1244         .glyph => |glyph| .{ .glyph = .{
1245             .x = glyph.x + dx,
1246             .y = glyph.y + dy,
1247             .glyph_id = glyph.glyph_id,
1248             .font_size = glyph.font_size,
1249         } },
1250     };
1251 }
1252 
1253 fn appendFrame(allocator: std.mem.Allocator, commands: *std.ArrayListUnmanaged(Command), width: i32, height: i32, rule: i32) !void {
1254     try commands.append(allocator, .{ .rect = .{ .x = 0, .y = 0, .width = width, .height = rule } });
1255     try commands.append(allocator, .{ .rect = .{ .x = 0, .y = height - rule, .width = width, .height = rule } });
1256     try commands.append(allocator, .{ .rect = .{ .x = 0, .y = 0, .width = rule, .height = height } });
1257     try commands.append(allocator, .{ .rect = .{ .x = width - rule, .y = 0, .width = rule, .height = height } });
1258 }
1259 
1260 fn appendStretchArrowShaft(
1261     allocator: std.mem.Allocator,
1262     commands: *std.ArrayListUnmanaged(Command),
1263     x0: i32,
1264     x1: i32,
1265     axis: i32,
1266     rule: i32,
1267 ) !void {
1268     if (x1 <= x0) return;
1269     try commands.append(allocator, .{ .rect = .{
1270         .x = x0,
1271         .y = axis - @divTrunc(rule, 2),
1272         .width = x1 - x0,
1273         .height = rule,
1274     } });
1275 }
1276 
1277 fn appendDoubleStretchArrowShaft(
1278     allocator: std.mem.Allocator,
1279     commands: *std.ArrayListUnmanaged(Command),
1280     x0: i32,
1281     x1: i32,
1282     axis: i32,
1283     rule: i32,
1284     style: Style,
1285 ) !void {
1286     const separation = @max(rule * 2 + 1, @divTrunc(style.font_size, 5));
1287     try appendStretchArrowShaft(allocator, commands, x0, x1, axis - @divTrunc(separation, 2), rule);
1288     try appendStretchArrowShaft(allocator, commands, x0, x1, axis + @divTrunc(separation, 2), rule);
1289 }
1290 
1291 fn appendSquiggleStretchArrowShaft(
1292     allocator: std.mem.Allocator,
1293     commands: *std.ArrayListUnmanaged(Command),
1294     x0: i32,
1295     x1: i32,
1296     axis: i32,
1297     rule: i32,
1298     style: Style,
1299 ) !void {
1300     if (x1 <= x0) return;
1301     const step = @max(3, @divTrunc(style.font_size, 5));
1302     const amplitude = @max(rule * 2, @divTrunc(style.font_size, 9));
1303     var x = x0;
1304     var y = axis;
1305     var up = true;
1306     while (x < x1) {
1307         const next_x = @min(x1, x + step);
1308         const next_y = axis + if (up) -amplitude else amplitude;
1309         try commands.append(allocator, .{ .line = .{
1310             .x0 = x,
1311             .y0 = y,
1312             .x1 = next_x,
1313             .y1 = next_y,
1314             .width = rule,
1315         } });
1316         x = next_x;
1317         y = next_y;
1318         up = !up;
1319     }
1320 }
1321 
1322 fn appendStretchArrowBar(
1323     allocator: std.mem.Allocator,
1324     commands: *std.ArrayListUnmanaged(Command),
1325     axis: i32,
1326     rule: i32,
1327     style: Style,
1328 ) !void {
1329     const height = @max(rule * 5, @divTrunc(style.font_size * 2, 3));
1330     try commands.append(allocator, .{ .rect = .{
1331         .x = 0,
1332         .y = axis - @divTrunc(height, 2),
1333         .width = rule,
1334         .height = height,
1335     } });
1336 }
1337 
1338 fn appendHorizontalBrace(
1339     allocator: std.mem.Allocator,
1340     commands: *std.ArrayListUnmanaged(Command),
1341     width: i32,
1342     y: i32,
1343     height: i32,
1344     rule: i32,
1345     over: bool,
1346 ) !void {
1347     if (width <= 0 or height <= 0) return;
1348     if (width <= rule * 4) {
1349         try commands.append(allocator, .{ .rect = .{ .x = 0, .y = y, .width = width, .height = rule } });
1350         return;
1351     }
1352 
1353     const right = width - 1;
1354     const half = @divTrunc(right, 2);
1355     const wing = @max(rule * 3, @min(@divTrunc(width, 4), @max(rule * 4, @divTrunc(width, 3))));
1356     const notch = @max(rule * 2, @min(@divTrunc(width, 10), @max(rule * 3, 1)));
1357     const left_peak = @min(wing, half);
1358     const right_peak = @max(half, right - wing);
1359     const mid_left = @max(left_peak, half - notch);
1360     const mid_right = @min(right_peak, half + notch);
1361     const high = y;
1362     const low = y + height - rule;
1363     const edge_y = if (over) low else high;
1364     const peak_y = if (over) high else low;
1365     const middle_y = if (over) low else high;
1366     const segments = [_]Line{
1367         .{ .x0 = 0, .y0 = edge_y, .x1 = left_peak, .y1 = peak_y, .width = rule },
1368         .{ .x0 = left_peak, .y0 = peak_y, .x1 = mid_left, .y1 = peak_y, .width = rule },
1369         .{ .x0 = mid_left, .y0 = peak_y, .x1 = half, .y1 = middle_y, .width = rule },
1370         .{ .x0 = half, .y0 = middle_y, .x1 = mid_right, .y1 = peak_y, .width = rule },
1371         .{ .x0 = mid_right, .y0 = peak_y, .x1 = right_peak, .y1 = peak_y, .width = rule },
1372         .{ .x0 = right_peak, .y0 = peak_y, .x1 = right, .y1 = edge_y, .width = rule },
1373     };
1374     for (segments) |segment| try commands.append(allocator, .{ .line = segment });
1375 }
1376 
1377 fn appendVerticalBrace(
1378     allocator: std.mem.Allocator,
1379     commands: *std.ArrayListUnmanaged(Command),
1380     width: i32,
1381     height: i32,
1382     rule: i32,
1383     left: bool,
1384     dx: i32,
1385 ) !void {
1386     if (width <= 0 or height <= 0) return;
1387     const edge_x = if (left) width - rule else 0;
1388     if (height <= rule * 4) {
1389         try commands.append(allocator, .{ .rect = .{ .x = dx + edge_x, .y = 0, .width = rule, .height = height } });
1390         return;
1391     }
1392 
1393     const bottom = height - rule;
1394     const half = @divTrunc(bottom, 2);
1395     const wing = @max(rule * 3, @min(@divTrunc(height, 4), @max(rule * 4, @divTrunc(height, 3))));
1396     const notch = @max(rule * 2, @min(@divTrunc(height, 10), @max(rule * 3, 1)));
1397     const top_peak = @min(wing, half);
1398     const bottom_peak = @max(half, bottom - wing);
1399     const mid_top = @max(top_peak, half - notch);
1400     const mid_bottom = @min(bottom_peak, half + notch);
1401     const peak_x = if (left) 0 else width - rule;
1402     const middle_x = edge_x;
1403     const segments = [_]Line{
1404         .{ .x0 = dx + edge_x, .y0 = 0, .x1 = dx + peak_x, .y1 = top_peak, .width = rule },
1405         .{ .x0 = dx + peak_x, .y0 = top_peak, .x1 = dx + peak_x, .y1 = mid_top, .width = rule },
1406         .{ .x0 = dx + peak_x, .y0 = mid_top, .x1 = dx + middle_x, .y1 = half, .width = rule },
1407         .{ .x0 = dx + middle_x, .y0 = half, .x1 = dx + peak_x, .y1 = mid_bottom, .width = rule },
1408         .{ .x0 = dx + peak_x, .y0 = mid_bottom, .x1 = dx + peak_x, .y1 = bottom_peak, .width = rule },
1409         .{ .x0 = dx + peak_x, .y0 = bottom_peak, .x1 = dx + edge_x, .y1 = bottom, .width = rule },
1410     };
1411     for (segments) |segment| try commands.append(allocator, .{ .line = segment });
1412 }
1413 
1414 fn appendWideHat(
1415     allocator: std.mem.Allocator,
1416     commands: *std.ArrayListUnmanaged(Command),
1417     width: i32,
1418     y: i32,
1419     height: i32,
1420     rule: i32,
1421 ) !void {
1422     if (width <= 0 or height <= 0) return;
1423     if (width <= rule * 3) {
1424         try commands.append(allocator, .{ .rect = .{ .x = 0, .y = y, .width = width, .height = rule } });
1425         return;
1426     }
1427     const right = width - 1;
1428     const middle = @divTrunc(right, 2);
1429     const low = y + height - rule;
1430     try commands.append(allocator, .{ .line = .{ .x0 = 0, .y0 = low, .x1 = middle, .y1 = y, .width = rule } });
1431     try commands.append(allocator, .{ .line = .{ .x0 = middle, .y0 = y, .x1 = right, .y1 = low, .width = rule } });
1432 }
1433 
1434 fn appendWideCheck(
1435     allocator: std.mem.Allocator,
1436     commands: *std.ArrayListUnmanaged(Command),
1437     width: i32,
1438     y: i32,
1439     height: i32,
1440     rule: i32,
1441 ) !void {
1442     if (width <= 0 or height <= 0) return;
1443     if (width <= rule * 3) {
1444         try commands.append(allocator, .{ .rect = .{ .x = 0, .y = y, .width = width, .height = rule } });
1445         return;
1446     }
1447     const right = width - 1;
1448     const middle = @divTrunc(right, 2);
1449     const low = y + height - rule;
1450     try commands.append(allocator, .{ .line = .{ .x0 = 0, .y0 = y, .x1 = middle, .y1 = low, .width = rule } });
1451     try commands.append(allocator, .{ .line = .{ .x0 = middle, .y0 = low, .x1 = right, .y1 = y, .width = rule } });
1452 }
1453 
1454 fn appendWideBreve(
1455     allocator: std.mem.Allocator,
1456     commands: *std.ArrayListUnmanaged(Command),
1457     width: i32,
1458     y: i32,
1459     height: i32,
1460     rule: i32,
1461 ) !void {
1462     if (width <= 0 or height <= 0) return;
1463     const right = width - 1;
1464     const quarter = @divTrunc(right, 4);
1465     const middle = @divTrunc(right, 2);
1466     const three_quarter = right - quarter;
1467     const high = y;
1468     const low = y + height - rule;
1469     const segments = [_]Line{
1470         .{ .x0 = 0, .y0 = high, .x1 = quarter, .y1 = low, .width = rule },
1471         .{ .x0 = quarter, .y0 = low, .x1 = middle, .y1 = low, .width = rule },
1472         .{ .x0 = middle, .y0 = low, .x1 = three_quarter, .y1 = low, .width = rule },
1473         .{ .x0 = three_quarter, .y0 = low, .x1 = right, .y1 = high, .width = rule },
1474     };
1475     for (segments) |segment| try commands.append(allocator, .{ .line = segment });
1476 }
1477 
1478 fn appendWideTilde(
1479     allocator: std.mem.Allocator,
1480     commands: *std.ArrayListUnmanaged(Command),
1481     width: i32,
1482     y: i32,
1483     height: i32,
1484     rule: i32,
1485 ) !void {
1486     if (width <= 0 or height <= 0) return;
1487     const right = width - 1;
1488     const high = y;
1489     const low = y + height - rule;
1490     const mid = y + @divTrunc(height - rule, 2);
1491     const step = @max(rule * 3, @divTrunc(width, 6));
1492     var x: i32 = 0;
1493     var current_y = mid;
1494     var up = true;
1495     while (x < right) {
1496         const next_x = @min(right, x + step);
1497         const next_y = if (up) high else low;
1498         try commands.append(allocator, .{ .line = .{
1499             .x0 = x,
1500             .y0 = current_y,
1501             .x1 = next_x,
1502             .y1 = next_y,
1503             .width = rule,
1504         } });
1505         x = next_x;
1506         current_y = next_y;
1507         up = !up;
1508     }
1509 }
1510 
1511 fn appendWideArrowAccent(
1512     allocator: std.mem.Allocator,
1513     commands: *std.ArrayListUnmanaged(Command),
1514     width: i32,
1515     y: i32,
1516     height: i32,
1517     rule: i32,
1518     left_head: bool,
1519     right_head: bool,
1520 ) !void {
1521     if (width <= 0 or height <= 0) return;
1522     const right = width - 1;
1523     const axis = y + @divTrunc(height - rule, 2);
1524     const head = @max(rule * 3, @min(@max(rule * 3, height - rule), @divTrunc(width, 4)));
1525     const shaft_start = if (left_head) head else 0;
1526     const shaft_end = if (right_head) @max(shaft_start, right - head) else right;
1527     try appendStretchArrowShaft(allocator, commands, shaft_start, shaft_end, axis, rule);
1528     if (right_head) {
1529         try commands.append(allocator, .{ .line = .{ .x0 = right - head, .y0 = y, .x1 = right, .y1 = axis, .width = rule } });
1530         try commands.append(allocator, .{ .line = .{ .x0 = right - head, .y0 = y + height - rule, .x1 = right, .y1 = axis, .width = rule } });
1531     }
1532     if (left_head) {
1533         try commands.append(allocator, .{ .line = .{ .x0 = head, .y0 = y, .x1 = 0, .y1 = axis, .width = rule } });
1534         try commands.append(allocator, .{ .line = .{ .x0 = head, .y0 = y + height - rule, .x1 = 0, .y1 = axis, .width = rule } });
1535     }
1536 }
1537 
1538 fn scriptStyle(context: *const Context, style: Style) Style {
1539     const percent = if (mathConstants(context)) |constants|
1540         scriptPercent(constants.script_percent_scale_down)
1541     else
1542         70;
1543     const scaled = @divTrunc(@as(i64, style.font_size) * percent + 50, 100);
1544     return .{ .font_size = @max(8, @as(i32, @intCast(scaled))) };
1545 }
1546 
1547 fn scriptScriptStyle(context: *const Context, style: Style) Style {
1548     const percent = if (mathConstants(context)) |constants|
1549         scriptPercentOr(constants.script_script_percent_scale_down, 50)
1550     else
1551         50;
1552     const scaled = @divTrunc(@as(i64, style.font_size) * percent + 50, 100);
1553     return .{ .font_size = @max(8, @as(i32, @intCast(scaled))) };
1554 }
1555 
1556 fn strokeWidth(context: *const Context, style: Style) i32 {
1557     return fractionRuleWidth(context, style);
1558 }
1559 
1560 fn horizontalBraceHeight(style: Style, rule: i32) i32 {
1561     return @max(rule * 4 + 1, @divTrunc(style.font_size, 4));
1562 }
1563 
1564 fn wideAccentHeight(style: Style, rule: i32) i32 {
1565     return @max(rule * 4 + 1, @divTrunc(style.font_size, 5));
1566 }
1567 
1568 fn wideAccentMinimumWidth(style: Style, rule: i32) i32 {
1569     return @max(rule * 6, @divTrunc(style.font_size, 2));
1570 }
1571 
1572 fn defaultStrokeWidth(style: Style) i32 {
1573     return @max(1, @divTrunc(style.font_size, 13));
1574 }
1575 
1576 fn fractionRuleWidth(context: *const Context, style: Style) i32 {
1577     const fallback = defaultStrokeWidth(style);
1578     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.fraction_rule_thickness, style, fallback);
1579     return fallback;
1580 }
1581 
1582 fn radicalRuleWidth(context: *const Context, style: Style) i32 {
1583     const fallback = fractionRuleWidth(context, style);
1584     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.radical_rule_thickness, style, fallback);
1585     return fallback;
1586 }
1587 
1588 fn overbarRuleWidth(context: *const Context, style: Style) i32 {
1589     const fallback = strokeWidth(context, style);
1590     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.overbar_rule_thickness, style, fallback);
1591     return fallback;
1592 }
1593 
1594 fn underbarRuleWidth(context: *const Context, style: Style) i32 {
1595     const fallback = strokeWidth(context, style);
1596     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.underbar_rule_thickness, style, fallback);
1597     return fallback;
1598 }
1599 
1600 fn fractionNumeratorGap(context: *const Context, style: Style, fraction_style: ast.FractionStyle) i32 {
1601     const fallback = @max(2, @divTrunc(style.font_size, 6));
1602     if (mathConstants(context)) |constants| {
1603         const value = switch (fraction_style) {
1604             .text => constants.fraction_numerator_gap_min,
1605             .display => constants.fraction_num_display_style_gap_min,
1606         };
1607         return positiveMathValuePixels(context, value, style, fallback);
1608     }
1609     return fallback;
1610 }
1611 
1612 fn fractionDenominatorGap(context: *const Context, style: Style, fraction_style: ast.FractionStyle) i32 {
1613     const fallback = @max(2, @divTrunc(style.font_size, 6));
1614     if (mathConstants(context)) |constants| {
1615         const value = switch (fraction_style) {
1616             .text => constants.fraction_denominator_gap_min,
1617             .display => constants.fraction_denom_display_style_gap_min,
1618         };
1619         return positiveMathValuePixels(context, value, style, fallback);
1620     }
1621     return fallback;
1622 }
1623 
1624 fn fractionNumeratorShiftUp(context: *const Context, style: Style, fraction_style: ast.FractionStyle) i32 {
1625     const fallback = @max(1, @divTrunc(style.font_size, 2));
1626     if (mathConstants(context)) |constants| {
1627         const value = switch (fraction_style) {
1628             .text => constants.fraction_numerator_shift_up,
1629             .display => constants.fraction_numerator_display_style_shift_up,
1630         };
1631         return positiveMathValuePixels(context, value, style, fallback);
1632     }
1633     return fallback;
1634 }
1635 
1636 fn fractionDenominatorShiftDown(context: *const Context, style: Style, fraction_style: ast.FractionStyle) i32 {
1637     const fallback = @max(1, @divTrunc(style.font_size, 2));
1638     if (mathConstants(context)) |constants| {
1639         const value = switch (fraction_style) {
1640             .text => constants.fraction_denominator_shift_down,
1641             .display => constants.fraction_denominator_display_style_shift_down,
1642         };
1643         return positiveMathValuePixels(context, value, style, fallback);
1644     }
1645     return fallback;
1646 }
1647 
1648 fn upperLimitGap(context: *const Context, style: Style) i32 {
1649     const fallback = @max(1, @divTrunc(style.font_size, 8));
1650     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.upper_limit_gap_min, style, fallback);
1651     return fallback;
1652 }
1653 
1654 fn upperLimitBaselineRise(context: *const Context, style: Style) i32 {
1655     const fallback = @max(1, @divTrunc(style.font_size, 2));
1656     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.upper_limit_baseline_rise_min, style, fallback);
1657     return fallback;
1658 }
1659 
1660 fn lowerLimitGap(context: *const Context, style: Style) i32 {
1661     const fallback = @max(1, @divTrunc(style.font_size, 8));
1662     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.lower_limit_gap_min, style, fallback);
1663     return fallback;
1664 }
1665 
1666 fn lowerLimitBaselineDrop(context: *const Context, style: Style) i32 {
1667     const fallback = @max(1, @divTrunc(style.font_size, 2));
1668     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.lower_limit_baseline_drop_min, style, fallback);
1669     return fallback;
1670 }
1671 
1672 fn scriptHorizontalGap(context: *const Context, style: Style) i32 {
1673     const fallback = @max(1, @divTrunc(style.font_size, 8));
1674     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.space_after_script, style, fallback);
1675     return fallback;
1676 }
1677 
1678 fn subscriptShiftDown(context: *const Context, style: Style) i32 {
1679     const fallback = @max(1, @divTrunc(style.font_size, 3));
1680     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.subscript_shift_down, style, fallback);
1681     return fallback;
1682 }
1683 
1684 fn subscriptTopMax(context: *const Context, style: Style) i32 {
1685     const fallback = @max(1, @divTrunc(style.font_size * 2, 5));
1686     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.subscript_top_max, style, fallback);
1687     return fallback;
1688 }
1689 
1690 fn subscriptBaselineDropMin(context: *const Context, style: Style) i32 {
1691     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.subscript_baseline_drop_min, style, 0);
1692     return 0;
1693 }
1694 
1695 fn superscriptShiftUp(context: *const Context, style: Style) i32 {
1696     const fallback = @max(1, @divTrunc(style.font_size * 2, 3));
1697     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.superscript_shift_up, style, fallback);
1698     return fallback;
1699 }
1700 
1701 fn superscriptBottomMin(context: *const Context, style: Style) i32 {
1702     const fallback = @max(1, @divTrunc(style.font_size, 4));
1703     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.superscript_bottom_min, style, fallback);
1704     return fallback;
1705 }
1706 
1707 fn superscriptBaselineDropMax(context: *const Context, style: Style) i32 {
1708     const fallback = @max(0, @divTrunc(style.font_size, 4));
1709     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.superscript_baseline_drop_max, style, fallback);
1710     return fallback;
1711 }
1712 
1713 fn subSuperscriptGap(context: *const Context, style: Style) i32 {
1714     const fallback = strokeWidth(context, style) * 4;
1715     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.sub_superscript_gap_min, style, fallback);
1716     return fallback;
1717 }
1718 
1719 fn superscriptBottomMaxWithSubscript(context: *const Context, style: Style) i32 {
1720     const fallback = @max(superscriptBottomMin(context, style), subscriptTopMax(context, style));
1721     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.superscript_bottom_max_with_subscript, style, fallback);
1722     return fallback;
1723 }
1724 
1725 fn stretchStackTopShiftUp(context: *const Context, style: Style) i32 {
1726     const fallback = @max(1, @divTrunc(style.font_size, 2));
1727     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.stretch_stack_top_shift_up, style, fallback);
1728     return fallback;
1729 }
1730 
1731 fn stretchStackBottomShiftDown(context: *const Context, style: Style) i32 {
1732     const fallback = @max(1, @divTrunc(style.font_size, 2));
1733     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.stretch_stack_bottom_shift_down, style, fallback);
1734     return fallback;
1735 }
1736 
1737 fn stretchStackGapAbove(context: *const Context, style: Style) i32 {
1738     const fallback = @max(1, @divTrunc(style.font_size, 8));
1739     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.stretch_stack_gap_above_min, style, fallback);
1740     return fallback;
1741 }
1742 
1743 fn stretchStackGapBelow(context: *const Context, style: Style) i32 {
1744     const fallback = @max(1, @divTrunc(style.font_size, 8));
1745     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.stretch_stack_gap_below_min, style, fallback);
1746     return fallback;
1747 }
1748 
1749 fn stackTopShiftUp(context: *const Context, style: Style) i32 {
1750     if (mathConstants(context)) |constants| {
1751         const display = constants.stack_top_display_style_shift_up;
1752         if (display.value > 0) return positiveMathValuePixels(context, display, style, 0);
1753         return positiveMathValuePixels(context, constants.stack_top_shift_up, style, 0);
1754     }
1755     return 0;
1756 }
1757 
1758 fn stackBottomShiftDown(context: *const Context, style: Style) i32 {
1759     if (mathConstants(context)) |constants| {
1760         const display = constants.stack_bottom_display_style_shift_down;
1761         if (display.value > 0) return positiveMathValuePixels(context, display, style, 0);
1762         return positiveMathValuePixels(context, constants.stack_bottom_shift_down, style, 0);
1763     }
1764     return 0;
1765 }
1766 
1767 fn stackGap(context: *const Context, style: Style) i32 {
1768     const fallback = @max(1, @divTrunc(style.font_size, 8));
1769     if (mathConstants(context)) |constants| {
1770         const display = constants.stack_display_style_gap_min;
1771         if (display.value > 0) return positiveMathValuePixels(context, display, style, fallback);
1772         return positiveMathValuePixels(context, constants.stack_gap_min, style, fallback);
1773     }
1774     return fallback;
1775 }
1776 
1777 fn overbarGap(context: *const Context, style: Style) i32 {
1778     const fallback = @max(2, @divTrunc(style.font_size, 8));
1779     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.overbar_vertical_gap, style, fallback);
1780     return fallback;
1781 }
1782 
1783 fn underbarGap(context: *const Context, style: Style) i32 {
1784     const fallback = @max(2, @divTrunc(style.font_size, 8));
1785     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.underbar_vertical_gap, style, fallback);
1786     return fallback;
1787 }
1788 
1789 fn overbarExtraAscender(context: *const Context, style: Style) i32 {
1790     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.overbar_extra_ascender, style, 0);
1791     return 0;
1792 }
1793 
1794 fn underbarExtraDescender(context: *const Context, style: Style) i32 {
1795     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.underbar_extra_descender, style, 0);
1796     return 0;
1797 }
1798 
1799 fn delimitedSubFormulaMinHeight(context: *const Context, style: Style) i32 {
1800     if (mathConstants(context)) |constants| {
1801         if (constants.delimited_sub_formula_min_height != 0) return @max(1, designUnitsToPixelsCeil(context, constants.delimited_sub_formula_min_height, style));
1802     }
1803     return 0;
1804 }
1805 
1806 fn displayOperatorMinHeight(context: *const Context, style: Style) ?i32 {
1807     if (mathConstants(context)) |constants| {
1808         if (constants.display_operator_min_height != 0) return @max(1, designUnitsToPixelsCeil(context, constants.display_operator_min_height, style));
1809     }
1810     return null;
1811 }
1812 
1813 fn radicalClearance(context: *const Context, style: Style) i32 {
1814     const rule = radicalRuleWidth(context, style);
1815     const fallback = @max(rule + @divTrunc(rule + 3, 4), @divTrunc(style.font_size, 8));
1816     if (mathConstants(context)) |constants| {
1817         const display = constants.radical_display_style_vertical_gap;
1818         if (display.value != 0) return positiveMathValuePixels(context, display, style, fallback);
1819         return positiveMathValuePixels(context, constants.radical_vertical_gap, style, fallback);
1820     }
1821     return fallback;
1822 }
1823 
1824 fn radicalExtraAscender(context: *const Context, style: Style) i32 {
1825     const fallback = radicalRuleWidth(context, style);
1826     if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.radical_extra_ascender, style, fallback);
1827     return fallback;
1828 }
1829 
1830 fn radicalKernBeforeDegree(context: *const Context, style: Style) i32 {
1831     const fallback = @divTrunc(style.font_size * 5 + 9, 18);
1832     if (mathConstants(context)) |constants| return scaleSigned(constants.radical_kern_before_degree.value, context.font.face.units_per_em, style.font_size);
1833     return fallback;
1834 }
1835 
1836 fn radicalKernAfterDegree(context: *const Context, style: Style) i32 {
1837     const fallback = -@divTrunc(style.font_size * 10 + 9, 18);
1838     if (mathConstants(context)) |constants| return scaleSigned(constants.radical_kern_after_degree.value, context.font.face.units_per_em, style.font_size);
1839     return fallback;
1840 }
1841 
1842 fn radicalDegreeBottomRaisePercent(context: *const Context) i64 {
1843     if (mathConstants(context)) |constants| {
1844         if (constants.radical_degree_bottom_raise_percent > 0) return constants.radical_degree_bottom_raise_percent;
1845     }
1846     return 60;
1847 }
1848 
1849 fn radicalWidth(context: *const Context, height: i32, style: Style) i32 {
1850     return @max(@divTrunc(style.font_size, 2), @divTrunc(height, 4) + radicalRuleWidth(context, style) * 2);
1851 }
1852 
1853 fn mathAxisHeight(context: *const Context, style: Style) i32 {
1854     const fallback = @max(1, @divTrunc(style.font_size, 4));
1855     if (mathConstants(context)) |constants| {
1856         if (constants.axis_height.value != 0) return @max(0, scaleSigned(constants.axis_height.value, context.font.face.units_per_em, style.font_size));
1857     }
1858     return fallback;
1859 }
1860 
1861 fn stretchArrowLabelPadding(style: Style) i32 {
1862     return @max(6, @divTrunc(style.font_size, 2));
1863 }
1864 
1865 fn stretchArrowHeadOverlap(style: Style) i32 {
1866     return @max(2, @divTrunc(style.font_size, 3));
1867 }
1868 
1869 fn stretchArrowMinimumWidth(left_head: ?Box, right_head: ?Box, style: Style) i32 {
1870     var head_width: i32 = 0;
1871     if (left_head) |box| head_width += box.width;
1872     if (right_head) |box| head_width += box.width;
1873     return @max(@max(style.font_size * 2, 12), head_width + @max(style.font_size, 8));
1874 }
1875 
1876 fn stretchArrowAxis(context: *const Context, style: Style, baseline: i32, height: i32, rule: i32) i32 {
1877     const raw = baseline - mathAxisHeight(context, style);
1878     const low = @max(0, @divTrunc(rule, 2));
1879     const high = @max(low, height - @divTrunc(rule + 1, 2));
1880     return @min(@max(raw, low), high);
1881 }
1882 
1883 fn accentMarkX(context: *Context, body_expr: *const ast.Expr, body: Box, mark_text: []const u8, mark: Box, body_style: Style, mark_style: Style) i32 {
1884     const body_anchor = accentAttachmentX(context, singleGlyphId(context, body_expr), body, body_style);
1885     const mark_anchor = accentAttachmentX(context, singleTextGlyphId(context, mark_text), mark, mark_style);
1886     return body_anchor - mark_anchor;
1887 }
1888 
1889 fn accentAttachmentX(context: *const Context, glyph_id: ?u32, box: Box, style: Style) i32 {
1890     if (glyph_id) |id| {
1891         if (context.font.face.mathTopAccentAttachment(id)) |value| {
1892             return scaleSigned(value.value, context.font.face.units_per_em, style.font_size);
1893         }
1894     }
1895     return @divTrunc(box.width, 2);
1896 }
1897 
1898 fn mathConstants(context: *const Context) ?filigree.MathConstants {
1899     return context.font.face.mathConstants();
1900 }
1901 
1902 fn positiveMathValuePixels(context: *const Context, value: filigree.MathValueRecord, style: Style, fallback: i32) i32 {
1903     if (value.value <= 0) return fallback;
1904     return @max(1, scaleSigned(value.value, context.font.face.units_per_em, style.font_size));
1905 }
1906 
1907 fn scriptPercent(value: i16) i64 {
1908     return scriptPercentOr(value, 70);
1909 }
1910 
1911 fn scriptPercentOr(value: i16, fallback: i64) i64 {
1912     if (value <= 0) return fallback;
1913     if (value > 100) return 100;
1914     return @intCast(value);
1915 }
1916 
1917 fn delimiterStyle(context: *Context, height: i32, style: Style) Style {
1918     const metrics = context.fontMetrics(style.font_size);
1919     if (height <= metrics.line_height) return style;
1920     const extra = @max(2, @divTrunc(style.font_size, 6));
1921     const desired = height + extra;
1922     const scaled = @divTrunc(@as(i64, style.font_size) * desired + metrics.line_height - 1, metrics.line_height);
1923     return .{ .font_size = @max(style.font_size, @as(i32, @intCast(scaled))) };
1924 }
1925 
1926 fn spacePixels(value: ast.Space, style: Style) i32 {
1927     if (value.numerator == 0) return 0;
1928     const numerator = @as(i64, style.font_size) * @as(i64, value.numerator);
1929     const denominator: i64 = @intCast(value.denominator);
1930     const rounded = if (numerator >= 0) numerator + @divTrunc(denominator, 2) else numerator - @divTrunc(denominator, 2);
1931     const pixels = @as(i32, @intCast(@divTrunc(rounded, denominator)));
1932     if (pixels == 0) return if (value.numerator > 0) 1 else -1;
1933     return pixels;
1934 }
1935 
1936 fn mathClassSpace(children: []const Box, index: usize, style: Style) i32 {
1937     const left = children[index - 1];
1938     const right = children[index];
1939     if (left.width == 0 or right.width == 0) return 0;
1940     if (left.class == .spacing or right.class == .spacing) return 0;
1941     if (left.class == .relation or right.class == .relation) return relationSpace(left.class, right.class, style);
1942     if (right.class == .binary) return binaryBeforeSpace(left.class, style);
1943     if (left.class == .binary) return binaryAfterSpace(children, index, right.class, style);
1944     if (left.class == .punctuation) return muSpace(style, 3);
1945     if (left.class == .operator and followsOperator(right.class)) return muSpace(style, 3);
1946     return 0;
1947 }
1948 
1949 fn relationSpace(left: MathClass, right: MathClass, style: Style) i32 {
1950     if (left == .open or right == .close or right == .punctuation) return 0;
1951     return muSpace(style, 5);
1952 }
1953 
1954 fn binaryBeforeSpace(left: MathClass, style: Style) i32 {
1955     if (precedesBinary(left)) return muSpace(style, 4);
1956     return 0;
1957 }
1958 
1959 fn binaryAfterSpace(children: []const Box, index: usize, right: MathClass, style: Style) i32 {
1960     if (index <= 1) return 0;
1961     if (!precedesBinary(children[index - 2].class)) return 0;
1962     if (followsBinary(right)) return muSpace(style, 4);
1963     return 0;
1964 }
1965 
1966 fn precedesBinary(class: MathClass) bool {
1967     return switch (class) {
1968         .ordinary, .operator, .close, .inner => true,
1969         .binary, .relation, .open, .punctuation, .spacing => false,
1970     };
1971 }
1972 
1973 fn followsBinary(class: MathClass) bool {
1974     return switch (class) {
1975         .ordinary, .operator, .open, .inner => true,
1976         .binary, .relation, .close, .punctuation, .spacing => false,
1977     };
1978 }
1979 
1980 fn followsOperator(class: MathClass) bool {
1981     return switch (class) {
1982         .ordinary, .operator, .open, .inner => true,
1983         .binary, .relation, .close, .punctuation, .spacing => false,
1984     };
1985 }
1986 
1987 fn muSpace(style: Style, mu: i32) i32 {
1988     return spacePixels(.{ .numerator = mu, .denominator = 18 }, style);
1989 }
1990 
1991 fn mathClassForText(value: []const u8) MathClass {
1992     if (isBinaryText(value)) return .binary;
1993     if (isRelationText(value)) return .relation;
1994     if (operator.limitsText(value)) return .operator;
1995     if (isOperatorText(value)) return .operator;
1996     if (isOpenText(value)) return .open;
1997     if (isCloseText(value)) return .close;
1998     if (isPunctuationText(value)) return .punctuation;
1999     return .ordinary;
2000 }
2001 
2002 fn isBinaryText(value: []const u8) bool {
2003     inline for (.{
2004         "+",
2005         "-",
2006         "*",
2007         "/",
2008         "±",
2009         "∓",
2010         "×",
2011         "÷",
2012         "·",
2013         "∗",
2014         "⋆",
2015         "⋄",
2016         "∘",
2017         "∧",
2018         "∨",
2019         "∪",
2020         "∩",
2021         "∖",
2022         "⊕",
2023         "⊖",
2024         "⊗",
2025         "⊘",
2026         "⊙",
2027         "⊛",
2028         "⊚",
2029         "⊝",
2030         "⊞",
2031         "⊟",
2032         "⊠",
2033         "⊔",
2034         "⊓",
2035         "⊎",
2036         "≀",
2037         "∣",
2038     }) |candidate| {
2039         if (std.mem.eql(u8, value, candidate)) return true;
2040     }
2041     return false;
2042 }
2043 
2044 fn isRelationText(value: []const u8) bool {
2045     inline for (.{
2046         "=",
2047         "<",
2048         ">",
2049         "≤",
2050         "≰",
2051         "≲",
2052         "⪅",
2053         "≪",
2054         "⋘",
2055         "≥",
2056         "≱",
2057         "≳",
2058         "⪆",
2059         "≫",
2060         "≠",
2061         "≮",
2062         "≯",
2063         "≐",
2064         "≜",
2065         "≔",
2066         "≕",
2067         "≡",
2068         "≢",
2069         "≈",
2070         "≉",
2071         "≍",
2072         "∼",
2073         "≁",
2074         "≃",
2075         "≄",
2076         "≾",
2077         "≿",
2078         "≅",
2079         "≇",
2080         "∝",
2081         "≺",
2082         "≻",
2083         "≼",
2084         "≽",
2085         "⊥",
2086         "⊤",
2087         "⋈",
2088         "∈",
2089         "∉",
2090         "∋",
2091         "∌",
2092         "⊂",
2093         "⊃",
2094         "⊆",
2095         "⊇",
2096         "⊈",
2097         "⊉",
2098         "⊊",
2099         "⊋",
2100         "⊏",
2101         "⊐",
2102         "⊑",
2103         "⊒",
2104         "⋢",
2105         "⋣",
2106         "⊢",
2107         "⊩",
2108         "⊨",
2109         "⊪",
2110         "⊣",
2111         "⊬",
2112         "⊭",
2113         "⊮",
2114         "⊯",
2115         "∥",
2116         "∦",
2117         "∤",
2118         "→",
2119         "⟶",
2120         "←",
2121         "⟵",
2122         "↔",
2123         "⟷",
2124         "⇒",
2125         "⟹",
2126         "⇐",
2127         "⟸",
2128         "⇔",
2129         "⟺",
2130         "⇏",
2131         "⇍",
2132         "⇎",
2133         "↦",
2134         "⟼",
2135         "⊸",
2136         "↪",
2137         "↩",
2138         "↠",
2139         "↞",
2140         "↣",
2141         "↢",
2142         "↝",
2143         "⇝",
2144         "⇀",
2145         "⇁",
2146         "↼",
2147         "↽",
2148         "⇌",
2149     }) |candidate| {
2150         if (std.mem.eql(u8, value, candidate)) return true;
2151     }
2152     return false;
2153 }
2154 
2155 fn isOperatorText(value: []const u8) bool {
2156     inline for (.{
2157         "∑",
2158         "⨁",
2159         "⨂",
2160         "⨀",
2161         "∏",
2162         "∐",
2163         "∫",
2164         "∬",
2165         "∭",
2166         "∮",
2167         "⋀",
2168         "⋁",
2169         "⋃",
2170         "⋂",
2171     }) |candidate| {
2172         if (std.mem.eql(u8, value, candidate)) return true;
2173     }
2174     return false;
2175 }
2176 
2177 fn isOpenText(value: []const u8) bool {
2178     inline for (.{
2179         "(",
2180         "[",
2181         "{",
2182         "⟨",
2183         "⟪",
2184         "⟦",
2185         "⌊",
2186         "⌈",
2187         "⌜",
2188         "⌞",
2189     }) |candidate| {
2190         if (std.mem.eql(u8, value, candidate)) return true;
2191     }
2192     return false;
2193 }
2194 
2195 fn isCloseText(value: []const u8) bool {
2196     inline for (.{
2197         ")",
2198         "]",
2199         "}",
2200         "⟩",
2201         "⟫",
2202         "⟧",
2203         "⌋",
2204         "⌉",
2205         "⌝",
2206         "⌟",
2207     }) |candidate| {
2208         if (std.mem.eql(u8, value, candidate)) return true;
2209     }
2210     return false;
2211 }
2212 
2213 fn isPunctuationText(value: []const u8) bool {
2214     inline for (.{
2215         ",",
2216         ";",
2217         "…",
2218         "⋯",
2219     }) |candidate| {
2220         if (std.mem.eql(u8, value, candidate)) return true;
2221     }
2222     return false;
2223 }
2224 
2225 fn accentText(mark: ast.AccentMark) []const u8 {
2226     return switch (mark) {
2227         .hat => "^",
2228         .vec => "→",
2229         .dot => "˙",
2230         .tilde => "~",
2231         .check => "ˇ",
2232         .breve => "˘",
2233         .ddot => "¨",
2234         .acute => "´",
2235         .grave => "`",
2236         .ring => "˚",
2237         .overleft => "←",
2238         .overleftright => "↔",
2239         .bar, .underline => unreachable,
2240     };
2241 }
2242 
2243 fn delimiterShapeText(shape: ast.DelimiterShape) []const u8 {
2244     return switch (shape) {
2245         .left_paren => "(",
2246         .right_paren => ")",
2247         .left_brace => "{",
2248         .right_brace => "}",
2249         .left_angle => "⟨",
2250         .right_angle => "⟩",
2251         .left_double_angle => "⟪",
2252         .right_double_angle => "⟫",
2253         .left_double_bracket => "⟦",
2254         .right_double_bracket => "⟧",
2255         .left_bracket, .right_bracket, .bar, .double_bar, .left_floor, .right_floor, .left_ceil, .right_ceil => "|",
2256     };
2257 }
2258 
2259 fn delimiterShapeCodepoint(shape: ast.DelimiterShape) ?u21 {
2260     return switch (shape) {
2261         .left_paren => '(',
2262         .right_paren => ')',
2263         .left_bracket => '[',
2264         .right_bracket => ']',
2265         .left_brace => '{',
2266         .right_brace => '}',
2267         .left_angle => 0x27e8,
2268         .right_angle => 0x27e9,
2269         .left_double_angle => 0x27ea,
2270         .right_double_angle => 0x27eb,
2271         .left_double_bracket => 0x27e6,
2272         .right_double_bracket => 0x27e7,
2273         .left_floor => 0x230a,
2274         .right_floor => 0x230b,
2275         .left_ceil => 0x2308,
2276         .right_ceil => 0x2309,
2277         .bar, .double_bar => null,
2278     };
2279 }
2280 
2281 fn gridLeftDelimiter(fence: ast.GridFence) ast.Delimiter {
2282     return switch (fence) {
2283         .none => .none,
2284         .paren => .{ .shape = .left_paren },
2285         .bracket => .{ .shape = .left_bracket },
2286         .brace, .left_brace => .{ .shape = .left_brace },
2287         .bar => .{ .shape = .bar },
2288         .double_bar => .{ .shape = .double_bar },
2289     };
2290 }
2291 
2292 fn gridRightDelimiter(fence: ast.GridFence) ast.Delimiter {
2293     return switch (fence) {
2294         .none, .left_brace => .none,
2295         .paren => .{ .shape = .right_paren },
2296         .bracket => .{ .shape = .right_bracket },
2297         .brace => .{ .shape = .right_brace },
2298         .bar => .{ .shape = .bar },
2299         .double_bar => .{ .shape = .double_bar },
2300     };
2301 }
2302 
2303 fn imageDimension(content: i32, padding: u32) !u32 {
2304     const padded = content + @as(i32, @intCast(padding)) * 2;
2305     if (padded <= 0) return 1;
2306     return std.math.cast(u32, padded) orelse error.InvalidDimensions;
2307 }
2308 
2309 fn centered(width: i32, inner: i32) i32 {
2310     if (inner >= width) return 0;
2311     return @divTrunc(width - inner, 2);
2312 }
2313 
2314 fn scaleSigned(value: i32, units_per_em: u16, font_size: i32) i32 {
2315     const numerator = @as(i64, value) * @as(i64, font_size);
2316     const denominator: i64 = units_per_em;
2317     const rounded = if (numerator >= 0) numerator + @divTrunc(denominator, 2) else numerator - @divTrunc(denominator, 2);
2318     return @intCast(@divTrunc(rounded, denominator));
2319 }
2320 
2321 fn pixelHeightToDesignUnits(context: *const Context, height: i32, style: Style) u16 {
2322     return pixelMeasurementToDesignUnits(context, height, style);
2323 }
2324 
2325 fn pixelWidthToDesignUnits(context: *const Context, width: i32, style: Style) u16 {
2326     return pixelMeasurementToDesignUnits(context, width, style);
2327 }
2328 
2329 fn pixelMeasurementToDesignUnits(context: *const Context, measurement: i32, style: Style) u16 {
2330     const pixels = @max(1, measurement);
2331     const numerator = @as(i64, pixels) * @as(i64, context.font.face.units_per_em) + @divTrunc(style.font_size, 2);
2332     const design = @divTrunc(numerator, style.font_size);
2333     if (design <= 0) return 1;
2334     if (design > std.math.maxInt(u16)) return std.math.maxInt(u16);
2335     return @intCast(design);
2336 }
2337 
2338 fn designUnitsToPixelsFloor(context: *const Context, value: u16, style: Style) i32 {
2339     return scaleFloor(value, context.font.face.units_per_em, style.font_size);
2340 }
2341 
2342 fn designUnitsToPixelsCeil(context: *const Context, value: u16, style: Style) i32 {
2343     return scaleCeil(value, context.font.face.units_per_em, style.font_size);
2344 }
2345 
2346 fn glyphInkBounds(context: *Context, glyph_id: u32, style: Style) !?GlyphInkBounds {
2347     var outline = filigree.glyphOutlineAlloc(context.scratch, context.font.face, glyph_id) catch |err| switch (err) {
2348         error.OutOfMemory => return err,
2349         else => return null,
2350     };
2351     defer outline.deinit(context.scratch);
2352     if (outline.points.len == 0 or outline.bounds.x_max <= outline.bounds.x_min or outline.bounds.y_max <= outline.bounds.y_min) return null;
2353     return .{
2354         .left = scaleFloor(outline.bounds.x_min, context.font.face.units_per_em, style.font_size),
2355         .top = scaleFloor(@as(i32, context.font.face.ascender) - outline.bounds.y_max, context.font.face.units_per_em, style.font_size),
2356         .right = scaleCeil(outline.bounds.x_max, context.font.face.units_per_em, style.font_size),
2357         .bottom = scaleCeil(@as(i32, context.font.face.ascender) - outline.bounds.y_min, context.font.face.units_per_em, style.font_size),
2358     };
2359 }
2360 
2361 fn singleGlyphId(context: *Context, expr: *const ast.Expr) ?u32 {
2362     const value = ast.textValue(expr) orelse return null;
2363     return singleTextGlyphId(context, value);
2364 }
2365 
2366 fn singleTextGlyphId(context: *const Context, value: []const u8) ?u32 {
2367     if (value.len == 0) return null;
2368     const sequence_len = std.unicode.utf8ByteSequenceLength(value[0]) catch return null;
2369     if (sequence_len != value.len) return null;
2370     const codepoint = std.unicode.utf8Decode(value[0..sequence_len]) catch return null;
2371     const glyph_id = context.font.face.glyphId(codepoint);
2372     if (glyph_id == 0) return null;
2373     return glyph_id;
2374 }
2375 
2376 fn scaleFloor(value: i32, units_per_em: u16, font_size: i32) i32 {
2377     const numerator = @as(i64, value) * @as(i64, font_size);
2378     return @intCast(divFloor(numerator, units_per_em));
2379 }
2380 
2381 fn scaleCeil(value: i32, units_per_em: u16, font_size: i32) i32 {
2382     const numerator = @as(i64, value) * @as(i64, font_size);
2383     return @intCast(-divFloor(-numerator, units_per_em));
2384 }
2385 
2386 fn divFloor(numerator: i64, denominator: i64) i64 {
2387     var quotient = @divTrunc(numerator, denominator);
2388     const remainder = @rem(numerator, denominator);
2389     if (remainder != 0 and numerator < 0) quotient -= 1;
2390     return quotient;
2391 }
2392 
2393 fn drawRect(canvas: filigree.render.Canvas, x: i32, y: i32, width: i32, height: i32, color: Color) void {
2394     if (width <= 0 or height <= 0) return;
2395     const x0 = @max(0, x);
2396     const y0 = @max(0, y);
2397     const x1 = @min(@as(i32, @intCast(canvas.width)), x + width);
2398     const y1 = @min(@as(i32, @intCast(canvas.height)), y + height);
2399     if (x0 >= x1 or y0 >= y1) return;
2400     var py: usize = @intCast(y0);
2401     while (py < @as(usize, @intCast(y1))) : (py += 1) {
2402         var px: usize = @intCast(x0);
2403         while (px < @as(usize, @intCast(x1))) : (px += 1) {
2404             const offset = py * canvas.stride + px * 4;
2405             canvas.pixels[offset] = color.r;
2406             canvas.pixels[offset + 1] = color.g;
2407             canvas.pixels[offset + 2] = color.b;
2408             canvas.pixels[offset + 3] = color.a;
2409         }
2410     }
2411 }
2412 
2413 fn drawLine(canvas: filigree.render.Canvas, x0: i32, y0: i32, x1: i32, y1: i32, width: i32, color: Color) void {
2414     if (width <= 0) return;
2415     var x = x0;
2416     var y = y0;
2417     const dx = if (x1 >= x0) x1 - x0 else x0 - x1;
2418     const dy = if (y1 >= y0) y1 - y0 else y0 - y1;
2419     const sx: i32 = if (x0 < x1) 1 else -1;
2420     const sy: i32 = if (y0 < y1) 1 else -1;
2421     var err = dx - dy;
2422     const offset = @divTrunc(width, 2);
2423     while (true) {
2424         drawRect(canvas, x - offset, y - offset, width, width, color);
2425         if (x == x1 and y == y1) break;
2426         const e2 = err * 2;
2427         if (e2 > -dy) {
2428             err -= dy;
2429             x += sx;
2430         }
2431         if (e2 < dx) {
2432             err += dx;
2433             y += sy;
2434         }
2435     }
2436 }
2437 
2438 fn validateOptions(options: Options) !void {
2439     if (options.font_bytes.len == 0) return error.InvalidFont;
2440     if (options.font_size <= 0) return error.InvalidPixelSize;
2441 }
2442 
2443 fn loadFallbackFonts(allocator: std.mem.Allocator, fonts: []const []const u8) ![]filigree.Font {
2444     const loaded = try allocator.alloc(filigree.Font, fonts.len);
2445     errdefer allocator.free(loaded);
2446     var index: usize = 0;
2447     errdefer {
2448         for (loaded[0..index]) |*font| font.deinit();
2449     }
2450     while (index < fonts.len) : (index += 1) {
2451         const bytes = fonts[index];
2452         loaded[index] = filigree.Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.InvalidFont;
2453     }
2454     return loaded;
2455 }
2456 
2457 fn unloadFallbackFonts(allocator: std.mem.Allocator, fonts: []filigree.Font) void {
2458     for (fonts) |*font| font.deinit();
2459     allocator.free(fonts);
2460 }
2461 
2462 fn fill(pixels: []u8, width: u32, height: u32, stride: u32, color: Color) void {
2463     var row: u32 = 0;
2464     while (row < height) : (row += 1) {
2465         var col: u32 = 0;
2466         while (col < width) : (col += 1) {
2467             const offset = @as(usize, row) * @as(usize, stride) + @as(usize, col) * 4;
2468             pixels[offset] = color.r;
2469             pixels[offset + 1] = color.g;
2470             pixels[offset + 2] = color.b;
2471             pixels[offset + 3] = color.a;
2472         }
2473     }
2474 }
2475 
2476 fn visiblePixels(pixels: []const u8) usize {
2477     var count: usize = 0;
2478     var index: usize = 3;
2479     while (index < pixels.len) : (index += 4) {
2480         if (pixels[index] != 0) count += 1;
2481     }
2482     return count;
2483 }
2484 
2485 fn visiblePixelsInRect(image: *const Image, x: u32, y: u32, width: u32, height: u32) usize {
2486     const x1 = @min(image.width, x + width);
2487     const y1 = @min(image.height, y + height);
2488     var count: usize = 0;
2489     var py = y;
2490     while (py < y1) : (py += 1) {
2491         var px = x;
2492         while (px < x1) : (px += 1) {
2493             const offset = @as(usize, py) * image.stride + @as(usize, px) * 4 + 3;
2494             if (image.pixels[offset] != 0) count += 1;
2495         }
2496     }
2497     return count;
2498 }
2499 
2500 test "image renderer draws parsed math into owned rgba pixels" {
2501     const allocator = std.testing.allocator;
2502     const bytes = try filigree.fixtures.createWithOutlines(allocator);
2503     defer allocator.free(bytes);
2504 
2505     var rendered = try render(allocator, "A+B", .{
2506         .font_bytes = bytes,
2507         .font_size = 20,
2508     });
2509     defer rendered.deinit();
2510 
2511     try std.testing.expect(rendered.width > 20);
2512     try std.testing.expect(rendered.height > 20);
2513     try std.testing.expect(visiblePixels(rendered.pixels) > 0);
2514 }
2515 
2516 test "image renderer lays out fractions as pixels instead of cells" {
2517     const allocator = std.testing.allocator;
2518     const bytes = try filigree.fixtures.createWithOutlines(allocator);
2519     defer allocator.free(bytes);
2520 
2521     var rendered = try render(allocator, "\\frac{A}{B}", .{
2522         .font_bytes = bytes,
2523         .font_size = 20,
2524         .padding_x = 0,
2525         .padding_y = 0,
2526     });
2527     defer rendered.deinit();
2528 
2529     try std.testing.expect(rendered.width < 40);
2530     try std.testing.expect(rendered.height > 40);
2531     try std.testing.expect(rendered.baseline < rendered.height);
2532     try std.testing.expect(visiblePixels(rendered.pixels) > 0);
2533 }
2534 
2535 test "image renderer scales delimiters around tall bodies" {
2536     const allocator = std.testing.allocator;
2537     const bytes = try filigree.fixtures.createWithOutlines(allocator);
2538     defer allocator.free(bytes);
2539 
2540     var fraction = try render(allocator, "\\frac{A+B}{C+D}", .{
2541         .font_bytes = bytes,
2542         .font_size = 20,
2543         .padding_x = 0,
2544         .padding_y = 0,
2545     });
2546     defer fraction.deinit();
2547     var delimited = try render(allocator, "\\left(\\frac{A+B}{C+D}\\right)", .{
2548         .font_bytes = bytes,
2549         .font_size = 20,
2550         .padding_x = 0,
2551         .padding_y = 0,
2552     });
2553     defer delimited.deinit();
2554 
2555     try std.testing.expect(delimited.width > fraction.width);
2556     try std.testing.expect(delimited.height > fraction.height);
2557     try std.testing.expect(delimited.baseline < delimited.height);
2558     try std.testing.expect(visiblePixels(delimited.pixels) > 0);
2559 }
2560 
2561 test "image renderer draws capped bracket fallbacks" {
2562     const allocator = std.testing.allocator;
2563     const bytes = try filigree.fixtures.createWithOutlines(allocator);
2564     defer allocator.free(bytes);
2565 
2566     var rendered = try render(allocator, "\\left[\\frac{A}{B}\\right]", .{
2567         .font_bytes = bytes,
2568         .font_size = 20,
2569         .padding_x = 0,
2570         .padding_y = 0,
2571     });
2572     defer rendered.deinit();
2573 
2574     try std.testing.expect(visiblePixelsInRect(&rendered, 0, 0, @max(1, rendered.width / 5), 4) > 0);
2575     try std.testing.expect(visiblePixelsInRect(&rendered, 0, rendered.height - @min(rendered.height, 4), @max(1, rendered.width / 5), @min(rendered.height, 4)) > 0);
2576 }
2577 
2578 test "image renderer draws double bracket fallbacks" {
2579     const allocator = std.testing.allocator;
2580     const bytes = try filigree.fixtures.createWithOutlines(allocator);
2581     defer allocator.free(bytes);
2582 
2583     var fraction = try render(allocator, "\\frac{A}{B}", .{
2584         .font_bytes = bytes,
2585         .font_size = 24,
2586         .padding_x = 0,
2587         .padding_y = 0,
2588     });
2589     defer fraction.deinit();
2590     var rendered = try render(allocator, "\\left\\llbracket\\frac{A}{B}\\right\\rrbracket", .{
2591         .font_bytes = bytes,
2592         .font_size = 24,
2593         .padding_x = 0,
2594         .padding_y = 0,
2595     });
2596     defer rendered.deinit();
2597 
2598     const edge_width = @min(rendered.width, 16);
2599     const half_edge = @max(1, edge_width / 2);
2600     const band_height = @min(rendered.height, 6);
2601     const right_x = rendered.width - edge_width;
2602     try std.testing.expect(rendered.width > fraction.width);
2603     try std.testing.expect(rendered.height >= fraction.height);
2604     try std.testing.expect(visiblePixelsInRect(&rendered, 0, 0, edge_width, band_height) > 0);
2605     try std.testing.expect(visiblePixelsInRect(&rendered, 0, rendered.height - band_height, edge_width, band_height) > 0);
2606     try std.testing.expect(visiblePixelsInRect(&rendered, 0, 0, half_edge, rendered.height) > 0);
2607     try std.testing.expect(visiblePixelsInRect(&rendered, half_edge, 0, edge_width - half_edge, rendered.height) > 0);
2608     try std.testing.expect(visiblePixelsInRect(&rendered, right_x, 0, edge_width, band_height) > 0);
2609     try std.testing.expect(visiblePixelsInRect(&rendered, right_x, rendered.height - band_height, edge_width, band_height) > 0);
2610     try std.testing.expect(visiblePixelsInRect(&rendered, right_x, 0, half_edge, rendered.height) > 0);
2611     try std.testing.expect(visiblePixelsInRect(&rendered, right_x + half_edge, 0, edge_width - half_edge, rendered.height) > 0);
2612 }
2613 
2614 test "image renderer draws stroked brace fallbacks around tall bodies" {
2615     const allocator = std.testing.allocator;
2616     const bytes = try filigree.fixtures.createWithOutlines(allocator);
2617     defer allocator.free(bytes);
2618 
2619     var fraction = try render(allocator, "\\frac{A}{B}", .{
2620         .font_bytes = bytes,
2621         .font_size = 24,
2622         .padding_x = 0,
2623         .padding_y = 0,
2624     });
2625     defer fraction.deinit();
2626     var rendered = try render(allocator, "\\left\\{\\frac{A}{B}\\right\\}", .{
2627         .font_bytes = bytes,
2628         .font_size = 24,
2629         .padding_x = 0,
2630         .padding_y = 0,
2631     });
2632     defer rendered.deinit();
2633 
2634     const edge_width = @min(rendered.width, 14);
2635     const band_height = @min(rendered.height, 6);
2636     const middle_y = rendered.height / 2 - @min(rendered.height / 2, band_height / 2);
2637     const right_x = rendered.width - edge_width;
2638     try std.testing.expect(rendered.width > fraction.width);
2639     try std.testing.expect(rendered.height > fraction.height);
2640     try std.testing.expect(visiblePixelsInRect(&rendered, 0, 0, edge_width, band_height) > 0);
2641     try std.testing.expect(visiblePixelsInRect(&rendered, 0, middle_y, edge_width, band_height) > 0);
2642     try std.testing.expect(visiblePixelsInRect(&rendered, 0, rendered.height - band_height, edge_width, band_height) > 0);
2643     try std.testing.expect(visiblePixelsInRect(&rendered, right_x, 0, edge_width, band_height) > 0);
2644     try std.testing.expect(visiblePixelsInRect(&rendered, right_x, middle_y, edge_width, band_height) > 0);
2645     try std.testing.expect(visiblePixelsInRect(&rendered, right_x, rendered.height - band_height, edge_width, band_height) > 0);
2646 }
2647 
2648 test "image renderer uses MATH assemblies for tall delimiters" {
2649     const allocator = std.testing.allocator;
2650     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
2651     defer allocator.free(fallback_bytes);
2652     const assembly_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
2653     defer allocator.free(assembly_bytes);
2654 
2655     var fallback = try render(allocator, "\\left(\\frac{A+B}{C+D}\\right)", .{
2656         .font_bytes = fallback_bytes,
2657         .font_size = 20,
2658         .padding_x = 0,
2659         .padding_y = 0,
2660     });
2661     defer fallback.deinit();
2662     var assembled = try render(allocator, "\\left(\\frac{A+B}{C+D}\\right)", .{
2663         .font_bytes = assembly_bytes,
2664         .font_size = 20,
2665         .padding_x = 0,
2666         .padding_y = 0,
2667     });
2668     defer assembled.deinit();
2669 
2670     const fallback_left = visiblePixelsInRect(&fallback, 0, 0, @min(fallback.width, 12), fallback.height);
2671     const assembled_left = visiblePixelsInRect(&assembled, 0, 0, @min(assembled.width, 12), assembled.height);
2672     try std.testing.expect(assembled_left > fallback_left * 2);
2673 }
2674 
2675 test "image renderer uses MATH minimum height for small delimiters" {
2676     const allocator = std.testing.allocator;
2677     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
2678     defer allocator.free(fallback_bytes);
2679     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
2680     defer allocator.free(math_bytes);
2681 
2682     var fallback = try render(allocator, "\\left(A\\right)", .{
2683         .font_bytes = fallback_bytes,
2684         .font_size = 24,
2685         .padding_x = 0,
2686         .padding_y = 0,
2687     });
2688     defer fallback.deinit();
2689     var math = try render(allocator, "\\left(A\\right)", .{
2690         .font_bytes = math_bytes,
2691         .font_size = 24,
2692         .padding_x = 0,
2693         .padding_y = 0,
2694     });
2695     defer math.deinit();
2696 
2697     try std.testing.expect(math.height > fallback.height);
2698     try std.testing.expect(math.baseline > fallback.baseline);
2699     try std.testing.expect(visiblePixelsInRect(&math, 0, 0, @min(math.width, 12), math.height) > 0);
2700 }
2701 
2702 test "image renderer draws scalable native radicals" {
2703     const allocator = std.testing.allocator;
2704     const bytes = try filigree.fixtures.createWithOutlines(allocator);
2705     defer allocator.free(bytes);
2706 
2707     var fraction = try render(allocator, "\\frac{A}{B}", .{
2708         .font_bytes = bytes,
2709         .font_size = 20,
2710         .padding_x = 0,
2711         .padding_y = 0,
2712     });
2713     defer fraction.deinit();
2714     var rooted = try render(allocator, "\\sqrt{\\frac{A}{B}}", .{
2715         .font_bytes = bytes,
2716         .font_size = 20,
2717         .padding_x = 0,
2718         .padding_y = 0,
2719     });
2720     defer rooted.deinit();
2721 
2722     try std.testing.expect(rooted.width > fraction.width + 8);
2723     try std.testing.expect(rooted.height > fraction.height);
2724     try std.testing.expect(rooted.baseline > fraction.baseline);
2725     try std.testing.expect(visiblePixelsInRect(&rooted, 0, rooted.height / 2, rooted.width / 3, rooted.height - rooted.height / 2) > 0);
2726 }
2727 
2728 test "image renderer uses MATH assemblies for radical signs" {
2729     const allocator = std.testing.allocator;
2730     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
2731     defer allocator.free(fallback_bytes);
2732     const assembly_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
2733     defer allocator.free(assembly_bytes);
2734 
2735     var fallback = try render(allocator, "\\sqrt{\\frac{A+B}{C+D}}", .{
2736         .font_bytes = fallback_bytes,
2737         .font_size = 20,
2738         .padding_x = 0,
2739         .padding_y = 0,
2740     });
2741     defer fallback.deinit();
2742     var assembled = try render(allocator, "\\sqrt{\\frac{A+B}{C+D}}", .{
2743         .font_bytes = assembly_bytes,
2744         .font_size = 20,
2745         .padding_x = 0,
2746         .padding_y = 0,
2747     });
2748     defer assembled.deinit();
2749 
2750     const fallback_left = visiblePixelsInRect(&fallback, 0, 0, @min(fallback.width, 12), fallback.height);
2751     const assembled_left = visiblePixelsInRect(&assembled, 0, 0, @min(assembled.width, 12), assembled.height);
2752     try std.testing.expect(assembled.height > fallback.height);
2753     try std.testing.expect(assembled_left > fallback_left * 2);
2754 }
2755 
2756 test "image renderer uses MATH display radical gap" {
2757     const allocator = std.testing.allocator;
2758     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
2759     defer allocator.free(fallback_bytes);
2760     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
2761     defer allocator.free(math_bytes);
2762 
2763     var fallback = try render(allocator, "\\sqrt{A}", .{
2764         .font_bytes = fallback_bytes,
2765         .font_size = 20,
2766         .padding_x = 0,
2767         .padding_y = 0,
2768     });
2769     defer fallback.deinit();
2770     var math = try render(allocator, "\\sqrt{A}", .{
2771         .font_bytes = math_bytes,
2772         .font_size = 20,
2773         .padding_x = 0,
2774         .padding_y = 0,
2775     });
2776     defer math.deinit();
2777 
2778     try std.testing.expect(math.height > fallback.height + 16);
2779     try std.testing.expect(math.baseline > fallback.baseline + 12);
2780     try std.testing.expect(visiblePixels(math.pixels) > 0);
2781 }
2782 
2783 test "image renderer uses MATH radical rule and ascender metrics" {
2784     const allocator = std.testing.allocator;
2785     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
2786     defer allocator.free(fallback_bytes);
2787     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
2788     defer allocator.free(math_bytes);
2789 
2790     var fallback = try render(allocator, "\\sqrt{A}", .{
2791         .font_bytes = fallback_bytes,
2792         .font_size = 20,
2793         .padding_x = 0,
2794         .padding_y = 0,
2795     });
2796     defer fallback.deinit();
2797     var math = try render(allocator, "\\sqrt{A}", .{
2798         .font_bytes = math_bytes,
2799         .font_size = 20,
2800         .padding_x = 0,
2801         .padding_y = 0,
2802     });
2803     defer math.deinit();
2804 
2805     try std.testing.expect(math.height > fallback.height + 22);
2806     try std.testing.expect(math.baseline > fallback.baseline + 18);
2807     try std.testing.expect(visiblePixels(math.pixels) > 0);
2808 }
2809 
2810 test "image renderer tucks radical degrees into the root sign" {
2811     const allocator = std.testing.allocator;
2812     const bytes = try filigree.fixtures.createWithOutlines(allocator);
2813     defer allocator.free(bytes);
2814 
2815     var square = try render(allocator, "\\sqrt{\\frac{A+B}{C+D}}", .{
2816         .font_bytes = bytes,
2817         .font_size = 20,
2818         .padding_x = 0,
2819         .padding_y = 0,
2820     });
2821     defer square.deinit();
2822     var cube = try render(allocator, "\\sqrt[3]{\\frac{A+B}{C+D}}", .{
2823         .font_bytes = bytes,
2824         .font_size = 20,
2825         .padding_x = 0,
2826         .padding_y = 0,
2827     });
2828     defer cube.deinit();
2829 
2830     try std.testing.expect(cube.width > square.width);
2831     try std.testing.expect(cube.width < square.width + 12);
2832 }
2833 
2834 test "image renderer uses MATH script-script scale percentage for radical degrees" {
2835     const allocator = std.testing.allocator;
2836     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
2837     defer allocator.free(math_bytes);
2838 
2839     var square = try render(allocator, "\\sqrt{A}", .{
2840         .font_bytes = math_bytes,
2841         .font_size = 24,
2842         .padding_x = 0,
2843         .padding_y = 0,
2844     });
2845     defer square.deinit();
2846     var degree = try render(allocator, "\\sqrt[BBBBBBBB]{A}", .{
2847         .font_bytes = math_bytes,
2848         .font_size = 24,
2849         .padding_x = 0,
2850         .padding_y = 0,
2851     });
2852     defer degree.deinit();
2853 
2854     try std.testing.expect(degree.width > square.width + 56);
2855     try std.testing.expect(visiblePixels(degree.pixels) > 0);
2856 }
2857 
2858 test "image renderer uses MATH radical degree metrics" {
2859     const allocator = std.testing.allocator;
2860     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
2861     defer allocator.free(math_bytes);
2862 
2863     var square = try render(allocator, "\\sqrt{A}", .{
2864         .font_bytes = math_bytes,
2865         .font_size = 24,
2866         .padding_x = 0,
2867         .padding_y = 0,
2868     });
2869     defer square.deinit();
2870     var degree = try render(allocator, "\\sqrt[3]{A}", .{
2871         .font_bytes = math_bytes,
2872         .font_size = 24,
2873         .padding_x = 0,
2874         .padding_y = 0,
2875     });
2876     defer degree.deinit();
2877 
2878     try std.testing.expect(degree.width > square.width + 26);
2879     try std.testing.expect(degree.height > square.height + 4);
2880     try std.testing.expect(degree.baseline > square.baseline + 4);
2881     try std.testing.expect(visiblePixels(degree.pixels) > 0);
2882 }
2883 
2884 test "image renderer stacks large operator limits" {
2885     const allocator = std.testing.allocator;
2886     const bytes = try filigree.fixtures.createWithOutlines(allocator);
2887     defer allocator.free(bytes);
2888 
2889     var limited = try render(allocator, "lim_{ABC}^{D}", .{
2890         .font_bytes = bytes,
2891         .font_size = 24,
2892         .padding_x = 0,
2893         .padding_y = 0,
2894     });
2895     defer limited.deinit();
2896     var side = try render(allocator, "x_{ABC}^{D}", .{
2897         .font_bytes = bytes,
2898         .font_size = 24,
2899         .padding_x = 0,
2900         .padding_y = 0,
2901     });
2902     defer side.deinit();
2903 
2904     try std.testing.expect(limited.width < side.width);
2905     try std.testing.expect(limited.height > side.height);
2906     try std.testing.expect(visiblePixels(limited.pixels) > 0);
2907 }
2908 
2909 test "image renderer uses MATH constants for operator limits" {
2910     const allocator = std.testing.allocator;
2911     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
2912     defer allocator.free(fallback_bytes);
2913     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
2914     defer allocator.free(math_bytes);
2915 
2916     var fallback = try render(allocator, "lim_{B}^{B}", .{
2917         .font_bytes = fallback_bytes,
2918         .font_size = 24,
2919         .padding_x = 0,
2920         .padding_y = 0,
2921     });
2922     defer fallback.deinit();
2923     var math = try render(allocator, "lim_{B}^{B}", .{
2924         .font_bytes = math_bytes,
2925         .font_size = 24,
2926         .padding_x = 0,
2927         .padding_y = 0,
2928     });
2929     defer math.deinit();
2930 
2931     try std.testing.expect(math.height > fallback.height + 48);
2932     try std.testing.expect(math.baseline > fallback.baseline + 32);
2933     try std.testing.expect(visiblePixels(math.pixels) > 0);
2934 }
2935 
2936 test "image renderer uses MATH display operator minimum height" {
2937     const allocator = std.testing.allocator;
2938     const bytes = try filigree.fixtures.createWithMathAssembly(allocator);
2939     defer allocator.free(bytes);
2940 
2941     var ordinary = try render(allocator, "A", .{
2942         .font_bytes = bytes,
2943         .font_size = 24,
2944         .padding_x = 0,
2945         .padding_y = 0,
2946     });
2947     defer ordinary.deinit();
2948     var sum = try render(allocator, "\\sum", .{
2949         .font_bytes = bytes,
2950         .font_size = 24,
2951         .padding_x = 0,
2952         .padding_y = 0,
2953     });
2954     defer sum.deinit();
2955     var text_operator = try render(allocator, "lim", .{
2956         .font_bytes = bytes,
2957         .font_size = 24,
2958         .padding_x = 0,
2959         .padding_y = 0,
2960     });
2961     defer text_operator.deinit();
2962 
2963     try std.testing.expect(sum.height > ordinary.height + 20);
2964     try std.testing.expect(sum.height > text_operator.height + 20);
2965     try std.testing.expect(visiblePixels(sum.pixels) > 0);
2966 }
2967 
2968 test "image renderer uses MATH constants for side scripts" {
2969     const allocator = std.testing.allocator;
2970     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
2971     defer allocator.free(fallback_bytes);
2972     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
2973     defer allocator.free(math_bytes);
2974 
2975     var fallback = try render(allocator, "A_{B}^{B}", .{
2976         .font_bytes = fallback_bytes,
2977         .font_size = 24,
2978         .padding_x = 0,
2979         .padding_y = 0,
2980     });
2981     defer fallback.deinit();
2982     var math = try render(allocator, "A_{B}^{B}", .{
2983         .font_bytes = math_bytes,
2984         .font_size = 24,
2985         .padding_x = 0,
2986         .padding_y = 0,
2987     });
2988     defer math.deinit();
2989 
2990     try std.testing.expect(math.baseline > fallback.baseline + 4);
2991     try std.testing.expect(math.height > fallback.height);
2992     try std.testing.expect(visiblePixels(math.pixels) > 0);
2993 }
2994 
2995 test "image renderer uses MATH script scale percentage" {
2996     const allocator = std.testing.allocator;
2997     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
2998     defer allocator.free(fallback_bytes);
2999     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
3000     defer allocator.free(math_bytes);
3001 
3002     var fallback = try render(allocator, "A_{BBBBBBBB}^{BBBBBBBB}", .{
3003         .font_bytes = fallback_bytes,
3004         .font_size = 24,
3005         .padding_x = 0,
3006         .padding_y = 0,
3007     });
3008     defer fallback.deinit();
3009     var math = try render(allocator, "A_{BBBBBBBB}^{BBBBBBBB}", .{
3010         .font_bytes = math_bytes,
3011         .font_size = 24,
3012         .padding_x = 0,
3013         .padding_y = 0,
3014     });
3015     defer math.deinit();
3016 
3017     try std.testing.expect(math.width > fallback.width + 8);
3018     try std.testing.expect(visiblePixels(math.pixels) > 0);
3019 }
3020 
3021 test "image renderer uses MATH side script bounds" {
3022     const allocator = std.testing.allocator;
3023     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
3024     defer allocator.free(math_bytes);
3025 
3026     var base = try render(allocator, "\\frac{A}{B}", .{
3027         .font_bytes = math_bytes,
3028         .font_size = 24,
3029         .padding_x = 0,
3030         .padding_y = 0,
3031     });
3032     defer base.deinit();
3033     var scripted = try render(allocator, "\\frac{A}{B}_{B}^{B}", .{
3034         .font_bytes = math_bytes,
3035         .font_size = 24,
3036         .padding_x = 0,
3037         .padding_y = 0,
3038     });
3039     defer scripted.deinit();
3040     var crowded = try render(allocator, "A_{\\frac{B}{B}}^{\\frac{B}{B}}", .{
3041         .font_bytes = math_bytes,
3042         .font_size = 24,
3043         .padding_x = 0,
3044         .padding_y = 0,
3045     });
3046     defer crowded.deinit();
3047 
3048     try std.testing.expect(scripted.height > base.height + 50);
3049     try std.testing.expect(scripted.baseline > base.baseline + 11);
3050     try std.testing.expect(crowded.baseline > scripted.baseline + 22);
3051     try std.testing.expect(visiblePixels(scripted.pixels) > 0);
3052     try std.testing.expect(visiblePixels(crowded.pixels) > 0);
3053 }
3054 
3055 test "image renderer uses MATH constants for fraction shifts" {
3056     const allocator = std.testing.allocator;
3057     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
3058     defer allocator.free(fallback_bytes);
3059     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
3060     defer allocator.free(math_bytes);
3061 
3062     var fallback = try render(allocator, "\\frac{A}{B}", .{
3063         .font_bytes = fallback_bytes,
3064         .font_size = 24,
3065         .padding_x = 0,
3066         .padding_y = 0,
3067     });
3068     defer fallback.deinit();
3069     var math = try render(allocator, "\\frac{A}{B}", .{
3070         .font_bytes = math_bytes,
3071         .font_size = 24,
3072         .padding_x = 0,
3073         .padding_y = 0,
3074     });
3075     defer math.deinit();
3076 
3077     try std.testing.expect(math.height > fallback.height);
3078     try std.testing.expect(math.baseline > fallback.baseline);
3079     try std.testing.expect(visiblePixels(math.pixels) > 0);
3080 }
3081 
3082 test "image renderer uses MATH text fraction gap constants" {
3083     const allocator = std.testing.allocator;
3084     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
3085     defer allocator.free(fallback_bytes);
3086     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
3087     defer allocator.free(math_bytes);
3088 
3089     var fallback = try render(allocator, "\\frac{A}{B}", .{
3090         .font_bytes = fallback_bytes,
3091         .font_size = 24,
3092         .padding_x = 0,
3093         .padding_y = 0,
3094     });
3095     defer fallback.deinit();
3096     var text = try render(allocator, "\\frac{A}{B}", .{
3097         .font_bytes = math_bytes,
3098         .font_size = 24,
3099         .padding_x = 0,
3100         .padding_y = 0,
3101     });
3102     defer text.deinit();
3103     var display = try render(allocator, "\\dfrac{A}{B}", .{
3104         .font_bytes = math_bytes,
3105         .font_size = 24,
3106         .padding_x = 0,
3107         .padding_y = 0,
3108     });
3109     defer display.deinit();
3110 
3111     try std.testing.expect(text.height > fallback.height + 18);
3112     try std.testing.expect(display.height > text.height);
3113     try std.testing.expect(visiblePixels(text.pixels) > 0);
3114 }
3115 
3116 test "image renderer uses MATH fraction rule thickness" {
3117     const allocator = std.testing.allocator;
3118     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
3119     defer allocator.free(fallback_bytes);
3120     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
3121     defer allocator.free(math_bytes);
3122 
3123     var fallback = try render(allocator, "\\frac{A}{B}", .{
3124         .font_bytes = fallback_bytes,
3125         .font_size = 24,
3126         .padding_x = 0,
3127         .padding_y = 0,
3128     });
3129     defer fallback.deinit();
3130     var math = try render(allocator, "\\frac{A}{B}", .{
3131         .font_bytes = math_bytes,
3132         .font_size = 24,
3133         .padding_x = 0,
3134         .padding_y = 0,
3135     });
3136     defer math.deinit();
3137 
3138     try std.testing.expect(math.height > fallback.height + 31);
3139     try std.testing.expect(math.baseline > fallback.baseline + 32);
3140     try std.testing.expect(visiblePixels(math.pixels) > 0);
3141 }
3142 
3143 test "image renderer uses MATH axis height for fractions" {
3144     const allocator = std.testing.allocator;
3145     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
3146     defer allocator.free(fallback_bytes);
3147     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
3148     defer allocator.free(math_bytes);
3149 
3150     var fallback = try render(allocator, "\\frac{A}{B}", .{
3151         .font_bytes = fallback_bytes,
3152         .font_size = 24,
3153         .padding_x = 0,
3154         .padding_y = 0,
3155     });
3156     defer fallback.deinit();
3157     var math = try render(allocator, "\\frac{A}{B}", .{
3158         .font_bytes = math_bytes,
3159         .font_size = 24,
3160         .padding_x = 0,
3161         .padding_y = 0,
3162     });
3163     defer math.deinit();
3164 
3165     try std.testing.expect(math.baseline > fallback.baseline + 24);
3166     try std.testing.expect(visiblePixels(math.pixels) > 0);
3167 }
3168 
3169 test "image renderer uses MATH display constants for display fractions" {
3170     const allocator = std.testing.allocator;
3171     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
3172     defer allocator.free(math_bytes);
3173 
3174     var text = try render(allocator, "\\frac{A}{B}", .{
3175         .font_bytes = math_bytes,
3176         .font_size = 24,
3177         .padding_x = 0,
3178         .padding_y = 0,
3179     });
3180     defer text.deinit();
3181     var text_alias = try render(allocator, "\\tfrac{A}{B}", .{
3182         .font_bytes = math_bytes,
3183         .font_size = 24,
3184         .padding_x = 0,
3185         .padding_y = 0,
3186     });
3187     defer text_alias.deinit();
3188     var display = try render(allocator, "\\dfrac{A}{B}", .{
3189         .font_bytes = math_bytes,
3190         .font_size = 24,
3191         .padding_x = 0,
3192         .padding_y = 0,
3193     });
3194     defer display.deinit();
3195 
3196     try std.testing.expectEqual(text.height, text_alias.height);
3197     try std.testing.expectEqual(text.baseline, text_alias.baseline);
3198     try std.testing.expect(display.height > text.height);
3199     try std.testing.expect(display.baseline > text.baseline);
3200     try std.testing.expect(visiblePixels(display.pixels) > 0);
3201 }
3202 
3203 test "image renderer stretches labeled arrows across wide labels" {
3204     const allocator = std.testing.allocator;
3205     const bytes = try filigree.fixtures.createWithOutlines(allocator);
3206     defer allocator.free(bytes);
3207 
3208     const font_size: i32 = 24;
3209     var right = try render(allocator, "\\xrightarrow{ABCDEFGH}", .{
3210         .font_bytes = bytes,
3211         .font_size = font_size,
3212         .padding_x = 0,
3213         .padding_y = 0,
3214     });
3215     defer right.deinit();
3216     var mapped = try render(allocator, "\\xmapsto{ABCDEFGH}", .{
3217         .font_bytes = bytes,
3218         .font_size = font_size,
3219         .padding_x = 0,
3220         .padding_y = 0,
3221     });
3222     defer mapped.deinit();
3223 
3224     const axis_drop: u32 = @intCast(@max(1, @divTrunc(font_size, 4)));
3225     const band_margin = axis_drop + 2;
3226     const band_y = if (right.baseline > band_margin) right.baseline - band_margin else 0;
3227     const band_height = @min(right.height - band_y, 7);
3228     const edge_width = @min(right.width, 12);
3229     try std.testing.expect(visiblePixelsInRect(&right, right.width - edge_width, band_y, edge_width, band_height) > 0);
3230     try std.testing.expect(visiblePixelsInRect(&mapped, 0, 0, @min(mapped.width, 4), mapped.height) > 0);
3231 }
3232 
3233 test "image renderer uses MATH assemblies for horizontal arrows" {
3234     const allocator = std.testing.allocator;
3235     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
3236     defer allocator.free(fallback_bytes);
3237     const assembly_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
3238     defer allocator.free(assembly_bytes);
3239 
3240     var fallback = try render(allocator, "\\xrightarrow{ABCDEFGH}", .{
3241         .font_bytes = fallback_bytes,
3242         .font_size = 24,
3243         .padding_x = 0,
3244         .padding_y = 0,
3245     });
3246     defer fallback.deinit();
3247     var assembled = try render(allocator, "\\xrightarrow{ABCDEFGH}", .{
3248         .font_bytes = assembly_bytes,
3249         .font_size = 24,
3250         .padding_x = 0,
3251         .padding_y = 0,
3252     });
3253     defer assembled.deinit();
3254 
3255     const fallback_base = visiblePixelsInRect(&fallback, 0, fallback.baseline / 2, fallback.width, @max(1, fallback.height - fallback.baseline / 2));
3256     const assembled_base = visiblePixelsInRect(&assembled, 0, assembled.baseline / 2, assembled.width, @max(1, assembled.height - assembled.baseline / 2));
3257     try std.testing.expect(assembled_base > fallback_base * 2);
3258 }
3259 
3260 test "image renderer uses MATH stretch stack constants for labeled arrows" {
3261     const allocator = std.testing.allocator;
3262     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
3263     defer allocator.free(fallback_bytes);
3264     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
3265     defer allocator.free(math_bytes);
3266 
3267     var fallback = try render(allocator, "\\xleftarrow[B]{B}", .{
3268         .font_bytes = fallback_bytes,
3269         .font_size = 24,
3270         .padding_x = 0,
3271         .padding_y = 0,
3272     });
3273     defer fallback.deinit();
3274     var math = try render(allocator, "\\xleftarrow[B]{B}", .{
3275         .font_bytes = math_bytes,
3276         .font_size = 24,
3277         .padding_x = 0,
3278         .padding_y = 0,
3279     });
3280     defer math.deinit();
3281 
3282     try std.testing.expect(math.height > fallback.height + 48);
3283     try std.testing.expect(math.baseline > fallback.baseline + 32);
3284     try std.testing.expect(visiblePixels(math.pixels) > 0);
3285 }
3286 
3287 test "image renderer draws brace annotations as shaped strokes" {
3288     const allocator = std.testing.allocator;
3289     const bytes = try filigree.fixtures.createWithOutlines(allocator);
3290     defer allocator.free(bytes);
3291 
3292     const font_size: i32 = 24;
3293     var over = try render(allocator, "\\overbrace{ABCDEFGH}", .{
3294         .font_bytes = bytes,
3295         .font_size = font_size,
3296         .padding_x = 0,
3297         .padding_y = 0,
3298     });
3299     defer over.deinit();
3300     var under = try render(allocator, "\\underbrace{ABCDEFGH}", .{
3301         .font_bytes = bytes,
3302         .font_size = font_size,
3303         .padding_x = 0,
3304         .padding_y = 0,
3305     });
3306     defer under.deinit();
3307 
3308     const brace_depth: u32 = @intCast(@max(5, @divTrunc(font_size, 4)));
3309     try std.testing.expect(visiblePixelsInRect(&over, 0, 2, over.width, @min(2, over.height - 2)) > 0);
3310     const under_band_y = under.height - @min(under.height, brace_depth);
3311     const under_middle_y = @min(under.height - 1, under_band_y + @min(2, brace_depth - 1));
3312     try std.testing.expect(visiblePixelsInRect(&under, 0, under_middle_y, under.width, @min(2, under.height - under_middle_y)) > 0);
3313 }
3314 
3315 test "image renderer stretches wide accent strokes" {
3316     const allocator = std.testing.allocator;
3317     const bytes = try filigree.fixtures.createWithOutlines(allocator);
3318     defer allocator.free(bytes);
3319 
3320     const font_size: i32 = 24;
3321     var hat = try render(allocator, "\\widehat{ABCDEFGH}", .{
3322         .font_bytes = bytes,
3323         .font_size = font_size,
3324         .padding_x = 0,
3325         .padding_y = 0,
3326     });
3327     defer hat.deinit();
3328     var arrow = try render(allocator, "\\overrightarrow{ABCDEFGH}", .{
3329         .font_bytes = bytes,
3330         .font_size = font_size,
3331         .padding_x = 0,
3332         .padding_y = 0,
3333     });
3334     defer arrow.deinit();
3335 
3336     const accent_depth: u32 = @intCast(@max(5, @divTrunc(font_size, 5)));
3337     const edge_width = @min(hat.width, 12);
3338     try std.testing.expect(visiblePixelsInRect(&hat, 0, 0, edge_width, @min(hat.height, accent_depth)) > 0);
3339     try std.testing.expect(visiblePixelsInRect(&hat, hat.width - edge_width, 0, edge_width, @min(hat.height, accent_depth)) > 0);
3340     try std.testing.expect(visiblePixelsInRect(&arrow, 0, 0, @min(arrow.width, edge_width), @min(arrow.height, accent_depth)) > 0);
3341     try std.testing.expect(visiblePixelsInRect(&arrow, arrow.width - @min(arrow.width, edge_width), 0, @min(arrow.width, edge_width), @min(arrow.height, accent_depth)) > 0);
3342 }
3343 
3344 test "image renderer uses MATH top accent attachment" {
3345     const allocator = std.testing.allocator;
3346     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
3347     defer allocator.free(fallback_bytes);
3348     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
3349     defer allocator.free(math_bytes);
3350 
3351     var fallback = try render(allocator, "\\grave{A}", .{
3352         .font_bytes = fallback_bytes,
3353         .font_size = 24,
3354         .padding_x = 0,
3355         .padding_y = 0,
3356     });
3357     defer fallback.deinit();
3358     var attached = try render(allocator, "\\grave{A}", .{
3359         .font_bytes = math_bytes,
3360         .font_size = 24,
3361         .padding_x = 0,
3362         .padding_y = 0,
3363     });
3364     defer attached.deinit();
3365 
3366     try std.testing.expect(attached.width > fallback.width);
3367     try std.testing.expect(visiblePixelsInRect(&attached, attached.width - @min(attached.width, 6), 0, @min(attached.width, 6), @min(attached.height, attached.baseline)) > 0);
3368 }
3369 
3370 test "image renderer uses MATH over and underbar metrics" {
3371     const allocator = std.testing.allocator;
3372     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
3373     defer allocator.free(fallback_bytes);
3374     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
3375     defer allocator.free(math_bytes);
3376 
3377     var fallback_bar = try render(allocator, "\\bar{A}", .{
3378         .font_bytes = fallback_bytes,
3379         .font_size = 24,
3380         .padding_x = 0,
3381         .padding_y = 0,
3382     });
3383     defer fallback_bar.deinit();
3384     var math_bar = try render(allocator, "\\bar{A}", .{
3385         .font_bytes = math_bytes,
3386         .font_size = 24,
3387         .padding_x = 0,
3388         .padding_y = 0,
3389     });
3390     defer math_bar.deinit();
3391     var fallback_under = try render(allocator, "\\underline{A}", .{
3392         .font_bytes = fallback_bytes,
3393         .font_size = 24,
3394         .padding_x = 0,
3395         .padding_y = 0,
3396     });
3397     defer fallback_under.deinit();
3398     var math_under = try render(allocator, "\\underline{A}", .{
3399         .font_bytes = math_bytes,
3400         .font_size = 24,
3401         .padding_x = 0,
3402         .padding_y = 0,
3403     });
3404     defer math_under.deinit();
3405 
3406     try std.testing.expect(math_bar.height > fallback_bar.height + 24);
3407     try std.testing.expect(math_bar.baseline > fallback_bar.baseline + 24);
3408     try std.testing.expect(math_under.height > fallback_under.height + 22);
3409     try std.testing.expectEqual(fallback_under.baseline, math_under.baseline);
3410 }
3411 
3412 test "image renderer uses MATH display stack constants for annotations" {
3413     const allocator = std.testing.allocator;
3414     const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator);
3415     defer allocator.free(fallback_bytes);
3416     const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator);
3417     defer allocator.free(math_bytes);
3418 
3419     var fallback_over = try render(allocator, "\\overset{B}{A}", .{
3420         .font_bytes = fallback_bytes,
3421         .font_size = 24,
3422         .padding_x = 0,
3423         .padding_y = 0,
3424     });
3425     defer fallback_over.deinit();
3426     var math_over = try render(allocator, "\\overset{B}{A}", .{
3427         .font_bytes = math_bytes,
3428         .font_size = 24,
3429         .padding_x = 0,
3430         .padding_y = 0,
3431     });
3432     defer math_over.deinit();
3433     var fallback_under = try render(allocator, "\\underset{B}{A}", .{
3434         .font_bytes = fallback_bytes,
3435         .font_size = 24,
3436         .padding_x = 0,
3437         .padding_y = 0,
3438     });
3439     defer fallback_under.deinit();
3440     var math_under = try render(allocator, "\\underset{B}{A}", .{
3441         .font_bytes = math_bytes,
3442         .font_size = 24,
3443         .padding_x = 0,
3444         .padding_y = 0,
3445     });
3446     defer math_under.deinit();
3447     var tall_over = try render(allocator, "\\overset{\\frac{B}{B}}{A}", .{
3448         .font_bytes = math_bytes,
3449         .font_size = 24,
3450         .padding_x = 0,
3451         .padding_y = 0,
3452     });
3453     defer tall_over.deinit();
3454     var tall_under = try render(allocator, "\\underset{\\frac{B}{B}}{A}", .{
3455         .font_bytes = math_bytes,
3456         .font_size = 24,
3457         .padding_x = 0,
3458         .padding_y = 0,
3459     });
3460     defer tall_under.deinit();
3461 
3462     try std.testing.expect(math_over.baseline > fallback_over.baseline + 24);
3463     try std.testing.expect(math_under.height > fallback_under.height + 24);
3464     try std.testing.expect(tall_over.baseline > math_over.baseline + 40);
3465     try std.testing.expect(tall_under.height > math_under.height + 40);
3466     try std.testing.expect(visiblePixels(math_over.pixels) > 0);
3467     try std.testing.expect(visiblePixels(math_under.pixels) > 0);
3468     try std.testing.expect(visiblePixels(tall_over.pixels) > 0);
3469     try std.testing.expect(visiblePixels(tall_under.pixels) > 0);
3470 }
3471 
3472 test "image renderer applies explicit operator limit policy" {
3473     const allocator = std.testing.allocator;
3474     const bytes = try filigree.fixtures.createWithOutlines(allocator);
3475     defer allocator.free(bytes);
3476 
3477     var starred = try render(allocator, "\\operatorname*{argmin}_{ABC}", .{
3478         .font_bytes = bytes,
3479         .font_size = 24,
3480         .padding_x = 0,
3481         .padding_y = 0,
3482     });
3483     defer starred.deinit();
3484     var plain = try render(allocator, "\\operatorname{argmin}_{ABC}", .{
3485         .font_bytes = bytes,
3486         .font_size = 24,
3487         .padding_x = 0,
3488         .padding_y = 0,
3489     });
3490     defer plain.deinit();
3491     var automatic = try render(allocator, "lim_{ABC}^{D}", .{
3492         .font_bytes = bytes,
3493         .font_size = 24,
3494         .padding_x = 0,
3495         .padding_y = 0,
3496     });
3497     defer automatic.deinit();
3498     var nolimits = try render(allocator, "lim\\nolimits_{ABC}^{D}", .{
3499         .font_bytes = bytes,
3500         .font_size = 24,
3501         .padding_x = 0,
3502         .padding_y = 0,
3503     });
3504     defer nolimits.deinit();
3505 
3506     try std.testing.expect(starred.width < plain.width);
3507     try std.testing.expect(automatic.width < nolimits.width);
3508     try std.testing.expect(automatic.height > nolimits.height);
3509     try std.testing.expect(visiblePixels(starred.pixels) > 0);
3510 }
3511 
3512 test "image renderer keeps grid fences in pixel layout" {
3513     const allocator = std.testing.allocator;
3514     const bytes = try filigree.fixtures.createWithOutlines(allocator);
3515     defer allocator.free(bytes);
3516 
3517     var rendered = try render(allocator, "\\begin{pmatrix}A&B\\\\C&D\\end{pmatrix}", .{
3518         .font_bytes = bytes,
3519         .font_size = 20,
3520     });
3521     defer rendered.deinit();
3522 
3523     try std.testing.expect(rendered.width > 40);
3524     try std.testing.expect(rendered.height > 35);
3525     try std.testing.expect(rendered.baseline < rendered.height);
3526     try std.testing.expect(visiblePixels(rendered.pixels) > 0);
3527 }
3528 
3529 test "image renderer scales explicit math spaces" {
3530     const allocator = std.testing.allocator;
3531     const bytes = try filigree.fixtures.createWithOutlines(allocator);
3532     defer allocator.free(bytes);
3533 
3534     var tight = try render(allocator, "AB", .{
3535         .font_bytes = bytes,
3536         .font_size = 24,
3537         .padding_x = 0,
3538         .padding_y = 0,
3539     });
3540     defer tight.deinit();
3541     var thin = try render(allocator, "A\\,B", .{
3542         .font_bytes = bytes,
3543         .font_size = 24,
3544         .padding_x = 0,
3545         .padding_y = 0,
3546     });
3547     defer thin.deinit();
3548     var quad = try render(allocator, "A\\quad B", .{
3549         .font_bytes = bytes,
3550         .font_size = 24,
3551         .padding_x = 0,
3552         .padding_y = 0,
3553     });
3554     defer quad.deinit();
3555     var negative = try render(allocator, "A\\!B", .{
3556         .font_bytes = bytes,
3557         .font_size = 24,
3558         .padding_x = 0,
3559         .padding_y = 0,
3560     });
3561     defer negative.deinit();
3562 
3563     try std.testing.expect(thin.width > tight.width);
3564     try std.testing.expect(quad.width > thin.width);
3565     try std.testing.expect(negative.width < tight.width);
3566 }
3567 
3568 test "image renderer computes math class spacing" {
3569     const style: Style = .{ .font_size = 36 };
3570     const ordinary: Box = .{ .width = 1, .height = 1, .baseline = 0, .commands = &.{}, .class = .ordinary };
3571     const binary: Box = .{ .width = 1, .height = 1, .baseline = 0, .commands = &.{}, .class = .binary };
3572     const relation: Box = .{ .width = 1, .height = 1, .baseline = 0, .commands = &.{}, .class = .relation };
3573     const open: Box = .{ .width = 1, .height = 1, .baseline = 0, .commands = &.{}, .class = .open };
3574     const punctuation: Box = .{ .width = 1, .height = 1, .baseline = 0, .commands = &.{}, .class = .punctuation };
3575     const explicit: Box = .{ .width = 1, .height = 1, .baseline = 0, .commands = &.{}, .class = .spacing };
3576 
3577     const plus = [_]Box{ ordinary, binary, ordinary };
3578     try std.testing.expectEqual(@as(i32, 8), mathClassSpace(&plus, 1, style));
3579     try std.testing.expectEqual(@as(i32, 8), mathClassSpace(&plus, 2, style));
3580 
3581     const unary = [_]Box{ binary, ordinary };
3582     try std.testing.expectEqual(@as(i32, 0), mathClassSpace(&unary, 1, style));
3583 
3584     const equals = [_]Box{ ordinary, relation, ordinary };
3585     try std.testing.expectEqual(@as(i32, 10), mathClassSpace(&equals, 1, style));
3586     try std.testing.expectEqual(@as(i32, 10), mathClassSpace(&equals, 2, style));
3587 
3588     const grouped = [_]Box{ open, binary, ordinary };
3589     try std.testing.expectEqual(@as(i32, 0), mathClassSpace(&grouped, 1, style));
3590 
3591     const comma = [_]Box{ ordinary, punctuation, ordinary };
3592     try std.testing.expectEqual(@as(i32, 6), mathClassSpace(&comma, 2, style));
3593 
3594     const manual = [_]Box{ ordinary, explicit, relation };
3595     try std.testing.expectEqual(@as(i32, 0), mathClassSpace(&manual, 1, style));
3596     try std.testing.expectEqual(@as(i32, 0), mathClassSpace(&manual, 2, style));
3597     try std.testing.expectEqual(MathClass.operator, mathClassForText("lim"));
3598     try std.testing.expectEqual(MathClass.operator, mathClassForText("max"));
3599     try std.testing.expectEqual(MathClass.relation, mathClassForText("→"));
3600 }
3601 
3602 test "image renderer adds automatic binary and relation spacing" {
3603     const allocator = std.testing.allocator;
3604     const bytes = try filigree.fixtures.createWithOutlines(allocator);
3605     defer allocator.free(bytes);
3606 
3607     var automatic = try render(allocator, "A+B=C", .{
3608         .font_bytes = bytes,
3609         .font_size = 24,
3610         .padding_x = 0,
3611         .padding_y = 0,
3612     });
3613     defer automatic.deinit();
3614     var tightened = try render(allocator, "A\\!+\\!B\\!=\\!C", .{
3615         .font_bytes = bytes,
3616         .font_size = 24,
3617         .padding_x = 0,
3618         .padding_y = 0,
3619     });
3620     defer tightened.deinit();
3621 
3622     try std.testing.expect(automatic.width > tightened.width + 10);
3623     try std.testing.expect(visiblePixels(automatic.pixels) > 0);
3624 }
3625 
3626 test "image renderer accepts fallback font bytes" {
3627     const allocator = std.testing.allocator;
3628     const bytes = try filigree.fixtures.createWithOutlines(allocator);
3629     defer allocator.free(bytes);
3630     const fallback_fonts = [_][]const u8{bytes};
3631 
3632     var rendered = try render(allocator, "\\mathbf{A}+B", .{
3633         .font_bytes = bytes,
3634         .fallback_font_bytes = &fallback_fonts,
3635         .font_size = 20,
3636     });
3637     defer rendered.deinit();
3638 
3639     try std.testing.expect(rendered.width > 20);
3640     try std.testing.expect(rendered.height > 20);
3641     try std.testing.expect(visiblePixels(rendered.pixels) > 0);
3642 }
3643 
3644 test "image renderer accepts CFF outline font bytes" {
3645     const allocator = std.testing.allocator;
3646     const bytes = try filigree.fixtures.createWithCffOutlines(allocator);
3647     defer allocator.free(bytes);
3648 
3649     var rendered = try render(allocator, "\\frac{A}{B}", .{
3650         .font_bytes = bytes,
3651         .font_size = 20,
3652     });
3653     defer rendered.deinit();
3654 
3655     try std.testing.expect(rendered.width > 20);
3656     try std.testing.expect(rendered.height > 35);
3657     try std.testing.expect(visiblePixels(rendered.pixels) > 0);
3658 }
3659 
3660 test "image renderer rejects invalid fallback font bytes" {
3661     const allocator = std.testing.allocator;
3662     const bytes = try filigree.fixtures.createWithOutlines(allocator);
3663     defer allocator.free(bytes);
3664     const fallback_fonts = [_][]const u8{&.{}};
3665 
3666     try std.testing.expectError(error.InvalidFont, render(allocator, "A", .{
3667         .font_bytes = bytes,
3668         .fallback_font_bytes = &fallback_fonts,
3669         .font_size = 20,
3670     }));
3671 }