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