tiny.termtex.image
Defined in tiny.termtex.
API (5)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
Source: lib/termtex/src/image.zig
zig
const std = @import("std");const filigree = @import("filigree");const ast = @import("ast.zig");const operator = @import("operator.zig");const parse = @import("parse.zig");pub const Color = filigree.render.Color;pub const Options = struct { font_bytes: []const u8, fallback_font_bytes: []const []const u8 = &.{}, font_size: i32 = 24, /// Output storage acquired once per image and reused for every text run. shaping_output: filigree.Output.Limits = .{ .max_glyphs = 4096, .max_ligature_carets = 4096, }, padding_x: u32 = 4, padding_y: u32 = 4, foreground: Color = .{}, background: Color = .{ .a = 0 },};pub const Image = struct { allocator: std.mem.Allocator, pixels: []u8, width: u32, height: u32, stride: u32, baseline: u32, pub fn deinit(self: *Image) void { self.allocator.free(self.pixels); self.* = undefined; }};const Style = struct { font_size: i32,};const Metrics = struct { ascender: i32, descender: i32, line_gap: i32, line_height: i32,};const Box = struct { width: i32, height: i32, baseline: i32, commands: []const Command, class: MathClass = .ordinary,};const MathClass = enum { ordinary, operator, binary, relation, open, close, punctuation, inner, spacing,};const Command = union(enum) { text: Text, rect: Rect, line: Line, glyph: Glyph,};const Text = struct { x: i32, y: i32, value: []const u8, font_size: i32,};const Rect = struct { x: i32, y: i32, width: i32, height: i32,};const Line = struct { x0: i32, y0: i32, x1: i32, y1: i32, width: i32,};const Glyph = struct { x: i32, y: i32, glyph_id: u32, font_size: i32,};const GlyphInkBounds = struct { left: i32, top: i32, right: i32, bottom: i32,};const MathRadicalSign = struct { width: i32, height: i32, commands: []const Command,};const Context = struct { allocator: std.mem.Allocator, scratch: std.mem.Allocator, options: Options, font: filigree.Font, fallback_fonts: []const filigree.Font, shaping_output: *filigree.Output, const layoutExpr = layoutExprImpl; const layoutText = layoutTextImpl; const layoutOperator = layoutOperatorImpl; const layoutSpace = layoutSpaceImpl; const layoutRow = layoutRowImpl; const layoutFraction = layoutFractionImpl; const layoutSqrt = layoutSqrtImpl; const layoutScripts = layoutScriptsImpl; const layoutLimits = layoutLimitsImpl; const layoutMathHorizontalArrow = layoutMathHorizontalArrowImpl; const layoutStretchArrow = layoutStretchArrowImpl; const layoutAccent = layoutAccentImpl; const layoutNativeAccent = layoutNativeAccentImpl; const layoutAnnotation = layoutAnnotationImpl; const layoutStack = layoutStackImpl; const layoutBrace = layoutBraceImpl; const layoutBoxed = layoutBoxedImpl; const layoutDelimited = layoutDelimitedImpl; const layoutDelimiter = layoutDelimiterImpl; const layoutDelimiterShape = layoutDelimiterShapeImpl; const layoutGrid = layoutGridImpl; const paint = paintImpl; const fontMetrics = fontMetricsImpl; const fallbackCandidates = fallbackCandidatesImpl; const makeBox = makeBoxImpl; const boxFromList = boxFromListImpl;};pub fn render(allocator: std.mem.Allocator, source: []const u8, options: Options) !Image { var arena = std.heap.ArenaAllocator.init(allocator); defer arena.deinit(); const expr = try parse.parse(arena.allocator(), source); return renderExpr(allocator, arena.allocator(), expr, options);}pub fn renderExpr( allocator: std.mem.Allocator, scratch: std.mem.Allocator, expr: *const ast.Expr, options: Options,) !Image { try validateOptions(options); var font = filigree.Font.initFromBytes(options.font_bytes.ptr, options.font_bytes.len) orelse return error.InvalidFont; defer font.deinit(); font.setScale(@floatFromInt(options.font_size), 72); const fallback_fonts = try loadFallbackFonts(allocator, options.fallback_font_bytes); defer unloadFallbackFonts(allocator, fallback_fonts); var shaping_output = try filigree.Output.init(allocator, options.shaping_output); defer shaping_output.deinit(allocator); var context = Context{ .allocator = allocator, .scratch = scratch, .options = options, .font = font, .fallback_fonts = fallback_fonts, .shaping_output = &shaping_output, }; const box = try context.layoutExpr(expr, .{ .font_size = options.font_size }); const width = try imageDimension(box.width, options.padding_x); const height = try imageDimension(box.height, options.padding_y); const stride = try std.math.mul(u32, width, 4); const pixels = try allocator.alloc(u8, try std.math.mul(usize, stride, height)); errdefer allocator.free(pixels); fill(pixels, width, height, stride, options.background); const canvas = try filigree.render.Canvas.init(pixels, width, height, stride); try context.paint(canvas, box.commands, @intCast(options.padding_x), @intCast(options.padding_y)); return .{ .allocator = allocator, .pixels = pixels, .width = width, .height = height, .stride = stride, .baseline = @intCast(@max(0, box.baseline + @as(i32, @intCast(options.padding_y)))), };}fn layoutExprImpl(context: *Context, expr: *const ast.Expr, style: Style) anyerror!Box { return switch (expr.*) { .row => |items| try context.layoutRow(items, style), .text => |value| try layoutTextWithDisplayOperator(context, value, style), .operator => |value| try context.layoutOperator(value, style), .space => |value| try context.layoutSpace(value, style), .fraction => |value| try context.layoutFraction(value, style), .sqrt => |value| try context.layoutSqrt(value, style), .scripts => |value| try context.layoutScripts(value, style), .accent => |value| try context.layoutAccent(value, style), .annotation => |value| try context.layoutAnnotation(value, style), .delimited => |value| try context.layoutDelimited(value, style), .grid => |value| try context.layoutGrid(value, style), };}fn layoutTextWithDisplayOperator(context: *Context, value: []const u8, style: Style) anyerror!Box { if (!operator.displayOperatorText(value)) return context.layoutText(value, style); const min_height = displayOperatorMinHeight(context, style) orelse return context.layoutText(value, style); const nominal = try context.layoutText(value, style); if (nominal.height >= min_height) return nominal; var out = try context.layoutText(value, delimiterStyle(context, min_height, style)); out.class = .operator; return out;}fn layoutTextImpl(context: *Context, value: []const u8, style: Style) anyerror!Box { const metrics = context.fontMetrics(style.font_size); if (value.len == 0) return context.makeBox(0, metrics.line_height, metrics.ascender, &.{}); var shaped_font = context.font; shaped_font.setScale(@floatFromInt(style.font_size), 72); const fallback_candidates = try context.fallbackCandidates(style.font_size); const shaped = if (fallback_candidates.len == 0) try filigree.measureUtf8(context.allocator, context.shaping_output, &shaped_font, value) else try filigree.measureFallbackUtf8( context.allocator, context.shaping_output, &shaped_font, fallback_candidates, value, ); var command = [_]Command{.{ .text = .{ .x = 0, .y = 0, .value = value, .font_size = style.font_size, } }}; var box = try context.makeBox(@max(1, shaped.advance_x), metrics.line_height, metrics.ascender, &command); box.class = mathClassForText(value); return box;}fn layoutOperatorImpl(context: *Context, value: ast.Operator, style: Style) anyerror!Box { var out = try context.layoutExpr(value.body, style); out.class = .operator; return out;}fn layoutSpaceImpl(context: *Context, value: ast.Space, style: Style) anyerror!Box { const metrics = context.fontMetrics(style.font_size); var box = try context.makeBox(spacePixels(value, style), metrics.line_height, metrics.ascender, &.{}); box.class = .spacing; return box;}fn layoutRowImpl(context: *Context, items: []const *ast.Expr, style: Style) anyerror!Box { if (items.len == 0) { const metrics = context.fontMetrics(style.font_size); return context.makeBox(0, metrics.line_height, metrics.ascender, &.{}); } const children = try context.scratch.alloc(Box, items.len); var baseline: i32 = 0; var descent: i32 = 0; for (items, 0..) |item, index| { children[index] = try context.layoutExpr(item, style); baseline = @max(baseline, children[index].baseline); descent = @max(descent, children[index].height - children[index].baseline); } var width_cursor: i32 = 0; var min_x: i32 = 0; var max_x: i32 = 0; for (children, 0..) |child, index| { if (index != 0) width_cursor += mathClassSpace(children, index, style); min_x = @min(min_x, width_cursor); max_x = @max(max_x, width_cursor + child.width); width_cursor += child.width; min_x = @min(min_x, width_cursor); max_x = @max(max_x, width_cursor); } var commands: std.ArrayListUnmanaged(Command) = .empty; var x: i32 = -min_x; for (children, 0..) |child, index| { if (index != 0) x += mathClassSpace(children, index, style); const y = baseline - child.baseline; try appendCommands(context.scratch, &commands, child.commands, x, y); x += child.width; } return context.boxFromList(max_x - min_x, baseline + descent, baseline, &commands);}fn layoutFractionImpl(context: *Context, value: ast.Fraction, style: Style) anyerror!Box { const numerator = try context.layoutExpr(value.numerator, style); const denominator = try context.layoutExpr(value.denominator, style); const side = @max(2, @divTrunc(style.font_size, 4)); const numerator_gap = fractionNumeratorGap(context, style, value.style); const denominator_gap = fractionDenominatorGap(context, style, value.style); const rule = fractionRuleWidth(context, style); const inner = @max(numerator.width, denominator.width); const width = inner + side * 2; if (mathConstants(context) != null) return try layoutMathFraction(context, numerator, denominator, width, rule, numerator_gap, denominator_gap, style, value.style); const rule_y = numerator.height + numerator_gap; const denominator_y = rule_y + rule + denominator_gap; var commands: std.ArrayListUnmanaged(Command) = .empty; try appendCommands(context.scratch, &commands, numerator.commands, centered(width, numerator.width), 0); try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = rule_y, .width = width, .height = rule } }); try appendCommands(context.scratch, &commands, denominator.commands, centered(width, denominator.width), denominator_y); return context.boxFromList(width, denominator_y + denominator.height, rule_y + rule, &commands);}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 { const rule_y = -mathAxisHeight(context, style) - @divTrunc(rule, 2); var numerator_y = -fractionNumeratorShiftUp(context, style, fraction_style) - numerator.baseline; const numerator_current_gap = rule_y - (numerator_y + numerator.height); if (numerator_current_gap < numerator_gap) numerator_y -= numerator_gap - numerator_current_gap; var denominator_y = fractionDenominatorShiftDown(context, style, fraction_style) - denominator.baseline; const denominator_current_gap = denominator_y - (rule_y + rule); if (denominator_current_gap < denominator_gap) denominator_y += denominator_gap - denominator_current_gap; const min_y = @min(numerator_y, rule_y); const max_y = @max(denominator_y + denominator.height, rule_y + rule); const y_shift = if (min_y < 0) -min_y else 0; var commands: std.ArrayListUnmanaged(Command) = .empty; try appendCommands(context.scratch, &commands, numerator.commands, centered(width, numerator.width), numerator_y + y_shift); try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = rule_y + y_shift, .width = width, .height = rule } }); try appendCommands(context.scratch, &commands, denominator.commands, centered(width, denominator.width), denominator_y + y_shift); return context.boxFromList(width, max_y - min_y, y_shift, &commands);}fn layoutSqrtImpl(context: *Context, value: ast.Radical, style: Style) anyerror!Box { const body = try context.layoutExpr(value.body, style); const rooted = try layoutSqrtBody(context, body, style); const index_expr = value.index orelse return rooted; const index = try context.layoutExpr(index_expr, scriptScriptStyle(context, style)); return try layoutRootDegree(context, rooted, index, style);}fn layoutSqrtBody(context: *Context, body: Box, style: Style) anyerror!Box { const rule = radicalRuleWidth(context, style); const clearance = radicalClearance(context, style); const extra_ascender = radicalExtraAscender(context, style); const rule_y = extra_ascender; const body_y = rule_y + rule + clearance; const target_height = body.height + clearance + rule; if (try layoutMathRadicalSign(context, target_height, style)) |radical| { const body_x = radical.width; const height = @max(body_y + body.height, rule_y + radical.height); var commands: std.ArrayListUnmanaged(Command) = .empty; try appendCommands(context.scratch, &commands, radical.commands, 0, rule_y); try commands.append(context.scratch, .{ .rect = .{ .x = body_x, .y = rule_y, .width = body.width, .height = rule, } }); try appendCommands(context.scratch, &commands, body.commands, body_x, body_y); return context.boxFromList(body_x + body.width, height, body_y + body.baseline, &commands); } const radical_width = radicalWidth(context, target_height, style); const body_gap = @max(2, @divTrunc(style.font_size, 8)); const body_x = radical_width + body_gap; const radical_x0 = 0; const radical_x1 = @max(rule * 2, @divTrunc(radical_width, 3)); const radical_x2 = body_x - body_gap; const radical_y0 = body_y + @divTrunc(body.height * 3, 5); const radical_y1 = body_y + body.height - rule; const radical_y2 = rule_y + rule; var commands: std.ArrayListUnmanaged(Command) = .empty; try commands.append(context.scratch, .{ .line = .{ .x0 = radical_x0, .y0 = radical_y0, .x1 = radical_x1, .y1 = radical_y1, .width = rule } }); try commands.append(context.scratch, .{ .line = .{ .x0 = radical_x1, .y0 = radical_y1, .x1 = radical_x2, .y1 = radical_y2, .width = rule } }); try commands.append(context.scratch, .{ .rect = .{ .x = radical_x2, .y = rule_y, .width = body.width + body_gap, .height = rule, } }); try appendCommands(context.scratch, &commands, body.commands, body_x, body_y); return context.boxFromList(body_x + body.width, body_y + body.height, body_y + body.baseline, &commands);}fn layoutRootDegree(context: *Context, rooted: Box, degree: Box, style: Style) anyerror!Box { const before = @max(0, radicalKernBeforeDegree(context, style)); const after = @max(-degree.width, radicalKernAfterDegree(context, style)); const radical_height = @max(1, rooted.height); const raise = @divTrunc(@as(i64, radical_height) * radicalDegreeBottomRaisePercent(context) + 50, 100); const degree_y = radical_height - @as(i32, @intCast(raise)) - degree.height; const min_y = @min(0, degree_y); const max_y = @max(rooted.height, degree_y + degree.height); const rooted_x = before + degree.width + after; const width = @max(rooted_x + rooted.width, before + degree.width); var commands: std.ArrayListUnmanaged(Command) = .empty; try appendCommands(context.scratch, &commands, degree.commands, before, degree_y - min_y); try appendCommands(context.scratch, &commands, rooted.commands, rooted_x, -min_y); var out = try context.boxFromList(width, max_y - min_y, rooted.baseline - min_y, &commands); out.class = rooted.class; return out;}fn layoutMathRadicalSign(context: *Context, target_height: i32, style: Style) anyerror!?MathRadicalSign { const glyph_id = context.font.face.glyphId(0x221a); if (glyph_id == 0) return null; const target = pixelHeightToDesignUnits(context, target_height, style); if (context.font.face.mathVerticalVariant(glyph_id, target)) |variant| { if (variant.advance_measurement >= target) return try layoutMathRadicalGlyph(context, variant.glyph_id, variant.advance_measurement, target_height, style); } return try layoutMathRadicalAssembly(context, glyph_id, target, target_height, style);}fn layoutMathRadicalGlyph(context: *Context, glyph_id: u32, advance_measurement: u16, target_height: i32, style: Style) anyerror!?MathRadicalSign { const bounds = (try glyphInkBounds(context, glyph_id, style)) orelse return null; const ink_width = @max(1, bounds.right - bounds.left); const ink_height = @max(1, bounds.bottom - bounds.top); const advance_height = designUnitsToPixelsCeil(context, advance_measurement, style); const height = @max(target_height, @max(ink_height, advance_height)); var commands = [_]Command{.{ .glyph = .{ .x = -bounds.left, .y = -bounds.top, .glyph_id = glyph_id, .font_size = style.font_size, } }}; const owned = try context.scratch.dupe(Command, &commands); return .{ .width = ink_width, .height = height, .commands = owned };}fn layoutMathRadicalAssembly(context: *Context, glyph_id: u32, target: u16, target_height: i32, style: Style) anyerror!?MathRadicalSign { const assembly = (try context.font.face.mathVerticalAssemblyAlloc(context.scratch, glyph_id, target)) orelse return null; if (assembly.parts.len == 0 or assembly.advance_measurement < target) return null; const assembly_height = @max(1, designUnitsToPixelsCeil(context, assembly.advance_measurement, style)); const bounds = try context.scratch.alloc(GlyphInkBounds, assembly.parts.len); var left_bound: i32 = std.math.maxInt(i32); var right_bound: i32 = std.math.minInt(i32); var height = @max(target_height, assembly_height); for (assembly.parts, 0..) |part, index| { bounds[index] = (try glyphInkBounds(context, part.glyph_id, style)) orelse return null; left_bound = @min(left_bound, bounds[index].left); right_bound = @max(right_bound, bounds[index].right); const part_end = @as(u32, part.advance_offset) + @as(u32, part.full_advance); if (part_end > assembly.advance_measurement) return null; const part_top_design: u16 = @intCast(@as(u32, assembly.advance_measurement) - part_end); const part_top = designUnitsToPixelsFloor(context, part_top_design, style); height = @max(height, part_top + @max(1, bounds[index].bottom - bounds[index].top)); } if (left_bound >= right_bound) return null; var commands: std.ArrayListUnmanaged(Command) = .empty; for (assembly.parts, 0..) |part, index| { const part_end = @as(u32, part.advance_offset) + @as(u32, part.full_advance); const part_top_design: u16 = @intCast(@as(u32, assembly.advance_measurement) - part_end); const part_top = designUnitsToPixelsFloor(context, part_top_design, style); try commands.append(context.scratch, .{ .glyph = .{ .x = -left_bound, .y = part_top - bounds[index].top, .glyph_id = part.glyph_id, .font_size = style.font_size, } }); } return .{ .width = right_bound - left_bound, .height = height, .commands = try commands.toOwnedSlice(context.scratch), };}fn layoutScriptsImpl(context: *Context, value: ast.Scripts, style: Style) anyerror!Box { if (operator.limitsBase(value.base)) return context.layoutLimits(value, style); const base = try context.layoutExpr(value.base, style); const script = scriptStyle(context, style); const sup = if (value.sup) |expr| try context.layoutExpr(expr, script) else null; const sub = if (value.sub) |expr| try context.layoutExpr(expr, script) else null; if (mathConstants(context) != null) return try layoutMathScripts(context, base, sup, sub, style); const gap = @max(1, @divTrunc(style.font_size, 8)); const script_width = @max(if (sup) |box| box.width else 0, if (sub) |box| box.width else 0); const base_y = if (sup) |box| @max(0, box.height - @divTrunc(base.baseline, 2)) else 0; const baseline = base_y + base.baseline; const sub_y = baseline + gap; var height = base_y + base.height; if (sub) |box| height = @max(height, sub_y + box.height); var commands: std.ArrayListUnmanaged(Command) = .empty; try appendCommands(context.scratch, &commands, base.commands, 0, base_y); if (sup) |box| try appendCommands(context.scratch, &commands, box.commands, base.width + gap, 0); if (sub) |box| try appendCommands(context.scratch, &commands, box.commands, base.width + gap, sub_y); var out = try context.boxFromList(base.width + if (script_width == 0) 0 else gap + script_width, height, baseline, &commands); out.class = base.class; return out;}fn layoutMathScripts(context: *Context, base: Box, sup: ?Box, sub: ?Box, style: Style) anyerror!Box { const script_gap = scriptHorizontalGap(context, style); const script_width = @max(if (sup) |box| box.width else 0, if (sub) |box| box.width else 0); const script_x = base.width + if (script_width == 0) 0 else script_gap; const metrics = context.fontMetrics(style.font_size); const extended_base = base.height > metrics.line_height; const base_y = -base.baseline; var sup_y: ?i32 = null; var sub_y: ?i32 = null; if (sup) |box| { var shift = @max( superscriptShiftUp(context, style), box.height - box.baseline + superscriptBottomMin(context, style), ); if (extended_base) shift = @max(shift, base.baseline - superscriptBaselineDropMax(context, style)); sup_y = -shift - box.baseline; } if (sub) |box| { var shift = @max( subscriptShiftDown(context, style), box.baseline - subscriptTopMax(context, style), ); if (extended_base) shift = @max(shift, base.height - base.baseline + subscriptBaselineDropMin(context, style)); sub_y = shift - box.baseline; } if (sup != null and sub != null) { const sup_box = sup.?; var above_y = sup_y.?; var below_y = sub_y.?; const needed_gap = subSuperscriptGap(context, style); const current_gap = below_y - (above_y + sup_box.height); if (current_gap < needed_gap) { var remaining = needed_gap - current_gap; const lowest_sup_bottom = -superscriptBottomMaxWithSubscript(context, style); const sup_bottom = above_y + sup_box.height; if (sup_bottom > lowest_sup_bottom) { const lift = @min(remaining, sup_bottom - lowest_sup_bottom); above_y -= lift; remaining -= lift; } below_y += remaining; } sup_y = above_y; sub_y = below_y; } var min_y = base_y; var max_y = base_y + base.height; if (sup) |box| { min_y = @min(min_y, sup_y.?); max_y = @max(max_y, sup_y.? + box.height); } if (sub) |box| { min_y = @min(min_y, sub_y.?); max_y = @max(max_y, sub_y.? + box.height); } const y_shift = if (min_y < 0) -min_y else 0; const width = base.width + if (script_width == 0) 0 else script_gap + script_width; var commands: std.ArrayListUnmanaged(Command) = .empty; try appendCommands(context.scratch, &commands, base.commands, 0, base_y + y_shift); if (sup) |box| try appendCommands(context.scratch, &commands, box.commands, script_x, sup_y.? + y_shift); if (sub) |box| try appendCommands(context.scratch, &commands, box.commands, script_x, sub_y.? + y_shift); var out = try context.boxFromList(width, max_y - min_y, y_shift, &commands); out.class = base.class; return out;}fn layoutLimitsImpl(context: *Context, value: ast.Scripts, style: Style) anyerror!Box { var base = try context.layoutExpr(value.base, style); const script = scriptStyle(context, style); const sup = if (value.sup) |expr| try context.layoutExpr(expr, script) else null; const sub = if (value.sub) |expr| try context.layoutExpr(expr, script) else null; const stretch_arrow = operator.stretchArrowBase(value.base); if (stretch_arrow) |arrow| { const label_width = @max(if (sup) |box| box.width else 0, if (sub) |box| box.width else 0); const target_width = @max(base.width, label_width + stretchArrowLabelPadding(style) * 2); base = try context.layoutStretchArrow(arrow, target_width, base, value.base, style); } const stretch_stack = stretch_arrow != null; const top_shift = if (stretch_stack) stretchStackTopShiftUp(context, style) else upperLimitBaselineRise(context, style); const top_gap = if (stretch_stack) stretchStackGapAbove(context, style) else upperLimitGap(context, style); const bottom_shift = if (stretch_stack) stretchStackBottomShiftDown(context, style) else lowerLimitBaselineDrop(context, style); const bottom_gap = if (stretch_stack) stretchStackGapBelow(context, style) else lowerLimitGap(context, style); const width = @max(base.width, @max(if (sup) |box| box.width else 0, if (sub) |box| box.width else 0)); const base_y = if (sup) |box| @max( top_shift + box.baseline, top_gap + box.height, ) else 0; const base_bottom = base_y + base.height; const sub_y = if (sub) |box| base_bottom + @max( bottom_gap, bottom_shift - box.baseline, ) else base_bottom; var height = base_bottom; if (sub) |box| height = sub_y + box.height; var commands: std.ArrayListUnmanaged(Command) = .empty; if (sup) |box| try appendCommands(context.scratch, &commands, box.commands, centered(width, box.width), 0); try appendCommands(context.scratch, &commands, base.commands, centered(width, base.width), base_y); if (sub) |box| try appendCommands(context.scratch, &commands, box.commands, centered(width, box.width), sub_y); var out = try context.boxFromList(width, height, base_y + base.baseline, &commands); out.class = base.class; return out;}fn layoutStretchArrowImpl(context: *Context, arrow: operator.StretchArrow, target_width: i32, nominal: Box, base_expr: *const ast.Expr, style: Style) anyerror!Box { if (try context.layoutMathHorizontalArrow(base_expr, target_width, nominal, style)) |box| return box; const left_head = if (arrow.left_head) |text| try context.layoutText(text, style) else null; const right_head = if (arrow.right_head) |text| try context.layoutText(text, style) else null; const width = @max(target_width, stretchArrowMinimumWidth(left_head, right_head, style)); const metrics = context.fontMetrics(style.font_size); var ascender = @max(nominal.baseline, metrics.ascender); var descender = @max(nominal.height - nominal.baseline, metrics.line_height - metrics.ascender); if (left_head) |box| { ascender = @max(ascender, box.baseline); descender = @max(descender, box.height - box.baseline); } if (right_head) |box| { ascender = @max(ascender, box.baseline); descender = @max(descender, box.height - box.baseline); } const baseline = ascender; const height = ascender + descender; const rule = strokeWidth(context, style); const axis = stretchArrowAxis(context, style, baseline, height, rule); const overlap = stretchArrowHeadOverlap(style); var shaft_start: i32 = 0; if (left_head) |box| shaft_start = @max(0, box.width - overlap); var shaft_end = width; if (right_head) |box| shaft_end = width - @max(0, box.width - overlap); if (shaft_end <= shaft_start) { shaft_start = 0; shaft_end = width; } var commands: std.ArrayListUnmanaged(Command) = .empty; switch (arrow.shaft) { .single => try appendStretchArrowShaft(context.scratch, &commands, shaft_start, shaft_end, axis, rule), .double => try appendDoubleStretchArrowShaft(context.scratch, &commands, shaft_start, shaft_end, axis, rule, style), .squiggle => try appendSquiggleStretchArrowShaft(context.scratch, &commands, shaft_start, shaft_end, axis, rule, style), } if (arrow.left_bar) try appendStretchArrowBar(context.scratch, &commands, axis, rule, style); if (left_head) |box| try appendCommands(context.scratch, &commands, box.commands, 0, baseline - box.baseline); if (right_head) |box| try appendCommands(context.scratch, &commands, box.commands, width - box.width, baseline - box.baseline); var out = try context.boxFromList(width, height, baseline, &commands); out.class = nominal.class; return out;}fn layoutMathHorizontalArrowImpl(context: *Context, base_expr: *const ast.Expr, target_width: i32, nominal: Box, style: Style) anyerror!?Box { const glyph_id = singleGlyphId(context, base_expr) orelse return null; const target = pixelWidthToDesignUnits(context, target_width, style); if (context.font.face.mathHorizontalVariant(glyph_id, target)) |variant| { if (variant.advance_measurement >= target) return try layoutMathHorizontalGlyphArrow(context, variant.glyph_id, variant.advance_measurement, target_width, nominal, style); } return try layoutMathHorizontalAssemblyArrow(context, glyph_id, target, target_width, nominal, style);}fn layoutMathHorizontalGlyphArrow(context: *Context, glyph_id: u32, advance_measurement: u16, target_width: i32, nominal: Box, style: Style) anyerror!?Box { const bounds = (try glyphInkBounds(context, glyph_id, style)) orelse return null; const metrics = context.fontMetrics(style.font_size); const baseline = @max(nominal.baseline, metrics.ascender); const height = @max(nominal.height, metrics.line_height); const x_shift = @max(0, -bounds.left); const advance_width = designUnitsToPixelsCeil(context, advance_measurement, style); const width = @max(target_width, @max(advance_width + x_shift, bounds.right + x_shift)); var commands = [_]Command{.{ .glyph = .{ .x = x_shift, .y = baseline - metrics.ascender, .glyph_id = glyph_id, .font_size = style.font_size, } }}; var out = try context.makeBox(width, height, baseline, &commands); out.class = nominal.class; return out;}fn layoutMathHorizontalAssemblyArrow(context: *Context, glyph_id: u32, target: u16, target_width: i32, nominal: Box, style: Style) anyerror!?Box { const assembly = (try context.font.face.mathHorizontalAssemblyAlloc(context.scratch, glyph_id, target)) orelse return null; if (assembly.parts.len == 0 or assembly.advance_measurement < target) return null; const metrics = context.fontMetrics(style.font_size); const baseline = @max(nominal.baseline, metrics.ascender); const height = @max(nominal.height, metrics.line_height); const bounds = try context.scratch.alloc(GlyphInkBounds, assembly.parts.len); var left_bound: i32 = std.math.maxInt(i32); var right_bound: i32 = std.math.minInt(i32); for (assembly.parts, 0..) |part, index| { const part_end = @as(u32, part.advance_offset) + @as(u32, part.full_advance); if (part_end > assembly.advance_measurement) return null; const part_x = designUnitsToPixelsFloor(context, part.advance_offset, style); bounds[index] = (try glyphInkBounds(context, part.glyph_id, style)) orelse return null; left_bound = @min(left_bound, part_x + bounds[index].left); right_bound = @max(right_bound, part_x + bounds[index].right); } if (left_bound >= right_bound) return null; const x_shift = @max(0, -left_bound); const assembly_width = designUnitsToPixelsCeil(context, assembly.advance_measurement, style); const width = @max(target_width, @max(assembly_width + x_shift, right_bound + x_shift)); var commands: std.ArrayListUnmanaged(Command) = .empty; for (assembly.parts) |part| { try commands.append(context.scratch, .{ .glyph = .{ .x = x_shift + designUnitsToPixelsFloor(context, part.advance_offset, style), .y = baseline - metrics.ascender, .glyph_id = part.glyph_id, .font_size = style.font_size, } }); } var out = try context.boxFromList(width, height, baseline, &commands); out.class = nominal.class; return out;}fn layoutAccentImpl(context: *Context, value: ast.Accent, style: Style) anyerror!Box { const body = try context.layoutExpr(value.body, style); var commands: std.ArrayListUnmanaged(Command) = .empty; switch (value.mark) { .bar => { const rule = overbarRuleWidth(context, style); const gap = overbarGap(context, style); const extra = overbarExtraAscender(context, style); const body_y = extra + rule + gap; try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = extra, .width = body.width, .height = rule } }); try appendCommands(context.scratch, &commands, body.commands, 0, body_y); var out = try context.boxFromList(body.width, body.height + body_y, body.baseline + body_y, &commands); out.class = body.class; return out; }, .underline => { const rule = underbarRuleWidth(context, style); const gap = underbarGap(context, style); const extra = underbarExtraDescender(context, style); try appendCommands(context.scratch, &commands, body.commands, 0, 0); try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = body.height + gap, .width = body.width, .height = rule } }); var out = try context.boxFromList(body.width, body.height + gap + rule + extra, body.baseline, &commands); out.class = body.class; return out; }, else => { if (try context.layoutNativeAccent(value.mark, body, style)) |box| return box; const mark_text = accentText(value.mark); const mark_style = scriptStyle(context, style); const mark_box = try context.layoutText(mark_text, mark_style); const mark_x = accentMarkX(context, value.body, body, mark_text, mark_box, style, mark_style); const body_y = @divTrunc(mark_box.height, 2); const min_x = @min(@as(i32, 0), mark_x); const max_x = @max(body.width, mark_x + mark_box.width); try appendCommands(context.scratch, &commands, mark_box.commands, mark_x - min_x, 0); try appendCommands(context.scratch, &commands, body.commands, -min_x, body_y); var out = try context.boxFromList(max_x - min_x, body.height + body_y, body.baseline + body_y, &commands); out.class = body.class; return out; }, }}fn layoutNativeAccentImpl(context: *Context, mark: ast.AccentMark, body: Box, style: Style) anyerror!?Box { const rule = strokeWidth(context, style); const accent_height = wideAccentHeight(style, rule); const accent_width = @max(body.width, wideAccentMinimumWidth(style, rule)); var accent_commands: std.ArrayListUnmanaged(Command) = .empty; switch (mark) { .hat => try appendWideHat(context.scratch, &accent_commands, accent_width, 0, accent_height, rule), .tilde => try appendWideTilde(context.scratch, &accent_commands, accent_width, 0, accent_height, rule), .check => try appendWideCheck(context.scratch, &accent_commands, accent_width, 0, accent_height, rule), .breve => try appendWideBreve(context.scratch, &accent_commands, accent_width, 0, accent_height, rule), .vec => try appendWideArrowAccent(context.scratch, &accent_commands, accent_width, 0, accent_height, rule, false, true), .overleft => try appendWideArrowAccent(context.scratch, &accent_commands, accent_width, 0, accent_height, rule, true, false), .overleftright => try appendWideArrowAccent(context.scratch, &accent_commands, accent_width, 0, accent_height, rule, true, true), else => return null, } const gap = @max(1, @divTrunc(style.font_size, 12)); const width = @max(body.width, accent_width); const body_y = accent_height + gap; var commands: std.ArrayListUnmanaged(Command) = .empty; try appendCommands(context.scratch, &commands, accent_commands.items, centered(width, accent_width), 0); try appendCommands(context.scratch, &commands, body.commands, centered(width, body.width), body_y); var out = try context.boxFromList(width, body_y + body.height, body_y + body.baseline, &commands); out.class = body.class; return out;}fn layoutAnnotationImpl(context: *Context, value: ast.Annotation, style: Style) anyerror!Box { return switch (value.kind) { .plain => try context.layoutStack(value.base, value.over, value.under, style), .overbrace => try context.layoutBrace(value.base, style, true), .underbrace => try context.layoutBrace(value.base, style, false), .boxed => try context.layoutBoxed(value.base, style), };}fn layoutStackImpl(context: *Context, base_expr: *ast.Expr, over_expr: ?*ast.Expr, under_expr: ?*ast.Expr, style: Style) anyerror!Box { const base = try context.layoutExpr(base_expr, style); const script = scriptStyle(context, style); const over = if (over_expr) |expr| try context.layoutExpr(expr, script) else null; const under = if (under_expr) |expr| try context.layoutExpr(expr, script) else null; const gap = stackGap(context, style); const top_shift = stackTopShiftUp(context, style); const bottom_shift = stackBottomShiftDown(context, style); const width = @max(base.width, @max(if (over) |box| box.width else 0, if (under) |box| box.width else 0)); const base_y = if (over) |box| @max( top_shift + box.baseline - base.baseline, gap + box.height, ) else 0; const base_baseline = base_y + base.baseline; const under_y = if (under) |box| @max( base_y + base.height + gap, base_baseline + bottom_shift - box.baseline, ) else base_y + base.height; var height = base_y + base.height; if (under) |box| height = under_y + box.height; var commands: std.ArrayListUnmanaged(Command) = .empty; if (over) |box| try appendCommands(context.scratch, &commands, box.commands, centered(width, box.width), 0); try appendCommands(context.scratch, &commands, base.commands, centered(width, base.width), base_y); if (under) |box| try appendCommands(context.scratch, &commands, box.commands, centered(width, box.width), under_y); return context.boxFromList(width, height, base_y + base.baseline, &commands);}fn layoutBraceImpl(context: *Context, body_expr: *ast.Expr, style: Style, over: bool) anyerror!Box { const body = try context.layoutExpr(body_expr, style); const rule = strokeWidth(context, style); const gap = @max(2, @divTrunc(style.font_size, 8)); const brace_height = horizontalBraceHeight(style, rule); var commands: std.ArrayListUnmanaged(Command) = .empty; if (over) { try appendHorizontalBrace(context.scratch, &commands, body.width, 0, brace_height, rule, true); try appendCommands(context.scratch, &commands, body.commands, 0, brace_height + gap); return context.boxFromList(body.width, body.height + brace_height + gap, body.baseline + brace_height + gap, &commands); } try appendCommands(context.scratch, &commands, body.commands, 0, 0); try appendHorizontalBrace(context.scratch, &commands, body.width, body.height + gap, brace_height, rule, false); return context.boxFromList(body.width, body.height + brace_height + gap, body.baseline, &commands);}fn layoutBoxedImpl(context: *Context, body_expr: *ast.Expr, style: Style) anyerror!Box { const body = try context.layoutExpr(body_expr, style); const rule = strokeWidth(context, style); const pad = @max(3, @divTrunc(style.font_size, 5)); const width = body.width + pad * 2; const height = body.height + pad * 2; var commands: std.ArrayListUnmanaged(Command) = .empty; try appendFrame(context.scratch, &commands, width, height, rule); try appendCommands(context.scratch, &commands, body.commands, pad, pad); return context.boxFromList(width, height, body.baseline + pad, &commands);}fn layoutDelimitedImpl(context: *Context, value: ast.Delimited, style: Style) anyerror!Box { const body = try context.layoutExpr(value.body, style); const gap = @max(1, @divTrunc(style.font_size, 10)); const target_height = @max(body.height, delimitedSubFormulaMinHeight(context, style)); const target_baseline = body.baseline + centered(target_height, body.height); const left = try context.layoutDelimiter(value.left, target_height, target_baseline, style, true); const right = try context.layoutDelimiter(value.right, target_height, target_baseline, style, false); const baseline = @max(body.baseline, @max(left.baseline, right.baseline)); const descent = @max(body.height - body.baseline, @max(left.height - left.baseline, right.height - right.baseline)); const height = baseline + descent; var commands: std.ArrayListUnmanaged(Command) = .empty; try appendCommands(context.scratch, &commands, left.commands, 0, baseline - left.baseline); try appendCommands(context.scratch, &commands, body.commands, left.width + gap, baseline - body.baseline); try appendCommands(context.scratch, &commands, right.commands, left.width + gap + body.width + gap, baseline - right.baseline); var out = try context.boxFromList(left.width + body.width + right.width + gap * 2, height, baseline, &commands); out.class = .inner; return out;}fn layoutDelimiterImpl(context: *Context, delimiter: ast.Delimiter, height: i32, baseline: i32, style: Style, left: bool) anyerror!Box { return switch (delimiter) { .none => context.makeBox(0, height, baseline, &.{}), .text => |value| context.layoutText(value, style), .shape => |shape| try context.layoutDelimiterShape(shape, height, baseline, style, left), };}fn layoutDelimiterShapeImpl(context: *Context, shape: ast.DelimiterShape, height: i32, baseline: i32, style: Style, left: bool) anyerror!Box { if (try layoutMathVariantDelimiter(context, shape, height, baseline, style)) |box| return box; const rule = strokeWidth(context, style); const width = @max(rule, @divTrunc(style.font_size, 3)); if (shape == .bar or shape == .double_bar) { var commands: std.ArrayListUnmanaged(Command) = .empty; const x = if (left) 0 else width - rule; try commands.append(context.scratch, .{ .rect = .{ .x = x, .y = 0, .width = rule, .height = height } }); if (shape == .double_bar) try commands.append(context.scratch, .{ .rect = .{ .x = x + rule * 2, .y = 0, .width = rule, .height = height } }); return context.boxFromList(if (shape == .double_bar) width + rule * 2 else width, height, baseline, &commands); } if (shape == .left_bracket or shape == .right_bracket or shape == .left_floor or shape == .right_floor or shape == .left_ceil or shape == .right_ceil) { var commands: std.ArrayListUnmanaged(Command) = .empty; const x = if (left) 0 else width - rule; try commands.append(context.scratch, .{ .rect = .{ .x = x, .y = 0, .width = rule, .height = height } }); if (shape == .left_bracket or shape == .right_bracket or shape == .left_ceil or shape == .right_ceil) { try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = 0, .width = width, .height = rule } }); } if (shape == .left_bracket or shape == .right_bracket or shape == .left_floor or shape == .right_floor) { try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = height - rule, .width = width, .height = rule } }); } return context.boxFromList(width, height, baseline, &commands); } if (shape == .left_double_bracket or shape == .right_double_bracket) { const double_width = @max(rule * 5, @max(@divTrunc(style.font_size, 2), width)); var commands: std.ArrayListUnmanaged(Command) = .empty; const outer = if (left) 0 else double_width - rule; const inner = if (left) rule * 2 else double_width - rule * 3; try commands.append(context.scratch, .{ .rect = .{ .x = outer, .y = 0, .width = rule, .height = height } }); try commands.append(context.scratch, .{ .rect = .{ .x = inner, .y = 0, .width = rule, .height = height } }); try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = 0, .width = double_width, .height = rule } }); try commands.append(context.scratch, .{ .rect = .{ .x = 0, .y = height - rule, .width = double_width, .height = rule } }); return context.boxFromList(double_width, height, baseline, &commands); } if (try layoutStrokedDelimiter(context, shape, height, baseline, style)) |box| return box; const box = try context.layoutText(delimiterShapeText(shape), delimiterStyle(context, height, style)); const out_height = @max(height, box.height); const out_baseline = baseline + centered(out_height, height); var commands: std.ArrayListUnmanaged(Command) = .empty; try appendCommands(context.scratch, &commands, box.commands, 0, centered(out_height, box.height)); return context.boxFromList(box.width, out_height, out_baseline, &commands);}fn layoutMathVariantDelimiter(context: *Context, shape: ast.DelimiterShape, height: i32, baseline: i32, style: Style) anyerror!?Box { const codepoint = delimiterShapeCodepoint(shape) orelse return null; const glyph_id = context.font.face.glyphId(codepoint); if (glyph_id == 0) return null; const target = pixelHeightToDesignUnits(context, height, style); if (context.font.face.mathVerticalVariant(glyph_id, target)) |variant| { if (variant.advance_measurement >= target) return try layoutMathGlyphDelimiter(context, variant.glyph_id, height, baseline, style); } return try layoutMathAssemblyDelimiter(context, glyph_id, target, height, baseline, style);}fn layoutMathGlyphDelimiter(context: *Context, glyph_id: u32, height: i32, baseline: i32, style: Style) anyerror!Box { const bounds = (try glyphInkBounds(context, glyph_id, style)) orelse return error.InvalidFont; const ink_width = @max(1, bounds.right - bounds.left); const ink_height = @max(1, bounds.bottom - bounds.top); const out_height = @max(height, ink_height); const out_baseline = baseline + centered(out_height, height); var commands = [_]Command{.{ .glyph = .{ .x = -bounds.left, .y = centered(out_height, ink_height) - bounds.top, .glyph_id = glyph_id, .font_size = style.font_size, } }}; return try context.makeBox(ink_width, out_height, out_baseline, &commands);}fn layoutMathAssemblyDelimiter(context: *Context, glyph_id: u32, target: u16, height: i32, baseline: i32, style: Style) anyerror!?Box { const assembly = (try context.font.face.mathVerticalAssemblyAlloc(context.scratch, glyph_id, target)) orelse return null; if (assembly.parts.len == 0 or assembly.advance_measurement < target) return null; const assembly_height = @max(1, designUnitsToPixelsCeil(context, assembly.advance_measurement, style)); const out_height = @max(height, assembly_height); const out_baseline = baseline + centered(out_height, height); const assembly_y = centered(out_height, assembly_height); const bounds = try context.scratch.alloc(GlyphInkBounds, assembly.parts.len); var left_bound: i32 = std.math.maxInt(i32); var right_bound: i32 = std.math.minInt(i32); for (assembly.parts, 0..) |part, index| { bounds[index] = (try glyphInkBounds(context, part.glyph_id, style)) orelse return null; left_bound = @min(left_bound, bounds[index].left); right_bound = @max(right_bound, bounds[index].right); } if (left_bound >= right_bound) return null; var commands: std.ArrayListUnmanaged(Command) = .empty; for (assembly.parts, 0..) |part, index| { const part_end = @as(u32, part.advance_offset) + @as(u32, part.full_advance); if (part_end > assembly.advance_measurement) return null; const part_top_design: u16 = @intCast(@as(u32, assembly.advance_measurement) - part_end); const part_top = assembly_y + designUnitsToPixelsFloor(context, part_top_design, style); try commands.append(context.scratch, .{ .glyph = .{ .x = -left_bound, .y = part_top - bounds[index].top, .glyph_id = part.glyph_id, .font_size = style.font_size, } }); } return try context.boxFromList(right_bound - left_bound, out_height, out_baseline, &commands);}fn layoutStrokedDelimiter(context: *Context, shape: ast.DelimiterShape, height: i32, baseline: i32, style: Style) anyerror!?Box { return switch (shape) { .left_paren => try layoutParenStroke(context, height, baseline, style, true), .right_paren => try layoutParenStroke(context, height, baseline, style, false), .left_angle => try layoutAngleStroke(context, height, baseline, style, true, false), .right_angle => try layoutAngleStroke(context, height, baseline, style, false, false), .left_double_angle => try layoutAngleStroke(context, height, baseline, style, true, true), .right_double_angle => try layoutAngleStroke(context, height, baseline, style, false, true), .left_brace => try layoutBraceStroke(context, height, baseline, style, true), .right_brace => try layoutBraceStroke(context, height, baseline, style, false), else => null, };}fn layoutParenStroke(context: *Context, height: i32, baseline: i32, style: Style, left: bool) !Box { const rule = strokeWidth(context, style); const out_height = strokedDelimiterHeight(height, style); const out_baseline = baseline + centered(out_height, height); const width = @max(rule * 4, @max(@divTrunc(style.font_size, 2), @divTrunc(out_height, 8))); var commands: std.ArrayListUnmanaged(Command) = .empty; const outer = if (left) width - rule else 0; const inner = if (left) 0 else width - rule; const shoulder = if (left) @divTrunc(width, 3) else width - rule - @divTrunc(width, 3); try commands.append(context.scratch, .{ .line = .{ .x0 = outer, .y0 = 0, .x1 = shoulder, .y1 = @divTrunc(out_height, 5), .width = rule } }); try commands.append(context.scratch, .{ .line = .{ .x0 = shoulder, .y0 = @divTrunc(out_height, 5), .x1 = inner, .y1 = @divTrunc(out_height, 2), .width = rule } }); try commands.append(context.scratch, .{ .line = .{ .x0 = inner, .y0 = @divTrunc(out_height, 2), .x1 = shoulder, .y1 = @divTrunc(out_height * 4, 5), .width = rule } }); try commands.append(context.scratch, .{ .line = .{ .x0 = shoulder, .y0 = @divTrunc(out_height * 4, 5), .x1 = outer, .y1 = out_height - rule, .width = rule } }); return context.boxFromList(width, out_height, out_baseline, &commands);}fn layoutAngleStroke(context: *Context, height: i32, baseline: i32, style: Style, left: bool, double: bool) !Box { const rule = strokeWidth(context, style); const out_height = strokedDelimiterHeight(height, style); const out_baseline = baseline + centered(out_height, height); const single_width = @max(rule * 4, @max(@divTrunc(style.font_size, 2), @divTrunc(out_height, 7))); const gap = if (double) @max(rule * 2, @divTrunc(single_width, 4)) else 0; const width = if (double) single_width + gap else single_width; var commands: std.ArrayListUnmanaged(Command) = .empty; try appendAngleStroke(context.scratch, &commands, single_width, out_height, rule, left, if (double and left) 0 else gap); if (double) try appendAngleStroke(context.scratch, &commands, single_width, out_height, rule, left, if (left) gap else 0); return context.boxFromList(width, out_height, out_baseline, &commands);}fn layoutBraceStroke(context: *Context, height: i32, baseline: i32, style: Style, left: bool) !Box { const rule = strokeWidth(context, style); const out_height = strokedDelimiterHeight(height, style); const out_baseline = baseline + centered(out_height, height); const width = @max(rule * 5, @max(@divTrunc(style.font_size, 2), @divTrunc(out_height, 9))); var commands: std.ArrayListUnmanaged(Command) = .empty; try appendVerticalBrace(context.scratch, &commands, width, out_height, rule, left, 0); return context.boxFromList(width, out_height, out_baseline, &commands);}fn appendAngleStroke(allocator: std.mem.Allocator, commands: *std.ArrayListUnmanaged(Command), width: i32, height: i32, rule: i32, left: bool, dx: i32) !void { const outer = if (left) width - rule else 0; const inner = if (left) 0 else width - rule; const middle_y = @divTrunc(height, 2); try commands.append(allocator, .{ .line = .{ .x0 = dx + outer, .y0 = 0, .x1 = dx + inner, .y1 = middle_y, .width = rule } }); try commands.append(allocator, .{ .line = .{ .x0 = dx + inner, .y0 = middle_y, .x1 = dx + outer, .y1 = height - rule, .width = rule } });}fn strokedDelimiterHeight(height: i32, style: Style) i32 { return height + @max(2, @divTrunc(style.font_size, 6));}fn layoutGridImpl(context: *Context, value: ast.Grid, style: Style) anyerror!Box { if (value.rows.len == 0) return context.layoutText("", style); var columns: usize = 0; for (value.rows) |row| columns = @max(columns, row.cells.len); if (columns == 0) return context.layoutText("", style); const column_widths = try context.scratch.alloc(i32, columns); @memset(column_widths, 0); const row_baselines = try context.scratch.alloc(i32, value.rows.len); const row_heights = try context.scratch.alloc(i32, value.rows.len); const boxes = try context.scratch.alloc([]Box, value.rows.len); for (value.rows, 0..) |row, row_index| { boxes[row_index] = try context.scratch.alloc(Box, row.cells.len); var baseline: i32 = 0; var descent: i32 = 0; for (row.cells, 0..) |cell, column_index| { const box = try context.layoutExpr(cell, style); boxes[row_index][column_index] = box; column_widths[column_index] = @max(column_widths[column_index], box.width); baseline = @max(baseline, box.baseline); descent = @max(descent, box.height - box.baseline); } row_baselines[row_index] = baseline; row_heights[row_index] = baseline + descent; } const gap = @max(4, @divTrunc(style.font_size, 2)); var body_width: i32 = gap * @as(i32, @intCast(columns - 1)); for (column_widths) |width| body_width += width; var body_height: i32 = 0; for (row_heights) |height| body_height += height; var commands: std.ArrayListUnmanaged(Command) = .empty; var y: i32 = 0; for (value.rows, 0..) |row, row_index| { var x: i32 = 0; for (0..columns) |column_index| { if (column_index < row.cells.len) { const box = boxes[row_index][column_index]; const offset = switch (value.alignment) { .center => centered(column_widths[column_index], box.width), .left => 0, }; try appendCommands(context.scratch, &commands, box.commands, x + offset, y + row_baselines[row_index] - box.baseline); } x += column_widths[column_index] + gap; } y += row_heights[row_index]; } const body = try context.boxFromList(body_width, body_height, @divTrunc(body_height, 2), &commands); const left = try context.layoutDelimiter(gridLeftDelimiter(value.fence), body.height, body.baseline, style, true); const right = try context.layoutDelimiter(gridRightDelimiter(value.fence), body.height, body.baseline, style, false); if (left.width == 0 and right.width == 0) return body; const fence_gap = @max(1, @divTrunc(style.font_size, 10)); var framed: std.ArrayListUnmanaged(Command) = .empty; try appendCommands(context.scratch, &framed, left.commands, 0, 0); try appendCommands(context.scratch, &framed, body.commands, left.width + fence_gap, 0); try appendCommands(context.scratch, &framed, right.commands, left.width + fence_gap + body.width + fence_gap, 0); return context.boxFromList(left.width + body.width + right.width + fence_gap * 2, body.height, body.baseline, &framed);}fn paintImpl(context: *Context, canvas: filigree.render.Canvas, commands: []const Command, origin_x: i32, origin_y: i32) !void { for (commands) |command| { switch (command) { .text => |text| { var shaped_font = context.font; shaped_font.setScale(@floatFromInt(text.font_size), 72); const fallback_candidates = try context.fallbackCandidates(text.font_size); if (fallback_candidates.len == 0) { _ = try filigree.drawUtf8( context.allocator, context.shaping_output, canvas, &shaped_font, text.value, text.font_size, origin_x + text.x, origin_y + text.y, .{ .color = context.options.foreground }, ); } else { _ = try filigree.drawFallbackUtf8( context.allocator, context.shaping_output, canvas, &shaped_font, fallback_candidates, text.value, text.font_size, origin_x + text.x, origin_y + text.y, .{ .color = context.options.foreground }, ); } }, .rect => |rect| drawRect(canvas, origin_x + rect.x, origin_y + rect.y, rect.width, rect.height, context.options.foreground), .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), .glyph => |glyph| try drawGlyphCommand(context, canvas, glyph, origin_x, origin_y), } }}fn drawGlyphCommand(context: *Context, canvas: filigree.render.Canvas, glyph: Glyph, origin_x: i32, origin_y: i32) !void { var glyphs = [_]filigree.ShapedGlyph{.{ .glyph_id = glyph.glyph_id, .cluster = 0, .x_advance = 0, .y_advance = 0, .x_offset = 0, .y_offset = 0, }}; const clusters = [_]filigree.Cluster{}; const carets = [_]filigree.LigatureCaret{}; const run = filigree.GlyphRun{ .glyphs = &glyphs, .clusters = &clusters, .ligature_carets = &carets, .total_x_advance = 0, .total_y_advance = 0, .direction = .ltr, .writing_mode = .horizontal, .output_order = .logical, }; _ = try filigree.drawGlyphRun( context.allocator, canvas, context.font.face, run, glyph.font_size, origin_x + glyph.x, origin_y + glyph.y, .{ .color = context.options.foreground }, );}fn fontMetricsImpl(context: *const Context, font_size: i32) Metrics { const ascender = @max(1, scaleSigned(context.font.face.ascender, context.font.face.units_per_em, font_size)); const descender = @max(1, -scaleSigned(context.font.face.descender, context.font.face.units_per_em, font_size)); const line_gap = @max(0, scaleSigned(context.font.face.line_gap, context.font.face.units_per_em, font_size)); return .{ .ascender = ascender, .descender = descender, .line_gap = line_gap, .line_height = @max(1, ascender + descender + line_gap), };}fn fallbackCandidatesImpl(context: *Context, font_size: i32) ![]const filigree.FallbackCandidate { if (context.fallback_fonts.len == 0) return &.{}; const fonts = try context.scratch.alloc(filigree.Font, context.fallback_fonts.len); const candidates = try context.scratch.alloc(filigree.FallbackCandidate, context.fallback_fonts.len); for (context.fallback_fonts, 0..) |font, index| { fonts[index] = font; fonts[index].setScale(@floatFromInt(font_size), 72); candidates[index] = .{ .font = &fonts[index] }; } return candidates;}fn makeBoxImpl(context: *Context, width: i32, height: i32, baseline: i32, commands: []const Command) !Box { const owned = try context.scratch.dupe(Command, commands); return .{ .width = width, .height = height, .baseline = baseline, .commands = owned };}fn boxFromListImpl(context: *Context, width: i32, height: i32, baseline: i32, commands: *std.ArrayListUnmanaged(Command)) !Box { return .{ .width = width, .height = height, .baseline = baseline, .commands = try commands.toOwnedSlice(context.scratch), };}fn appendCommands( allocator: std.mem.Allocator, out: *std.ArrayListUnmanaged(Command), commands: []const Command, dx: i32, dy: i32,) !void { for (commands) |command| { try out.append(allocator, offsetCommand(command, dx, dy)); }}fn offsetCommand(command: Command, dx: i32, dy: i32) Command { return switch (command) { .text => |text| .{ .text = .{ .x = text.x + dx, .y = text.y + dy, .value = text.value, .font_size = text.font_size, } }, .rect => |rect| .{ .rect = .{ .x = rect.x + dx, .y = rect.y + dy, .width = rect.width, .height = rect.height, } }, .line => |line| .{ .line = .{ .x0 = line.x0 + dx, .y0 = line.y0 + dy, .x1 = line.x1 + dx, .y1 = line.y1 + dy, .width = line.width, } }, .glyph => |glyph| .{ .glyph = .{ .x = glyph.x + dx, .y = glyph.y + dy, .glyph_id = glyph.glyph_id, .font_size = glyph.font_size, } }, };}fn appendFrame(allocator: std.mem.Allocator, commands: *std.ArrayListUnmanaged(Command), width: i32, height: i32, rule: i32) !void { try commands.append(allocator, .{ .rect = .{ .x = 0, .y = 0, .width = width, .height = rule } }); try commands.append(allocator, .{ .rect = .{ .x = 0, .y = height - rule, .width = width, .height = rule } }); try commands.append(allocator, .{ .rect = .{ .x = 0, .y = 0, .width = rule, .height = height } }); try commands.append(allocator, .{ .rect = .{ .x = width - rule, .y = 0, .width = rule, .height = height } });}fn appendStretchArrowShaft( allocator: std.mem.Allocator, commands: *std.ArrayListUnmanaged(Command), x0: i32, x1: i32, axis: i32, rule: i32,) !void { if (x1 <= x0) return; try commands.append(allocator, .{ .rect = .{ .x = x0, .y = axis - @divTrunc(rule, 2), .width = x1 - x0, .height = rule, } });}fn appendDoubleStretchArrowShaft( allocator: std.mem.Allocator, commands: *std.ArrayListUnmanaged(Command), x0: i32, x1: i32, axis: i32, rule: i32, style: Style,) !void { const separation = @max(rule * 2 + 1, @divTrunc(style.font_size, 5)); try appendStretchArrowShaft(allocator, commands, x0, x1, axis - @divTrunc(separation, 2), rule); try appendStretchArrowShaft(allocator, commands, x0, x1, axis + @divTrunc(separation, 2), rule);}fn appendSquiggleStretchArrowShaft( allocator: std.mem.Allocator, commands: *std.ArrayListUnmanaged(Command), x0: i32, x1: i32, axis: i32, rule: i32, style: Style,) !void { if (x1 <= x0) return; const step = @max(3, @divTrunc(style.font_size, 5)); const amplitude = @max(rule * 2, @divTrunc(style.font_size, 9)); var x = x0; var y = axis; var up = true; while (x < x1) { const next_x = @min(x1, x + step); const next_y = axis + if (up) -amplitude else amplitude; try commands.append(allocator, .{ .line = .{ .x0 = x, .y0 = y, .x1 = next_x, .y1 = next_y, .width = rule, } }); x = next_x; y = next_y; up = !up; }}fn appendStretchArrowBar( allocator: std.mem.Allocator, commands: *std.ArrayListUnmanaged(Command), axis: i32, rule: i32, style: Style,) !void { const height = @max(rule * 5, @divTrunc(style.font_size * 2, 3)); try commands.append(allocator, .{ .rect = .{ .x = 0, .y = axis - @divTrunc(height, 2), .width = rule, .height = height, } });}fn appendHorizontalBrace( allocator: std.mem.Allocator, commands: *std.ArrayListUnmanaged(Command), width: i32, y: i32, height: i32, rule: i32, over: bool,) !void { if (width <= 0 or height <= 0) return; if (width <= rule * 4) { try commands.append(allocator, .{ .rect = .{ .x = 0, .y = y, .width = width, .height = rule } }); return; } const right = width - 1; const half = @divTrunc(right, 2); const wing = @max(rule * 3, @min(@divTrunc(width, 4), @max(rule * 4, @divTrunc(width, 3)))); const notch = @max(rule * 2, @min(@divTrunc(width, 10), @max(rule * 3, 1))); const left_peak = @min(wing, half); const right_peak = @max(half, right - wing); const mid_left = @max(left_peak, half - notch); const mid_right = @min(right_peak, half + notch); const high = y; const low = y + height - rule; const edge_y = if (over) low else high; const peak_y = if (over) high else low; const middle_y = if (over) low else high; const segments = [_]Line{ .{ .x0 = 0, .y0 = edge_y, .x1 = left_peak, .y1 = peak_y, .width = rule }, .{ .x0 = left_peak, .y0 = peak_y, .x1 = mid_left, .y1 = peak_y, .width = rule }, .{ .x0 = mid_left, .y0 = peak_y, .x1 = half, .y1 = middle_y, .width = rule }, .{ .x0 = half, .y0 = middle_y, .x1 = mid_right, .y1 = peak_y, .width = rule }, .{ .x0 = mid_right, .y0 = peak_y, .x1 = right_peak, .y1 = peak_y, .width = rule }, .{ .x0 = right_peak, .y0 = peak_y, .x1 = right, .y1 = edge_y, .width = rule }, }; for (segments) |segment| try commands.append(allocator, .{ .line = segment });}fn appendVerticalBrace( allocator: std.mem.Allocator, commands: *std.ArrayListUnmanaged(Command), width: i32, height: i32, rule: i32, left: bool, dx: i32,) !void { if (width <= 0 or height <= 0) return; const edge_x = if (left) width - rule else 0; if (height <= rule * 4) { try commands.append(allocator, .{ .rect = .{ .x = dx + edge_x, .y = 0, .width = rule, .height = height } }); return; } const bottom = height - rule; const half = @divTrunc(bottom, 2); const wing = @max(rule * 3, @min(@divTrunc(height, 4), @max(rule * 4, @divTrunc(height, 3)))); const notch = @max(rule * 2, @min(@divTrunc(height, 10), @max(rule * 3, 1))); const top_peak = @min(wing, half); const bottom_peak = @max(half, bottom - wing); const mid_top = @max(top_peak, half - notch); const mid_bottom = @min(bottom_peak, half + notch); const peak_x = if (left) 0 else width - rule; const middle_x = edge_x; const segments = [_]Line{ .{ .x0 = dx + edge_x, .y0 = 0, .x1 = dx + peak_x, .y1 = top_peak, .width = rule }, .{ .x0 = dx + peak_x, .y0 = top_peak, .x1 = dx + peak_x, .y1 = mid_top, .width = rule }, .{ .x0 = dx + peak_x, .y0 = mid_top, .x1 = dx + middle_x, .y1 = half, .width = rule }, .{ .x0 = dx + middle_x, .y0 = half, .x1 = dx + peak_x, .y1 = mid_bottom, .width = rule }, .{ .x0 = dx + peak_x, .y0 = mid_bottom, .x1 = dx + peak_x, .y1 = bottom_peak, .width = rule }, .{ .x0 = dx + peak_x, .y0 = bottom_peak, .x1 = dx + edge_x, .y1 = bottom, .width = rule }, }; for (segments) |segment| try commands.append(allocator, .{ .line = segment });}fn appendWideHat( allocator: std.mem.Allocator, commands: *std.ArrayListUnmanaged(Command), width: i32, y: i32, height: i32, rule: i32,) !void { if (width <= 0 or height <= 0) return; if (width <= rule * 3) { try commands.append(allocator, .{ .rect = .{ .x = 0, .y = y, .width = width, .height = rule } }); return; } const right = width - 1; const middle = @divTrunc(right, 2); const low = y + height - rule; try commands.append(allocator, .{ .line = .{ .x0 = 0, .y0 = low, .x1 = middle, .y1 = y, .width = rule } }); try commands.append(allocator, .{ .line = .{ .x0 = middle, .y0 = y, .x1 = right, .y1 = low, .width = rule } });}fn appendWideCheck( allocator: std.mem.Allocator, commands: *std.ArrayListUnmanaged(Command), width: i32, y: i32, height: i32, rule: i32,) !void { if (width <= 0 or height <= 0) return; if (width <= rule * 3) { try commands.append(allocator, .{ .rect = .{ .x = 0, .y = y, .width = width, .height = rule } }); return; } const right = width - 1; const middle = @divTrunc(right, 2); const low = y + height - rule; try commands.append(allocator, .{ .line = .{ .x0 = 0, .y0 = y, .x1 = middle, .y1 = low, .width = rule } }); try commands.append(allocator, .{ .line = .{ .x0 = middle, .y0 = low, .x1 = right, .y1 = y, .width = rule } });}fn appendWideBreve( allocator: std.mem.Allocator, commands: *std.ArrayListUnmanaged(Command), width: i32, y: i32, height: i32, rule: i32,) !void { if (width <= 0 or height <= 0) return; const right = width - 1; const quarter = @divTrunc(right, 4); const middle = @divTrunc(right, 2); const three_quarter = right - quarter; const high = y; const low = y + height - rule; const segments = [_]Line{ .{ .x0 = 0, .y0 = high, .x1 = quarter, .y1 = low, .width = rule }, .{ .x0 = quarter, .y0 = low, .x1 = middle, .y1 = low, .width = rule }, .{ .x0 = middle, .y0 = low, .x1 = three_quarter, .y1 = low, .width = rule }, .{ .x0 = three_quarter, .y0 = low, .x1 = right, .y1 = high, .width = rule }, }; for (segments) |segment| try commands.append(allocator, .{ .line = segment });}fn appendWideTilde( allocator: std.mem.Allocator, commands: *std.ArrayListUnmanaged(Command), width: i32, y: i32, height: i32, rule: i32,) !void { if (width <= 0 or height <= 0) return; const right = width - 1; const high = y; const low = y + height - rule; const mid = y + @divTrunc(height - rule, 2); const step = @max(rule * 3, @divTrunc(width, 6)); var x: i32 = 0; var current_y = mid; var up = true; while (x < right) { const next_x = @min(right, x + step); const next_y = if (up) high else low; try commands.append(allocator, .{ .line = .{ .x0 = x, .y0 = current_y, .x1 = next_x, .y1 = next_y, .width = rule, } }); x = next_x; current_y = next_y; up = !up; }}fn appendWideArrowAccent( allocator: std.mem.Allocator, commands: *std.ArrayListUnmanaged(Command), width: i32, y: i32, height: i32, rule: i32, left_head: bool, right_head: bool,) !void { if (width <= 0 or height <= 0) return; const right = width - 1; const axis = y + @divTrunc(height - rule, 2); const head = @max(rule * 3, @min(@max(rule * 3, height - rule), @divTrunc(width, 4))); const shaft_start = if (left_head) head else 0; const shaft_end = if (right_head) @max(shaft_start, right - head) else right; try appendStretchArrowShaft(allocator, commands, shaft_start, shaft_end, axis, rule); if (right_head) { try commands.append(allocator, .{ .line = .{ .x0 = right - head, .y0 = y, .x1 = right, .y1 = axis, .width = rule } }); try commands.append(allocator, .{ .line = .{ .x0 = right - head, .y0 = y + height - rule, .x1 = right, .y1 = axis, .width = rule } }); } if (left_head) { try commands.append(allocator, .{ .line = .{ .x0 = head, .y0 = y, .x1 = 0, .y1 = axis, .width = rule } }); try commands.append(allocator, .{ .line = .{ .x0 = head, .y0 = y + height - rule, .x1 = 0, .y1 = axis, .width = rule } }); }}fn scriptStyle(context: *const Context, style: Style) Style { const percent = if (mathConstants(context)) |constants| scriptPercent(constants.script_percent_scale_down) else 70; const scaled = @divTrunc(@as(i64, style.font_size) * percent + 50, 100); return .{ .font_size = @max(8, @as(i32, @intCast(scaled))) };}fn scriptScriptStyle(context: *const Context, style: Style) Style { const percent = if (mathConstants(context)) |constants| scriptPercentOr(constants.script_script_percent_scale_down, 50) else 50; const scaled = @divTrunc(@as(i64, style.font_size) * percent + 50, 100); return .{ .font_size = @max(8, @as(i32, @intCast(scaled))) };}fn strokeWidth(context: *const Context, style: Style) i32 { return fractionRuleWidth(context, style);}fn horizontalBraceHeight(style: Style, rule: i32) i32 { return @max(rule * 4 + 1, @divTrunc(style.font_size, 4));}fn wideAccentHeight(style: Style, rule: i32) i32 { return @max(rule * 4 + 1, @divTrunc(style.font_size, 5));}fn wideAccentMinimumWidth(style: Style, rule: i32) i32 { return @max(rule * 6, @divTrunc(style.font_size, 2));}fn defaultStrokeWidth(style: Style) i32 { return @max(1, @divTrunc(style.font_size, 13));}fn fractionRuleWidth(context: *const Context, style: Style) i32 { const fallback = defaultStrokeWidth(style); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.fraction_rule_thickness, style, fallback); return fallback;}fn radicalRuleWidth(context: *const Context, style: Style) i32 { const fallback = fractionRuleWidth(context, style); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.radical_rule_thickness, style, fallback); return fallback;}fn overbarRuleWidth(context: *const Context, style: Style) i32 { const fallback = strokeWidth(context, style); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.overbar_rule_thickness, style, fallback); return fallback;}fn underbarRuleWidth(context: *const Context, style: Style) i32 { const fallback = strokeWidth(context, style); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.underbar_rule_thickness, style, fallback); return fallback;}fn fractionNumeratorGap(context: *const Context, style: Style, fraction_style: ast.FractionStyle) i32 { const fallback = @max(2, @divTrunc(style.font_size, 6)); if (mathConstants(context)) |constants| { const value = switch (fraction_style) { .text => constants.fraction_numerator_gap_min, .display => constants.fraction_num_display_style_gap_min, }; return positiveMathValuePixels(context, value, style, fallback); } return fallback;}fn fractionDenominatorGap(context: *const Context, style: Style, fraction_style: ast.FractionStyle) i32 { const fallback = @max(2, @divTrunc(style.font_size, 6)); if (mathConstants(context)) |constants| { const value = switch (fraction_style) { .text => constants.fraction_denominator_gap_min, .display => constants.fraction_denom_display_style_gap_min, }; return positiveMathValuePixels(context, value, style, fallback); } return fallback;}fn fractionNumeratorShiftUp(context: *const Context, style: Style, fraction_style: ast.FractionStyle) i32 { const fallback = @max(1, @divTrunc(style.font_size, 2)); if (mathConstants(context)) |constants| { const value = switch (fraction_style) { .text => constants.fraction_numerator_shift_up, .display => constants.fraction_numerator_display_style_shift_up, }; return positiveMathValuePixels(context, value, style, fallback); } return fallback;}fn fractionDenominatorShiftDown(context: *const Context, style: Style, fraction_style: ast.FractionStyle) i32 { const fallback = @max(1, @divTrunc(style.font_size, 2)); if (mathConstants(context)) |constants| { const value = switch (fraction_style) { .text => constants.fraction_denominator_shift_down, .display => constants.fraction_denominator_display_style_shift_down, }; return positiveMathValuePixels(context, value, style, fallback); } return fallback;}fn upperLimitGap(context: *const Context, style: Style) i32 { const fallback = @max(1, @divTrunc(style.font_size, 8)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.upper_limit_gap_min, style, fallback); return fallback;}fn upperLimitBaselineRise(context: *const Context, style: Style) i32 { const fallback = @max(1, @divTrunc(style.font_size, 2)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.upper_limit_baseline_rise_min, style, fallback); return fallback;}fn lowerLimitGap(context: *const Context, style: Style) i32 { const fallback = @max(1, @divTrunc(style.font_size, 8)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.lower_limit_gap_min, style, fallback); return fallback;}fn lowerLimitBaselineDrop(context: *const Context, style: Style) i32 { const fallback = @max(1, @divTrunc(style.font_size, 2)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.lower_limit_baseline_drop_min, style, fallback); return fallback;}fn scriptHorizontalGap(context: *const Context, style: Style) i32 { const fallback = @max(1, @divTrunc(style.font_size, 8)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.space_after_script, style, fallback); return fallback;}fn subscriptShiftDown(context: *const Context, style: Style) i32 { const fallback = @max(1, @divTrunc(style.font_size, 3)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.subscript_shift_down, style, fallback); return fallback;}fn subscriptTopMax(context: *const Context, style: Style) i32 { const fallback = @max(1, @divTrunc(style.font_size * 2, 5)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.subscript_top_max, style, fallback); return fallback;}fn subscriptBaselineDropMin(context: *const Context, style: Style) i32 { if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.subscript_baseline_drop_min, style, 0); return 0;}fn superscriptShiftUp(context: *const Context, style: Style) i32 { const fallback = @max(1, @divTrunc(style.font_size * 2, 3)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.superscript_shift_up, style, fallback); return fallback;}fn superscriptBottomMin(context: *const Context, style: Style) i32 { const fallback = @max(1, @divTrunc(style.font_size, 4)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.superscript_bottom_min, style, fallback); return fallback;}fn superscriptBaselineDropMax(context: *const Context, style: Style) i32 { const fallback = @max(0, @divTrunc(style.font_size, 4)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.superscript_baseline_drop_max, style, fallback); return fallback;}fn subSuperscriptGap(context: *const Context, style: Style) i32 { const fallback = strokeWidth(context, style) * 4; if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.sub_superscript_gap_min, style, fallback); return fallback;}fn superscriptBottomMaxWithSubscript(context: *const Context, style: Style) i32 { const fallback = @max(superscriptBottomMin(context, style), subscriptTopMax(context, style)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.superscript_bottom_max_with_subscript, style, fallback); return fallback;}fn stretchStackTopShiftUp(context: *const Context, style: Style) i32 { const fallback = @max(1, @divTrunc(style.font_size, 2)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.stretch_stack_top_shift_up, style, fallback); return fallback;}fn stretchStackBottomShiftDown(context: *const Context, style: Style) i32 { const fallback = @max(1, @divTrunc(style.font_size, 2)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.stretch_stack_bottom_shift_down, style, fallback); return fallback;}fn stretchStackGapAbove(context: *const Context, style: Style) i32 { const fallback = @max(1, @divTrunc(style.font_size, 8)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.stretch_stack_gap_above_min, style, fallback); return fallback;}fn stretchStackGapBelow(context: *const Context, style: Style) i32 { const fallback = @max(1, @divTrunc(style.font_size, 8)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.stretch_stack_gap_below_min, style, fallback); return fallback;}fn stackTopShiftUp(context: *const Context, style: Style) i32 { if (mathConstants(context)) |constants| { const display = constants.stack_top_display_style_shift_up; if (display.value > 0) return positiveMathValuePixels(context, display, style, 0); return positiveMathValuePixels(context, constants.stack_top_shift_up, style, 0); } return 0;}fn stackBottomShiftDown(context: *const Context, style: Style) i32 { if (mathConstants(context)) |constants| { const display = constants.stack_bottom_display_style_shift_down; if (display.value > 0) return positiveMathValuePixels(context, display, style, 0); return positiveMathValuePixels(context, constants.stack_bottom_shift_down, style, 0); } return 0;}fn stackGap(context: *const Context, style: Style) i32 { const fallback = @max(1, @divTrunc(style.font_size, 8)); if (mathConstants(context)) |constants| { const display = constants.stack_display_style_gap_min; if (display.value > 0) return positiveMathValuePixels(context, display, style, fallback); return positiveMathValuePixels(context, constants.stack_gap_min, style, fallback); } return fallback;}fn overbarGap(context: *const Context, style: Style) i32 { const fallback = @max(2, @divTrunc(style.font_size, 8)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.overbar_vertical_gap, style, fallback); return fallback;}fn underbarGap(context: *const Context, style: Style) i32 { const fallback = @max(2, @divTrunc(style.font_size, 8)); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.underbar_vertical_gap, style, fallback); return fallback;}fn overbarExtraAscender(context: *const Context, style: Style) i32 { if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.overbar_extra_ascender, style, 0); return 0;}fn underbarExtraDescender(context: *const Context, style: Style) i32 { if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.underbar_extra_descender, style, 0); return 0;}fn delimitedSubFormulaMinHeight(context: *const Context, style: Style) i32 { if (mathConstants(context)) |constants| { if (constants.delimited_sub_formula_min_height != 0) return @max(1, designUnitsToPixelsCeil(context, constants.delimited_sub_formula_min_height, style)); } return 0;}fn displayOperatorMinHeight(context: *const Context, style: Style) ?i32 { if (mathConstants(context)) |constants| { if (constants.display_operator_min_height != 0) return @max(1, designUnitsToPixelsCeil(context, constants.display_operator_min_height, style)); } return null;}fn radicalClearance(context: *const Context, style: Style) i32 { const rule = radicalRuleWidth(context, style); const fallback = @max(rule + @divTrunc(rule + 3, 4), @divTrunc(style.font_size, 8)); if (mathConstants(context)) |constants| { const display = constants.radical_display_style_vertical_gap; if (display.value != 0) return positiveMathValuePixels(context, display, style, fallback); return positiveMathValuePixels(context, constants.radical_vertical_gap, style, fallback); } return fallback;}fn radicalExtraAscender(context: *const Context, style: Style) i32 { const fallback = radicalRuleWidth(context, style); if (mathConstants(context)) |constants| return positiveMathValuePixels(context, constants.radical_extra_ascender, style, fallback); return fallback;}fn radicalKernBeforeDegree(context: *const Context, style: Style) i32 { const fallback = @divTrunc(style.font_size * 5 + 9, 18); if (mathConstants(context)) |constants| return scaleSigned(constants.radical_kern_before_degree.value, context.font.face.units_per_em, style.font_size); return fallback;}fn radicalKernAfterDegree(context: *const Context, style: Style) i32 { const fallback = -@divTrunc(style.font_size * 10 + 9, 18); if (mathConstants(context)) |constants| return scaleSigned(constants.radical_kern_after_degree.value, context.font.face.units_per_em, style.font_size); return fallback;}fn radicalDegreeBottomRaisePercent(context: *const Context) i64 { if (mathConstants(context)) |constants| { if (constants.radical_degree_bottom_raise_percent > 0) return constants.radical_degree_bottom_raise_percent; } return 60;}fn radicalWidth(context: *const Context, height: i32, style: Style) i32 { return @max(@divTrunc(style.font_size, 2), @divTrunc(height, 4) + radicalRuleWidth(context, style) * 2);}fn mathAxisHeight(context: *const Context, style: Style) i32 { const fallback = @max(1, @divTrunc(style.font_size, 4)); if (mathConstants(context)) |constants| { if (constants.axis_height.value != 0) return @max(0, scaleSigned(constants.axis_height.value, context.font.face.units_per_em, style.font_size)); } return fallback;}fn stretchArrowLabelPadding(style: Style) i32 { return @max(6, @divTrunc(style.font_size, 2));}fn stretchArrowHeadOverlap(style: Style) i32 { return @max(2, @divTrunc(style.font_size, 3));}fn stretchArrowMinimumWidth(left_head: ?Box, right_head: ?Box, style: Style) i32 { var head_width: i32 = 0; if (left_head) |box| head_width += box.width; if (right_head) |box| head_width += box.width; return @max(@max(style.font_size * 2, 12), head_width + @max(style.font_size, 8));}fn stretchArrowAxis(context: *const Context, style: Style, baseline: i32, height: i32, rule: i32) i32 { const raw = baseline - mathAxisHeight(context, style); const low = @max(0, @divTrunc(rule, 2)); const high = @max(low, height - @divTrunc(rule + 1, 2)); return @min(@max(raw, low), high);}fn accentMarkX(context: *Context, body_expr: *const ast.Expr, body: Box, mark_text: []const u8, mark: Box, body_style: Style, mark_style: Style) i32 { const body_anchor = accentAttachmentX(context, singleGlyphId(context, body_expr), body, body_style); const mark_anchor = accentAttachmentX(context, singleTextGlyphId(context, mark_text), mark, mark_style); return body_anchor - mark_anchor;}fn accentAttachmentX(context: *const Context, glyph_id: ?u32, box: Box, style: Style) i32 { if (glyph_id) |id| { if (context.font.face.mathTopAccentAttachment(id)) |value| { return scaleSigned(value.value, context.font.face.units_per_em, style.font_size); } } return @divTrunc(box.width, 2);}fn mathConstants(context: *const Context) ?filigree.MathConstants { return context.font.face.mathConstants();}fn positiveMathValuePixels(context: *const Context, value: filigree.MathValueRecord, style: Style, fallback: i32) i32 { if (value.value <= 0) return fallback; return @max(1, scaleSigned(value.value, context.font.face.units_per_em, style.font_size));}fn scriptPercent(value: i16) i64 { return scriptPercentOr(value, 70);}fn scriptPercentOr(value: i16, fallback: i64) i64 { if (value <= 0) return fallback; if (value > 100) return 100; return @intCast(value);}fn delimiterStyle(context: *Context, height: i32, style: Style) Style { const metrics = context.fontMetrics(style.font_size); if (height <= metrics.line_height) return style; const extra = @max(2, @divTrunc(style.font_size, 6)); const desired = height + extra; const scaled = @divTrunc(@as(i64, style.font_size) * desired + metrics.line_height - 1, metrics.line_height); return .{ .font_size = @max(style.font_size, @as(i32, @intCast(scaled))) };}fn spacePixels(value: ast.Space, style: Style) i32 { if (value.numerator == 0) return 0; const numerator = @as(i64, style.font_size) * @as(i64, value.numerator); const denominator: i64 = @intCast(value.denominator); const rounded = if (numerator >= 0) numerator + @divTrunc(denominator, 2) else numerator - @divTrunc(denominator, 2); const pixels = @as(i32, @intCast(@divTrunc(rounded, denominator))); if (pixels == 0) return if (value.numerator > 0) 1 else -1; return pixels;}fn mathClassSpace(children: []const Box, index: usize, style: Style) i32 { const left = children[index - 1]; const right = children[index]; if (left.width == 0 or right.width == 0) return 0; if (left.class == .spacing or right.class == .spacing) return 0; if (left.class == .relation or right.class == .relation) return relationSpace(left.class, right.class, style); if (right.class == .binary) return binaryBeforeSpace(left.class, style); if (left.class == .binary) return binaryAfterSpace(children, index, right.class, style); if (left.class == .punctuation) return muSpace(style, 3); if (left.class == .operator and followsOperator(right.class)) return muSpace(style, 3); return 0;}fn relationSpace(left: MathClass, right: MathClass, style: Style) i32 { if (left == .open or right == .close or right == .punctuation) return 0; return muSpace(style, 5);}fn binaryBeforeSpace(left: MathClass, style: Style) i32 { if (precedesBinary(left)) return muSpace(style, 4); return 0;}fn binaryAfterSpace(children: []const Box, index: usize, right: MathClass, style: Style) i32 { if (index <= 1) return 0; if (!precedesBinary(children[index - 2].class)) return 0; if (followsBinary(right)) return muSpace(style, 4); return 0;}fn precedesBinary(class: MathClass) bool { return switch (class) { .ordinary, .operator, .close, .inner => true, .binary, .relation, .open, .punctuation, .spacing => false, };}fn followsBinary(class: MathClass) bool { return switch (class) { .ordinary, .operator, .open, .inner => true, .binary, .relation, .close, .punctuation, .spacing => false, };}fn followsOperator(class: MathClass) bool { return switch (class) { .ordinary, .operator, .open, .inner => true, .binary, .relation, .close, .punctuation, .spacing => false, };}fn muSpace(style: Style, mu: i32) i32 { return spacePixels(.{ .numerator = mu, .denominator = 18 }, style);}fn mathClassForText(value: []const u8) MathClass { if (isBinaryText(value)) return .binary; if (isRelationText(value)) return .relation; if (operator.limitsText(value)) return .operator; if (isOperatorText(value)) return .operator; if (isOpenText(value)) return .open; if (isCloseText(value)) return .close; if (isPunctuationText(value)) return .punctuation; return .ordinary;}fn isBinaryText(value: []const u8) bool { inline for (.{ "+", "-", "*", "/", "±", "∓", "×", "÷", "·", "∗", "⋆", "⋄", "∘", "∧", "∨", "∪", "∩", "∖", "⊕", "⊖", "⊗", "⊘", "⊙", "⊛", "⊚", "⊝", "⊞", "⊟", "⊠", "⊔", "⊓", "⊎", "≀", "∣", }) |candidate| { if (std.mem.eql(u8, value, candidate)) return true; } return false;}fn isRelationText(value: []const u8) bool { inline for (.{ "=", "<", ">", "≤", "≰", "≲", "⪅", "≪", "⋘", "≥", "≱", "≳", "⪆", "≫", "≠", "≮", "≯", "≐", "≜", "≔", "≕", "≡", "≢", "≈", "≉", "≍", "∼", "≁", "≃", "≄", "≾", "≿", "≅", "≇", "∝", "≺", "≻", "≼", "≽", "⊥", "⊤", "⋈", "∈", "∉", "∋", "∌", "⊂", "⊃", "⊆", "⊇", "⊈", "⊉", "⊊", "⊋", "⊏", "⊐", "⊑", "⊒", "⋢", "⋣", "⊢", "⊩", "⊨", "⊪", "⊣", "⊬", "⊭", "⊮", "⊯", "∥", "∦", "∤", "→", "⟶", "←", "⟵", "↔", "⟷", "⇒", "⟹", "⇐", "⟸", "⇔", "⟺", "⇏", "⇍", "⇎", "↦", "⟼", "⊸", "↪", "↩", "↠", "↞", "↣", "↢", "↝", "⇝", "⇀", "⇁", "↼", "↽", "⇌", }) |candidate| { if (std.mem.eql(u8, value, candidate)) return true; } return false;}fn isOperatorText(value: []const u8) bool { inline for (.{ "∑", "⨁", "⨂", "⨀", "∏", "∐", "∫", "∬", "∭", "∮", "⋀", "⋁", "⋃", "⋂", }) |candidate| { if (std.mem.eql(u8, value, candidate)) return true; } return false;}fn isOpenText(value: []const u8) bool { inline for (.{ "(", "[", "{", "⟨", "⟪", "⟦", "⌊", "⌈", "⌜", "⌞", }) |candidate| { if (std.mem.eql(u8, value, candidate)) return true; } return false;}fn isCloseText(value: []const u8) bool { inline for (.{ ")", "]", "}", "⟩", "⟫", "⟧", "⌋", "⌉", "⌝", "⌟", }) |candidate| { if (std.mem.eql(u8, value, candidate)) return true; } return false;}fn isPunctuationText(value: []const u8) bool { inline for (.{ ",", ";", "…", "⋯", }) |candidate| { if (std.mem.eql(u8, value, candidate)) return true; } return false;}fn accentText(mark: ast.AccentMark) []const u8 { return switch (mark) { .hat => "^", .vec => "→", .dot => "˙", .tilde => "~", .check => "ˇ", .breve => "˘", .ddot => "¨", .acute => "´", .grave => "`", .ring => "˚", .overleft => "←", .overleftright => "↔", .bar, .underline => unreachable, };}fn delimiterShapeText(shape: ast.DelimiterShape) []const u8 { return switch (shape) { .left_paren => "(", .right_paren => ")", .left_brace => "{", .right_brace => "}", .left_angle => "⟨", .right_angle => "⟩", .left_double_angle => "⟪", .right_double_angle => "⟫", .left_double_bracket => "⟦", .right_double_bracket => "⟧", .left_bracket, .right_bracket, .bar, .double_bar, .left_floor, .right_floor, .left_ceil, .right_ceil => "|", };}fn delimiterShapeCodepoint(shape: ast.DelimiterShape) ?u21 { return switch (shape) { .left_paren => '(', .right_paren => ')', .left_bracket => '[', .right_bracket => ']', .left_brace => '{', .right_brace => '}', .left_angle => 0x27e8, .right_angle => 0x27e9, .left_double_angle => 0x27ea, .right_double_angle => 0x27eb, .left_double_bracket => 0x27e6, .right_double_bracket => 0x27e7, .left_floor => 0x230a, .right_floor => 0x230b, .left_ceil => 0x2308, .right_ceil => 0x2309, .bar, .double_bar => null, };}fn gridLeftDelimiter(fence: ast.GridFence) ast.Delimiter { return switch (fence) { .none => .none, .paren => .{ .shape = .left_paren }, .bracket => .{ .shape = .left_bracket }, .brace, .left_brace => .{ .shape = .left_brace }, .bar => .{ .shape = .bar }, .double_bar => .{ .shape = .double_bar }, };}fn gridRightDelimiter(fence: ast.GridFence) ast.Delimiter { return switch (fence) { .none, .left_brace => .none, .paren => .{ .shape = .right_paren }, .bracket => .{ .shape = .right_bracket }, .brace => .{ .shape = .right_brace }, .bar => .{ .shape = .bar }, .double_bar => .{ .shape = .double_bar }, };}fn imageDimension(content: i32, padding: u32) !u32 { const padded = content + @as(i32, @intCast(padding)) * 2; if (padded <= 0) return 1; return std.math.cast(u32, padded) orelse error.InvalidDimensions;}fn centered(width: i32, inner: i32) i32 { if (inner >= width) return 0; return @divTrunc(width - inner, 2);}fn scaleSigned(value: i32, units_per_em: u16, font_size: i32) i32 { const numerator = @as(i64, value) * @as(i64, font_size); const denominator: i64 = units_per_em; const rounded = if (numerator >= 0) numerator + @divTrunc(denominator, 2) else numerator - @divTrunc(denominator, 2); return @intCast(@divTrunc(rounded, denominator));}fn pixelHeightToDesignUnits(context: *const Context, height: i32, style: Style) u16 { return pixelMeasurementToDesignUnits(context, height, style);}fn pixelWidthToDesignUnits(context: *const Context, width: i32, style: Style) u16 { return pixelMeasurementToDesignUnits(context, width, style);}fn pixelMeasurementToDesignUnits(context: *const Context, measurement: i32, style: Style) u16 { const pixels = @max(1, measurement); const numerator = @as(i64, pixels) * @as(i64, context.font.face.units_per_em) + @divTrunc(style.font_size, 2); const design = @divTrunc(numerator, style.font_size); if (design <= 0) return 1; if (design > std.math.maxInt(u16)) return std.math.maxInt(u16); return @intCast(design);}fn designUnitsToPixelsFloor(context: *const Context, value: u16, style: Style) i32 { return scaleFloor(value, context.font.face.units_per_em, style.font_size);}fn designUnitsToPixelsCeil(context: *const Context, value: u16, style: Style) i32 { return scaleCeil(value, context.font.face.units_per_em, style.font_size);}fn glyphInkBounds(context: *Context, glyph_id: u32, style: Style) !?GlyphInkBounds { var outline = filigree.glyphOutlineAlloc(context.scratch, context.font.face, glyph_id) catch |err| switch (err) { error.OutOfMemory => return err, else => return null, }; defer outline.deinit(context.scratch); 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; return .{ .left = scaleFloor(outline.bounds.x_min, context.font.face.units_per_em, style.font_size), .top = scaleFloor(@as(i32, context.font.face.ascender) - outline.bounds.y_max, context.font.face.units_per_em, style.font_size), .right = scaleCeil(outline.bounds.x_max, context.font.face.units_per_em, style.font_size), .bottom = scaleCeil(@as(i32, context.font.face.ascender) - outline.bounds.y_min, context.font.face.units_per_em, style.font_size), };}fn singleGlyphId(context: *Context, expr: *const ast.Expr) ?u32 { const value = ast.textValue(expr) orelse return null; return singleTextGlyphId(context, value);}fn singleTextGlyphId(context: *const Context, value: []const u8) ?u32 { if (value.len == 0) return null; const sequence_len = std.unicode.utf8ByteSequenceLength(value[0]) catch return null; if (sequence_len != value.len) return null; const codepoint = std.unicode.utf8Decode(value[0..sequence_len]) catch return null; const glyph_id = context.font.face.glyphId(codepoint); if (glyph_id == 0) return null; return glyph_id;}fn scaleFloor(value: i32, units_per_em: u16, font_size: i32) i32 { const numerator = @as(i64, value) * @as(i64, font_size); return @intCast(divFloor(numerator, units_per_em));}fn scaleCeil(value: i32, units_per_em: u16, font_size: i32) i32 { const numerator = @as(i64, value) * @as(i64, font_size); return @intCast(-divFloor(-numerator, units_per_em));}fn divFloor(numerator: i64, denominator: i64) i64 { var quotient = @divTrunc(numerator, denominator); const remainder = @rem(numerator, denominator); if (remainder != 0 and numerator < 0) quotient -= 1; return quotient;}fn drawRect(canvas: filigree.render.Canvas, x: i32, y: i32, width: i32, height: i32, color: Color) void { if (width <= 0 or height <= 0) return; const x0 = @max(0, x); const y0 = @max(0, y); const x1 = @min(@as(i32, @intCast(canvas.width)), x + width); const y1 = @min(@as(i32, @intCast(canvas.height)), y + height); if (x0 >= x1 or y0 >= y1) return; var py: usize = @intCast(y0); while (py < @as(usize, @intCast(y1))) : (py += 1) { var px: usize = @intCast(x0); while (px < @as(usize, @intCast(x1))) : (px += 1) { const offset = py * canvas.stride + px * 4; canvas.pixels[offset] = color.r; canvas.pixels[offset + 1] = color.g; canvas.pixels[offset + 2] = color.b; canvas.pixels[offset + 3] = color.a; } }}fn drawLine(canvas: filigree.render.Canvas, x0: i32, y0: i32, x1: i32, y1: i32, width: i32, color: Color) void { if (width <= 0) return; var x = x0; var y = y0; const dx = if (x1 >= x0) x1 - x0 else x0 - x1; const dy = if (y1 >= y0) y1 - y0 else y0 - y1; const sx: i32 = if (x0 < x1) 1 else -1; const sy: i32 = if (y0 < y1) 1 else -1; var err = dx - dy; const offset = @divTrunc(width, 2); while (true) { drawRect(canvas, x - offset, y - offset, width, width, color); if (x == x1 and y == y1) break; const e2 = err * 2; if (e2 > -dy) { err -= dy; x += sx; } if (e2 < dx) { err += dx; y += sy; } }}fn validateOptions(options: Options) !void { if (options.font_bytes.len == 0) return error.InvalidFont; if (options.font_size <= 0) return error.InvalidPixelSize;}fn loadFallbackFonts(allocator: std.mem.Allocator, fonts: []const []const u8) ![]filigree.Font { const loaded = try allocator.alloc(filigree.Font, fonts.len); errdefer allocator.free(loaded); var index: usize = 0; errdefer { for (loaded[0..index]) |*font| font.deinit(); } while (index < fonts.len) : (index += 1) { const bytes = fonts[index]; loaded[index] = filigree.Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.InvalidFont; } return loaded;}fn unloadFallbackFonts(allocator: std.mem.Allocator, fonts: []filigree.Font) void { for (fonts) |*font| font.deinit(); allocator.free(fonts);}fn fill(pixels: []u8, width: u32, height: u32, stride: u32, color: Color) void { var row: u32 = 0; while (row < height) : (row += 1) { var col: u32 = 0; while (col < width) : (col += 1) { const offset = @as(usize, row) * @as(usize, stride) + @as(usize, col) * 4; pixels[offset] = color.r; pixels[offset + 1] = color.g; pixels[offset + 2] = color.b; pixels[offset + 3] = color.a; } }}fn visiblePixels(pixels: []const u8) usize { var count: usize = 0; var index: usize = 3; while (index < pixels.len) : (index += 4) { if (pixels[index] != 0) count += 1; } return count;}fn visiblePixelsInRect(image: *const Image, x: u32, y: u32, width: u32, height: u32) usize { const x1 = @min(image.width, x + width); const y1 = @min(image.height, y + height); var count: usize = 0; var py = y; while (py < y1) : (py += 1) { var px = x; while (px < x1) : (px += 1) { const offset = @as(usize, py) * image.stride + @as(usize, px) * 4 + 3; if (image.pixels[offset] != 0) count += 1; } } return count;}test "image renderer draws parsed math into owned rgba pixels" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); var rendered = try render(allocator, "A+B", .{ .font_bytes = bytes, .font_size = 20, }); defer rendered.deinit(); try std.testing.expect(rendered.width > 20); try std.testing.expect(rendered.height > 20); try std.testing.expect(visiblePixels(rendered.pixels) > 0);}test "image renderer lays out fractions as pixels instead of cells" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); var rendered = try render(allocator, "\\frac{A}{B}", .{ .font_bytes = bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer rendered.deinit(); try std.testing.expect(rendered.width < 40); try std.testing.expect(rendered.height > 40); try std.testing.expect(rendered.baseline < rendered.height); try std.testing.expect(visiblePixels(rendered.pixels) > 0);}test "image renderer scales delimiters around tall bodies" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); var fraction = try render(allocator, "\\frac{A+B}{C+D}", .{ .font_bytes = bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer fraction.deinit(); var delimited = try render(allocator, "\\left(\\frac{A+B}{C+D}\\right)", .{ .font_bytes = bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer delimited.deinit(); try std.testing.expect(delimited.width > fraction.width); try std.testing.expect(delimited.height > fraction.height); try std.testing.expect(delimited.baseline < delimited.height); try std.testing.expect(visiblePixels(delimited.pixels) > 0);}test "image renderer draws capped bracket fallbacks" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); var rendered = try render(allocator, "\\left[\\frac{A}{B}\\right]", .{ .font_bytes = bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer rendered.deinit(); try std.testing.expect(visiblePixelsInRect(&rendered, 0, 0, @max(1, rendered.width / 5), 4) > 0); try std.testing.expect(visiblePixelsInRect(&rendered, 0, rendered.height - @min(rendered.height, 4), @max(1, rendered.width / 5), @min(rendered.height, 4)) > 0);}test "image renderer draws double bracket fallbacks" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); var fraction = try render(allocator, "\\frac{A}{B}", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fraction.deinit(); var rendered = try render(allocator, "\\left\\llbracket\\frac{A}{B}\\right\\rrbracket", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer rendered.deinit(); const edge_width = @min(rendered.width, 16); const half_edge = @max(1, edge_width / 2); const band_height = @min(rendered.height, 6); const right_x = rendered.width - edge_width; try std.testing.expect(rendered.width > fraction.width); try std.testing.expect(rendered.height >= fraction.height); try std.testing.expect(visiblePixelsInRect(&rendered, 0, 0, edge_width, band_height) > 0); try std.testing.expect(visiblePixelsInRect(&rendered, 0, rendered.height - band_height, edge_width, band_height) > 0); try std.testing.expect(visiblePixelsInRect(&rendered, 0, 0, half_edge, rendered.height) > 0); try std.testing.expect(visiblePixelsInRect(&rendered, half_edge, 0, edge_width - half_edge, rendered.height) > 0); try std.testing.expect(visiblePixelsInRect(&rendered, right_x, 0, edge_width, band_height) > 0); try std.testing.expect(visiblePixelsInRect(&rendered, right_x, rendered.height - band_height, edge_width, band_height) > 0); try std.testing.expect(visiblePixelsInRect(&rendered, right_x, 0, half_edge, rendered.height) > 0); try std.testing.expect(visiblePixelsInRect(&rendered, right_x + half_edge, 0, edge_width - half_edge, rendered.height) > 0);}test "image renderer draws stroked brace fallbacks around tall bodies" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); var fraction = try render(allocator, "\\frac{A}{B}", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fraction.deinit(); var rendered = try render(allocator, "\\left\\{\\frac{A}{B}\\right\\}", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer rendered.deinit(); const edge_width = @min(rendered.width, 14); const band_height = @min(rendered.height, 6); const middle_y = rendered.height / 2 - @min(rendered.height / 2, band_height / 2); const right_x = rendered.width - edge_width; try std.testing.expect(rendered.width > fraction.width); try std.testing.expect(rendered.height > fraction.height); try std.testing.expect(visiblePixelsInRect(&rendered, 0, 0, edge_width, band_height) > 0); try std.testing.expect(visiblePixelsInRect(&rendered, 0, middle_y, edge_width, band_height) > 0); try std.testing.expect(visiblePixelsInRect(&rendered, 0, rendered.height - band_height, edge_width, band_height) > 0); try std.testing.expect(visiblePixelsInRect(&rendered, right_x, 0, edge_width, band_height) > 0); try std.testing.expect(visiblePixelsInRect(&rendered, right_x, middle_y, edge_width, band_height) > 0); try std.testing.expect(visiblePixelsInRect(&rendered, right_x, rendered.height - band_height, edge_width, band_height) > 0);}test "image renderer uses MATH assemblies for tall delimiters" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const assembly_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(assembly_bytes); var fallback = try render(allocator, "\\left(\\frac{A+B}{C+D}\\right)", .{ .font_bytes = fallback_bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer fallback.deinit(); var assembled = try render(allocator, "\\left(\\frac{A+B}{C+D}\\right)", .{ .font_bytes = assembly_bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer assembled.deinit(); const fallback_left = visiblePixelsInRect(&fallback, 0, 0, @min(fallback.width, 12), fallback.height); const assembled_left = visiblePixelsInRect(&assembled, 0, 0, @min(assembled.width, 12), assembled.height); try std.testing.expect(assembled_left > fallback_left * 2);}test "image renderer uses MATH minimum height for small delimiters" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var fallback = try render(allocator, "\\left(A\\right)", .{ .font_bytes = fallback_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fallback.deinit(); var math = try render(allocator, "\\left(A\\right)", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer math.deinit(); try std.testing.expect(math.height > fallback.height); try std.testing.expect(math.baseline > fallback.baseline); try std.testing.expect(visiblePixelsInRect(&math, 0, 0, @min(math.width, 12), math.height) > 0);}test "image renderer draws scalable native radicals" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); var fraction = try render(allocator, "\\frac{A}{B}", .{ .font_bytes = bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer fraction.deinit(); var rooted = try render(allocator, "\\sqrt{\\frac{A}{B}}", .{ .font_bytes = bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer rooted.deinit(); try std.testing.expect(rooted.width > fraction.width + 8); try std.testing.expect(rooted.height > fraction.height); try std.testing.expect(rooted.baseline > fraction.baseline); try std.testing.expect(visiblePixelsInRect(&rooted, 0, rooted.height / 2, rooted.width / 3, rooted.height - rooted.height / 2) > 0);}test "image renderer uses MATH assemblies for radical signs" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const assembly_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(assembly_bytes); var fallback = try render(allocator, "\\sqrt{\\frac{A+B}{C+D}}", .{ .font_bytes = fallback_bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer fallback.deinit(); var assembled = try render(allocator, "\\sqrt{\\frac{A+B}{C+D}}", .{ .font_bytes = assembly_bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer assembled.deinit(); const fallback_left = visiblePixelsInRect(&fallback, 0, 0, @min(fallback.width, 12), fallback.height); const assembled_left = visiblePixelsInRect(&assembled, 0, 0, @min(assembled.width, 12), assembled.height); try std.testing.expect(assembled.height > fallback.height); try std.testing.expect(assembled_left > fallback_left * 2);}test "image renderer uses MATH display radical gap" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var fallback = try render(allocator, "\\sqrt{A}", .{ .font_bytes = fallback_bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer fallback.deinit(); var math = try render(allocator, "\\sqrt{A}", .{ .font_bytes = math_bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer math.deinit(); try std.testing.expect(math.height > fallback.height + 16); try std.testing.expect(math.baseline > fallback.baseline + 12); try std.testing.expect(visiblePixels(math.pixels) > 0);}test "image renderer uses MATH radical rule and ascender metrics" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var fallback = try render(allocator, "\\sqrt{A}", .{ .font_bytes = fallback_bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer fallback.deinit(); var math = try render(allocator, "\\sqrt{A}", .{ .font_bytes = math_bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer math.deinit(); try std.testing.expect(math.height > fallback.height + 22); try std.testing.expect(math.baseline > fallback.baseline + 18); try std.testing.expect(visiblePixels(math.pixels) > 0);}test "image renderer tucks radical degrees into the root sign" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); var square = try render(allocator, "\\sqrt{\\frac{A+B}{C+D}}", .{ .font_bytes = bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer square.deinit(); var cube = try render(allocator, "\\sqrt[3]{\\frac{A+B}{C+D}}", .{ .font_bytes = bytes, .font_size = 20, .padding_x = 0, .padding_y = 0, }); defer cube.deinit(); try std.testing.expect(cube.width > square.width); try std.testing.expect(cube.width < square.width + 12);}test "image renderer uses MATH script-script scale percentage for radical degrees" { const allocator = std.testing.allocator; const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var square = try render(allocator, "\\sqrt{A}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer square.deinit(); var degree = try render(allocator, "\\sqrt[BBBBBBBB]{A}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer degree.deinit(); try std.testing.expect(degree.width > square.width + 56); try std.testing.expect(visiblePixels(degree.pixels) > 0);}test "image renderer uses MATH radical degree metrics" { const allocator = std.testing.allocator; const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var square = try render(allocator, "\\sqrt{A}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer square.deinit(); var degree = try render(allocator, "\\sqrt[3]{A}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer degree.deinit(); try std.testing.expect(degree.width > square.width + 26); try std.testing.expect(degree.height > square.height + 4); try std.testing.expect(degree.baseline > square.baseline + 4); try std.testing.expect(visiblePixels(degree.pixels) > 0);}test "image renderer stacks large operator limits" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); var limited = try render(allocator, "lim_{ABC}^{D}", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer limited.deinit(); var side = try render(allocator, "x_{ABC}^{D}", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer side.deinit(); try std.testing.expect(limited.width < side.width); try std.testing.expect(limited.height > side.height); try std.testing.expect(visiblePixels(limited.pixels) > 0);}test "image renderer uses MATH constants for operator limits" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var fallback = try render(allocator, "lim_{B}^{B}", .{ .font_bytes = fallback_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fallback.deinit(); var math = try render(allocator, "lim_{B}^{B}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer math.deinit(); try std.testing.expect(math.height > fallback.height + 48); try std.testing.expect(math.baseline > fallback.baseline + 32); try std.testing.expect(visiblePixels(math.pixels) > 0);}test "image renderer uses MATH display operator minimum height" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(bytes); var ordinary = try render(allocator, "A", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer ordinary.deinit(); var sum = try render(allocator, "\\sum", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer sum.deinit(); var text_operator = try render(allocator, "lim", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer text_operator.deinit(); try std.testing.expect(sum.height > ordinary.height + 20); try std.testing.expect(sum.height > text_operator.height + 20); try std.testing.expect(visiblePixels(sum.pixels) > 0);}test "image renderer uses MATH constants for side scripts" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var fallback = try render(allocator, "A_{B}^{B}", .{ .font_bytes = fallback_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fallback.deinit(); var math = try render(allocator, "A_{B}^{B}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer math.deinit(); try std.testing.expect(math.baseline > fallback.baseline + 4); try std.testing.expect(math.height > fallback.height); try std.testing.expect(visiblePixels(math.pixels) > 0);}test "image renderer uses MATH script scale percentage" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var fallback = try render(allocator, "A_{BBBBBBBB}^{BBBBBBBB}", .{ .font_bytes = fallback_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fallback.deinit(); var math = try render(allocator, "A_{BBBBBBBB}^{BBBBBBBB}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer math.deinit(); try std.testing.expect(math.width > fallback.width + 8); try std.testing.expect(visiblePixels(math.pixels) > 0);}test "image renderer uses MATH side script bounds" { const allocator = std.testing.allocator; const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var base = try render(allocator, "\\frac{A}{B}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer base.deinit(); var scripted = try render(allocator, "\\frac{A}{B}_{B}^{B}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer scripted.deinit(); var crowded = try render(allocator, "A_{\\frac{B}{B}}^{\\frac{B}{B}}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer crowded.deinit(); try std.testing.expect(scripted.height > base.height + 50); try std.testing.expect(scripted.baseline > base.baseline + 11); try std.testing.expect(crowded.baseline > scripted.baseline + 22); try std.testing.expect(visiblePixels(scripted.pixels) > 0); try std.testing.expect(visiblePixels(crowded.pixels) > 0);}test "image renderer uses MATH constants for fraction shifts" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var fallback = try render(allocator, "\\frac{A}{B}", .{ .font_bytes = fallback_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fallback.deinit(); var math = try render(allocator, "\\frac{A}{B}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer math.deinit(); try std.testing.expect(math.height > fallback.height); try std.testing.expect(math.baseline > fallback.baseline); try std.testing.expect(visiblePixels(math.pixels) > 0);}test "image renderer uses MATH text fraction gap constants" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var fallback = try render(allocator, "\\frac{A}{B}", .{ .font_bytes = fallback_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fallback.deinit(); var text = try render(allocator, "\\frac{A}{B}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer text.deinit(); var display = try render(allocator, "\\dfrac{A}{B}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer display.deinit(); try std.testing.expect(text.height > fallback.height + 18); try std.testing.expect(display.height > text.height); try std.testing.expect(visiblePixels(text.pixels) > 0);}test "image renderer uses MATH fraction rule thickness" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var fallback = try render(allocator, "\\frac{A}{B}", .{ .font_bytes = fallback_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fallback.deinit(); var math = try render(allocator, "\\frac{A}{B}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer math.deinit(); try std.testing.expect(math.height > fallback.height + 31); try std.testing.expect(math.baseline > fallback.baseline + 32); try std.testing.expect(visiblePixels(math.pixels) > 0);}test "image renderer uses MATH axis height for fractions" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var fallback = try render(allocator, "\\frac{A}{B}", .{ .font_bytes = fallback_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fallback.deinit(); var math = try render(allocator, "\\frac{A}{B}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer math.deinit(); try std.testing.expect(math.baseline > fallback.baseline + 24); try std.testing.expect(visiblePixels(math.pixels) > 0);}test "image renderer uses MATH display constants for display fractions" { const allocator = std.testing.allocator; const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var text = try render(allocator, "\\frac{A}{B}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer text.deinit(); var text_alias = try render(allocator, "\\tfrac{A}{B}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer text_alias.deinit(); var display = try render(allocator, "\\dfrac{A}{B}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer display.deinit(); try std.testing.expectEqual(text.height, text_alias.height); try std.testing.expectEqual(text.baseline, text_alias.baseline); try std.testing.expect(display.height > text.height); try std.testing.expect(display.baseline > text.baseline); try std.testing.expect(visiblePixels(display.pixels) > 0);}test "image renderer stretches labeled arrows across wide labels" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); const font_size: i32 = 24; var right = try render(allocator, "\\xrightarrow{ABCDEFGH}", .{ .font_bytes = bytes, .font_size = font_size, .padding_x = 0, .padding_y = 0, }); defer right.deinit(); var mapped = try render(allocator, "\\xmapsto{ABCDEFGH}", .{ .font_bytes = bytes, .font_size = font_size, .padding_x = 0, .padding_y = 0, }); defer mapped.deinit(); const axis_drop: u32 = @intCast(@max(1, @divTrunc(font_size, 4))); const band_margin = axis_drop + 2; const band_y = if (right.baseline > band_margin) right.baseline - band_margin else 0; const band_height = @min(right.height - band_y, 7); const edge_width = @min(right.width, 12); try std.testing.expect(visiblePixelsInRect(&right, right.width - edge_width, band_y, edge_width, band_height) > 0); try std.testing.expect(visiblePixelsInRect(&mapped, 0, 0, @min(mapped.width, 4), mapped.height) > 0);}test "image renderer uses MATH assemblies for horizontal arrows" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const assembly_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(assembly_bytes); var fallback = try render(allocator, "\\xrightarrow{ABCDEFGH}", .{ .font_bytes = fallback_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fallback.deinit(); var assembled = try render(allocator, "\\xrightarrow{ABCDEFGH}", .{ .font_bytes = assembly_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer assembled.deinit(); const fallback_base = visiblePixelsInRect(&fallback, 0, fallback.baseline / 2, fallback.width, @max(1, fallback.height - fallback.baseline / 2)); const assembled_base = visiblePixelsInRect(&assembled, 0, assembled.baseline / 2, assembled.width, @max(1, assembled.height - assembled.baseline / 2)); try std.testing.expect(assembled_base > fallback_base * 2);}test "image renderer uses MATH stretch stack constants for labeled arrows" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var fallback = try render(allocator, "\\xleftarrow[B]{B}", .{ .font_bytes = fallback_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fallback.deinit(); var math = try render(allocator, "\\xleftarrow[B]{B}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer math.deinit(); try std.testing.expect(math.height > fallback.height + 48); try std.testing.expect(math.baseline > fallback.baseline + 32); try std.testing.expect(visiblePixels(math.pixels) > 0);}test "image renderer draws brace annotations as shaped strokes" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); const font_size: i32 = 24; var over = try render(allocator, "\\overbrace{ABCDEFGH}", .{ .font_bytes = bytes, .font_size = font_size, .padding_x = 0, .padding_y = 0, }); defer over.deinit(); var under = try render(allocator, "\\underbrace{ABCDEFGH}", .{ .font_bytes = bytes, .font_size = font_size, .padding_x = 0, .padding_y = 0, }); defer under.deinit(); const brace_depth: u32 = @intCast(@max(5, @divTrunc(font_size, 4))); try std.testing.expect(visiblePixelsInRect(&over, 0, 2, over.width, @min(2, over.height - 2)) > 0); const under_band_y = under.height - @min(under.height, brace_depth); const under_middle_y = @min(under.height - 1, under_band_y + @min(2, brace_depth - 1)); try std.testing.expect(visiblePixelsInRect(&under, 0, under_middle_y, under.width, @min(2, under.height - under_middle_y)) > 0);}test "image renderer stretches wide accent strokes" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); const font_size: i32 = 24; var hat = try render(allocator, "\\widehat{ABCDEFGH}", .{ .font_bytes = bytes, .font_size = font_size, .padding_x = 0, .padding_y = 0, }); defer hat.deinit(); var arrow = try render(allocator, "\\overrightarrow{ABCDEFGH}", .{ .font_bytes = bytes, .font_size = font_size, .padding_x = 0, .padding_y = 0, }); defer arrow.deinit(); const accent_depth: u32 = @intCast(@max(5, @divTrunc(font_size, 5))); const edge_width = @min(hat.width, 12); try std.testing.expect(visiblePixelsInRect(&hat, 0, 0, edge_width, @min(hat.height, accent_depth)) > 0); try std.testing.expect(visiblePixelsInRect(&hat, hat.width - edge_width, 0, edge_width, @min(hat.height, accent_depth)) > 0); try std.testing.expect(visiblePixelsInRect(&arrow, 0, 0, @min(arrow.width, edge_width), @min(arrow.height, accent_depth)) > 0); 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);}test "image renderer uses MATH top accent attachment" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var fallback = try render(allocator, "\\grave{A}", .{ .font_bytes = fallback_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fallback.deinit(); var attached = try render(allocator, "\\grave{A}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer attached.deinit(); try std.testing.expect(attached.width > fallback.width); try std.testing.expect(visiblePixelsInRect(&attached, attached.width - @min(attached.width, 6), 0, @min(attached.width, 6), @min(attached.height, attached.baseline)) > 0);}test "image renderer uses MATH over and underbar metrics" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var fallback_bar = try render(allocator, "\\bar{A}", .{ .font_bytes = fallback_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fallback_bar.deinit(); var math_bar = try render(allocator, "\\bar{A}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer math_bar.deinit(); var fallback_under = try render(allocator, "\\underline{A}", .{ .font_bytes = fallback_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fallback_under.deinit(); var math_under = try render(allocator, "\\underline{A}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer math_under.deinit(); try std.testing.expect(math_bar.height > fallback_bar.height + 24); try std.testing.expect(math_bar.baseline > fallback_bar.baseline + 24); try std.testing.expect(math_under.height > fallback_under.height + 22); try std.testing.expectEqual(fallback_under.baseline, math_under.baseline);}test "image renderer uses MATH display stack constants for annotations" { const allocator = std.testing.allocator; const fallback_bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(fallback_bytes); const math_bytes = try filigree.fixtures.createWithMathAssembly(allocator); defer allocator.free(math_bytes); var fallback_over = try render(allocator, "\\overset{B}{A}", .{ .font_bytes = fallback_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fallback_over.deinit(); var math_over = try render(allocator, "\\overset{B}{A}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer math_over.deinit(); var fallback_under = try render(allocator, "\\underset{B}{A}", .{ .font_bytes = fallback_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer fallback_under.deinit(); var math_under = try render(allocator, "\\underset{B}{A}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer math_under.deinit(); var tall_over = try render(allocator, "\\overset{\\frac{B}{B}}{A}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer tall_over.deinit(); var tall_under = try render(allocator, "\\underset{\\frac{B}{B}}{A}", .{ .font_bytes = math_bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer tall_under.deinit(); try std.testing.expect(math_over.baseline > fallback_over.baseline + 24); try std.testing.expect(math_under.height > fallback_under.height + 24); try std.testing.expect(tall_over.baseline > math_over.baseline + 40); try std.testing.expect(tall_under.height > math_under.height + 40); try std.testing.expect(visiblePixels(math_over.pixels) > 0); try std.testing.expect(visiblePixels(math_under.pixels) > 0); try std.testing.expect(visiblePixels(tall_over.pixels) > 0); try std.testing.expect(visiblePixels(tall_under.pixels) > 0);}test "image renderer applies explicit operator limit policy" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); var starred = try render(allocator, "\\operatorname*{argmin}_{ABC}", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer starred.deinit(); var plain = try render(allocator, "\\operatorname{argmin}_{ABC}", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer plain.deinit(); var automatic = try render(allocator, "lim_{ABC}^{D}", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer automatic.deinit(); var nolimits = try render(allocator, "lim\\nolimits_{ABC}^{D}", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer nolimits.deinit(); try std.testing.expect(starred.width < plain.width); try std.testing.expect(automatic.width < nolimits.width); try std.testing.expect(automatic.height > nolimits.height); try std.testing.expect(visiblePixels(starred.pixels) > 0);}test "image renderer keeps grid fences in pixel layout" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); var rendered = try render(allocator, "\\begin{pmatrix}A&B\\\\C&D\\end{pmatrix}", .{ .font_bytes = bytes, .font_size = 20, }); defer rendered.deinit(); try std.testing.expect(rendered.width > 40); try std.testing.expect(rendered.height > 35); try std.testing.expect(rendered.baseline < rendered.height); try std.testing.expect(visiblePixels(rendered.pixels) > 0);}test "image renderer scales explicit math spaces" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); var tight = try render(allocator, "AB", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer tight.deinit(); var thin = try render(allocator, "A\\,B", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer thin.deinit(); var quad = try render(allocator, "A\\quad B", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer quad.deinit(); var negative = try render(allocator, "A\\!B", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer negative.deinit(); try std.testing.expect(thin.width > tight.width); try std.testing.expect(quad.width > thin.width); try std.testing.expect(negative.width < tight.width);}test "image renderer computes math class spacing" { const style: Style = .{ .font_size = 36 }; const ordinary: Box = .{ .width = 1, .height = 1, .baseline = 0, .commands = &.{}, .class = .ordinary }; const binary: Box = .{ .width = 1, .height = 1, .baseline = 0, .commands = &.{}, .class = .binary }; const relation: Box = .{ .width = 1, .height = 1, .baseline = 0, .commands = &.{}, .class = .relation }; const open: Box = .{ .width = 1, .height = 1, .baseline = 0, .commands = &.{}, .class = .open }; const punctuation: Box = .{ .width = 1, .height = 1, .baseline = 0, .commands = &.{}, .class = .punctuation }; const explicit: Box = .{ .width = 1, .height = 1, .baseline = 0, .commands = &.{}, .class = .spacing }; const plus = [_]Box{ ordinary, binary, ordinary }; try std.testing.expectEqual(@as(i32, 8), mathClassSpace(&plus, 1, style)); try std.testing.expectEqual(@as(i32, 8), mathClassSpace(&plus, 2, style)); const unary = [_]Box{ binary, ordinary }; try std.testing.expectEqual(@as(i32, 0), mathClassSpace(&unary, 1, style)); const equals = [_]Box{ ordinary, relation, ordinary }; try std.testing.expectEqual(@as(i32, 10), mathClassSpace(&equals, 1, style)); try std.testing.expectEqual(@as(i32, 10), mathClassSpace(&equals, 2, style)); const grouped = [_]Box{ open, binary, ordinary }; try std.testing.expectEqual(@as(i32, 0), mathClassSpace(&grouped, 1, style)); const comma = [_]Box{ ordinary, punctuation, ordinary }; try std.testing.expectEqual(@as(i32, 6), mathClassSpace(&comma, 2, style)); const manual = [_]Box{ ordinary, explicit, relation }; try std.testing.expectEqual(@as(i32, 0), mathClassSpace(&manual, 1, style)); try std.testing.expectEqual(@as(i32, 0), mathClassSpace(&manual, 2, style)); try std.testing.expectEqual(MathClass.operator, mathClassForText("lim")); try std.testing.expectEqual(MathClass.operator, mathClassForText("max")); try std.testing.expectEqual(MathClass.relation, mathClassForText("→"));}test "image renderer adds automatic binary and relation spacing" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); var automatic = try render(allocator, "A+B=C", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer automatic.deinit(); var tightened = try render(allocator, "A\\!+\\!B\\!=\\!C", .{ .font_bytes = bytes, .font_size = 24, .padding_x = 0, .padding_y = 0, }); defer tightened.deinit(); try std.testing.expect(automatic.width > tightened.width + 10); try std.testing.expect(visiblePixels(automatic.pixels) > 0);}test "image renderer accepts fallback font bytes" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); const fallback_fonts = [_][]const u8{bytes}; var rendered = try render(allocator, "\\mathbf{A}+B", .{ .font_bytes = bytes, .fallback_font_bytes = &fallback_fonts, .font_size = 20, }); defer rendered.deinit(); try std.testing.expect(rendered.width > 20); try std.testing.expect(rendered.height > 20); try std.testing.expect(visiblePixels(rendered.pixels) > 0);}test "image renderer accepts CFF outline font bytes" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithCffOutlines(allocator); defer allocator.free(bytes); var rendered = try render(allocator, "\\frac{A}{B}", .{ .font_bytes = bytes, .font_size = 20, }); defer rendered.deinit(); try std.testing.expect(rendered.width > 20); try std.testing.expect(rendered.height > 35); try std.testing.expect(visiblePixels(rendered.pixels) > 0);}test "image renderer rejects invalid fallback font bytes" { const allocator = std.testing.allocator; const bytes = try filigree.fixtures.createWithOutlines(allocator); defer allocator.free(bytes); const fallback_fonts = [_][]const u8{&.{}}; try std.testing.expectError(error.InvalidFont, render(allocator, "A", .{ .font_bytes = bytes, .fallback_font_bytes = &fallback_fonts, .font_size = 20, }));}Source: lib/termtex/src/root.zig:12
zig
pub const image = @import("image.zig");Complete caller list for image.render
42 direct callers.
lib.termtex.src.image.test_image_renderer_accepts_CFF_outline_font_bytes[function] — test source atlib/termtex/src/image.zig:3644in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_accepts_fallback_font_bytes[function] — test source atlib/termtex/src/image.zig:3626in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_adds_automatic_binary_and_relation_spacing[function] — test source atlib/termtex/src/image.zig:3602in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_applies_explicit_operator_limit_policy[function] — test source atlib/termtex/src/image.zig:3472in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_draws_brace_annotations_as_shaped_strokes[function] — test source atlib/termtex/src/image.zig:3287in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_draws_capped_bracket_fallbacks[function] — test source atlib/termtex/src/image.zig:2561in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_draws_double_bracket_fallbacks[function] — test source atlib/termtex/src/image.zig:2578in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_draws_parsed_math_into_owned_rgba_pixels[function] — test source atlib/termtex/src/image.zig:2500in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_draws_scalable_native_radicals[function] — test source atlib/termtex/src/image.zig:2702in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_draws_stroked_brace_fallbacks_around_tall_bodies[function] — test source atlib/termtex/src/image.zig:2614in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_keeps_grid_fences_in_pixel_layout[function] — test source atlib/termtex/src/image.zig:3512in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_lays_out_fractions_as_pixels_instead_of_cells[function] — test source atlib/termtex/src/image.zig:2516in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_rejects_invalid_fallback_font_bytes[function] — test source atlib/termtex/src/image.zig:3660in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_scales_delimiters_around_tall_bodies[function] — test source atlib/termtex/src/image.zig:2535in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_scales_explicit_math_spaces[function] — test source atlib/termtex/src/image.zig:3529in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_stacks_large_operator_limits[function] — test source atlib/termtex/src/image.zig:2884in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_stretches_labeled_arrows_across_wide_labels[function] — test source atlib/termtex/src/image.zig:3203in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_stretches_wide_accent_strokes[function] — test source atlib/termtex/src/image.zig:3315in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_tucks_radical_degrees_into_the_root_sign[function] — test source atlib/termtex/src/image.zig:2810in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_assemblies_for_horizontal_arrows[function] — test source atlib/termtex/src/image.zig:3233in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_assemblies_for_radical_signs[function] — test source atlib/termtex/src/image.zig:2728in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_assemblies_for_tall_delimiters[function] — test source atlib/termtex/src/image.zig:2648in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_axis_height_for_fractions[function] — test source atlib/termtex/src/image.zig:3143in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_constants_for_fraction_shifts[function] — test source atlib/termtex/src/image.zig:3055in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_constants_for_operator_limits[function] — test source atlib/termtex/src/image.zig:2909in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_constants_for_side_scripts[function] — test source atlib/termtex/src/image.zig:2968in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_display_constants_for_display_fractions[function] — test source atlib/termtex/src/image.zig:3169in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_display_operator_minimum_height[function] — test source atlib/termtex/src/image.zig:2936in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_display_radical_gap[function] — test source atlib/termtex/src/image.zig:2756in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_display_stack_constants_for_annotations[function] — test source atlib/termtex/src/image.zig:3412in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_fraction_rule_thickness[function] — test source atlib/termtex/src/image.zig:3116in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_minimum_height_for_small_delimiters[function] — test source atlib/termtex/src/image.zig:2675in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_over_and_underbar_metrics[function] — test source atlib/termtex/src/image.zig:3370in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_radical_degree_metrics[function] — test source atlib/termtex/src/image.zig:2858in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_radical_rule_and_ascender_metrics[function] — test source atlib/termtex/src/image.zig:2783in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_script-script_scale_percentage_for_radical_degrees[function] — test source atlib/termtex/src/image.zig:2834in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_script_scale_percentage[function] — test source atlib/termtex/src/image.zig:2995in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_side_script_bounds[function] — test source atlib/termtex/src/image.zig:3021in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_stretch_stack_constants_for_labeled_arrows[function] — test source atlib/termtex/src/image.zig:3260in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_text_fraction_gap_constants[function] — test source atlib/termtex/src/image.zig:3082in nearest public ownertiny.termtex.imagelib.termtex.src.image.test_image_renderer_uses_MATH_top_accent_attachment[function] — test source atlib/termtex/src/image.zig:3344in nearest public ownertiny.termtex.imagetiny.termtex.renderer.renderImage[function] atlib/termtex/src/render.zig:60
Audit
| Definitions | 3 |
|---|---|
| Public names | 3 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |