lib/filigree/src/shape/engine.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const cluster_data = @import("cluster.zig");
3 const font = @import("../font/root.zig");
4 const test_font = @import("../fixture/root.zig");
5 const fixture_binary = test_font.binary;
6 const unicode_data = @import("unicode");
7
8 const Allocator = std.mem.Allocator;
9 const dotted_circle_codepoint: u21 = 0x25cc;
10
11 const shape_model = @import("model.zig");
12
13 const FeatureRange = shape_model.FeatureRange;
14 const FeatureSetting = shape_model.FeatureSetting;
15 const VariationSetting = shape_model.VariationSetting;
16 const Direction = shape_model.Direction;
17 const WritingMode = shape_model.WritingMode;
18 const OutputOrder = shape_model.OutputOrder;
19 const ClusterMode = shape_model.ClusterMode;
20 const DefaultIgnorablePolicy = shape_model.DefaultIgnorablePolicy;
21 const MissingGlyphPolicy = shape_model.MissingGlyphPolicy;
22 const DottedCirclePolicy = shape_model.DottedCirclePolicy;
23 const Source = shape_model.Source;
24 const SourceRange = shape_model.SourceRange;
25 const GlyphSpan = shape_model.GlyphSpan;
26 const GlyphFlags = shape_model.GlyphFlags;
27 const GlyphClass = shape_model.GlyphClass;
28 const GlyphAttachmentKind = shape_model.GlyphAttachmentKind;
29 const GlyphAttachment = shape_model.GlyphAttachment;
30 const LigatureCaret = shape_model.LigatureCaret;
31 const ShapedGlyph = shape_model.ShapedGlyph;
32 const Cluster = shape_model.Cluster;
33
34 const joining = @import("joining.zig");
35 const JoiningType = joining.JoiningType;
36 const joiningType = joining.joiningType;
37 const joiningTypesConnect = joining.joiningTypesConnect;
38
39 const numerals = @import("numerals.zig");
40 const isDecimalNumber = numerals.isDecimalNumber;
41
42 const fraction_slash_codepoint: u21 = 0x2044;
43
44 pub const Input = struct {
45 font: *const Font,
46 text: Source,
47 source_offset: u32 = 0,
48 pre_context: ?Source = null,
49 post_context: ?Source = null,
50 script: font.Tag = font.defaultScriptTag,
51 script_tags: []const font.Tag = &.{},
52 language: ?font.Tag = null,
53 features: []const FeatureSetting = &.{},
54 variations: []const VariationSetting = &.{},
55 direction: Direction = .ltr,
56 writing_mode: WritingMode = .horizontal,
57 cluster_mode: ClusterMode = .monotone_characters,
58 output_order: OutputOrder = .visual,
59 default_ignorable_policy: DefaultIgnorablePolicy = .hide,
60 missing_glyph_policy: MissingGlyphPolicy = .preserve,
61 dotted_circle_policy: DottedCirclePolicy = .disabled,
62
63 fn layoutSelection(self: Input) font.LayoutSelection {
64 return self.layoutSelectionWithFeatures(self.features);
65 }
66
67 fn layoutSelectionWithFeatures(self: Input, features: []const FeatureSetting) font.LayoutSelection {
68 return .{
69 .script = self.script,
70 .script_tags = self.script_tags,
71 .language = self.language,
72 .features = features,
73 .variations = self.variations,
74 .vertical = self.writing_mode == .vertical,
75 .right_to_left = self.direction == .rtl,
76 };
77 }
78 };
79
80 const SourceScalar = struct {
81 codepoint: u21,
82 start: u32,
83 end: u32,
84 };
85
86 const InputPlan = struct {
87 has_variation_sequences: bool,
88 automatic_fractions: bool,
89 arabic_joining_forms: bool,
90
91 fn init(input: Input, has_variation_sequences: bool) InputPlan {
92 const selection = input.layoutSelection();
93 const has_gsub = input.font.face.hasGsub();
94 return .{
95 .has_variation_sequences = has_variation_sequences,
96 .automatic_fractions = has_gsub and input.font.face.usesAutomaticFractions(selection),
97 .arabic_joining_forms = has_gsub and input.font.face.usesArabicJoiningForms(selection),
98 };
99 }
100 };
101
102 const ShapePlan = struct {
103 selection: font.LayoutSelection,
104 has_variation_sequences: bool,
105 gsub_lookup_count: u16,
106 has_gpos: bool,
107 gpos_lookup_count: u16,
108 has_gpos_single: bool,
109 has_gpos_contextual_pair: bool,
110 horizontal: bool,
111 needs_context_glyph_ids: bool,
112 needs_visible_glyph_ids: bool,
113
114 fn init(input: Input, has_variation_sequences: bool, glyph_count: usize) !ShapePlan {
115 const selection = input.layoutSelection();
116 const has_gpos = input.font.face.hasGpos();
117 const has_gpos_single = has_gpos and input.font.face.hasGposSingleAdjustmentFor(selection);
118 const has_gpos_contextual_pair = has_gpos and input.font.face.hasGposContextualPairAdjustmentFor(selection);
119 const horizontal = input.writing_mode == .horizontal;
120 return .{
121 .selection = selection,
122 .has_variation_sequences = has_variation_sequences,
123 .gsub_lookup_count = if (glyph_count == 0) 0 else try input.font.face.gsubLookupCount(),
124 .has_gpos = has_gpos,
125 .gpos_lookup_count = input.font.face.gposLookupCount(),
126 .has_gpos_single = has_gpos_single,
127 .has_gpos_contextual_pair = has_gpos_contextual_pair,
128 .horizontal = horizontal,
129 .needs_context_glyph_ids = has_gpos_single or has_gpos_contextual_pair,
130 .needs_visible_glyph_ids = has_gpos or horizontal,
131 };
132 }
133 };
134
135 const Output = @import("output.zig").Output;
136
137 const ContextOptions = struct {};
138
139 pub const Context = struct {
140 allocator: Allocator,
141 glyph_ids: std.ArrayListUnmanaged(u32) = .empty,
142 glyph_indexes: std.ArrayListUnmanaged(usize) = .empty,
143 context_glyph_ids: std.ArrayListUnmanaged(u32) = .empty,
144 replacement_glyphs: std.ArrayListUnmanaged(ShapedGlyph) = .empty,
145 replacement_clusters: std.ArrayListUnmanaged(Cluster) = .empty,
146 replacement_ids: std.ArrayListUnmanaged(u32) = .empty,
147 feature_settings: std.ArrayListUnmanaged(FeatureSetting) = .empty,
148 source_scalars: std.ArrayListUnmanaged(SourceScalar) = .empty,
149
150 pub const Options: type = ContextOptions;
151
152 pub const init = initImpl;
153 pub const deinit = deinitImpl;
154 pub const reset = resetImpl;
155 pub const shapeRun = shapeRunImpl;
156 const featuresWithAutomaticRanges = featuresWithAutomaticRangesImpl;
157 const collectSourceScalars = collectSourceScalarsImpl;
158 const appendAutomaticFractionFeatures = appendAutomaticFractionFeaturesImpl;
159 const appendAutomaticArabicJoiningFeatures = appendAutomaticArabicJoiningFeaturesImpl;
160 const applyArabicTatweelSafety = applyArabicTatweelSafetyImpl;
161 const applySubstitutions = applySubstitutionsImpl;
162 const applySubstitutionAt = applySubstitutionAtImpl;
163 const applyOutputOrder = applyOutputOrderImpl;
164 };
165
166 fn initImpl(allocator: Allocator, options: ContextOptions) Context {
167 _ = options;
168 return .{ .allocator = allocator };
169 }
170
171 fn deinitImpl(self: *Context) void {
172 self.glyph_ids.deinit(self.allocator);
173 self.glyph_indexes.deinit(self.allocator);
174 self.context_glyph_ids.deinit(self.allocator);
175 self.replacement_glyphs.deinit(self.allocator);
176 self.replacement_clusters.deinit(self.allocator);
177 self.replacement_ids.deinit(self.allocator);
178 self.feature_settings.deinit(self.allocator);
179 self.source_scalars.deinit(self.allocator);
180 self.* = undefined;
181 }
182
183 fn resetImpl(self: *Context) void {
184 self.glyph_ids.clearRetainingCapacity();
185 self.glyph_indexes.clearRetainingCapacity();
186 self.context_glyph_ids.clearRetainingCapacity();
187 self.replacement_glyphs.clearRetainingCapacity();
188 self.replacement_clusters.clearRetainingCapacity();
189 self.replacement_ids.clearRetainingCapacity();
190 self.feature_settings.clearRetainingCapacity();
191 self.source_scalars.clearRetainingCapacity();
192 }
193
194 fn shapeRunImpl(self: *Context, input: Input, output: *Output) !void {
195 output.clearRetainingCapacity();
196 self.reset();
197 errdefer output.clearRetainingCapacity();
198 errdefer self.reset();
199
200 switch (input.writing_mode) {
201 .horizontal, .vertical => {},
202 }
203
204 const source_byte_len = try input.text.byteLen();
205 if (source_byte_len > std.math.maxInt(u32)) return error.SourceTooLong;
206 const remaining_source_bytes: usize = std.math.maxInt(u32) - input.source_offset;
207 if (source_byte_len > remaining_source_bytes) return error.SourceTooLong;
208 try input.font.face.validateVariations(input.variations);
209
210 output.direction = input.direction;
211 output.writing_mode = input.writing_mode;
212 output.output_order = input.output_order;
213
214 if (source_byte_len == 0) return;
215
216 const has_variation_sequences = input.font.face.hasVariationSequences();
217 switch (input.text) {
218 .utf8 => |text| if (has_variation_sequences) try shapeUtf8WithVariations(input, output, text) else try shapeUtf8(input, output, text),
219 .utf16 => |text| if (has_variation_sequences) try shapeUtf16WithVariations(input, output, text) else try shapeUtf16(input, output, text),
220 .utf32 => |text| if (has_variation_sequences) try shapeUtf32WithVariations(input, output, text) else try shapeUtf32(input, output, text),
221 }
222
223 const input_plan = InputPlan.init(input, has_variation_sequences);
224 var shaped_input = input;
225 shaped_input.features = try self.featuresWithAutomaticRanges(input, input_plan);
226 const shape_plan = try ShapePlan.init(shaped_input, input_plan.has_variation_sequences, output.glyphs.items.len);
227
228 const clusters_changed = try self.applySubstitutions(shaped_input, shape_plan, output);
229 if (shaped_input.default_ignorable_policy == .hide) hideUnsubstitutedDefaultIgnorables(output);
230 if (clusters_changed) try rebuildClusters(output);
231 refreshClusterFlags(output);
232 try positionGlyphs(shaped_input, shape_plan, output, &self.glyph_ids, &self.glyph_indexes, &self.context_glyph_ids, self.allocator);
233 refreshClusterFlags(output);
234 try self.applyArabicTatweelSafety(input, input_plan, output);
235 try self.applyOutputOrder(shaped_input, output);
236 }
237
238 fn featuresWithAutomaticRangesImpl(self: *Context, input: Input, plan: InputPlan) ![]const FeatureSetting {
239 self.feature_settings.clearRetainingCapacity();
240 if (!plan.automatic_fractions and !plan.arabic_joining_forms) return input.features;
241 try self.collectSourceScalars(input);
242 if (plan.automatic_fractions) try self.appendAutomaticFractionFeatures(input);
243 if (plan.arabic_joining_forms) try self.appendAutomaticArabicJoiningFeatures(input);
244 if (self.feature_settings.items.len == 0) return input.features;
245 try self.feature_settings.appendSlice(self.allocator, input.features);
246 return self.feature_settings.items;
247 }
248
249 fn collectSourceScalarsImpl(self: *Context, input: Input) !void {
250 self.source_scalars.clearRetainingCapacity();
251 try self.source_scalars.ensureTotalCapacity(self.allocator, input.text.scalarCapacityHint());
252 var iterator = try unicode_data.SourceIterator.init(input.text, input.source_offset);
253 while (try iterator.next()) |scalar| {
254 self.source_scalars.appendAssumeCapacity(.{
255 .codepoint = scalar.codepoint,
256 .start = scalar.source.start,
257 .end = scalar.source.end,
258 });
259 }
260 }
261
262 fn appendAutomaticFractionFeaturesImpl(self: *Context, input: Input) !void {
263 const before_tag = if (input.direction == .rtl) font.tag("dnom") else font.tag("numr");
264 const after_tag = if (input.direction == .rtl) font.tag("numr") else font.tag("dnom");
265 var scalar_index: usize = 0;
266 while (scalar_index < self.source_scalars.items.len) : (scalar_index += 1) {
267 if (self.source_scalars.items[scalar_index].codepoint != fraction_slash_codepoint) continue;
268
269 var start = scalar_index;
270 while (start > 0 and isDecimalNumber(self.source_scalars.items[start - 1].codepoint)) start -= 1;
271
272 var end = scalar_index + 1;
273 while (end < self.source_scalars.items.len and isDecimalNumber(self.source_scalars.items[end].codepoint)) end += 1;
274
275 if (start == scalar_index or end == scalar_index + 1) continue;
276
277 try self.feature_settings.append(self.allocator, .{
278 .tag = before_tag,
279 .source = .{
280 .start = self.source_scalars.items[start].start,
281 .end = self.source_scalars.items[scalar_index].start,
282 },
283 });
284 try self.feature_settings.append(self.allocator, .{
285 .tag = font.tag("frac"),
286 .source = .{
287 .start = self.source_scalars.items[start].start,
288 .end = self.source_scalars.items[end - 1].end,
289 },
290 });
291 try self.feature_settings.append(self.allocator, .{
292 .tag = after_tag,
293 .source = .{
294 .start = self.source_scalars.items[scalar_index + 1].start,
295 .end = self.source_scalars.items[end - 1].end,
296 },
297 });
298 scalar_index = end - 1;
299 }
300 }
301
302 fn appendAutomaticArabicJoiningFeaturesImpl(self: *Context, input: Input) !void {
303 const run_previous_type = try lastJoiningTypeInSource(input.pre_context);
304 const run_next_type = try firstJoiningTypeInSource(input.post_context);
305 for (self.source_scalars.items, 0..) |scalar, scalar_index| {
306 const feature_tag = arabicJoiningFeatureTag(
307 joiningType(scalar.codepoint),
308 previousJoiningType(self.source_scalars.items, scalar_index) orelse run_previous_type,
309 nextJoiningType(self.source_scalars.items, scalar_index) orelse run_next_type,
310 ) orelse continue;
311 try self.feature_settings.append(self.allocator, .{
312 .tag = feature_tag,
313 .source = .{
314 .start = scalar.start,
315 .end = scalar.end,
316 },
317 });
318 }
319 }
320
321 fn applyArabicTatweelSafetyImpl(self: *Context, input: Input, plan: InputPlan, output: *Output) !void {
322 if (!plan.arabic_joining_forms) return;
323 if (self.source_scalars.items.len == 0) try self.collectSourceScalars(input);
324
325 const run_previous_type = try lastJoiningTypeInSource(input.pre_context);
326 const run_next_type = try firstJoiningTypeInSource(input.post_context);
327 var changed = false;
328 for (self.source_scalars.items, 0..) |scalar, scalar_index| {
329 const feature_tag = arabicJoiningFeatureTag(
330 joiningType(scalar.codepoint),
331 previousJoiningType(self.source_scalars.items, scalar_index) orelse run_previous_type,
332 nextJoiningType(self.source_scalars.items, scalar_index) orelse run_next_type,
333 ) orelse continue;
334 if (!arabicJoiningFeatureAcceptsTatweel(feature_tag)) continue;
335 changed = markSourceSafeToInsertTatweel(output, scalar.start, scalar.end) or changed;
336 }
337 if (changed) refreshClusterFlags(output);
338 }
339
340 fn applySubstitutionsImpl(self: *Context, input: Input, plan: ShapePlan, output: *Output) !bool {
341 if (output.glyphs.items.len == 0) return false;
342 if (plan.gsub_lookup_count == 0) return false;
343
344 try buildOutputGlyphIds(output, &self.glyph_ids, self.allocator);
345 const context_run_start = try buildGlyphIdContext(input, plan.has_variation_sequences, self.glyph_ids.items, &self.context_glyph_ids, self.allocator);
346
347 var clusters_changed = false;
348 for (0..plan.gsub_lookup_count) |lookup_index| {
349 const lookup_index_u16: u16 = @intCast(lookup_index);
350 if (input.font.face.gsubLookupIsReverseFor(plan.selection, lookup_index_u16) catch false) {
351 var glyph_index = output.glyphs.items.len;
352 while (glyph_index > 0) {
353 glyph_index -= 1;
354 if (try self.applySubstitutionAt(input, plan.selection, output, lookup_index_u16, glyph_index, context_run_start)) |result| {
355 clusters_changed = clusters_changed or result.component_count > 1 or result.replacement_count != result.component_count;
356 }
357 }
358 } else {
359 var glyph_index: usize = 0;
360 while (glyph_index < output.glyphs.items.len) {
361 if (try self.applySubstitutionAt(input, plan.selection, output, lookup_index_u16, glyph_index, context_run_start)) |result| {
362 clusters_changed = clusters_changed or result.component_count > 1 or result.replacement_count != result.component_count;
363 glyph_index += substitutionAdvance(result);
364 } else {
365 glyph_index += 1;
366 }
367 }
368 }
369 }
370 return clusters_changed;
371 }
372
373 fn applySubstitutionAtImpl(self: *Context, input: Input, selection: font.LayoutSelection, output: *Output, lookup_index: u16, glyph_index: usize, context_run_start: usize) !?font.GsubSubstitution {
374 const glyph = output.glyphs.items[glyph_index];
375 if (glyph.flags.missing_glyph or isHiddenDefaultIgnorableGlyph(glyph)) return null;
376 const context_glyph_index = context_run_start + glyph_index;
377 if (context_glyph_index >= self.context_glyph_ids.items.len) return null;
378 const substitution = input.font.face.substitutionAtForRange(
379 selection,
380 lookup_index,
381 self.context_glyph_ids.items,
382 context_glyph_index,
383 glyphFeatureRange(glyph),
384 ) catch null;
385 if (substitution) |result| {
386 const target_index = glyph_index + result.target_offset;
387 const context_target_index = context_glyph_index + result.target_offset;
388 if (target_index >= output.glyphs.items.len) return null;
389 const component_count: usize = result.component_count;
390 const replacement_count: usize = result.replacement_count;
391 if (component_count > output.glyphs.items.len - target_index) return null;
392 if (context_target_index >= self.context_glyph_ids.items.len or component_count > self.context_glyph_ids.items.len - context_target_index) return null;
393 const target_glyph = output.glyphs.items[target_index];
394 if (target_glyph.flags.missing_glyph or isHiddenDefaultIgnorableGlyph(target_glyph)) return null;
395 if (substitutionUsesContext(result)) markGlyphSpanUnsafe(output, glyph_index, substitutionSafetySpan(result));
396 try replaceGlyphSequence(output, &self.glyph_ids, &self.replacement_glyphs, &self.replacement_ids, self.allocator, target_index, result, input.font);
397 if (replacement_count > component_count) try self.context_glyph_ids.ensureUnusedCapacity(self.allocator, replacement_count - component_count);
398 self.context_glyph_ids.replaceRangeAssumeCapacity(context_target_index, component_count, self.glyph_ids.items[target_index..][0..replacement_count]);
399 return result;
400 }
401 return null;
402 }
403
404 fn applyOutputOrderImpl(self: *Context, input: Input, output: *Output) !void {
405 if (input.direction != .rtl or input.output_order != .visual) return;
406 if (output.clusters.items.len < 2) return;
407
408 self.replacement_glyphs.clearRetainingCapacity();
409 self.replacement_clusters.clearRetainingCapacity();
410 self.glyph_indexes.clearRetainingCapacity();
411 try self.replacement_glyphs.ensureTotalCapacity(self.allocator, output.glyphs.items.len);
412 try self.replacement_clusters.ensureTotalCapacity(self.allocator, output.clusters.items.len);
413 try self.glyph_indexes.ensureTotalCapacity(self.allocator, output.glyphs.items.len);
414 try self.glyph_indexes.appendNTimes(self.allocator, 0, output.glyphs.items.len);
415
416 var read_cluster_index = output.clusters.items.len;
417 while (read_cluster_index > 0) {
418 read_cluster_index -= 1;
419 const source_cluster = output.clusters.items[read_cluster_index];
420 const glyph_start: usize = @intCast(source_cluster.glyphs.start);
421 const glyph_end: usize = @intCast(source_cluster.glyphs.end);
422 if (glyph_start > glyph_end or glyph_end > output.glyphs.items.len) return error.InvalidClusterMap;
423
424 const new_cluster_index: u32 = @intCast(self.replacement_clusters.items.len);
425 const new_glyph_start: u32 = @intCast(self.replacement_glyphs.items.len);
426 for (output.glyphs.items[glyph_start..glyph_end], glyph_start..) |glyph, old_glyph_index| {
427 var ordered = glyph;
428 ordered.cluster_index = new_cluster_index;
429 ordered.cluster = source_cluster.source.start;
430 self.glyph_indexes.items[old_glyph_index] = self.replacement_glyphs.items.len;
431 self.replacement_glyphs.appendAssumeCapacity(ordered);
432 }
433 const new_glyph_end: u32 = @intCast(self.replacement_glyphs.items.len);
434 var ordered_cluster = source_cluster;
435 ordered_cluster.glyphs = .{ .start = new_glyph_start, .end = new_glyph_end };
436 self.replacement_clusters.appendAssumeCapacity(ordered_cluster);
437 }
438
439 for (self.replacement_glyphs.items) |*glyph| {
440 const attachment = glyph.attachment orelse continue;
441 const old_target_index: usize = @intCast(attachment.target_glyph_index);
442 if (old_target_index >= self.glyph_indexes.items.len) {
443 glyph.attachment = null;
444 continue;
445 }
446 var ordered_attachment = attachment;
447 ordered_attachment.target_glyph_index = @intCast(self.glyph_indexes.items[old_target_index]);
448 glyph.attachment = ordered_attachment;
449 }
450
451 @memcpy(output.glyphs.items, self.replacement_glyphs.items);
452 @memcpy(output.clusters.items, self.replacement_clusters.items);
453 }
454
455 fn substitutionAdvance(substitution: font.GsubSubstitution) usize {
456 if (substitution.skip_count != 0) return substitution.skip_count;
457 return @max(@as(usize, 1), @as(usize, substitution.replacement_count));
458 }
459
460 fn substitutionUsesContext(substitution: font.GsubSubstitution) bool {
461 return substitution.target_offset != 0 or substitution.skip_count != 0;
462 }
463
464 fn substitutionSafetySpan(substitution: font.GsubSubstitution) usize {
465 if (substitution.skip_count != 0) return substitution.skip_count;
466 return substitution.target_offset + @as(usize, substitution.component_count);
467 }
468
469 fn shapeUtf8(input: Input, output: *Output, text: []const u8) !void {
470 const source_origin: usize = @intCast(input.source_offset);
471 var index: usize = 0;
472 var grapheme_state: GraphemeState = .{};
473 while (index < text.len) {
474 const start = source_origin + index;
475 const sequence_len = std.unicode.utf8ByteSequenceLength(text[index]) catch return error.InvalidUtf8;
476 if (sequence_len == 0 or index + sequence_len > text.len) return error.InvalidUtf8;
477 const codepoint: u21 = std.unicode.utf8Decode(text[index .. index + sequence_len]) catch return error.InvalidUtf8;
478 index += sequence_len;
479 try appendScalar(input, output, codepoint, start, source_origin + index, grapheme_state.consume(input.cluster_mode, codepoint));
480 }
481 }
482
483 fn shapeUtf8WithVariations(input: Input, output: *Output, text: []const u8) !void {
484 const source_origin: usize = @intCast(input.source_offset);
485 var index: usize = 0;
486 var grapheme_state: GraphemeState = .{};
487 while (index < text.len) {
488 const start = source_origin + index;
489 const scalar = try decodeUtf8Scalar(text, index);
490 index = scalar.end;
491 const merge_with_previous = grapheme_state.consume(input.cluster_mode, scalar.codepoint);
492 if (index < text.len and utf8StartsVariationSelector(text[index])) {
493 const next = try decodeUtf8Scalar(text, index);
494 if (isVariationSelector(next.codepoint)) {
495 if (input.font.face.glyphIdForVariation(scalar.codepoint, next.codepoint)) |glyph_id| {
496 _ = grapheme_state.consume(input.cluster_mode, next.codepoint);
497 index = next.end;
498 try appendGlyph(input, output, glyph_id, start, source_origin + index, 2, merge_with_previous);
499 continue;
500 }
501 }
502 }
503 try appendScalar(input, output, scalar.codepoint, start, source_origin + index, merge_with_previous);
504 }
505 }
506
507 fn shapeUtf16(input: Input, output: *Output, text: []const u16) !void {
508 const source_origin: usize = @intCast(input.source_offset);
509 var index: usize = 0;
510 var grapheme_state: GraphemeState = .{};
511 while (index < text.len) {
512 const start = source_origin + index * 2;
513 const unit = text[index];
514 if (unit >= 0xd800 and unit <= 0xdbff) {
515 if (index + 1 >= text.len) return error.InvalidUtf16;
516 const low = text[index + 1];
517 if (low < 0xdc00 or low > 0xdfff) return error.InvalidUtf16;
518 const high_ten = @as(u21, unit) - 0xd800;
519 const low_ten = @as(u21, low) - 0xdc00;
520 const codepoint: u21 = 0x10000 + (high_ten << 10) + low_ten;
521 index += 2;
522 try appendScalar(input, output, codepoint, start, source_origin + index * 2, grapheme_state.consume(input.cluster_mode, codepoint));
523 } else if (unit >= 0xdc00 and unit <= 0xdfff) {
524 return error.InvalidUtf16;
525 } else {
526 const codepoint: u21 = @intCast(unit);
527 index += 1;
528 try appendScalar(input, output, codepoint, start, source_origin + index * 2, grapheme_state.consume(input.cluster_mode, codepoint));
529 }
530 }
531 }
532
533 fn shapeUtf16WithVariations(input: Input, output: *Output, text: []const u16) !void {
534 const source_origin: usize = @intCast(input.source_offset);
535 var index: usize = 0;
536 var grapheme_state: GraphemeState = .{};
537 while (index < text.len) {
538 const start = source_origin + index * 2;
539 const scalar = try decodeUtf16Scalar(text, index);
540 index = scalar.end;
541 const merge_with_previous = grapheme_state.consume(input.cluster_mode, scalar.codepoint);
542 if (index < text.len and utf16StartsVariationSelector(text[index])) {
543 const next = try decodeUtf16Scalar(text, index);
544 if (isVariationSelector(next.codepoint)) {
545 if (input.font.face.glyphIdForVariation(scalar.codepoint, next.codepoint)) |glyph_id| {
546 _ = grapheme_state.consume(input.cluster_mode, next.codepoint);
547 index = next.end;
548 try appendGlyph(input, output, glyph_id, start, source_origin + index * 2, 2, merge_with_previous);
549 continue;
550 }
551 }
552 }
553 try appendScalar(input, output, scalar.codepoint, start, source_origin + index * 2, merge_with_previous);
554 }
555 }
556
557 fn shapeUtf32(input: Input, output: *Output, text: []const u32) !void {
558 const source_origin: usize = @intCast(input.source_offset);
559 var index: usize = 0;
560 var grapheme_state: GraphemeState = .{};
561 while (index < text.len) {
562 const start = source_origin + index * 4;
563 const unit = text[index];
564 if (unit > 0x10ffff or (unit >= 0xd800 and unit <= 0xdfff)) return error.InvalidUtf32;
565 const codepoint: u21 = @intCast(unit);
566 index += 1;
567 try appendScalar(input, output, codepoint, start, source_origin + index * 4, grapheme_state.consume(input.cluster_mode, codepoint));
568 }
569 }
570
571 fn shapeUtf32WithVariations(input: Input, output: *Output, text: []const u32) !void {
572 const source_origin: usize = @intCast(input.source_offset);
573 var index: usize = 0;
574 var grapheme_state: GraphemeState = .{};
575 while (index < text.len) {
576 const start = source_origin + index * 4;
577 const scalar = try decodeUtf32Scalar(text, index);
578 index = scalar.end;
579 const merge_with_previous = grapheme_state.consume(input.cluster_mode, scalar.codepoint);
580 if (index < text.len and isVariationSelectorScalar(text[index])) {
581 const next = try decodeUtf32Scalar(text, index);
582 if (isVariationSelector(next.codepoint)) {
583 if (input.font.face.glyphIdForVariation(scalar.codepoint, next.codepoint)) |glyph_id| {
584 _ = grapheme_state.consume(input.cluster_mode, next.codepoint);
585 index = next.end;
586 try appendGlyph(input, output, glyph_id, start, source_origin + index * 4, 2, merge_with_previous);
587 continue;
588 }
589 }
590 }
591 try appendScalar(input, output, scalar.codepoint, start, source_origin + index * 4, merge_with_previous);
592 }
593 }
594
595 fn appendScalar(input: Input, output: *Output, codepoint: u21, source_start: usize, source_end: usize, merge_with_previous: bool) !void {
596 const previous_is_mark = output.glyphs.items.len != 0 and output.glyphs.items[output.glyphs.items.len - 1].glyph_class == .mark;
597 var merge_scalar = merge_with_previous or cluster_data.shouldMergeWithPreviousMark(input.cluster_mode == .monotone_characters, previous_is_mark, codepoint);
598 var flags = GlyphFlags{};
599 if (output.glyphs.items.len != 0 and cluster_data.unsafeBreakContinuation(codepoint)) flags.unsafe_to_break = true;
600 if (try appendDottedCircleBeforeLeadingMark(input, output, codepoint, source_start)) {
601 merge_scalar = true;
602 }
603
604 if (isDefaultIgnorable(codepoint)) {
605 switch (input.default_ignorable_policy) {
606 .hide => {
607 flags.default_ignorable = true;
608 try appendGlyphWithFlags(input, output, input.font.face.glyphId(codepoint), source_start, source_end, 1, flags, merge_scalar);
609 return;
610 },
611 .preserve => {
612 const glyph_id = input.font.face.glyphId(codepoint);
613 flags.missing_glyph = glyph_id == 0;
614 flags.default_ignorable = true;
615 try appendGlyphWithFlags(input, output, glyph_id, source_start, source_end, 1, flags, merge_scalar);
616 return;
617 },
618 .remove => return,
619 }
620 }
621 const glyph_id = input.font.face.glyphId(codepoint);
622 flags.missing_glyph = glyph_id == 0;
623 try appendGlyphWithFlags(input, output, glyph_id, source_start, source_end, 1, flags, merge_scalar);
624 }
625
626 fn appendDottedCircleBeforeLeadingMark(input: Input, output: *Output, codepoint: u21, source_start: usize) !bool {
627 if (input.dotted_circle_policy != .insert) return false;
628 if (output.glyphs.items.len != 0) return false;
629 if (!cluster_data.isLeadingCombiningMark(codepoint)) return false;
630
631 const glyph_id = input.font.face.glyphId(dotted_circle_codepoint);
632 if (glyph_id == 0) return false;
633
634 try appendGlyphWithFlags(input, output, glyph_id, source_start, source_start, 0, .{ .synthetic = true }, false);
635 return true;
636 }
637
638 const DecodedScalar = struct {
639 codepoint: u21,
640 end: usize,
641 };
642
643 const GraphemeState = struct {
644 state: unicode_data.GraphemeState = .{},
645
646 fn consume(self: *GraphemeState, cluster_mode: ClusterMode, codepoint: u21) bool {
647 if (!clusterModeUsesGraphemes(cluster_mode)) {
648 self.* = .{};
649 return false;
650 }
651 return self.state.consume(codepoint);
652 }
653 };
654
655 fn clusterModeUsesGraphemes(cluster_mode: ClusterMode) bool {
656 return switch (cluster_mode) {
657 .monotone_graphemes, .graphemes => true,
658 .monotone_characters, .characters => false,
659 };
660 }
661
662 fn decodeUtf8Scalar(text: []const u8, index: usize) !DecodedScalar {
663 const sequence_len = std.unicode.utf8ByteSequenceLength(text[index]) catch return error.InvalidUtf8;
664 if (sequence_len == 0 or index + sequence_len > text.len) return error.InvalidUtf8;
665 const codepoint: u21 = std.unicode.utf8Decode(text[index .. index + sequence_len]) catch return error.InvalidUtf8;
666 return .{ .codepoint = codepoint, .end = index + sequence_len };
667 }
668
669 fn decodeUtf16Scalar(text: []const u16, index: usize) !DecodedScalar {
670 const unit = text[index];
671 if (unit >= 0xd800 and unit <= 0xdbff) {
672 if (index + 1 >= text.len) return error.InvalidUtf16;
673 const low = text[index + 1];
674 if (low < 0xdc00 or low > 0xdfff) return error.InvalidUtf16;
675 const high_ten = @as(u21, unit) - 0xd800;
676 const low_ten = @as(u21, low) - 0xdc00;
677 return .{ .codepoint = 0x10000 + (high_ten << 10) + low_ten, .end = index + 2 };
678 }
679 if (unit >= 0xdc00 and unit <= 0xdfff) return error.InvalidUtf16;
680 return .{ .codepoint = @intCast(unit), .end = index + 1 };
681 }
682
683 fn decodeUtf32Scalar(text: []const u32, index: usize) !DecodedScalar {
684 const unit = text[index];
685 if (unit > 0x10ffff or (unit >= 0xd800 and unit <= 0xdfff)) return error.InvalidUtf32;
686 return .{ .codepoint = @intCast(unit), .end = index + 1 };
687 }
688
689 fn isVariationSelector(codepoint: u21) bool {
690 return (codepoint >= 0xfe00 and codepoint <= 0xfe0f) or
691 (codepoint >= 0xe0100 and codepoint <= 0xe01ef);
692 }
693
694 fn arabicJoiningFeatureTag(current: JoiningType, previous: ?JoiningType, next: ?JoiningType) ?font.Tag {
695 if (current == .non_joining or current == .transparent) return null;
696 const joins_previous = if (previous) |previous_type|
697 joiningTypesConnect(previous_type, current)
698 else
699 false;
700 const joins_next = if (next) |next_type|
701 joiningTypesConnect(current, next_type)
702 else
703 false;
704 if (joins_previous and joins_next) return font.tag("medi");
705 if (joins_previous) return font.tag("fina");
706 if (joins_next) return font.tag("init");
707 return font.tag("isol");
708 }
709
710 fn arabicJoiningFeatureAcceptsTatweel(feature_tag: font.Tag) bool {
711 return feature_tag == font.tag("fina") or feature_tag == font.tag("medi");
712 }
713
714 fn previousJoiningType(scalars: []const SourceScalar, before: usize) ?JoiningType {
715 var index = before;
716 while (index > 0) {
717 index -= 1;
718 const found = joiningType(scalars[index].codepoint);
719 if (found != .transparent) return found;
720 }
721 return null;
722 }
723
724 fn nextJoiningType(scalars: []const SourceScalar, after: usize) ?JoiningType {
725 var index = after + 1;
726 while (index < scalars.len) : (index += 1) {
727 const found = joiningType(scalars[index].codepoint);
728 if (found != .transparent) return found;
729 }
730 return null;
731 }
732
733 fn firstJoiningTypeInSource(source: ?Source) !?JoiningType {
734 const context = source orelse return null;
735 return switch (context) {
736 .utf8 => |text| firstUtf8JoiningType(text),
737 .utf16 => |text| firstUtf16JoiningType(text),
738 .utf32 => |text| firstUtf32JoiningType(text),
739 };
740 }
741
742 fn lastJoiningTypeInSource(source: ?Source) !?JoiningType {
743 const context = source orelse return null;
744 return switch (context) {
745 .utf8 => |text| lastUtf8JoiningType(text),
746 .utf16 => |text| lastUtf16JoiningType(text),
747 .utf32 => |text| lastUtf32JoiningType(text),
748 };
749 }
750
751 fn firstUtf8JoiningType(text: []const u8) !?JoiningType {
752 var index: usize = 0;
753 while (index < text.len) {
754 const scalar = try decodeUtf8Scalar(text, index);
755 index = scalar.end;
756 const found = joiningType(scalar.codepoint);
757 if (found != .transparent) return found;
758 }
759 return null;
760 }
761
762 fn lastUtf8JoiningType(text: []const u8) !?JoiningType {
763 var index: usize = 0;
764 var result: ?JoiningType = null;
765 while (index < text.len) {
766 const scalar = try decodeUtf8Scalar(text, index);
767 index = scalar.end;
768 const found = joiningType(scalar.codepoint);
769 if (found != .transparent) result = found;
770 }
771 return result;
772 }
773
774 fn firstUtf16JoiningType(text: []const u16) !?JoiningType {
775 var index: usize = 0;
776 while (index < text.len) {
777 const scalar = try decodeUtf16Scalar(text, index);
778 index = scalar.end;
779 const found = joiningType(scalar.codepoint);
780 if (found != .transparent) return found;
781 }
782 return null;
783 }
784
785 fn lastUtf16JoiningType(text: []const u16) !?JoiningType {
786 var index: usize = 0;
787 var result: ?JoiningType = null;
788 while (index < text.len) {
789 const scalar = try decodeUtf16Scalar(text, index);
790 index = scalar.end;
791 const found = joiningType(scalar.codepoint);
792 if (found != .transparent) result = found;
793 }
794 return result;
795 }
796
797 fn firstUtf32JoiningType(text: []const u32) !?JoiningType {
798 var index: usize = 0;
799 while (index < text.len) {
800 const scalar = try decodeUtf32Scalar(text, index);
801 index = scalar.end;
802 const found = joiningType(scalar.codepoint);
803 if (found != .transparent) return found;
804 }
805 return null;
806 }
807
808 fn lastUtf32JoiningType(text: []const u32) !?JoiningType {
809 var index: usize = 0;
810 var result: ?JoiningType = null;
811 while (index < text.len) {
812 const scalar = try decodeUtf32Scalar(text, index);
813 index = scalar.end;
814 const found = joiningType(scalar.codepoint);
815 if (found != .transparent) result = found;
816 }
817 return result;
818 }
819
820 fn isDefaultIgnorable(codepoint: u21) bool {
821 return codepoint == 0x00ad or
822 codepoint == 0x034f or
823 codepoint == 0x061c or
824 (codepoint >= 0x115f and codepoint <= 0x1160) or
825 (codepoint >= 0x17b4 and codepoint <= 0x17b5) or
826 (codepoint >= 0x180b and codepoint <= 0x180f) or
827 (codepoint >= 0x200b and codepoint <= 0x200f) or
828 (codepoint >= 0x202a and codepoint <= 0x202e) or
829 (codepoint >= 0x2060 and codepoint <= 0x206f) or
830 codepoint == 0x3164 or
831 (codepoint >= 0xfe00 and codepoint <= 0xfe0f) or
832 codepoint == 0xfeff or
833 codepoint == 0xffa0 or
834 (codepoint >= 0xfff0 and codepoint <= 0xfff8) or
835 (codepoint >= 0x1bca0 and codepoint <= 0x1bca3) or
836 (codepoint >= 0x1d173 and codepoint <= 0x1d17a) or
837 (codepoint >= 0xe0000 and codepoint <= 0xe0fff);
838 }
839
840 fn utf8StartsVariationSelector(byte: u8) bool {
841 return byte == 0xef or byte == 0xf3;
842 }
843
844 fn utf16StartsVariationSelector(unit: u16) bool {
845 return (unit >= 0xfe00 and unit <= 0xfe0f) or unit == 0xdb40;
846 }
847
848 fn isVariationSelectorScalar(unit: u32) bool {
849 return (unit >= 0xfe00 and unit <= 0xfe0f) or
850 (unit >= 0xe0100 and unit <= 0xe01ef);
851 }
852
853 fn appendGlyph(input: Input, output: *Output, glyph_id: u32, source_start: usize, source_end: usize, source_codepoint_count: u16, merge_with_previous: bool) !void {
854 try appendGlyphWithFlags(input, output, glyph_id, source_start, source_end, source_codepoint_count, .{ .missing_glyph = glyph_id == 0 }, merge_with_previous);
855 }
856
857 fn appendGlyphWithFlags(input: Input, output: *Output, glyph_id: u32, source_start: usize, source_end: usize, source_codepoint_count: u16, flags: GlyphFlags, merge_with_previous: bool) !void {
858 if (flags.missing_glyph and input.missing_glyph_policy == .fail) return error.MissingGlyph;
859
860 const glyph_start = output.glyphs.items.len;
861 const cluster_index: u32 = @intCast(output.clusters.items.len);
862
863 try output.appendGlyph(.{
864 .glyph_id = glyph_id,
865 .cluster = @intCast(source_start),
866 .cluster_index = cluster_index,
867 .source_start = @intCast(source_start),
868 .source_end = @intCast(source_end),
869 .source_codepoint_count = source_codepoint_count,
870 .x_advance = 0,
871 .y_advance = 0,
872 .x_offset = 0,
873 .y_offset = 0,
874 .flags = flags,
875 .glyph_class = input.font.face.glyphClass(glyph_id),
876 });
877
878 try output.appendCluster(.{
879 .source = .{ .start = @intCast(source_start), .end = @intCast(source_end) },
880 .glyphs = .{ .start = @intCast(glyph_start), .end = @intCast(glyph_start + 1) },
881 .flags = flags,
882 .codepoint_count = source_codepoint_count,
883 });
884
885 if (merge_with_previous) mergeLastClusterWithPrevious(output);
886 }
887
888 fn mergeLastClusterWithPrevious(output: *Output) void {
889 const cluster_count = output.clusters.items.len;
890 if (cluster_count < 2) return;
891
892 const previous_index = cluster_count - 2;
893 const last = output.clusters.items[cluster_count - 1];
894 const previous = &output.clusters.items[previous_index];
895 previous.source.end = @max(previous.source.end, last.source.end);
896 previous.glyphs.end = last.glyphs.end;
897 previous.flags = mergeFlags(previous.flags, last.flags);
898 previous.codepoint_count = addSaturatingU16(previous.codepoint_count, last.codepoint_count);
899
900 const glyph_start: usize = @intCast(previous.glyphs.start);
901 const glyph_end: usize = @intCast(previous.glyphs.end);
902 const merged_source_start = previous.source.start;
903 const merged_source_end = previous.source.end;
904 const merged_codepoint_count = previous.codepoint_count;
905 const merged_flags = previous.flags;
906 for (output.glyphs.items[glyph_start..glyph_end]) |*glyph| {
907 glyph.cluster = merged_source_start;
908 glyph.cluster_index = @intCast(previous_index);
909 glyph.source_start = merged_source_start;
910 glyph.source_end = merged_source_end;
911 glyph.source_codepoint_count = merged_codepoint_count;
912 glyph.flags = mergeFlags(glyph.flags, merged_flags);
913 }
914
915 output.clusters.items.len = cluster_count - 1;
916 }
917
918 fn replaceGlyphSequence(
919 output: *Output,
920 glyph_ids: *std.ArrayListUnmanaged(u32),
921 replacement_glyphs: *std.ArrayListUnmanaged(ShapedGlyph),
922 replacement_ids: *std.ArrayListUnmanaged(u32),
923 scratch_allocator: Allocator,
924 glyph_index: usize,
925 substitution: font.GsubSubstitution,
926 shaped_font: *const Font,
927 ) !void {
928 const component_count: usize = substitution.component_count;
929 const replacement_count: usize = substitution.replacement_count;
930 if (component_count == 0 or replacement_count == 0 or glyph_index + component_count > output.glyphs.items.len or glyph_index + component_count > glyph_ids.items.len) return;
931
932 const first = output.glyphs.items[glyph_index];
933 var flags = first.flags;
934 var source_start = first.source_start;
935 var source_end = first.source_end;
936 var source_codepoint_count: u16 = 0;
937
938 for (output.glyphs.items[glyph_index..][0..component_count]) |glyph| {
939 flags = mergeFlags(flags, glyph.flags);
940 source_start = @min(source_start, glyph.source_start);
941 source_end = @max(source_end, glyph.source_end);
942 source_codepoint_count = addSaturatingU16(source_codepoint_count, glyph.source_codepoint_count);
943 }
944
945 if (substitutionUsesContext(substitution)) {
946 flags.unsafe_to_break = true;
947 flags.unsafe_to_concat = true;
948 } else if (component_count > 1 or replacement_count > 1) {
949 flags.unsafe_to_concat = true;
950 }
951
952 replacement_glyphs.clearRetainingCapacity();
953 replacement_ids.clearRetainingCapacity();
954 try replacement_glyphs.ensureTotalCapacity(scratch_allocator, replacement_count);
955 try replacement_ids.ensureTotalCapacity(scratch_allocator, replacement_count);
956
957 for (0..replacement_count) |replacement_index| {
958 const replacement_glyph_id = shaped_font.face.substitutionGlyph(substitution, replacement_index) orelse return;
959 var replacement = first;
960 replacement.glyph_id = replacement_glyph_id;
961 replacement.cluster = source_start;
962 replacement.source_start = source_start;
963 replacement.source_end = source_end;
964 replacement.source_codepoint_count = source_codepoint_count;
965 replacement.flags = flags;
966 replacement.glyph_class = shaped_font.face.glyphClass(replacement_glyph_id);
967 replacement.ligature_caret_start = 0;
968 replacement.ligature_caret_count = 0;
969
970 if (component_count > 1 and replacement_index == 0) {
971 if (replacement.glyph_class == .unknown) replacement.glyph_class = .ligature;
972 try appendLigatureCarets(output, shaped_font, &replacement);
973 }
974
975 replacement_glyphs.appendAssumeCapacity(replacement);
976 replacement_ids.appendAssumeCapacity(replacement_glyph_id);
977 }
978
979 if (replacement_count > component_count) {
980 try output.ensureUnused(replacement_count - component_count, 0, 0);
981 try glyph_ids.ensureUnusedCapacity(scratch_allocator, replacement_count - component_count);
982 }
983 output.glyphs.replaceRangeAssumeCapacity(glyph_index, component_count, replacement_glyphs.items);
984 glyph_ids.replaceRangeAssumeCapacity(glyph_index, component_count, replacement_ids.items);
985 }
986
987 fn appendLigatureCarets(output: *Output, shaped_font: *const Font, glyph: *ShapedGlyph) !void {
988 const caret_count = shaped_font.face.ligatureCaretCount(glyph.glyph_id);
989 if (caret_count == 0) return;
990 const caret_start = output.ligature_carets.items.len;
991 try output.ensureUnused(0, 0, caret_count);
992 var appended: u16 = 0;
993 for (0..caret_count) |caret_index| {
994 const caret = shaped_font.face.ligatureCaret(glyph.glyph_id, caret_index) orelse continue;
995 output.ligature_carets.appendAssumeCapacity(.{
996 .x_offset = shaped_font.scaleDesignSigned(caret.coordinate),
997 .synthesized = caret.synthesized,
998 });
999 appended += 1;
1000 }
1001 if (appended == 0) return;
1002 glyph.ligature_caret_start = @intCast(caret_start);
1003 glyph.ligature_caret_count = appended;
1004 }
1005
1006 fn rebuildClusters(output: *Output) !void {
1007 output.clusters.clearRetainingCapacity();
1008
1009 var glyph_index: usize = 0;
1010 while (glyph_index < output.glyphs.items.len) {
1011 const cluster_index: u32 = @intCast(output.clusters.items.len);
1012 const source_start = output.glyphs.items[glyph_index].source_start;
1013 const source_end = output.glyphs.items[glyph_index].source_end;
1014 var flags = output.glyphs.items[glyph_index].flags;
1015 var codepoint_count: u16 = 0;
1016 const glyph_start = glyph_index;
1017
1018 while (glyph_index < output.glyphs.items.len and
1019 output.glyphs.items[glyph_index].source_start == source_start and
1020 output.glyphs.items[glyph_index].source_end == source_end)
1021 {
1022 output.glyphs.items[glyph_index].cluster = source_start;
1023 output.glyphs.items[glyph_index].cluster_index = cluster_index;
1024 flags = mergeFlags(flags, output.glyphs.items[glyph_index].flags);
1025 codepoint_count = @max(codepoint_count, output.glyphs.items[glyph_index].source_codepoint_count);
1026 glyph_index += 1;
1027 }
1028
1029 try output.appendCluster(.{
1030 .source = .{ .start = source_start, .end = source_end },
1031 .glyphs = .{ .start = @intCast(glyph_start), .end = @intCast(glyph_index) },
1032 .flags = flags,
1033 .codepoint_count = codepoint_count,
1034 });
1035 }
1036 }
1037
1038 fn refreshClusterFlags(output: *Output) void {
1039 for (output.clusters.items) |*cluster| {
1040 var flags = GlyphFlags{};
1041 const glyph_start: usize = @intCast(cluster.glyphs.start);
1042 const glyph_end: usize = @intCast(cluster.glyphs.end);
1043 if (glyph_start > glyph_end or glyph_end > output.glyphs.items.len) continue;
1044 for (output.glyphs.items[glyph_start..glyph_end]) |glyph| {
1045 flags = mergeFlags(flags, glyph.flags);
1046 }
1047 cluster.flags = flags;
1048 for (output.glyphs.items[glyph_start..glyph_end]) |*glyph| {
1049 glyph.flags = mergeFlags(glyph.flags, flags);
1050 }
1051 }
1052 }
1053
1054 fn markSourceSafeToInsertTatweel(output: *Output, source_start: u32, source_end: u32) bool {
1055 var changed = false;
1056 for (output.glyphs.items) |*glyph| {
1057 if (glyph.source_start > source_start or glyph.source_end < source_end) continue;
1058 if (glyph.flags.unsafe_to_break or glyph.flags.missing_glyph or isHiddenDefaultIgnorableGlyph(glyph.*)) continue;
1059 if (glyph.flags.safe_to_insert_tatweel) continue;
1060 glyph.flags.safe_to_insert_tatweel = true;
1061 glyph.flags.unsafe_to_break = true;
1062 glyph.flags.unsafe_to_concat = true;
1063 changed = true;
1064 }
1065 return changed;
1066 }
1067
1068 fn buildOutputGlyphIds(output: *const Output, glyph_ids: *std.ArrayListUnmanaged(u32), allocator: Allocator) !void {
1069 glyph_ids.clearRetainingCapacity();
1070 try glyph_ids.ensureTotalCapacity(allocator, output.glyphs.items.len);
1071 for (output.glyphs.items) |glyph| {
1072 glyph_ids.appendAssumeCapacity(glyph.glyph_id);
1073 }
1074 }
1075
1076 fn buildGlyphIdContext(input: Input, has_variation_sequences: bool, run_glyph_ids: []const u32, context_glyph_ids: *std.ArrayListUnmanaged(u32), allocator: Allocator) !usize {
1077 context_glyph_ids.clearRetainingCapacity();
1078 const capacity_hint =
1079 contextScalarCapacityHint(input.pre_context) +
1080 run_glyph_ids.len +
1081 contextScalarCapacityHint(input.post_context);
1082 try context_glyph_ids.ensureTotalCapacity(allocator, capacity_hint);
1083
1084 try appendContextGlyphs(input, has_variation_sequences, input.pre_context, context_glyph_ids, allocator);
1085 const run_start = context_glyph_ids.items.len;
1086 try context_glyph_ids.appendSlice(allocator, run_glyph_ids);
1087 try appendContextGlyphs(input, has_variation_sequences, input.post_context, context_glyph_ids, allocator);
1088 return run_start;
1089 }
1090
1091 fn contextScalarCapacityHint(source: ?Source) usize {
1092 const context = source orelse return 0;
1093 return context.scalarCapacityHint();
1094 }
1095
1096 fn appendContextGlyphs(input: Input, has_variation_sequences: bool, source: ?Source, glyph_ids: *std.ArrayListUnmanaged(u32), allocator: Allocator) !void {
1097 const context = source orelse return;
1098 switch (context) {
1099 .utf8 => |text| if (has_variation_sequences) try appendUtf8ContextGlyphsWithVariations(input, text, glyph_ids, allocator) else try appendUtf8ContextGlyphs(input, text, glyph_ids, allocator),
1100 .utf16 => |text| if (has_variation_sequences) try appendUtf16ContextGlyphsWithVariations(input, text, glyph_ids, allocator) else try appendUtf16ContextGlyphs(input, text, glyph_ids, allocator),
1101 .utf32 => |text| if (has_variation_sequences) try appendUtf32ContextGlyphsWithVariations(input, text, glyph_ids, allocator) else try appendUtf32ContextGlyphs(input, text, glyph_ids, allocator),
1102 }
1103 }
1104
1105 fn appendUtf8ContextGlyphs(input: Input, text: []const u8, glyph_ids: *std.ArrayListUnmanaged(u32), allocator: Allocator) !void {
1106 var index: usize = 0;
1107 while (index < text.len) {
1108 const scalar = try decodeUtf8Scalar(text, index);
1109 index = scalar.end;
1110 try appendContextScalar(input, glyph_ids, allocator, scalar.codepoint);
1111 }
1112 }
1113
1114 fn appendUtf8ContextGlyphsWithVariations(input: Input, text: []const u8, glyph_ids: *std.ArrayListUnmanaged(u32), allocator: Allocator) !void {
1115 var index: usize = 0;
1116 while (index < text.len) {
1117 const scalar = try decodeUtf8Scalar(text, index);
1118 index = scalar.end;
1119 if (index < text.len and utf8StartsVariationSelector(text[index])) {
1120 const next = try decodeUtf8Scalar(text, index);
1121 if (isVariationSelector(next.codepoint)) {
1122 if (input.font.face.glyphIdForVariation(scalar.codepoint, next.codepoint)) |glyph_id| {
1123 index = next.end;
1124 try glyph_ids.append(allocator, glyph_id);
1125 continue;
1126 }
1127 }
1128 }
1129 try appendContextScalar(input, glyph_ids, allocator, scalar.codepoint);
1130 }
1131 }
1132
1133 fn appendUtf16ContextGlyphs(input: Input, text: []const u16, glyph_ids: *std.ArrayListUnmanaged(u32), allocator: Allocator) !void {
1134 var index: usize = 0;
1135 while (index < text.len) {
1136 const scalar = try decodeUtf16Scalar(text, index);
1137 index = scalar.end;
1138 try appendContextScalar(input, glyph_ids, allocator, scalar.codepoint);
1139 }
1140 }
1141
1142 fn appendUtf16ContextGlyphsWithVariations(input: Input, text: []const u16, glyph_ids: *std.ArrayListUnmanaged(u32), allocator: Allocator) !void {
1143 var index: usize = 0;
1144 while (index < text.len) {
1145 const scalar = try decodeUtf16Scalar(text, index);
1146 index = scalar.end;
1147 if (index < text.len and utf16StartsVariationSelector(text[index])) {
1148 const next = try decodeUtf16Scalar(text, index);
1149 if (isVariationSelector(next.codepoint)) {
1150 if (input.font.face.glyphIdForVariation(scalar.codepoint, next.codepoint)) |glyph_id| {
1151 index = next.end;
1152 try glyph_ids.append(allocator, glyph_id);
1153 continue;
1154 }
1155 }
1156 }
1157 try appendContextScalar(input, glyph_ids, allocator, scalar.codepoint);
1158 }
1159 }
1160
1161 fn appendUtf32ContextGlyphs(input: Input, text: []const u32, glyph_ids: *std.ArrayListUnmanaged(u32), allocator: Allocator) !void {
1162 var index: usize = 0;
1163 while (index < text.len) {
1164 const scalar = try decodeUtf32Scalar(text, index);
1165 index = scalar.end;
1166 try appendContextScalar(input, glyph_ids, allocator, scalar.codepoint);
1167 }
1168 }
1169
1170 fn appendUtf32ContextGlyphsWithVariations(input: Input, text: []const u32, glyph_ids: *std.ArrayListUnmanaged(u32), allocator: Allocator) !void {
1171 var index: usize = 0;
1172 while (index < text.len) {
1173 const scalar = try decodeUtf32Scalar(text, index);
1174 index = scalar.end;
1175 if (index < text.len and isVariationSelectorScalar(text[index])) {
1176 const next = try decodeUtf32Scalar(text, index);
1177 if (isVariationSelector(next.codepoint)) {
1178 if (input.font.face.glyphIdForVariation(scalar.codepoint, next.codepoint)) |glyph_id| {
1179 index = next.end;
1180 try glyph_ids.append(allocator, glyph_id);
1181 continue;
1182 }
1183 }
1184 }
1185 try appendContextScalar(input, glyph_ids, allocator, scalar.codepoint);
1186 }
1187 }
1188
1189 fn appendContextScalar(input: Input, glyph_ids: *std.ArrayListUnmanaged(u32), allocator: Allocator, codepoint: u21) !void {
1190 if (isDefaultIgnorable(codepoint)) {
1191 switch (input.default_ignorable_policy) {
1192 .hide => {
1193 try glyph_ids.append(allocator, input.font.face.glyphId(codepoint));
1194 return;
1195 },
1196 .preserve => {},
1197 .remove => return,
1198 }
1199 }
1200 try glyph_ids.append(allocator, input.font.face.glyphId(codepoint));
1201 }
1202
1203 fn glyphFeatureRange(glyph: ShapedGlyph) font.FeatureRange {
1204 return .{ .start = glyph.source_start, .end = glyph.source_end };
1205 }
1206
1207 fn positionGlyphs(
1208 input: Input,
1209 plan: ShapePlan,
1210 output: *Output,
1211 glyph_ids: *std.ArrayListUnmanaged(u32),
1212 glyph_indexes: *std.ArrayListUnmanaged(usize),
1213 context_glyph_ids: *std.ArrayListUnmanaged(u32),
1214 allocator: std.mem.Allocator,
1215 ) !void {
1216 output.total_x_advance = 0;
1217 output.total_y_advance = 0;
1218 var context_run_start: usize = 0;
1219
1220 for (output.glyphs.items) |*glyph| {
1221 if (isHiddenDefaultIgnorableGlyph(glyph.*)) {
1222 glyph.x_advance = 0;
1223 glyph.y_advance = 0;
1224 glyph.x_offset = 0;
1225 glyph.y_offset = 0;
1226 continue;
1227 }
1228
1229 if (plan.horizontal) {
1230 glyph.x_advance = input.font.scaleDesignSigned(input.font.face.advanceWidthForVariations(glyph.glyph_id, input.variations));
1231 glyph.y_advance = 0;
1232 glyph.x_offset = 0;
1233 glyph.y_offset = 0;
1234 } else {
1235 const vertical_advance = input.font.scaleDesignSigned(input.font.face.advanceHeightForVariations(glyph.glyph_id, input.variations));
1236 glyph.x_advance = 0;
1237 glyph.y_advance = subtractClampedI32(0, vertical_advance);
1238 if (input.font.face.verticalOriginY(glyph.glyph_id)) |vertical_origin_y| {
1239 const horizontal_advance = input.font.scaleDesignSigned(input.font.face.advanceWidthForVariations(glyph.glyph_id, input.variations));
1240 glyph.x_offset = subtractClampedI32(0, @divTrunc(horizontal_advance, 2));
1241 glyph.y_offset = subtractClampedI32(0, input.font.scaleDesignSigned(vertical_origin_y));
1242 } else {
1243 glyph.x_offset = 0;
1244 glyph.y_offset = 0;
1245 }
1246 }
1247 output.total_x_advance = addClampedI32(output.total_x_advance, glyph.x_advance);
1248 output.total_y_advance = addClampedI32(output.total_y_advance, glyph.y_advance);
1249 }
1250
1251 if (plan.needs_context_glyph_ids) {
1252 try buildOutputGlyphIds(output, glyph_ids, allocator);
1253 context_run_start = try buildGlyphIdContext(input, plan.has_variation_sequences, glyph_ids.items, context_glyph_ids, allocator);
1254 }
1255
1256 if (plan.needs_visible_glyph_ids) {
1257 glyph_ids.clearRetainingCapacity();
1258 glyph_indexes.clearRetainingCapacity();
1259 try glyph_ids.ensureTotalCapacity(allocator, output.glyphs.items.len);
1260 try glyph_indexes.ensureTotalCapacity(allocator, output.glyphs.items.len);
1261 for (output.glyphs.items, 0..) |glyph, glyph_index| {
1262 if (isHiddenDefaultIgnorableGlyph(glyph)) continue;
1263 glyph_ids.appendAssumeCapacity(glyph.glyph_id);
1264 glyph_indexes.appendAssumeCapacity(glyph_index);
1265 }
1266 } else {
1267 glyph_ids.clearRetainingCapacity();
1268 glyph_indexes.clearRetainingCapacity();
1269 }
1270
1271 if (plan.has_gpos) {
1272 for (0..plan.gpos_lookup_count) |lookup_index_usize| {
1273 const lookup_index: u16 = @intCast(lookup_index_usize);
1274 switch (input.font.face.gposLookupType(lookup_index)) {
1275 1 => if (plan.has_gpos_single) applyGposSingleAdjustmentsAtLookup(input, plan.selection, lookup_index, output, context_glyph_ids.items, context_run_start),
1276 2 => applyGposPairAdjustmentsAtLookup(input, plan.selection, lookup_index, output, glyph_ids.items, glyph_indexes.items),
1277 3 => applyGposCursiveAttachmentAtLookup(input.font, plan.selection, lookup_index, output),
1278 4 => applyGposMarkToBasePositioningAtLookup(input, plan.selection, lookup_index, output),
1279 5 => applyGposMarkToLigaturePositioningAtLookup(input, plan.selection, lookup_index, output),
1280 6 => applyGposMarkToMarkPositioningAtLookup(input, plan.selection, lookup_index, output),
1281 7, 8 => {
1282 if (plan.has_gpos_single) applyGposSingleAdjustmentsAtLookup(input, plan.selection, lookup_index, output, context_glyph_ids.items, context_run_start);
1283 if (plan.has_gpos_contextual_pair) applyGposContextualPairAdjustmentsAtLookup(input, plan.selection, lookup_index, output, context_glyph_ids.items, context_run_start);
1284 },
1285 else => {},
1286 }
1287 }
1288 }
1289 if (plan.horizontal) {
1290 applyClassicKerningFallback(input, plan.selection, plan.has_gpos, output, glyph_ids.items, glyph_indexes.items);
1291 }
1292 try synthesizeMissingLigatureCarets(output);
1293 }
1294
1295 fn applyGposSingleAdjustmentsAtLookup(
1296 input: Input,
1297 selection: font.LayoutSelection,
1298 lookup_index: u16,
1299 output: *Output,
1300 context_glyph_ids: []const u32,
1301 context_run_start: usize,
1302 ) void {
1303 var glyph_index: usize = 0;
1304 while (glyph_index < output.glyphs.items.len) {
1305 const glyph = output.glyphs.items[glyph_index];
1306 if (glyph.flags.missing_glyph or isHiddenDefaultIgnorableGlyph(glyph)) {
1307 glyph_index += 1;
1308 continue;
1309 }
1310 const context_glyph_index = context_run_start + glyph_index;
1311 if (context_glyph_index >= context_glyph_ids.len) {
1312 glyph_index += 1;
1313 continue;
1314 }
1315 if (input.font.face.gposSingleAdjustmentAtLookupForRange(selection, lookup_index, context_glyph_ids, context_glyph_index, glyphFeatureRange(glyph))) |single_adjustment| {
1316 const target_index = glyph_index + single_adjustment.target_offset;
1317 if (target_index < output.glyphs.items.len) {
1318 const target_glyph = output.glyphs.items[target_index];
1319 if (!target_glyph.flags.missing_glyph and !isHiddenDefaultIgnorableGlyph(target_glyph) and !single_adjustment.isZero()) {
1320 if (gposSingleAdjustmentUsesContext(single_adjustment)) markGlyphSpanUnsafe(output, glyph_index, gposSingleAdjustmentSafetySpan(single_adjustment));
1321 applyGposValueAdjustment(input.font, input.variations, &output.glyphs.items[target_index], single_adjustment.adjustment, output);
1322 }
1323 }
1324 glyph_index += gposAdjustmentAdvance(single_adjustment);
1325 } else {
1326 glyph_index += 1;
1327 }
1328 }
1329 }
1330
1331 fn applyGposContextualPairAdjustmentsAtLookup(
1332 input: Input,
1333 selection: font.LayoutSelection,
1334 lookup_index: u16,
1335 output: *Output,
1336 context_glyph_ids: []const u32,
1337 context_run_start: usize,
1338 ) void {
1339 var glyph_index: usize = 0;
1340 while (glyph_index < output.glyphs.items.len) {
1341 const glyph = output.glyphs.items[glyph_index];
1342 if (glyph.flags.missing_glyph or isHiddenDefaultIgnorableGlyph(glyph)) {
1343 glyph_index += 1;
1344 continue;
1345 }
1346 const context_glyph_index = context_run_start + glyph_index;
1347 if (context_glyph_index >= context_glyph_ids.len) {
1348 glyph_index += 1;
1349 continue;
1350 }
1351 if (input.font.face.gposContextualPairAdjustmentAtLookupForRange(selection, lookup_index, context_glyph_ids, context_glyph_index, glyphFeatureRange(glyph))) |pair_adjustment| {
1352 const target_index = glyph_index + pair_adjustment.target_offset;
1353 if (target_index + 1 < output.glyphs.items.len and !pair_adjustment.isZero()) {
1354 const left = output.glyphs.items[target_index];
1355 const right = output.glyphs.items[target_index + 1];
1356 if (!left.flags.missing_glyph and !right.flags.missing_glyph and !isHiddenDefaultIgnorableGlyph(left) and !isHiddenDefaultIgnorableGlyph(right)) {
1357 markGlyphSpanUnsafe(output, glyph_index, gposContextualPairSafetySpan(pair_adjustment));
1358 markGlyphSpanUnsafe(output, target_index, 2);
1359 applyGposPairAdjustment(input.font, input.variations, &output.glyphs.items[target_index], &output.glyphs.items[target_index + 1], pair_adjustment.adjustment, output);
1360 }
1361 }
1362 glyph_index += gposContextualPairAdvance(pair_adjustment);
1363 } else {
1364 glyph_index += 1;
1365 }
1366 }
1367 }
1368
1369 fn applyGposPairAdjustmentsAtLookup(
1370 input: Input,
1371 selection: font.LayoutSelection,
1372 lookup_index: u16,
1373 output: *Output,
1374 glyph_ids: []const u32,
1375 glyph_indexes: []const usize,
1376 ) void {
1377 for (glyph_ids, 0..) |_, visible_index| {
1378 const glyph_index = glyph_indexes[visible_index];
1379 const glyph = &output.glyphs.items[glyph_index];
1380 if (glyph.flags.missing_glyph) continue;
1381 if (input.font.face.gposPairAdjustmentBeforeAtLookupForRange(selection, lookup_index, glyph_ids, visible_index, glyphFeatureRange(glyph.*))) |gpos_adjustment| {
1382 if (visible_index >= gpos_adjustment.left_offset and !gpos_adjustment.isZero()) {
1383 const previous_index = glyph_indexes[visible_index - gpos_adjustment.left_offset];
1384 const previous = &output.glyphs.items[previous_index];
1385 if (!previous.flags.missing_glyph) {
1386 markGlyphSpanUnsafeToConcat(output, previous_index, 1);
1387 markGlyphSpanUnsafe(output, glyph_index, 1);
1388 applyGposPairAdjustment(input.font, input.variations, previous, glyph, gpos_adjustment.adjustment, output);
1389 }
1390 }
1391 }
1392 }
1393 }
1394
1395 fn applyClassicKerningFallback(
1396 input: Input,
1397 selection: font.LayoutSelection,
1398 has_gpos: bool,
1399 output: *Output,
1400 glyph_ids: []const u32,
1401 glyph_indexes: []const usize,
1402 ) void {
1403 for (glyph_ids, 0..) |_, visible_index| {
1404 if (visible_index == 0) continue;
1405 const glyph_index = glyph_indexes[visible_index];
1406 const glyph = &output.glyphs.items[glyph_index];
1407 if (glyph.flags.missing_glyph) continue;
1408 if (has_gpos) {
1409 if (input.font.face.gposPairAdjustmentBeforeForRange(selection, glyph_ids, visible_index, glyphFeatureRange(glyph.*))) |gpos_adjustment| {
1410 if (visible_index >= gpos_adjustment.left_offset and !gpos_adjustment.isZero()) continue;
1411 }
1412 }
1413 const previous_index = glyph_indexes[visible_index - 1];
1414 const previous = &output.glyphs.items[previous_index];
1415 if (previous.flags.missing_glyph) continue;
1416 if (!font.featureEnabled(selection, font.tag("kern"), true, glyphFeatureRange(glyph.*))) continue;
1417 const kerning = input.font.face.kerning(previous.glyph_id, glyph.glyph_id);
1418 if (kerning == 0) continue;
1419 const scaled_kerning = input.font.scaleDesignSigned(kerning);
1420 markGlyphSpanUnsafeToConcat(output, previous_index, 1);
1421 markGlyphSpanUnsafe(output, glyph_index, 1);
1422 previous.x_advance = addClampedI32(previous.x_advance, scaled_kerning);
1423 output.total_x_advance = addClampedI32(output.total_x_advance, scaled_kerning);
1424 }
1425 }
1426
1427 fn gposAdjustmentAdvance(adjustment: font.GposSingleAdjustment) usize {
1428 if (adjustment.skip_count != 0) return adjustment.skip_count;
1429 return 1;
1430 }
1431
1432 fn gposContextualPairAdvance(adjustment: font.GposContextualPairAdjustment) usize {
1433 if (adjustment.skip_count != 0) return adjustment.skip_count;
1434 return 1;
1435 }
1436
1437 fn gposSingleAdjustmentUsesContext(adjustment: font.GposSingleAdjustment) bool {
1438 return adjustment.target_offset != 0 or adjustment.skip_count != 0;
1439 }
1440
1441 fn gposSingleAdjustmentSafetySpan(adjustment: font.GposSingleAdjustment) usize {
1442 if (adjustment.skip_count != 0) return adjustment.skip_count;
1443 return adjustment.target_offset + 1;
1444 }
1445
1446 fn gposContextualPairSafetySpan(adjustment: font.GposContextualPairAdjustment) usize {
1447 if (adjustment.skip_count != 0) return adjustment.skip_count;
1448 return adjustment.target_offset + 2;
1449 }
1450
1451 fn markGlyphSpanUnsafe(output: *Output, glyph_start: usize, glyph_count: usize) void {
1452 if (glyph_count == 0 or glyph_start >= output.glyphs.items.len) return;
1453 const glyph_end = @min(output.glyphs.items.len, glyph_start + glyph_count);
1454 for (output.glyphs.items[glyph_start..glyph_end]) |*glyph| {
1455 glyph.flags.unsafe_to_break = true;
1456 glyph.flags.unsafe_to_concat = true;
1457 const cluster_index: usize = @intCast(glyph.cluster_index);
1458 if (cluster_index < output.clusters.items.len) {
1459 output.clusters.items[cluster_index].flags.unsafe_to_break = true;
1460 output.clusters.items[cluster_index].flags.unsafe_to_concat = true;
1461 }
1462 }
1463 }
1464
1465 fn markGlyphSpanUnsafeToConcat(output: *Output, glyph_start: usize, glyph_count: usize) void {
1466 if (glyph_count == 0 or glyph_start >= output.glyphs.items.len) return;
1467 const glyph_end = @min(output.glyphs.items.len, glyph_start + glyph_count);
1468 for (output.glyphs.items[glyph_start..glyph_end]) |*glyph| {
1469 glyph.flags.unsafe_to_concat = true;
1470 const cluster_index: usize = @intCast(glyph.cluster_index);
1471 if (cluster_index < output.clusters.items.len) output.clusters.items[cluster_index].flags.unsafe_to_concat = true;
1472 }
1473 }
1474
1475 fn hideUnsubstitutedDefaultIgnorables(output: *Output) void {
1476 for (output.glyphs.items) |*glyph| {
1477 if (!glyph.flags.default_ignorable) continue;
1478 if (glyph.source_codepoint_count != 1) continue;
1479 glyph.glyph_id = 0;
1480 glyph.flags.missing_glyph = false;
1481 }
1482 }
1483
1484 fn isHiddenDefaultIgnorableGlyph(glyph: ShapedGlyph) bool {
1485 return glyph.flags.default_ignorable and !glyph.flags.missing_glyph and glyph.glyph_id == 0;
1486 }
1487
1488 fn previousVisibleGlyphIndex(glyphs: []const ShapedGlyph, before: usize) ?usize {
1489 var index = before;
1490 while (index > 0) {
1491 index -= 1;
1492 if (!isHiddenDefaultIgnorableGlyph(glyphs[index])) return index;
1493 }
1494 return null;
1495 }
1496
1497 fn applyGposPairAdjustment(
1498 shaped_font: *const Font,
1499 variations: []const font.VariationSetting,
1500 previous: *ShapedGlyph,
1501 current: *ShapedGlyph,
1502 adjustment: font.GposPairAdjustment,
1503 output: *Output,
1504 ) void {
1505 applyGposValueAdjustment(shaped_font, variations, previous, adjustment.left, output);
1506 applyGposValueAdjustment(shaped_font, variations, current, adjustment.right, output);
1507 }
1508
1509 fn applyGposCursiveAttachmentAtLookup(shaped_font: *const Font, selection: font.LayoutSelection, lookup_index: u16, output: *Output) void {
1510 if (output.glyphs.items.len < 2) return;
1511
1512 for (output.glyphs.items[1..], 1..) |*current, glyph_index| {
1513 if (isHiddenDefaultIgnorableGlyph(current.*)) continue;
1514 const previous_index = previousVisibleGlyphIndex(output.glyphs.items, glyph_index) orelse continue;
1515 const previous = &output.glyphs.items[previous_index];
1516 const attachment = shaped_font.face.gposCursiveAttachmentAtLookupForRange(selection, lookup_index, previous.glyph_id, current.glyph_id, glyphFeatureRange(current.*)) orelse continue;
1517 const x_anchor_delta = shaped_font.scaleDesignSigned(attachment.x_anchor_delta);
1518 const y_anchor_delta = shaped_font.scaleDesignSigned(attachment.y_anchor_delta);
1519 const target_advance = subtractClampedI32(addClampedI32(previous.x_offset, x_anchor_delta), current.x_offset);
1520 const advance_delta = subtractClampedI32(target_advance, previous.x_advance);
1521 markGlyphSpanUnsafe(output, previous_index, 1);
1522 markGlyphSpanUnsafe(output, glyph_index, 1);
1523 previous.x_advance = addClampedI32(previous.x_advance, advance_delta);
1524 output.total_x_advance = addClampedI32(output.total_x_advance, advance_delta);
1525 current.y_offset = addClampedI32(previous.y_offset, y_anchor_delta);
1526 }
1527 }
1528
1529 fn applyGposValueAdjustment(
1530 shaped_font: *const Font,
1531 variations: []const font.VariationSetting,
1532 glyph: *ShapedGlyph,
1533 adjustment: font.GposValueAdjustment,
1534 output: *Output,
1535 ) void {
1536 const resolved = adjustment.withVariations(shaped_font.face, variations);
1537 const x_placement = shaped_font.scaleDesignSigned(resolved.x_placement);
1538 const y_placement = shaped_font.scaleDesignSigned(resolved.y_placement);
1539 const x_advance = shaped_font.scaleDesignSigned(resolved.x_advance);
1540 const y_advance = shaped_font.scaleDesignSigned(resolved.y_advance);
1541 glyph.x_offset = addClampedI32(glyph.x_offset, x_placement);
1542 glyph.y_offset = addClampedI32(glyph.y_offset, y_placement);
1543 glyph.x_advance = addClampedI32(glyph.x_advance, x_advance);
1544 glyph.y_advance = addClampedI32(glyph.y_advance, y_advance);
1545 output.total_x_advance = addClampedI32(output.total_x_advance, x_advance);
1546 output.total_y_advance = addClampedI32(output.total_y_advance, y_advance);
1547 }
1548
1549 fn applyGposMarkToBasePositioningAtLookup(input: Input, selection: font.LayoutSelection, lookup_index: u16, output: *Output) void {
1550 for (output.glyphs.items, 0..) |*mark, mark_index| {
1551 if (mark_index == 0) continue;
1552 if (isHiddenDefaultIgnorableGlyph(mark.*)) continue;
1553 if (mark.glyph_class != .mark and mark.glyph_class != .unknown) continue;
1554
1555 var base_index = mark_index;
1556 while (base_index > 0) {
1557 base_index -= 1;
1558 const base = &output.glyphs.items[base_index];
1559 if (isHiddenDefaultIgnorableGlyph(base.*)) continue;
1560 if (base.glyph_class == .mark) continue;
1561
1562 if (input.font.face.gposMarkToBaseAdjustmentAtLookupForRange(selection, lookup_index, base.glyph_id, mark.glyph_id, glyphFeatureRange(mark.*))) |adjustment| {
1563 markAttachmentUnsafe(output, base_index, mark_index);
1564 applyGposAttachment(input, base, mark, adjustment, output.glyphs.items[base_index..mark_index], .{
1565 .kind = .base,
1566 .target_glyph_index = @intCast(base_index),
1567 });
1568 break;
1569 }
1570
1571 if (base.glyph_class != .unknown) break;
1572 }
1573 }
1574 }
1575
1576 fn applyGposMarkToLigaturePositioningAtLookup(input: Input, selection: font.LayoutSelection, lookup_index: u16, output: *Output) void {
1577 for (output.glyphs.items, 0..) |*mark, mark_index| {
1578 if (mark_index == 0) continue;
1579 if (isHiddenDefaultIgnorableGlyph(mark.*)) continue;
1580 if (mark.glyph_class != .mark and mark.glyph_class != .unknown) continue;
1581
1582 var ligature_index = mark_index;
1583 while (ligature_index > 0) {
1584 ligature_index -= 1;
1585 const ligature = &output.glyphs.items[ligature_index];
1586 if (isHiddenDefaultIgnorableGlyph(ligature.*)) continue;
1587 if (ligature.glyph_class == .mark) continue;
1588
1589 if (ligature.glyph_class == .ligature) {
1590 const component_index = trailingLigatureMarkComponent(ligature.*, mark.*) orelse break;
1591 if (input.font.face.gposMarkToLigatureAdjustmentAtLookupForRange(selection, lookup_index, ligature.glyph_id, mark.glyph_id, component_index, glyphFeatureRange(mark.*))) |adjustment| {
1592 markAttachmentUnsafe(output, ligature_index, mark_index);
1593 applyGposAttachment(input, ligature, mark, adjustment, output.glyphs.items[ligature_index..mark_index], .{
1594 .kind = .ligature,
1595 .target_glyph_index = @intCast(ligature_index),
1596 .ligature_component = component_index,
1597 });
1598 }
1599 break;
1600 }
1601
1602 if (ligature.glyph_class != .unknown) break;
1603 }
1604 }
1605 }
1606
1607 fn applyGposMarkToMarkPositioningAtLookup(input: Input, selection: font.LayoutSelection, lookup_index: u16, output: *Output) void {
1608 for (output.glyphs.items, 0..) |*mark, mark_index| {
1609 if (mark_index == 0) continue;
1610 if (isHiddenDefaultIgnorableGlyph(mark.*)) continue;
1611 if (mark.glyph_class != .mark and mark.glyph_class != .unknown) continue;
1612
1613 var base_mark_index = mark_index;
1614 while (base_mark_index > 0) {
1615 base_mark_index -= 1;
1616 const base_mark = &output.glyphs.items[base_mark_index];
1617 if (isHiddenDefaultIgnorableGlyph(base_mark.*)) continue;
1618 if (base_mark.glyph_class != .mark and base_mark.glyph_class != .unknown) break;
1619
1620 if (input.font.face.gposMarkToMarkAdjustmentAtLookupForRange(selection, lookup_index, base_mark.glyph_id, mark.glyph_id, glyphFeatureRange(mark.*))) |adjustment| {
1621 markAttachmentUnsafe(output, base_mark_index, mark_index);
1622 applyGposAttachment(input, base_mark, mark, adjustment, output.glyphs.items[base_mark_index..mark_index], .{
1623 .kind = .mark,
1624 .target_glyph_index = @intCast(base_mark_index),
1625 });
1626 break;
1627 }
1628 }
1629 }
1630 }
1631
1632 fn markAttachmentUnsafe(output: *Output, base_index: usize, mark_index: usize) void {
1633 if (mark_index <= base_index) return;
1634 markGlyphSpanUnsafeToConcat(output, base_index, mark_index - base_index + 1);
1635 markGlyphSpanUnsafe(output, mark_index, 1);
1636 }
1637
1638 fn applyGposAttachment(
1639 input: Input,
1640 base: *const ShapedGlyph,
1641 mark: *ShapedGlyph,
1642 adjustment: font.GposValueAdjustment,
1643 origin_glyphs: []const ShapedGlyph,
1644 attachment: GlyphAttachment,
1645 ) void {
1646 const resolved = adjustment.withVariations(input.font.face, input.variations);
1647 const x_delta = input.font.scaleDesignSigned(resolved.x_placement);
1648 const y_delta = input.font.scaleDesignSigned(resolved.y_placement);
1649 const x_origin_delta = if (input.direction == .rtl and input.output_order == .visual) 0 else glyphOriginDelta(origin_glyphs);
1650 mark.x_offset = addClampedI32(mark.x_offset, addClampedI32(addClampedI32(base.x_offset, x_delta), -x_origin_delta));
1651 mark.y_offset = addClampedI32(mark.y_offset, addClampedI32(base.y_offset, y_delta));
1652 mark.attachment = attachment;
1653 }
1654
1655 fn trailingLigatureMarkComponent(ligature: ShapedGlyph, mark: ShapedGlyph) ?u16 {
1656 if (ligature.source_codepoint_count == 0) return null;
1657 if (mark.source_start < ligature.source_end) return null;
1658 return ligature.source_codepoint_count - 1;
1659 }
1660
1661 fn glyphOriginDelta(glyphs: []const ShapedGlyph) i32 {
1662 var delta: i32 = 0;
1663 for (glyphs) |glyph| {
1664 delta = addClampedI32(delta, glyph.x_advance);
1665 }
1666 return delta;
1667 }
1668
1669 fn synthesizeMissingLigatureCarets(output: *Output) !void {
1670 for (output.glyphs.items) |*glyph| {
1671 if (glyph.glyph_class != .ligature) continue;
1672 if (glyph.ligature_caret_count != 0) continue;
1673 if (glyph.source_codepoint_count < 2) continue;
1674
1675 const caret_count: u16 = glyph.source_codepoint_count - 1;
1676 const caret_start = output.ligature_carets.items.len;
1677 try output.ensureUnused(0, 0, caret_count);
1678 for (1..glyph.source_codepoint_count) |caret_index| {
1679 output.ligature_carets.appendAssumeCapacity(.{
1680 .x_offset = proportionalCaretOffset(glyph.x_advance, @intCast(caret_index), glyph.source_codepoint_count),
1681 .synthesized = true,
1682 });
1683 }
1684 glyph.ligature_caret_start = @intCast(caret_start);
1685 glyph.ligature_caret_count = caret_count;
1686 }
1687 }
1688
1689 fn proportionalCaretOffset(advance: i32, caret_index: u16, component_count: u16) i32 {
1690 if (component_count == 0) return 0;
1691 const numerator = @as(i64, advance) * @as(i64, caret_index);
1692 const denominator: i64 = component_count;
1693 const rounded = if (numerator >= 0)
1694 numerator + @divTrunc(denominator, 2)
1695 else
1696 numerator - @divTrunc(denominator, 2);
1697 const offset = @divTrunc(rounded, denominator);
1698 if (offset > std.math.maxInt(i32)) return std.math.maxInt(i32);
1699 if (offset < std.math.minInt(i32)) return std.math.minInt(i32);
1700 return @intCast(offset);
1701 }
1702
1703 fn mergeFlags(left: GlyphFlags, right: GlyphFlags) GlyphFlags {
1704 return .{
1705 .unsafe_to_break = left.unsafe_to_break or right.unsafe_to_break,
1706 .unsafe_to_concat = left.unsafe_to_concat or right.unsafe_to_concat,
1707 .safe_to_insert_tatweel = left.safe_to_insert_tatweel or right.safe_to_insert_tatweel,
1708 .missing_glyph = left.missing_glyph or right.missing_glyph,
1709 .default_ignorable = left.default_ignorable or right.default_ignorable,
1710 .synthetic = left.synthetic or right.synthetic,
1711 };
1712 }
1713
1714 fn addSaturatingU16(left: u16, right: u16) u16 {
1715 const sum = @as(u32, left) + @as(u32, right);
1716 return if (sum > std.math.maxInt(u16)) std.math.maxInt(u16) else @intCast(sum);
1717 }
1718
1719 pub const Font = struct {
1720 face: font.Face,
1721 scale_26dot6: i32,
1722
1723 /// Borrows `data[0..len]` for the entire lifetime of the returned font.
1724 /// Returns null when the bytes do not contain a supported face at index zero.
1725 pub fn initFromBytes(data: [*]const u8, len: usize) ?Font {
1726 const slice = data[0..len];
1727 return initFromSliceAt(slice, 0);
1728 }
1729
1730 pub fn initFromBytesAt(data: [*]const u8, len: usize, face_index: u32) ?Font {
1731 return initFromSliceAt(data[0..len], face_index);
1732 }
1733
1734 fn initFromSliceAt(data: []const u8, face_index: u32) ?Font {
1735 const face = font.Face.initAt(data, face_index) catch return null;
1736 return .{
1737 .face = face,
1738 .scale_26dot6 = @as(i32, face.units_per_em) * 64,
1739 };
1740 }
1741
1742 pub fn deinit(self: *Font) void {
1743 self.* = undefined;
1744 }
1745
1746 pub fn setScale(self: *Font, point_size: f64, dpi: u32) void {
1747 self.scale_26dot6 = fontScale26Dot6(point_size, dpi);
1748 }
1749
1750 pub fn setPixelHeightScale(self: *Font, pixel_height: f64) void {
1751 self.scale_26dot6 = pixelHeightScale26Dot6ForMetrics(
1752 pixel_height,
1753 self.face.units_per_em,
1754 self.face.heightUnits(),
1755 );
1756 }
1757
1758 pub fn setPixelHeightScaleForVariations(self: *Font, pixel_height: f64, variations: []const font.VariationSetting) void {
1759 self.scale_26dot6 = pixelHeightScale26Dot6ForMetricUnits(
1760 pixel_height,
1761 self.face.units_per_em,
1762 self.face.heightUnitsForVariations(variations),
1763 );
1764 }
1765
1766 fn scaleDesign(self: *const Font, value: u16) i32 {
1767 const numerator = @as(i64, value) * @as(i64, self.scale_26dot6);
1768 const denominator: i64 = self.face.units_per_em;
1769 const rounded = numerator + @divTrunc(denominator, 2);
1770 const scaled = @divTrunc(rounded, denominator);
1771 if (scaled > std.math.maxInt(i32)) return std.math.maxInt(i32);
1772 if (scaled < std.math.minInt(i32)) return std.math.minInt(i32);
1773 return @intCast(scaled);
1774 }
1775
1776 fn scaleDesignSigned(self: *const Font, value: i32) i32 {
1777 const numerator = @as(i64, value) * @as(i64, self.scale_26dot6);
1778 const denominator: i64 = self.face.units_per_em;
1779 const rounded = if (numerator >= 0)
1780 numerator + @divTrunc(denominator, 2)
1781 else
1782 numerator - @divTrunc(denominator, 2);
1783 const scaled = @divTrunc(rounded, denominator);
1784 if (scaled > std.math.maxInt(i32)) return std.math.maxInt(i32);
1785 if (scaled < std.math.minInt(i32)) return std.math.minInt(i32);
1786 return @intCast(scaled);
1787 }
1788 };
1789
1790 fn fontScale26Dot6(point_size: f64, dpi: u32) i32 {
1791 const sane_point = if (std.math.isFinite(point_size) and point_size > 0) point_size else 1.0;
1792 const ppem = sane_point * @as(f64, @floatFromInt(dpi)) / 72.0;
1793 return clampScale26Dot6(ppem * 64.0);
1794 }
1795
1796 fn pixelHeightScale26Dot6ForMetrics(pixel_height: f64, upem: u16, height_units: u16) i32 {
1797 return pixelHeightScale26Dot6ForMetricUnits(pixel_height, upem, @intCast(height_units));
1798 }
1799
1800 fn pixelHeightScale26Dot6ForMetricUnits(pixel_height: f64, upem: u16, height_units: i32) i32 {
1801 if (!std.math.isFinite(pixel_height) or pixel_height <= 0) return 64;
1802 const sane_height = pixel_height;
1803 const sane_upem: f64 = @floatFromInt(if (upem == 0) 1000 else upem);
1804 const metric_units: i32 = if (height_units <= 0) @intCast(if (upem == 0) 1000 else upem) else height_units;
1805 const sane_units: f64 = @floatFromInt(metric_units);
1806 return clampScale26Dot6(sane_height * sane_upem / sane_units * 64.0);
1807 }
1808
1809 fn clampScale26Dot6(raw_scale: f64) i32 {
1810 const scaled = @round(raw_scale);
1811 if (!std.math.isFinite(scaled) or scaled <= 0) return 64;
1812 const max_scale: f64 = @floatFromInt(std.math.maxInt(i32));
1813 if (scaled >= max_scale) return std.math.maxInt(i32);
1814 return @intFromFloat(scaled);
1815 }
1816
1817 fn addClampedI32(a: i32, b: i32) i32 {
1818 const sum = @as(i64, a) + @as(i64, b);
1819 if (sum > std.math.maxInt(i32)) return std.math.maxInt(i32);
1820 if (sum < std.math.minInt(i32)) return std.math.minInt(i32);
1821 return @intCast(sum);
1822 }
1823
1824 fn subtractClampedI32(a: i32, b: i32) i32 {
1825 const difference = @as(i64, a) - @as(i64, b);
1826 if (difference > std.math.maxInt(i32)) return std.math.maxInt(i32);
1827 if (difference < std.math.minInt(i32)) return std.math.minInt(i32);
1828 return @intCast(difference);
1829 }
1830
1831 test "font scale preserves fractional ppem" {
1832 try std.testing.expectEqual(@as(i32, 1365), fontScale26Dot6(16.0, 96));
1833 try std.testing.expectEqual(@as(i32, 1365), fontScale26Dot6(21.333333333, 72));
1834 try std.testing.expectEqual(@as(i32, 64), fontScale26Dot6(-1, 72));
1835 }
1836
1837 test "pixel-height scale matches stb truetype metrics" {
1838 try std.testing.expectEqual(@as(i32, 1139), pixelHeightScale26Dot6ForMetrics(21, 1000, 1180));
1839 try std.testing.expectEqual(@as(i32, 1247), pixelHeightScale26Dot6ForMetrics(23, 1000, 1180));
1840 try std.testing.expectEqual(@as(i32, 64), pixelHeightScale26Dot6ForMetrics(-1, 1000, 1180));
1841 }
1842
1843 test "pixel-height scale can use variable font metrics" {
1844 const bytes = try test_font.createWithMetricVariations(std.testing.allocator);
1845 defer std.testing.allocator.free(bytes);
1846
1847 var shaper = Font.initFromBytes(bytes.ptr, bytes.len).?;
1848 shaper.setPixelHeightScaleForVariations(20, &.{.{ .tag = font.tag("wght"), .value = 900.0 }});
1849
1850 try std.testing.expectEqual(@as(i32, 1067), shaper.scale_26dot6);
1851 }
1852
1853 test "shapeRun maps utf8 to nominal glyphs and byte clusters" {
1854 const bytes = try test_font.create(std.testing.allocator);
1855 defer std.testing.allocator.free(bytes);
1856
1857 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
1858 defer shaper.deinit();
1859 shaper.setPixelHeightScale(20);
1860
1861 var ctx = Context.init(std.testing.allocator, .{});
1862 defer ctx.deinit();
1863
1864 var output = try Output.init(std.testing.allocator, .{
1865 .max_glyphs = 256,
1866 .max_ligature_carets = 256,
1867 });
1868 defer output.deinit(std.testing.allocator);
1869
1870 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "Az!" } }, &output);
1871 const run = output.run();
1872 const glyphs = run.glyphs;
1873
1874 try std.testing.expectEqual(@as(usize, 3), glyphs.len);
1875 try std.testing.expectEqual(@as(usize, 3), run.clusters.len);
1876 try std.testing.expectEqual(@as(u32, 'A' - 31), glyphs[0].glyph_id);
1877 try std.testing.expectEqual(@as(u32, 'z' - 31), glyphs[1].glyph_id);
1878 try std.testing.expectEqual(@as(u32, '!' - 31), glyphs[2].glyph_id);
1879 try std.testing.expectEqual(@as(u32, 0), glyphs[0].cluster);
1880 try std.testing.expectEqual(@as(u32, 1), glyphs[1].cluster);
1881 try std.testing.expectEqual(@as(u32, 2), glyphs[2].cluster);
1882 try std.testing.expectEqual(@as(i32, 640), glyphs[0].x_advance);
1883 try std.testing.expectEqual(@as(i32, 1920), run.total_x_advance);
1884 }
1885
1886 test "Font opens indexed OpenType collection faces" {
1887 const bytes = try test_font.createCollection(std.testing.allocator);
1888 defer std.testing.allocator.free(bytes);
1889
1890 var first_font = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
1891 defer first_font.deinit();
1892 first_font.setPixelHeightScale(20);
1893
1894 var second_font = Font.initFromBytesAt(bytes.ptr, bytes.len, 1) orelse return error.TestUnexpectedResult;
1895 defer second_font.deinit();
1896 second_font.setPixelHeightScale(20);
1897
1898 var ctx = Context.init(std.testing.allocator, .{});
1899 defer ctx.deinit();
1900
1901 var output = try Output.init(std.testing.allocator, .{
1902 .max_glyphs = 256,
1903 .max_ligature_carets = 256,
1904 });
1905 defer output.deinit(std.testing.allocator);
1906
1907 try ctx.shapeRun(.{ .font = &first_font, .text = .{ .utf8 = "A" } }, &output);
1908 try std.testing.expectEqual(@as(u32, 'A' - 31), output.run().glyphs[0].glyph_id);
1909
1910 try ctx.shapeRun(.{ .font = &second_font, .text = .{ .utf8 = "A" } }, &output);
1911 try std.testing.expectEqual(@as(u32, 'Z' - 31), output.run().glyphs[0].glyph_id);
1912 try std.testing.expect(Font.initFromBytesAt(bytes.ptr, bytes.len, 2) == null);
1913 }
1914
1915 test "shapeRun maps vertical writing to vertical advances" {
1916 const bytes = try test_font.createWithVerticalMetrics(std.testing.allocator);
1917 defer std.testing.allocator.free(bytes);
1918
1919 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
1920 defer shaper.deinit();
1921 shaper.setPixelHeightScale(20);
1922
1923 var ctx = Context.init(std.testing.allocator, .{});
1924 defer ctx.deinit();
1925
1926 var output = try Output.init(std.testing.allocator, .{
1927 .max_glyphs = 256,
1928 .max_ligature_carets = 256,
1929 });
1930 defer output.deinit(std.testing.allocator);
1931
1932 try ctx.shapeRun(.{
1933 .font = &shaper,
1934 .text = .{ .utf8 = "AB!" },
1935 .writing_mode = .vertical,
1936 }, &output);
1937 const run = output.run();
1938
1939 try std.testing.expectEqual(WritingMode.vertical, run.writing_mode);
1940 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
1941 try std.testing.expectEqual(@as(i32, 0), run.glyphs[0].x_advance);
1942 try std.testing.expectEqual(@as(i32, -896), run.glyphs[0].y_advance);
1943 try std.testing.expectEqual(@as(i32, 0), run.glyphs[1].x_advance);
1944 try std.testing.expectEqual(@as(i32, -1152), run.glyphs[1].y_advance);
1945 try std.testing.expectEqual(@as(i32, 0), run.glyphs[2].x_advance);
1946 try std.testing.expectEqual(@as(i32, -896), run.glyphs[2].y_advance);
1947 try std.testing.expectEqual(@as(i32, 0), run.total_x_advance);
1948 try std.testing.expectEqual(@as(i32, -2944), run.total_y_advance);
1949 }
1950
1951 test "shapeRun maps vertical origins to vertical offsets" {
1952 const bytes = try test_font.createWithVerticalOrigins(std.testing.allocator);
1953 defer std.testing.allocator.free(bytes);
1954
1955 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
1956 defer shaper.deinit();
1957 shaper.scale_26dot6 = shaper.face.units_per_em;
1958
1959 var ctx = Context.init(std.testing.allocator, .{});
1960 defer ctx.deinit();
1961
1962 var output = try Output.init(std.testing.allocator, .{
1963 .max_glyphs = 256,
1964 .max_ligature_carets = 256,
1965 });
1966 defer output.deinit(std.testing.allocator);
1967
1968 try ctx.shapeRun(.{
1969 .font = &shaper,
1970 .text = .{ .utf8 = "AB" },
1971 .writing_mode = .vertical,
1972 }, &output);
1973 const run = output.run();
1974
1975 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
1976 try std.testing.expectEqual(@as(i32, -250), run.glyphs[0].x_offset);
1977 try std.testing.expectEqual(@as(i32, -760), run.glyphs[0].y_offset);
1978 try std.testing.expectEqual(@as(i32, -700), run.glyphs[0].y_advance);
1979 try std.testing.expectEqual(@as(i32, -250), run.glyphs[1].x_offset);
1980 try std.testing.expectEqual(@as(i32, -880), run.glyphs[1].y_offset);
1981 try std.testing.expectEqual(@as(i32, -900), run.glyphs[1].y_advance);
1982 try std.testing.expectEqual(@as(i32, -1600), run.total_y_advance);
1983 }
1984
1985 test "shapeRun applies GSUB vertical alternates in vertical writing" {
1986 const bytes = try test_font.createWithGsubVerticalAlternates(std.testing.allocator);
1987 defer std.testing.allocator.free(bytes);
1988
1989 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
1990 defer shaper.deinit();
1991 shaper.setPixelHeightScale(20);
1992
1993 var ctx = Context.init(std.testing.allocator, .{});
1994 defer ctx.deinit();
1995
1996 var output = try Output.init(std.testing.allocator, .{
1997 .max_glyphs = 256,
1998 .max_ligature_carets = 256,
1999 });
2000 defer output.deinit(std.testing.allocator);
2001
2002 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A!" } }, &output);
2003 const horizontal = output.run();
2004
2005 try std.testing.expectEqual(@as(usize, 2), horizontal.glyphs.len);
2006 try std.testing.expectEqual(@as(u32, 'A' - 31), horizontal.glyphs[0].glyph_id);
2007
2008 try ctx.shapeRun(.{
2009 .font = &shaper,
2010 .text = .{ .utf8 = "A!" },
2011 .writing_mode = .vertical,
2012 }, &output);
2013 const vertical = output.run();
2014
2015 try std.testing.expectEqual(@as(usize, 2), vertical.glyphs.len);
2016 try std.testing.expectEqual(@as(u32, 'B' - 31), vertical.glyphs[0].glyph_id);
2017 try std.testing.expectEqual(@as(i32, 0), vertical.glyphs[0].x_advance);
2018 try std.testing.expectEqual(@as(i32, -1280), vertical.glyphs[0].y_advance);
2019
2020 try ctx.shapeRun(.{
2021 .font = &shaper,
2022 .text = .{ .utf8 = "A!" },
2023 .writing_mode = .vertical,
2024 .features = &.{.{ .tag = font.tag("vert"), .value = 0 }},
2025 }, &output);
2026 const disabled = output.run();
2027
2028 try std.testing.expectEqual(@as(usize, 2), disabled.glyphs.len);
2029 try std.testing.expectEqual(@as(u32, 'A' - 31), disabled.glyphs[0].glyph_id);
2030 }
2031
2032 test "shapeRun falls back to line height for vertical advances" {
2033 const bytes = try test_font.create(std.testing.allocator);
2034 defer std.testing.allocator.free(bytes);
2035
2036 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2037 defer shaper.deinit();
2038 shaper.setPixelHeightScale(20);
2039
2040 var ctx = Context.init(std.testing.allocator, .{});
2041 defer ctx.deinit();
2042
2043 var output = try Output.init(std.testing.allocator, .{
2044 .max_glyphs = 256,
2045 .max_ligature_carets = 256,
2046 });
2047 defer output.deinit(std.testing.allocator);
2048
2049 try ctx.shapeRun(.{
2050 .font = &shaper,
2051 .text = .{ .utf8 = "A!" },
2052 .writing_mode = .vertical,
2053 }, &output);
2054 const run = output.run();
2055
2056 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
2057 try std.testing.expectEqual(@as(i32, 0), run.total_x_advance);
2058 try std.testing.expectEqual(@as(i32, -2560), run.total_y_advance);
2059 }
2060
2061 test "shapeRun builds cluster map for multibyte utf8 and missing glyphs" {
2062 const bytes = try test_font.create(std.testing.allocator);
2063 defer std.testing.allocator.free(bytes);
2064
2065 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2066 defer shaper.deinit();
2067 shaper.setPixelHeightScale(20);
2068
2069 var ctx = Context.init(std.testing.allocator, .{});
2070 defer ctx.deinit();
2071
2072 var output = try Output.init(std.testing.allocator, .{
2073 .max_glyphs = 256,
2074 .max_ligature_carets = 256,
2075 });
2076 defer output.deinit(std.testing.allocator);
2077
2078 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AĆ©!" } }, &output);
2079 const run = output.run();
2080 const map = run.clusterMap();
2081
2082 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
2083 try std.testing.expectEqual(@as(usize, 3), run.clusters.len);
2084 try std.testing.expect(run.glyphs[1].flags.missing_glyph);
2085 try std.testing.expect(run.clusters[1].flags.missing_glyph);
2086 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(1).?);
2087 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(2).?);
2088 try std.testing.expectEqual(@as(usize, 2), map.byteOffsetToCluster(3).?);
2089 try std.testing.expectEqual(@as(?usize, null), map.byteOffsetToCluster(4));
2090 try std.testing.expectEqual(SourceRange{ .start = 1, .end = 3 }, map.clusterSourceRange(1).?);
2091 try std.testing.expectEqual(GlyphSpan{ .start = 1, .end = 2 }, map.clusterGlyphSpan(1).?);
2092 try std.testing.expectEqual(@as(usize, 1), map.glyphToCluster(1).?);
2093 try std.testing.expectEqual(@as(usize, 2), map.clusterCaretStopCount(1).?);
2094 try std.testing.expectEqual(@as(u32, 1), map.clusterCaretStop(1, 0).?);
2095 try std.testing.expectEqual(@as(u32, 3), map.clusterCaretStop(1, 1).?);
2096 try std.testing.expectEqual(false, map.selectableAsUnitOnly(1).?);
2097 try std.testing.expectEqual(false, map.breakRequiresReshaping(1).?);
2098 }
2099
2100 test "shapeRun maps utf8 clusters from source byte offsets" {
2101 const bytes = try test_font.create(std.testing.allocator);
2102 defer std.testing.allocator.free(bytes);
2103
2104 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2105 defer shaper.deinit();
2106 shaper.setPixelHeightScale(20);
2107
2108 var ctx = Context.init(std.testing.allocator, .{});
2109 defer ctx.deinit();
2110
2111 var output = try Output.init(std.testing.allocator, .{
2112 .max_glyphs = 256,
2113 .max_ligature_carets = 256,
2114 });
2115 defer output.deinit(std.testing.allocator);
2116
2117 try ctx.shapeRun(.{
2118 .font = &shaper,
2119 .text = .{ .utf8 = "AĆ©!" },
2120 .source_offset = 10,
2121 }, &output);
2122 const run = output.run();
2123 const map = run.clusterMap();
2124
2125 try std.testing.expectEqual(SourceRange{ .start = 10, .end = 11 }, map.clusterSourceRange(0).?);
2126 try std.testing.expectEqual(SourceRange{ .start = 11, .end = 13 }, map.clusterSourceRange(1).?);
2127 try std.testing.expectEqual(SourceRange{ .start = 13, .end = 14 }, map.clusterSourceRange(2).?);
2128 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(10).?);
2129 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(12).?);
2130 try std.testing.expectEqual(@as(usize, 2), map.byteOffsetToCluster(13).?);
2131 try std.testing.expectEqual(@as(?usize, null), map.byteOffsetToCluster(1));
2132 try std.testing.expectEqual(@as(?usize, null), map.byteOffsetToCluster(14));
2133 try std.testing.expectEqual(@as(u32, 11), run.glyphs[1].source_start);
2134 try std.testing.expectEqual(@as(u32, 13), run.glyphs[1].source_end);
2135 }
2136
2137 test "shapeRun rejects source byte offsets that overflow cluster ranges" {
2138 const bytes = try test_font.create(std.testing.allocator);
2139 defer std.testing.allocator.free(bytes);
2140
2141 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2142 defer shaper.deinit();
2143
2144 var ctx = Context.init(std.testing.allocator, .{});
2145 defer ctx.deinit();
2146
2147 var output = try Output.init(std.testing.allocator, .{
2148 .max_glyphs = 256,
2149 .max_ligature_carets = 256,
2150 });
2151 defer output.deinit(std.testing.allocator);
2152
2153 try std.testing.expectError(error.SourceTooLong, ctx.shapeRun(.{
2154 .font = &shaper,
2155 .text = .{ .utf8 = "A" },
2156 .source_offset = std.math.maxInt(u32),
2157 }, &output));
2158 const run = output.run();
2159
2160 try std.testing.expectEqual(@as(usize, 0), run.glyphs.len);
2161 try std.testing.expectEqual(@as(usize, 0), run.clusters.len);
2162 }
2163
2164 test "shapeRun rejects missing glyphs when requested" {
2165 const bytes = try test_font.create(std.testing.allocator);
2166 defer std.testing.allocator.free(bytes);
2167
2168 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2169 defer shaper.deinit();
2170
2171 var ctx = Context.init(std.testing.allocator, .{});
2172 defer ctx.deinit();
2173
2174 var output = try Output.init(std.testing.allocator, .{
2175 .max_glyphs = 256,
2176 .max_ligature_carets = 256,
2177 });
2178 defer output.deinit(std.testing.allocator);
2179
2180 try std.testing.expectError(error.MissingGlyph, ctx.shapeRun(.{
2181 .font = &shaper,
2182 .text = .{ .utf8 = "AĆ©!" },
2183 .missing_glyph_policy = .fail,
2184 }, &output));
2185 const run = output.run();
2186
2187 try std.testing.expectEqual(@as(usize, 0), run.glyphs.len);
2188 try std.testing.expectEqual(@as(usize, 0), run.clusters.len);
2189 try std.testing.expectEqual(@as(i32, 0), run.total_x_advance);
2190 }
2191
2192 test "shapeRun maps utf8 variation sequences through one cluster" {
2193 const bytes = try test_font.createWithVariationSequences(std.testing.allocator);
2194 defer std.testing.allocator.free(bytes);
2195
2196 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2197 defer shaper.deinit();
2198 shaper.setPixelHeightScale(20);
2199
2200 var ctx = Context.init(std.testing.allocator, .{});
2201 defer ctx.deinit();
2202
2203 var output = try Output.init(std.testing.allocator, .{
2204 .max_glyphs = 256,
2205 .max_ligature_carets = 256,
2206 });
2207 defer output.deinit(std.testing.allocator);
2208
2209 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A\u{fe0f}!" } }, &output);
2210 const run = output.run();
2211 const map = run.clusterMap();
2212
2213 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
2214 try std.testing.expectEqual(@as(usize, 2), run.clusters.len);
2215 try std.testing.expectEqual(@as(u32, 96), run.glyphs[0].glyph_id);
2216 try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].source_start);
2217 try std.testing.expectEqual(@as(u32, 4), run.glyphs[0].source_end);
2218 try std.testing.expectEqual(@as(u16, 2), run.glyphs[0].source_codepoint_count);
2219 try std.testing.expectEqual(SourceRange{ .start = 0, .end = 4 }, map.clusterSourceRange(0).?);
2220 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(2).?);
2221 try std.testing.expectEqual(true, map.selectableAsUnitOnly(0).?);
2222 try std.testing.expectEqual(@as(i32, 1280), run.total_x_advance);
2223 }
2224
2225 test "shapeRun maps utf8 variation clusters from source byte offsets" {
2226 const bytes = try test_font.createWithVariationSequences(std.testing.allocator);
2227 defer std.testing.allocator.free(bytes);
2228
2229 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2230 defer shaper.deinit();
2231 shaper.setPixelHeightScale(20);
2232
2233 var ctx = Context.init(std.testing.allocator, .{});
2234 defer ctx.deinit();
2235
2236 var output = try Output.init(std.testing.allocator, .{
2237 .max_glyphs = 256,
2238 .max_ligature_carets = 256,
2239 });
2240 defer output.deinit(std.testing.allocator);
2241
2242 try ctx.shapeRun(.{
2243 .font = &shaper,
2244 .text = .{ .utf8 = "A\u{fe0f}!" },
2245 .source_offset = 40,
2246 }, &output);
2247 const run = output.run();
2248 const map = run.clusterMap();
2249
2250 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
2251 try std.testing.expectEqual(SourceRange{ .start = 40, .end = 44 }, map.clusterSourceRange(0).?);
2252 try std.testing.expectEqual(SourceRange{ .start = 44, .end = 45 }, map.clusterSourceRange(1).?);
2253 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(42).?);
2254 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(44).?);
2255 try std.testing.expectEqual(@as(u32, 40), run.glyphs[0].source_start);
2256 try std.testing.expectEqual(@as(u32, 44), run.glyphs[0].source_end);
2257 }
2258
2259 test "shapeRun hides unsupported utf8 variation selectors by default" {
2260 const bytes = try test_font.createWithVariationSequences(std.testing.allocator);
2261 defer std.testing.allocator.free(bytes);
2262
2263 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2264 defer shaper.deinit();
2265 shaper.setPixelHeightScale(20);
2266
2267 var ctx = Context.init(std.testing.allocator, .{});
2268 defer ctx.deinit();
2269
2270 var output = try Output.init(std.testing.allocator, .{
2271 .max_glyphs = 256,
2272 .max_ligature_carets = 256,
2273 });
2274 defer output.deinit(std.testing.allocator);
2275
2276 try ctx.shapeRun(.{
2277 .font = &shaper,
2278 .text = .{ .utf8 = "B\u{fe0f}!" },
2279 .missing_glyph_policy = .fail,
2280 }, &output);
2281 const run = output.run();
2282
2283 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
2284 try std.testing.expectEqual(@as(usize, 3), run.clusters.len);
2285 try std.testing.expectEqual(@as(u32, 'B' - 31), run.glyphs[0].glyph_id);
2286 try std.testing.expectEqual(@as(u32, 0), run.glyphs[1].glyph_id);
2287 try std.testing.expect(run.glyphs[1].flags.default_ignorable);
2288 try std.testing.expect(!run.glyphs[1].flags.missing_glyph);
2289 try std.testing.expect(run.clusters[1].flags.default_ignorable);
2290 try std.testing.expectEqual(@as(u32, 1), run.glyphs[1].source_start);
2291 try std.testing.expectEqual(@as(u32, 4), run.glyphs[1].source_end);
2292 try std.testing.expectEqual(@as(i32, 0), run.glyphs[1].x_advance);
2293 try std.testing.expectEqual(@as(i32, 1280), run.total_x_advance);
2294 }
2295
2296 test "shapeRun preserves unsupported default ignorables when requested" {
2297 const bytes = try test_font.createWithVariationSequences(std.testing.allocator);
2298 defer std.testing.allocator.free(bytes);
2299
2300 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2301 defer shaper.deinit();
2302 shaper.setPixelHeightScale(20);
2303
2304 var ctx = Context.init(std.testing.allocator, .{});
2305 defer ctx.deinit();
2306
2307 var output = try Output.init(std.testing.allocator, .{
2308 .max_glyphs = 256,
2309 .max_ligature_carets = 256,
2310 });
2311 defer output.deinit(std.testing.allocator);
2312
2313 try ctx.shapeRun(.{
2314 .font = &shaper,
2315 .text = .{ .utf8 = "B\u{fe0f}!" },
2316 .default_ignorable_policy = .preserve,
2317 }, &output);
2318 const run = output.run();
2319
2320 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
2321 try std.testing.expect(run.glyphs[1].flags.default_ignorable);
2322 try std.testing.expect(run.glyphs[1].flags.missing_glyph);
2323 try std.testing.expect(run.clusters[1].flags.default_ignorable);
2324 try std.testing.expect(run.clusters[1].flags.missing_glyph);
2325 try std.testing.expectEqual(@as(i32, 640), run.glyphs[1].x_advance);
2326 try std.testing.expectEqual(@as(i32, 1920), run.total_x_advance);
2327 }
2328
2329 test "shapeRun removes unsupported default ignorables when requested" {
2330 const bytes = try test_font.createWithVariationSequences(std.testing.allocator);
2331 defer std.testing.allocator.free(bytes);
2332
2333 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2334 defer shaper.deinit();
2335 shaper.setPixelHeightScale(20);
2336
2337 var ctx = Context.init(std.testing.allocator, .{});
2338 defer ctx.deinit();
2339
2340 var output = try Output.init(std.testing.allocator, .{
2341 .max_glyphs = 256,
2342 .max_ligature_carets = 256,
2343 });
2344 defer output.deinit(std.testing.allocator);
2345
2346 try ctx.shapeRun(.{
2347 .font = &shaper,
2348 .text = .{ .utf8 = "B\u{fe0f}!" },
2349 .default_ignorable_policy = .remove,
2350 }, &output);
2351 const run = output.run();
2352 const map = run.clusterMap();
2353
2354 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
2355 try std.testing.expectEqual(@as(usize, 2), run.clusters.len);
2356 try std.testing.expectEqual(@as(u32, 'B' - 31), run.glyphs[0].glyph_id);
2357 try std.testing.expectEqual(@as(u32, '!' - 31), run.glyphs[1].glyph_id);
2358 try std.testing.expectEqual(SourceRange{ .start = 0, .end = 1 }, map.clusterSourceRange(0).?);
2359 try std.testing.expectEqual(SourceRange{ .start = 4, .end = 5 }, map.clusterSourceRange(1).?);
2360 try std.testing.expectEqual(@as(?usize, null), map.byteOffsetToCluster(1));
2361 try std.testing.expectEqual(@as(?usize, null), map.byteOffsetToCluster(2));
2362 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(4).?);
2363 try std.testing.expectEqual(@as(i32, 1280), run.total_x_advance);
2364 }
2365
2366 test "shapeRun maps utf16 to nominal glyphs and byte clusters" {
2367 const bytes = try test_font.createWithKern(std.testing.allocator);
2368 defer std.testing.allocator.free(bytes);
2369
2370 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2371 defer shaper.deinit();
2372 shaper.setPixelHeightScale(20);
2373
2374 var ctx = Context.init(std.testing.allocator, .{});
2375 defer ctx.deinit();
2376
2377 var output = try Output.init(std.testing.allocator, .{
2378 .max_glyphs = 256,
2379 .max_ligature_carets = 256,
2380 });
2381 defer output.deinit(std.testing.allocator);
2382
2383 const text = [_]u16{ 'A', 'V' };
2384 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf16 = &text } }, &output);
2385 const run = output.run();
2386 const map = run.clusterMap();
2387
2388 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
2389 try std.testing.expectEqual(@as(u32, 'A' - 31), run.glyphs[0].glyph_id);
2390 try std.testing.expectEqual(@as(u32, 'V' - 31), run.glyphs[1].glyph_id);
2391 try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].source_start);
2392 try std.testing.expectEqual(@as(u32, 2), run.glyphs[0].source_end);
2393 try std.testing.expectEqual(@as(u32, 2), run.glyphs[1].source_start);
2394 try std.testing.expectEqual(@as(u32, 4), run.glyphs[1].source_end);
2395 try std.testing.expectEqual(@as(i32, 538), run.glyphs[0].x_advance);
2396 try std.testing.expectEqual(@as(i32, 640), run.glyphs[1].x_advance);
2397 try std.testing.expectEqual(@as(i32, 1178), run.total_x_advance);
2398 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(0).?);
2399 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(1).?);
2400 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(2).?);
2401 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(3).?);
2402 try std.testing.expectEqual(@as(?usize, null), map.byteOffsetToCluster(4));
2403 try std.testing.expectEqual(SourceRange{ .start = 0, .end = 2 }, map.clusterSourceRange(0).?);
2404 try std.testing.expectEqual(SourceRange{ .start = 2, .end = 4 }, map.clusterSourceRange(1).?);
2405 }
2406
2407 test "shapeRun maps utf16 clusters from source byte offsets" {
2408 const bytes = try test_font.create(std.testing.allocator);
2409 defer std.testing.allocator.free(bytes);
2410
2411 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2412 defer shaper.deinit();
2413
2414 var ctx = Context.init(std.testing.allocator, .{});
2415 defer ctx.deinit();
2416
2417 var output = try Output.init(std.testing.allocator, .{
2418 .max_glyphs = 256,
2419 .max_ligature_carets = 256,
2420 });
2421 defer output.deinit(std.testing.allocator);
2422
2423 const text = [_]u16{ 'A', 'B' };
2424 try ctx.shapeRun(.{
2425 .font = &shaper,
2426 .text = .{ .utf16 = &text },
2427 .source_offset = 20,
2428 }, &output);
2429 const map = output.run().clusterMap();
2430
2431 try std.testing.expectEqual(SourceRange{ .start = 20, .end = 22 }, map.clusterSourceRange(0).?);
2432 try std.testing.expectEqual(SourceRange{ .start = 22, .end = 24 }, map.clusterSourceRange(1).?);
2433 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(21).?);
2434 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(22).?);
2435 try std.testing.expectEqual(@as(?usize, null), map.byteOffsetToCluster(24));
2436 }
2437
2438 test "shapeRun maps utf16 variation sequences through one cluster" {
2439 const bytes = try test_font.createWithVariationSequences(std.testing.allocator);
2440 defer std.testing.allocator.free(bytes);
2441
2442 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2443 defer shaper.deinit();
2444 shaper.setPixelHeightScale(20);
2445
2446 var ctx = Context.init(std.testing.allocator, .{});
2447 defer ctx.deinit();
2448
2449 var output = try Output.init(std.testing.allocator, .{
2450 .max_glyphs = 256,
2451 .max_ligature_carets = 256,
2452 });
2453 defer output.deinit(std.testing.allocator);
2454
2455 const text = [_]u16{ 'A', 0xfe0f, '!' };
2456 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf16 = &text } }, &output);
2457 const run = output.run();
2458
2459 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
2460 try std.testing.expectEqual(@as(u32, 96), run.glyphs[0].glyph_id);
2461 try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].source_start);
2462 try std.testing.expectEqual(@as(u32, 4), run.glyphs[0].source_end);
2463 try std.testing.expectEqual(@as(u16, 2), run.glyphs[0].source_codepoint_count);
2464 try std.testing.expectEqual(@as(u32, 4), run.glyphs[1].source_start);
2465 }
2466
2467 test "shapeRun decodes utf16 surrogate pairs as one source cluster" {
2468 const bytes = try test_font.create(std.testing.allocator);
2469 defer std.testing.allocator.free(bytes);
2470
2471 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2472 defer shaper.deinit();
2473 shaper.setPixelHeightScale(20);
2474
2475 var ctx = Context.init(std.testing.allocator, .{});
2476 defer ctx.deinit();
2477
2478 var output = try Output.init(std.testing.allocator, .{
2479 .max_glyphs = 256,
2480 .max_ligature_carets = 256,
2481 });
2482 defer output.deinit(std.testing.allocator);
2483
2484 const text = [_]u16{ 0xd83d, 0xde00, '!' };
2485 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf16 = &text } }, &output);
2486 const run = output.run();
2487 const map = run.clusterMap();
2488
2489 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
2490 try std.testing.expect(run.glyphs[0].flags.missing_glyph);
2491 try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].source_start);
2492 try std.testing.expectEqual(@as(u32, 4), run.glyphs[0].source_end);
2493 try std.testing.expectEqual(@as(u32, 4), run.glyphs[1].source_start);
2494 try std.testing.expectEqual(@as(u32, 6), run.glyphs[1].source_end);
2495 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(0).?);
2496 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(3).?);
2497 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(4).?);
2498 try std.testing.expectEqual(@as(?usize, null), map.byteOffsetToCluster(6));
2499 try std.testing.expectEqual(SourceRange{ .start = 0, .end = 4 }, map.clusterSourceRange(0).?);
2500 try std.testing.expectEqual(SourceRange{ .start = 4, .end = 6 }, map.clusterSourceRange(1).?);
2501 }
2502
2503 test "shapeRun rejects malformed utf16" {
2504 const bytes = try test_font.create(std.testing.allocator);
2505 defer std.testing.allocator.free(bytes);
2506
2507 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2508 defer shaper.deinit();
2509
2510 var ctx = Context.init(std.testing.allocator, .{});
2511 defer ctx.deinit();
2512
2513 var output = try Output.init(std.testing.allocator, .{
2514 .max_glyphs = 256,
2515 .max_ligature_carets = 256,
2516 });
2517 defer output.deinit(std.testing.allocator);
2518
2519 const high = [_]u16{0xd83d};
2520 try std.testing.expectError(error.InvalidUtf16, ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf16 = &high } }, &output));
2521 try std.testing.expectEqual(@as(usize, 0), output.run().glyphs.len);
2522
2523 const low = [_]u16{0xde00};
2524 try std.testing.expectError(error.InvalidUtf16, ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf16 = &low } }, &output));
2525 try std.testing.expectEqual(@as(usize, 0), output.run().glyphs.len);
2526 }
2527
2528 test "shapeRun maps utf32 to nominal glyphs and byte clusters" {
2529 const bytes = try test_font.createWithKern(std.testing.allocator);
2530 defer std.testing.allocator.free(bytes);
2531
2532 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2533 defer shaper.deinit();
2534 shaper.setPixelHeightScale(20);
2535
2536 var ctx = Context.init(std.testing.allocator, .{});
2537 defer ctx.deinit();
2538
2539 var output = try Output.init(std.testing.allocator, .{
2540 .max_glyphs = 256,
2541 .max_ligature_carets = 256,
2542 });
2543 defer output.deinit(std.testing.allocator);
2544
2545 const text = [_]u32{ 'A', 'V' };
2546 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf32 = &text } }, &output);
2547 const run = output.run();
2548 const map = run.clusterMap();
2549
2550 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
2551 try std.testing.expectEqual(@as(u32, 'A' - 31), run.glyphs[0].glyph_id);
2552 try std.testing.expectEqual(@as(u32, 'V' - 31), run.glyphs[1].glyph_id);
2553 try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].source_start);
2554 try std.testing.expectEqual(@as(u32, 4), run.glyphs[0].source_end);
2555 try std.testing.expectEqual(@as(u32, 4), run.glyphs[1].source_start);
2556 try std.testing.expectEqual(@as(u32, 8), run.glyphs[1].source_end);
2557 try std.testing.expectEqual(@as(i32, 538), run.glyphs[0].x_advance);
2558 try std.testing.expectEqual(@as(i32, 640), run.glyphs[1].x_advance);
2559 try std.testing.expectEqual(@as(i32, 1178), run.total_x_advance);
2560 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(0).?);
2561 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(3).?);
2562 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(4).?);
2563 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(7).?);
2564 try std.testing.expectEqual(@as(?usize, null), map.byteOffsetToCluster(8));
2565 try std.testing.expectEqual(SourceRange{ .start = 0, .end = 4 }, map.clusterSourceRange(0).?);
2566 try std.testing.expectEqual(SourceRange{ .start = 4, .end = 8 }, map.clusterSourceRange(1).?);
2567 }
2568
2569 test "shapeRun maps utf32 clusters from source byte offsets" {
2570 const bytes = try test_font.create(std.testing.allocator);
2571 defer std.testing.allocator.free(bytes);
2572
2573 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2574 defer shaper.deinit();
2575
2576 var ctx = Context.init(std.testing.allocator, .{});
2577 defer ctx.deinit();
2578
2579 var output = try Output.init(std.testing.allocator, .{
2580 .max_glyphs = 256,
2581 .max_ligature_carets = 256,
2582 });
2583 defer output.deinit(std.testing.allocator);
2584
2585 const text = [_]u32{ 'A', 'B' };
2586 try ctx.shapeRun(.{
2587 .font = &shaper,
2588 .text = .{ .utf32 = &text },
2589 .source_offset = 30,
2590 }, &output);
2591 const map = output.run().clusterMap();
2592
2593 try std.testing.expectEqual(SourceRange{ .start = 30, .end = 34 }, map.clusterSourceRange(0).?);
2594 try std.testing.expectEqual(SourceRange{ .start = 34, .end = 38 }, map.clusterSourceRange(1).?);
2595 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(33).?);
2596 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(34).?);
2597 try std.testing.expectEqual(@as(?usize, null), map.byteOffsetToCluster(38));
2598 }
2599
2600 test "shapeRun maps utf32 variation sequences through one cluster" {
2601 const bytes = try test_font.createWithVariationSequences(std.testing.allocator);
2602 defer std.testing.allocator.free(bytes);
2603
2604 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2605 defer shaper.deinit();
2606 shaper.setPixelHeightScale(20);
2607
2608 var ctx = Context.init(std.testing.allocator, .{});
2609 defer ctx.deinit();
2610
2611 var output = try Output.init(std.testing.allocator, .{
2612 .max_glyphs = 256,
2613 .max_ligature_carets = 256,
2614 });
2615 defer output.deinit(std.testing.allocator);
2616
2617 const text = [_]u32{ 'A', 0xfe0f, '!' };
2618 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf32 = &text } }, &output);
2619 const run = output.run();
2620
2621 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
2622 try std.testing.expectEqual(@as(u32, 96), run.glyphs[0].glyph_id);
2623 try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].source_start);
2624 try std.testing.expectEqual(@as(u32, 8), run.glyphs[0].source_end);
2625 try std.testing.expectEqual(@as(u16, 2), run.glyphs[0].source_codepoint_count);
2626 try std.testing.expectEqual(@as(u32, 8), run.glyphs[1].source_start);
2627 }
2628
2629 test "shapeRun applies GSUB required single substitutions" {
2630 const bytes = try test_font.createWithGsubSingleSubstitution(std.testing.allocator);
2631 defer std.testing.allocator.free(bytes);
2632
2633 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2634 defer shaper.deinit();
2635 shaper.setPixelHeightScale(20);
2636
2637 var ctx = Context.init(std.testing.allocator, .{});
2638 defer ctx.deinit();
2639
2640 var output = try Output.init(std.testing.allocator, .{
2641 .max_glyphs = 256,
2642 .max_ligature_carets = 256,
2643 });
2644 defer output.deinit(std.testing.allocator);
2645
2646 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AB" } }, &output);
2647 const run = output.run();
2648
2649 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
2650 try std.testing.expectEqual(@as(u32, 'Z' - 31), run.glyphs[0].glyph_id);
2651 try std.testing.expectEqual(@as(u32, 'B' - 31), run.glyphs[1].glyph_id);
2652 try std.testing.expect(!run.glyphs[0].flags.missing_glyph);
2653 try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].source_start);
2654 try std.testing.expectEqual(@as(u32, 1), run.glyphs[0].source_end);
2655 }
2656
2657 test "shapeRun applies GSUB single substitutions with mark filters" {
2658 const bytes = try test_font.createWithGsubSingleIgnoreMarks(std.testing.allocator);
2659 defer std.testing.allocator.free(bytes);
2660
2661 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2662 defer shaper.deinit();
2663 shaper.setPixelHeightScale(20);
2664
2665 var ctx = Context.init(std.testing.allocator, .{});
2666 defer ctx.deinit();
2667
2668 var output = try Output.init(std.testing.allocator, .{
2669 .max_glyphs = 256,
2670 .max_ligature_carets = 256,
2671 });
2672 defer output.deinit(std.testing.allocator);
2673
2674 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A^B" } }, &output);
2675 const run = output.run();
2676
2677 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
2678 try std.testing.expectEqual(@as(u32, 'Z' - 31), run.glyphs[0].glyph_id);
2679 try std.testing.expectEqual(@as(u32, '^' - 31), run.glyphs[1].glyph_id);
2680 try std.testing.expectEqual(@as(u32, 'B' - 31), run.glyphs[2].glyph_id);
2681 try std.testing.expectEqual(GlyphClass.mark, run.glyphs[1].glyph_class);
2682 try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].source_start);
2683 try std.testing.expectEqual(@as(u32, 1), run.glyphs[0].source_end);
2684 }
2685
2686 test "shapeRun applies GSUB alternate substitutions" {
2687 const bytes = try test_font.createWithGsubAlternateSubstitution(std.testing.allocator);
2688 defer std.testing.allocator.free(bytes);
2689
2690 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2691 defer shaper.deinit();
2692 shaper.setPixelHeightScale(20);
2693
2694 var ctx = Context.init(std.testing.allocator, .{});
2695 defer ctx.deinit();
2696
2697 var output = try Output.init(std.testing.allocator, .{
2698 .max_glyphs = 256,
2699 .max_ligature_carets = 256,
2700 });
2701 defer output.deinit(std.testing.allocator);
2702
2703 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A!" } }, &output);
2704 const run = output.run();
2705
2706 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
2707 try std.testing.expectEqual(@as(u32, 96), run.glyphs[0].glyph_id);
2708 try std.testing.expectEqual(@as(u32, '!' - 31), run.glyphs[1].glyph_id);
2709 try std.testing.expect(!run.glyphs[0].flags.missing_glyph);
2710 try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].source_start);
2711 try std.testing.expectEqual(@as(u32, 1), run.glyphs[0].source_end);
2712 try std.testing.expectEqual(@as(i32, 1280), run.total_x_advance);
2713 }
2714
2715 test "shapeRun applies GSUB contextual substitutions" {
2716 const bytes = try test_font.createWithGsubContextualSubstitution(std.testing.allocator);
2717 defer std.testing.allocator.free(bytes);
2718
2719 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2720 defer shaper.deinit();
2721 shaper.setPixelHeightScale(20);
2722
2723 var ctx = Context.init(std.testing.allocator, .{});
2724 defer ctx.deinit();
2725
2726 var output = try Output.init(std.testing.allocator, .{
2727 .max_glyphs = 256,
2728 .max_ligature_carets = 256,
2729 });
2730 defer output.deinit(std.testing.allocator);
2731
2732 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ABC!" } }, &output);
2733 const contextual = output.run();
2734
2735 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
2736 try std.testing.expectEqual(@as(u32, 'A' - 31), contextual.glyphs[0].glyph_id);
2737 try std.testing.expectEqual(@as(u32, 'Z' - 31), contextual.glyphs[1].glyph_id);
2738 try std.testing.expectEqual(@as(u32, 'C' - 31), contextual.glyphs[2].glyph_id);
2739 try std.testing.expectEqual(@as(u32, '!' - 31), contextual.glyphs[3].glyph_id);
2740 try std.testing.expectEqual(@as(u32, 1), contextual.glyphs[1].source_start);
2741 try std.testing.expectEqual(@as(u32, 2), contextual.glyphs[1].source_end);
2742 try std.testing.expectEqual(@as(i32, 2560), contextual.total_x_advance);
2743
2744 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AB!" } }, &output);
2745 const mismatch = output.run();
2746
2747 try std.testing.expectEqual(@as(usize, 3), mismatch.glyphs.len);
2748 try std.testing.expectEqual(@as(u32, 'A' - 31), mismatch.glyphs[0].glyph_id);
2749 try std.testing.expectEqual(@as(u32, 'B' - 31), mismatch.glyphs[1].glyph_id);
2750 try std.testing.expectEqual(@as(u32, '!' - 31), mismatch.glyphs[2].glyph_id);
2751 }
2752
2753 test "shapeRun applies GSUB class contextual substitutions" {
2754 const bytes = try test_font.createWithGsubClassContextualSubstitution(std.testing.allocator);
2755 defer std.testing.allocator.free(bytes);
2756
2757 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2758 defer shaper.deinit();
2759 shaper.setPixelHeightScale(20);
2760
2761 var ctx = Context.init(std.testing.allocator, .{});
2762 defer ctx.deinit();
2763
2764 var output = try Output.init(std.testing.allocator, .{
2765 .max_glyphs = 256,
2766 .max_ligature_carets = 256,
2767 });
2768 defer output.deinit(std.testing.allocator);
2769
2770 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ABC!" } }, &output);
2771 const contextual = output.run();
2772
2773 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
2774 try std.testing.expectEqual(@as(u32, 'A' - 31), contextual.glyphs[0].glyph_id);
2775 try std.testing.expectEqual(@as(u32, 'Z' - 31), contextual.glyphs[1].glyph_id);
2776 try std.testing.expectEqual(@as(u32, 'C' - 31), contextual.glyphs[2].glyph_id);
2777 try std.testing.expectEqual(@as(u32, '!' - 31), contextual.glyphs[3].glyph_id);
2778 try std.testing.expectEqual(@as(u32, 1), contextual.glyphs[1].source_start);
2779 try std.testing.expectEqual(@as(u32, 2), contextual.glyphs[1].source_end);
2780 try std.testing.expectEqual(@as(i32, 2560), contextual.total_x_advance);
2781
2782 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AB!" } }, &output);
2783 const mismatch = output.run();
2784
2785 try std.testing.expectEqual(@as(usize, 3), mismatch.glyphs.len);
2786 try std.testing.expectEqual(@as(u32, 'A' - 31), mismatch.glyphs[0].glyph_id);
2787 try std.testing.expectEqual(@as(u32, 'B' - 31), mismatch.glyphs[1].glyph_id);
2788 try std.testing.expectEqual(@as(u32, '!' - 31), mismatch.glyphs[2].glyph_id);
2789 }
2790
2791 test "shapeRun applies GSUB coverage contextual substitutions" {
2792 const bytes = try test_font.createWithGsubCoverageContextualSubstitution(std.testing.allocator);
2793 defer std.testing.allocator.free(bytes);
2794
2795 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2796 defer shaper.deinit();
2797 shaper.setPixelHeightScale(20);
2798
2799 var ctx = Context.init(std.testing.allocator, .{});
2800 defer ctx.deinit();
2801
2802 var output = try Output.init(std.testing.allocator, .{
2803 .max_glyphs = 256,
2804 .max_ligature_carets = 256,
2805 });
2806 defer output.deinit(std.testing.allocator);
2807
2808 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ABC!" } }, &output);
2809 const contextual = output.run();
2810
2811 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
2812 try std.testing.expectEqual(@as(u32, 'A' - 31), contextual.glyphs[0].glyph_id);
2813 try std.testing.expectEqual(@as(u32, 'Z' - 31), contextual.glyphs[1].glyph_id);
2814 try std.testing.expectEqual(@as(u32, 'C' - 31), contextual.glyphs[2].glyph_id);
2815 try std.testing.expectEqual(@as(u32, '!' - 31), contextual.glyphs[3].glyph_id);
2816 try std.testing.expectEqual(@as(u32, 1), contextual.glyphs[1].source_start);
2817 try std.testing.expectEqual(@as(u32, 2), contextual.glyphs[1].source_end);
2818 try std.testing.expectEqual(@as(i32, 2560), contextual.total_x_advance);
2819
2820 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AB!" } }, &output);
2821 const mismatch = output.run();
2822
2823 try std.testing.expectEqual(@as(usize, 3), mismatch.glyphs.len);
2824 try std.testing.expectEqual(@as(u32, 'A' - 31), mismatch.glyphs[0].glyph_id);
2825 try std.testing.expectEqual(@as(u32, 'B' - 31), mismatch.glyphs[1].glyph_id);
2826 try std.testing.expectEqual(@as(u32, '!' - 31), mismatch.glyphs[2].glyph_id);
2827 }
2828
2829 test "shapeRun applies GSUB chained contextual substitutions" {
2830 const bytes = try test_font.createWithGsubChainedContextualSubstitution(std.testing.allocator);
2831 defer std.testing.allocator.free(bytes);
2832
2833 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2834 defer shaper.deinit();
2835 shaper.setPixelHeightScale(20);
2836
2837 var ctx = Context.init(std.testing.allocator, .{});
2838 defer ctx.deinit();
2839
2840 var output = try Output.init(std.testing.allocator, .{
2841 .max_glyphs = 256,
2842 .max_ligature_carets = 256,
2843 });
2844 defer output.deinit(std.testing.allocator);
2845
2846 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ABC!" } }, &output);
2847 const contextual = output.run();
2848
2849 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
2850 try std.testing.expectEqual(@as(u32, 'A' - 31), contextual.glyphs[0].glyph_id);
2851 try std.testing.expectEqual(@as(u32, 'Z' - 31), contextual.glyphs[1].glyph_id);
2852 try std.testing.expectEqual(@as(u32, 'C' - 31), contextual.glyphs[2].glyph_id);
2853 try std.testing.expectEqual(@as(u32, '!' - 31), contextual.glyphs[3].glyph_id);
2854 try std.testing.expectEqual(@as(u32, 1), contextual.glyphs[1].source_start);
2855 try std.testing.expectEqual(@as(u32, 2), contextual.glyphs[1].source_end);
2856 try std.testing.expectEqual(@as(i32, 2560), contextual.total_x_advance);
2857
2858 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "!BC" } }, &output);
2859 const missing_backtrack = output.run();
2860
2861 try std.testing.expectEqual(@as(usize, 3), missing_backtrack.glyphs.len);
2862 try std.testing.expectEqual(@as(u32, '!' - 31), missing_backtrack.glyphs[0].glyph_id);
2863 try std.testing.expectEqual(@as(u32, 'B' - 31), missing_backtrack.glyphs[1].glyph_id);
2864 try std.testing.expectEqual(@as(u32, 'C' - 31), missing_backtrack.glyphs[2].glyph_id);
2865
2866 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AB!" } }, &output);
2867 const missing_lookahead = output.run();
2868
2869 try std.testing.expectEqual(@as(usize, 3), missing_lookahead.glyphs.len);
2870 try std.testing.expectEqual(@as(u32, 'A' - 31), missing_lookahead.glyphs[0].glyph_id);
2871 try std.testing.expectEqual(@as(u32, 'B' - 31), missing_lookahead.glyphs[1].glyph_id);
2872 try std.testing.expectEqual(@as(u32, '!' - 31), missing_lookahead.glyphs[2].glyph_id);
2873 }
2874
2875 test "shapeRun uses pre and post context for GSUB chained substitutions" {
2876 const bytes = try test_font.createWithGsubChainedContextualSubstitution(std.testing.allocator);
2877 defer std.testing.allocator.free(bytes);
2878
2879 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2880 defer shaper.deinit();
2881 shaper.setPixelHeightScale(20);
2882
2883 var ctx = Context.init(std.testing.allocator, .{});
2884 defer ctx.deinit();
2885
2886 var output = try Output.init(std.testing.allocator, .{
2887 .max_glyphs = 256,
2888 .max_ligature_carets = 256,
2889 });
2890 defer output.deinit(std.testing.allocator);
2891
2892 try ctx.shapeRun(.{
2893 .font = &shaper,
2894 .text = .{ .utf8 = "B" },
2895 .source_offset = 1,
2896 .pre_context = .{ .utf8 = "A" },
2897 .post_context = .{ .utf8 = "C!" },
2898 }, &output);
2899 const contextual = output.run();
2900 const map = contextual.clusterMap();
2901
2902 try std.testing.expectEqual(@as(usize, 1), contextual.glyphs.len);
2903 try std.testing.expectEqual(@as(u32, 'Z' - 31), contextual.glyphs[0].glyph_id);
2904 try std.testing.expectEqual(@as(u32, 1), contextual.glyphs[0].source_start);
2905 try std.testing.expectEqual(@as(u32, 2), contextual.glyphs[0].source_end);
2906 try std.testing.expectEqual(SourceRange{ .start = 1, .end = 2 }, map.clusterSourceRange(0).?);
2907 try std.testing.expectEqual(@as(?usize, null), map.byteOffsetToCluster(0));
2908 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(1).?);
2909 try std.testing.expect(contextual.glyphs[0].flags.unsafe_to_break);
2910 try std.testing.expect(contextual.glyphs[0].flags.unsafe_to_concat);
2911 try std.testing.expectEqual(true, map.breakRequiresReshaping(0).?);
2912
2913 try ctx.shapeRun(.{
2914 .font = &shaper,
2915 .text = .{ .utf8 = "B" },
2916 .source_offset = 1,
2917 .pre_context = .{ .utf8 = "A" },
2918 }, &output);
2919 const missing_lookahead = output.run();
2920
2921 try std.testing.expectEqual(@as(usize, 1), missing_lookahead.glyphs.len);
2922 try std.testing.expectEqual(@as(u32, 'B' - 31), missing_lookahead.glyphs[0].glyph_id);
2923 }
2924
2925 test "shapeRun applies GSUB chained simple contextual substitutions" {
2926 const bytes = try test_font.createWithGsubChainedSimpleContextualSubstitution(std.testing.allocator);
2927 defer std.testing.allocator.free(bytes);
2928
2929 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2930 defer shaper.deinit();
2931 shaper.setPixelHeightScale(20);
2932
2933 var ctx = Context.init(std.testing.allocator, .{});
2934 defer ctx.deinit();
2935
2936 var output = try Output.init(std.testing.allocator, .{
2937 .max_glyphs = 256,
2938 .max_ligature_carets = 256,
2939 });
2940 defer output.deinit(std.testing.allocator);
2941
2942 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ABC!" } }, &output);
2943 const contextual = output.run();
2944
2945 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
2946 try std.testing.expectEqual(@as(u32, 'A' - 31), contextual.glyphs[0].glyph_id);
2947 try std.testing.expectEqual(@as(u32, 'Z' - 31), contextual.glyphs[1].glyph_id);
2948 try std.testing.expectEqual(@as(u32, 'C' - 31), contextual.glyphs[2].glyph_id);
2949 try std.testing.expectEqual(@as(u32, '!' - 31), contextual.glyphs[3].glyph_id);
2950 try std.testing.expectEqual(@as(u32, 1), contextual.glyphs[1].source_start);
2951 try std.testing.expectEqual(@as(u32, 2), contextual.glyphs[1].source_end);
2952 try std.testing.expectEqual(@as(i32, 2560), contextual.total_x_advance);
2953
2954 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "!BC" } }, &output);
2955 const missing_backtrack = output.run();
2956
2957 try std.testing.expectEqual(@as(usize, 3), missing_backtrack.glyphs.len);
2958 try std.testing.expectEqual(@as(u32, '!' - 31), missing_backtrack.glyphs[0].glyph_id);
2959 try std.testing.expectEqual(@as(u32, 'B' - 31), missing_backtrack.glyphs[1].glyph_id);
2960 try std.testing.expectEqual(@as(u32, 'C' - 31), missing_backtrack.glyphs[2].glyph_id);
2961
2962 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AB!" } }, &output);
2963 const missing_lookahead = output.run();
2964
2965 try std.testing.expectEqual(@as(usize, 3), missing_lookahead.glyphs.len);
2966 try std.testing.expectEqual(@as(u32, 'A' - 31), missing_lookahead.glyphs[0].glyph_id);
2967 try std.testing.expectEqual(@as(u32, 'B' - 31), missing_lookahead.glyphs[1].glyph_id);
2968 try std.testing.expectEqual(@as(u32, '!' - 31), missing_lookahead.glyphs[2].glyph_id);
2969 }
2970
2971 test "shapeRun applies GSUB chained class contextual substitutions" {
2972 const bytes = try test_font.createWithGsubChainedClassContextualSubstitution(std.testing.allocator);
2973 defer std.testing.allocator.free(bytes);
2974
2975 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
2976 defer shaper.deinit();
2977 shaper.setPixelHeightScale(20);
2978
2979 var ctx = Context.init(std.testing.allocator, .{});
2980 defer ctx.deinit();
2981
2982 var output = try Output.init(std.testing.allocator, .{
2983 .max_glyphs = 256,
2984 .max_ligature_carets = 256,
2985 });
2986 defer output.deinit(std.testing.allocator);
2987
2988 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ABC!" } }, &output);
2989 const contextual = output.run();
2990
2991 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
2992 try std.testing.expectEqual(@as(u32, 'A' - 31), contextual.glyphs[0].glyph_id);
2993 try std.testing.expectEqual(@as(u32, 'Z' - 31), contextual.glyphs[1].glyph_id);
2994 try std.testing.expectEqual(@as(u32, 'C' - 31), contextual.glyphs[2].glyph_id);
2995 try std.testing.expectEqual(@as(u32, '!' - 31), contextual.glyphs[3].glyph_id);
2996 try std.testing.expectEqual(@as(u32, 1), contextual.glyphs[1].source_start);
2997 try std.testing.expectEqual(@as(u32, 2), contextual.glyphs[1].source_end);
2998 try std.testing.expectEqual(@as(i32, 2560), contextual.total_x_advance);
2999
3000 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "!BC" } }, &output);
3001 const missing_backtrack = output.run();
3002
3003 try std.testing.expectEqual(@as(usize, 3), missing_backtrack.glyphs.len);
3004 try std.testing.expectEqual(@as(u32, '!' - 31), missing_backtrack.glyphs[0].glyph_id);
3005 try std.testing.expectEqual(@as(u32, 'B' - 31), missing_backtrack.glyphs[1].glyph_id);
3006 try std.testing.expectEqual(@as(u32, 'C' - 31), missing_backtrack.glyphs[2].glyph_id);
3007
3008 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AB!" } }, &output);
3009 const missing_lookahead = output.run();
3010
3011 try std.testing.expectEqual(@as(usize, 3), missing_lookahead.glyphs.len);
3012 try std.testing.expectEqual(@as(u32, 'A' - 31), missing_lookahead.glyphs[0].glyph_id);
3013 try std.testing.expectEqual(@as(u32, 'B' - 31), missing_lookahead.glyphs[1].glyph_id);
3014 try std.testing.expectEqual(@as(u32, '!' - 31), missing_lookahead.glyphs[2].glyph_id);
3015 }
3016
3017 test "shapeRun applies GSUB default ligature substitutions" {
3018 const bytes = try test_font.createWithGsubLigature(std.testing.allocator);
3019 defer std.testing.allocator.free(bytes);
3020
3021 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3022 defer shaper.deinit();
3023 shaper.setPixelHeightScale(20);
3024
3025 var ctx = Context.init(std.testing.allocator, .{});
3026 defer ctx.deinit();
3027
3028 var output = try Output.init(std.testing.allocator, .{
3029 .max_glyphs = 256,
3030 .max_ligature_carets = 256,
3031 });
3032 defer output.deinit(std.testing.allocator);
3033
3034 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "fi!" } }, &output);
3035 const run = output.run();
3036 const map = run.clusterMap();
3037
3038 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
3039 try std.testing.expectEqual(@as(usize, 2), run.clusters.len);
3040 try std.testing.expectEqual(@as(u32, 96), run.glyphs[0].glyph_id);
3041 try std.testing.expectEqual(@as(u32, '!' - 31), run.glyphs[1].glyph_id);
3042 try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].source_start);
3043 try std.testing.expectEqual(@as(u32, 2), run.glyphs[0].source_end);
3044 try std.testing.expect(!run.glyphs[0].flags.unsafe_to_break);
3045 try std.testing.expect(run.glyphs[0].flags.unsafe_to_concat);
3046 try std.testing.expectEqual(GlyphClass.ligature, run.glyphs[0].glyph_class);
3047 try std.testing.expectEqual(@as(usize, 1), run.ligature_carets.len);
3048 try std.testing.expectEqual(@as(u16, 1), run.glyphs[0].ligature_caret_count);
3049 try std.testing.expectEqual(SourceRange{ .start = 0, .end = 2 }, map.clusterSourceRange(0).?);
3050 try std.testing.expectEqual(GlyphSpan{ .start = 0, .end = 1 }, map.clusterGlyphSpan(0).?);
3051 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(1).?);
3052 try std.testing.expectEqual(@as(usize, 3), map.clusterCaretStopCount(0).?);
3053 try std.testing.expectEqual(@as(u32, 1), map.clusterCaretStop(0, 1).?);
3054 const caret = map.clusterLigatureCaret(0, 0).?;
3055 try std.testing.expectEqual(@as(i32, 320), caret.x_offset);
3056 try std.testing.expect(caret.synthesized);
3057 try std.testing.expectEqual(true, map.selectableAsUnitOnly(0).?);
3058 try std.testing.expectEqual(false, map.breakRequiresReshaping(0).?);
3059 try std.testing.expectEqual(@as(i32, 1280), run.total_x_advance);
3060 }
3061
3062 test "shapeRun leaves standard ligatures off in vertical writing" {
3063 const bytes = try test_font.createWithGsubLigature(std.testing.allocator);
3064 defer std.testing.allocator.free(bytes);
3065
3066 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3067 defer shaper.deinit();
3068 shaper.setPixelHeightScale(20);
3069
3070 var ctx = Context.init(std.testing.allocator, .{});
3071 defer ctx.deinit();
3072
3073 var output = try Output.init(std.testing.allocator, .{
3074 .max_glyphs = 256,
3075 .max_ligature_carets = 256,
3076 });
3077 defer output.deinit(std.testing.allocator);
3078
3079 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "fi" }, .writing_mode = .vertical }, &output);
3080 const run = output.run();
3081
3082 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
3083 try std.testing.expectEqual(@as(u32, 'f' - 31), run.glyphs[0].glyph_id);
3084 try std.testing.expectEqual(@as(u32, 'i' - 31), run.glyphs[1].glyph_id);
3085 }
3086
3087 test "shapeRun applies GSUB localized forms by default" {
3088 const bytes = try test_font.createWithGsubLocalizedForms(std.testing.allocator);
3089 defer std.testing.allocator.free(bytes);
3090
3091 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3092 defer shaper.deinit();
3093 shaper.setPixelHeightScale(20);
3094
3095 var ctx = Context.init(std.testing.allocator, .{});
3096 defer ctx.deinit();
3097
3098 var output = try Output.init(std.testing.allocator, .{
3099 .max_glyphs = 256,
3100 .max_ligature_carets = 256,
3101 });
3102 defer output.deinit(std.testing.allocator);
3103
3104 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A" } }, &output);
3105 try std.testing.expectEqual(@as(usize, 1), output.run().glyphs.len);
3106 try std.testing.expectEqual(@as(u32, 'Z' - 31), output.run().glyphs[0].glyph_id);
3107
3108 try ctx.shapeRun(.{
3109 .font = &shaper,
3110 .text = .{ .utf8 = "A" },
3111 .features = &.{.{ .tag = font.tag("locl"), .value = 0 }},
3112 }, &output);
3113 try std.testing.expectEqual(@as(u32, 'A' - 31), output.run().glyphs[0].glyph_id);
3114 }
3115
3116 test "shapeRun applies GSUB horizontal contextual defaults" {
3117 const clig_bytes = try test_font.createWithGsubContextualLigature(std.testing.allocator);
3118 defer std.testing.allocator.free(clig_bytes);
3119 const calt_bytes = try test_font.createWithGsubContextualAlternates(std.testing.allocator);
3120 defer std.testing.allocator.free(calt_bytes);
3121 const rclt_bytes = try test_font.createWithGsubRequiredContextualAlternates(std.testing.allocator);
3122 defer std.testing.allocator.free(rclt_bytes);
3123
3124 var clig_shaper = Font.initFromBytes(clig_bytes.ptr, clig_bytes.len) orelse return error.TestUnexpectedResult;
3125 defer clig_shaper.deinit();
3126 var calt_shaper = Font.initFromBytes(calt_bytes.ptr, calt_bytes.len) orelse return error.TestUnexpectedResult;
3127 defer calt_shaper.deinit();
3128 var rclt_shaper = Font.initFromBytes(rclt_bytes.ptr, rclt_bytes.len) orelse return error.TestUnexpectedResult;
3129 defer rclt_shaper.deinit();
3130 clig_shaper.setPixelHeightScale(20);
3131 calt_shaper.setPixelHeightScale(20);
3132 rclt_shaper.setPixelHeightScale(20);
3133
3134 var ctx = Context.init(std.testing.allocator, .{});
3135 defer ctx.deinit();
3136
3137 var output = try Output.init(std.testing.allocator, .{
3138 .max_glyphs = 256,
3139 .max_ligature_carets = 256,
3140 });
3141 defer output.deinit(std.testing.allocator);
3142
3143 try ctx.shapeRun(.{ .font = &clig_shaper, .text = .{ .utf8 = "fi" } }, &output);
3144 try std.testing.expectEqual(@as(usize, 1), output.run().glyphs.len);
3145 try std.testing.expectEqual(@as(u32, 96), output.run().glyphs[0].glyph_id);
3146
3147 try ctx.shapeRun(.{ .font = &calt_shaper, .text = .{ .utf8 = "ABC" } }, &output);
3148 try std.testing.expectEqual(@as(usize, 3), output.run().glyphs.len);
3149 try std.testing.expectEqual(@as(u32, 'Z' - 31), output.run().glyphs[1].glyph_id);
3150
3151 try ctx.shapeRun(.{ .font = &rclt_shaper, .text = .{ .utf8 = "ABC" } }, &output);
3152 try std.testing.expectEqual(@as(usize, 3), output.run().glyphs.len);
3153 try std.testing.expectEqual(@as(u32, 'Z' - 31), output.run().glyphs[1].glyph_id);
3154
3155 try ctx.shapeRun(.{
3156 .font = &rclt_shaper,
3157 .text = .{ .utf8 = "ABC" },
3158 .features = &.{.{ .tag = font.tag("rclt"), .value = 0 }},
3159 }, &output);
3160 try std.testing.expectEqual(@as(usize, 3), output.run().glyphs.len);
3161 try std.testing.expectEqual(@as(u32, 'Z' - 31), output.run().glyphs[1].glyph_id);
3162 }
3163
3164 test "shapeRun applies GSUB direction alternates by resolved direction" {
3165 const ltra_bytes = try test_font.createWithGsubLeftToRightAlternates(std.testing.allocator);
3166 defer std.testing.allocator.free(ltra_bytes);
3167 const rtla_bytes = try test_font.createWithGsubRightToLeftAlternates(std.testing.allocator);
3168 defer std.testing.allocator.free(rtla_bytes);
3169
3170 var ltra_shaper = Font.initFromBytes(ltra_bytes.ptr, ltra_bytes.len) orelse return error.TestUnexpectedResult;
3171 defer ltra_shaper.deinit();
3172 var rtla_shaper = Font.initFromBytes(rtla_bytes.ptr, rtla_bytes.len) orelse return error.TestUnexpectedResult;
3173 defer rtla_shaper.deinit();
3174 ltra_shaper.setPixelHeightScale(20);
3175 rtla_shaper.setPixelHeightScale(20);
3176
3177 var ctx = Context.init(std.testing.allocator, .{});
3178 defer ctx.deinit();
3179
3180 var output = try Output.init(std.testing.allocator, .{
3181 .max_glyphs = 256,
3182 .max_ligature_carets = 256,
3183 });
3184 defer output.deinit(std.testing.allocator);
3185
3186 try ctx.shapeRun(.{ .font = <ra_shaper, .text = .{ .utf8 = "A" } }, &output);
3187 try std.testing.expectEqual(@as(usize, 1), output.run().glyphs.len);
3188 try std.testing.expectEqual(@as(u32, 'Z' - 31), output.run().glyphs[0].glyph_id);
3189
3190 try ctx.shapeRun(.{ .font = <ra_shaper, .text = .{ .utf8 = "A" }, .direction = .rtl }, &output);
3191 try std.testing.expectEqual(@as(usize, 1), output.run().glyphs.len);
3192 try std.testing.expectEqual(@as(u32, 'A' - 31), output.run().glyphs[0].glyph_id);
3193
3194 try ctx.shapeRun(.{ .font = &rtla_shaper, .text = .{ .utf8 = "A" } }, &output);
3195 try std.testing.expectEqual(@as(usize, 1), output.run().glyphs.len);
3196 try std.testing.expectEqual(@as(u32, 'A' - 31), output.run().glyphs[0].glyph_id);
3197
3198 try ctx.shapeRun(.{ .font = &rtla_shaper, .text = .{ .utf8 = "A" }, .direction = .rtl }, &output);
3199 try std.testing.expectEqual(@as(usize, 1), output.run().glyphs.len);
3200 try std.testing.expectEqual(@as(u32, 'Z' - 31), output.run().glyphs[0].glyph_id);
3201 }
3202
3203 test "shapeRun applies automatic GSUB fraction ranges" {
3204 const frac_bytes = try test_font.createWithGsubFractions(std.testing.allocator);
3205 defer std.testing.allocator.free(frac_bytes);
3206 const component_bytes = try test_font.createWithGsubFractionComponents(std.testing.allocator);
3207 defer std.testing.allocator.free(component_bytes);
3208
3209 var frac_shaper = Font.initFromBytes(frac_bytes.ptr, frac_bytes.len) orelse return error.TestUnexpectedResult;
3210 defer frac_shaper.deinit();
3211 var component_shaper = Font.initFromBytes(component_bytes.ptr, component_bytes.len) orelse return error.TestUnexpectedResult;
3212 defer component_shaper.deinit();
3213 frac_shaper.setPixelHeightScale(20);
3214 component_shaper.setPixelHeightScale(20);
3215
3216 var ctx = Context.init(std.testing.allocator, .{});
3217 defer ctx.deinit();
3218
3219 var output = try Output.init(std.testing.allocator, .{
3220 .max_glyphs = 256,
3221 .max_ligature_carets = 256,
3222 });
3223 defer output.deinit(std.testing.allocator);
3224
3225 try ctx.shapeRun(.{ .font = &frac_shaper, .text = .{ .utf8 = "1\u{2044}2" } }, &output);
3226 var run = output.run();
3227 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
3228 try std.testing.expectEqual(@as(u32, 'F' - 31), run.glyphs[0].glyph_id);
3229 try std.testing.expectEqual(@as(u32, 0), run.glyphs[1].glyph_id);
3230 try std.testing.expectEqual(@as(u32, '2' - 31), run.glyphs[2].glyph_id);
3231
3232 try ctx.shapeRun(.{ .font = &frac_shaper, .text = .{ .utf8 = "1/2" } }, &output);
3233 run = output.run();
3234 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
3235 try std.testing.expectEqual(@as(u32, '1' - 31), run.glyphs[0].glyph_id);
3236
3237 try ctx.shapeRun(.{ .font = &frac_shaper, .text = .{ .utf8 = "1\u{2044}A" } }, &output);
3238 run = output.run();
3239 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
3240 try std.testing.expectEqual(@as(u32, '1' - 31), run.glyphs[0].glyph_id);
3241
3242 try ctx.shapeRun(.{
3243 .font = &frac_shaper,
3244 .text = .{ .utf8 = "1\u{2044}2" },
3245 .features = &.{.{ .tag = font.tag("frac"), .value = 0 }},
3246 }, &output);
3247 run = output.run();
3248 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
3249 try std.testing.expectEqual(@as(u32, '1' - 31), run.glyphs[0].glyph_id);
3250
3251 try ctx.shapeRun(.{ .font = &component_shaper, .text = .{ .utf8 = "1\u{2044}2" } }, &output);
3252 run = output.run();
3253 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
3254 try std.testing.expectEqual(@as(u32, 'N' - 31), run.glyphs[0].glyph_id);
3255 try std.testing.expectEqual(@as(u32, 'D' - 31), run.glyphs[2].glyph_id);
3256
3257 try ctx.shapeRun(.{
3258 .font = &component_shaper,
3259 .text = .{ .utf8 = "2\u{2044}1" },
3260 .direction = .rtl,
3261 .output_order = .logical,
3262 }, &output);
3263 run = output.run();
3264 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
3265 try std.testing.expectEqual(@as(u32, 'D' - 31), run.glyphs[0].glyph_id);
3266 try std.testing.expectEqual(@as(u32, 'N' - 31), run.glyphs[2].glyph_id);
3267 }
3268
3269 test "shapeRun applies automatic Arabic joining form ranges" {
3270 const bytes = try test_font.createWithGsubArabicJoiningForms(std.testing.allocator);
3271 defer std.testing.allocator.free(bytes);
3272
3273 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3274 defer shaper.deinit();
3275 shaper.setPixelHeightScale(20);
3276
3277 var ctx = Context.init(std.testing.allocator, .{});
3278 defer ctx.deinit();
3279
3280 var output = try Output.init(std.testing.allocator, .{
3281 .max_glyphs = 256,
3282 .max_ligature_carets = 256,
3283 });
3284 defer output.deinit(std.testing.allocator);
3285
3286 try ctx.shapeRun(.{
3287 .font = &shaper,
3288 .text = .{ .utf8 = "\u{0628}" },
3289 .direction = .rtl,
3290 .output_order = .logical,
3291 }, &output);
3292 var run = output.run();
3293 try std.testing.expectEqual(@as(usize, 1), run.glyphs.len);
3294 try std.testing.expectEqual(@as(u32, 'O' - 31), run.glyphs[0].glyph_id);
3295 try std.testing.expect(!run.glyphs[0].flags.safe_to_insert_tatweel);
3296
3297 try ctx.shapeRun(.{
3298 .font = &shaper,
3299 .text = .{ .utf8 = "\u{0628}\u{0628}\u{0628}" },
3300 .direction = .rtl,
3301 .output_order = .logical,
3302 }, &output);
3303 run = output.run();
3304 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
3305 try std.testing.expectEqual(@as(u32, 'I' - 31), run.glyphs[0].glyph_id);
3306 try std.testing.expectEqual(@as(u32, 'M' - 31), run.glyphs[1].glyph_id);
3307 try std.testing.expectEqual(@as(u32, 'F' - 31), run.glyphs[2].glyph_id);
3308 try std.testing.expect(!run.glyphs[0].flags.safe_to_insert_tatweel);
3309 try std.testing.expect(run.glyphs[1].flags.safe_to_insert_tatweel);
3310 try std.testing.expect(run.glyphs[2].flags.safe_to_insert_tatweel);
3311 try std.testing.expect(run.glyphs[1].flags.unsafe_to_break);
3312 try std.testing.expect(run.glyphs[2].flags.unsafe_to_concat);
3313 try std.testing.expect(!run.clusters[0].flags.safe_to_insert_tatweel);
3314 try std.testing.expect(run.clusters[1].flags.safe_to_insert_tatweel);
3315 try std.testing.expect(run.clusters[2].flags.safe_to_insert_tatweel);
3316
3317 try ctx.shapeRun(.{
3318 .font = &shaper,
3319 .text = .{ .utf8 = "\u{0628}\u{200c}\u{0628}" },
3320 .direction = .rtl,
3321 .output_order = .logical,
3322 }, &output);
3323 run = output.run();
3324 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
3325 try std.testing.expectEqual(@as(u32, 'O' - 31), run.glyphs[0].glyph_id);
3326 try std.testing.expectEqual(@as(u32, 0), run.glyphs[1].glyph_id);
3327 try std.testing.expect(run.glyphs[1].flags.default_ignorable);
3328 try std.testing.expectEqual(@as(u32, 'O' - 31), run.glyphs[2].glyph_id);
3329 try std.testing.expect(!run.glyphs[0].flags.safe_to_insert_tatweel);
3330 try std.testing.expect(!run.glyphs[1].flags.safe_to_insert_tatweel);
3331 try std.testing.expect(!run.glyphs[2].flags.safe_to_insert_tatweel);
3332
3333 try ctx.shapeRun(.{
3334 .font = &shaper,
3335 .text = .{ .utf8 = "\u{0628}" },
3336 .pre_context = .{ .utf8 = "\u{0628}" },
3337 .post_context = .{ .utf8 = "\u{0628}" },
3338 .direction = .rtl,
3339 .output_order = .logical,
3340 }, &output);
3341 run = output.run();
3342 try std.testing.expectEqual(@as(usize, 1), run.glyphs.len);
3343 try std.testing.expectEqual(@as(u32, 'M' - 31), run.glyphs[0].glyph_id);
3344 try std.testing.expect(run.glyphs[0].flags.safe_to_insert_tatweel);
3345 try std.testing.expect(run.glyphs[0].flags.unsafe_to_break);
3346 try std.testing.expect(run.glyphs[0].flags.unsafe_to_concat);
3347 try std.testing.expect(run.clusters[0].flags.safe_to_insert_tatweel);
3348
3349 try ctx.shapeRun(.{
3350 .font = &shaper,
3351 .text = .{ .utf8 = "\u{0628}\u{0627}" },
3352 .direction = .rtl,
3353 .output_order = .logical,
3354 }, &output);
3355 run = output.run();
3356 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
3357 try std.testing.expectEqual(@as(u32, 'I' - 31), run.glyphs[0].glyph_id);
3358 try std.testing.expectEqual(@as(u32, 'B' - 31), run.glyphs[1].glyph_id);
3359 try std.testing.expect(!run.glyphs[0].flags.safe_to_insert_tatweel);
3360 try std.testing.expect(run.glyphs[1].flags.safe_to_insert_tatweel);
3361
3362 try ctx.shapeRun(.{
3363 .font = &shaper,
3364 .text = .{ .utf8 = "\u{0628}\u{0628}\u{0628}" },
3365 .direction = .rtl,
3366 .output_order = .logical,
3367 .features = &.{.{ .tag = font.tag("medi"), .value = 0 }},
3368 }, &output);
3369 run = output.run();
3370 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
3371 try std.testing.expectEqual(@as(u32, 'I' - 31), run.glyphs[0].glyph_id);
3372 try std.testing.expectEqual(@as(u32, 'A' - 31), run.glyphs[1].glyph_id);
3373 try std.testing.expectEqual(@as(u32, 'F' - 31), run.glyphs[2].glyph_id);
3374 try std.testing.expect(!run.glyphs[0].flags.safe_to_insert_tatweel);
3375 try std.testing.expect(run.glyphs[1].flags.safe_to_insert_tatweel);
3376 try std.testing.expect(run.glyphs[2].flags.safe_to_insert_tatweel);
3377 }
3378
3379 test "shapeRun disables GSUB ligatures with feature settings" {
3380 const bytes = try test_font.createWithGsubLigature(std.testing.allocator);
3381 defer std.testing.allocator.free(bytes);
3382
3383 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3384 defer shaper.deinit();
3385 shaper.setPixelHeightScale(20);
3386
3387 var ctx = Context.init(std.testing.allocator, .{});
3388 defer ctx.deinit();
3389
3390 var output = try Output.init(std.testing.allocator, .{
3391 .max_glyphs = 256,
3392 .max_ligature_carets = 256,
3393 });
3394 defer output.deinit(std.testing.allocator);
3395
3396 try ctx.shapeRun(.{
3397 .font = &shaper,
3398 .text = .{ .utf8 = "fi!" },
3399 .features = &.{.{ .tag = font.tag("liga"), .value = 0 }},
3400 }, &output);
3401 const run = output.run();
3402
3403 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
3404 try std.testing.expectEqual(@as(u32, 'f' - 31), run.glyphs[0].glyph_id);
3405 try std.testing.expectEqual(@as(u32, 'i' - 31), run.glyphs[1].glyph_id);
3406 try std.testing.expectEqual(@as(u32, '!' - 31), run.glyphs[2].glyph_id);
3407 try std.testing.expectEqual(@as(i32, 1920), run.total_x_advance);
3408 }
3409
3410 test "shapeRun applies GSUB feature settings by source range" {
3411 const bytes = try test_font.createWithGsubLigature(std.testing.allocator);
3412 defer std.testing.allocator.free(bytes);
3413
3414 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3415 defer shaper.deinit();
3416 shaper.setPixelHeightScale(20);
3417
3418 var ctx = Context.init(std.testing.allocator, .{});
3419 defer ctx.deinit();
3420
3421 var output = try Output.init(std.testing.allocator, .{
3422 .max_glyphs = 256,
3423 .max_ligature_carets = 256,
3424 });
3425 defer output.deinit(std.testing.allocator);
3426
3427 try ctx.shapeRun(.{
3428 .font = &shaper,
3429 .text = .{ .utf8 = "fifi" },
3430 .features = &.{.{ .tag = font.tag("liga"), .value = 0, .source = .{ .start = 0, .end = 2 } }},
3431 }, &output);
3432 const run = output.run();
3433
3434 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
3435 try std.testing.expectEqual(@as(u32, 'f' - 31), run.glyphs[0].glyph_id);
3436 try std.testing.expectEqual(@as(u32, 'i' - 31), run.glyphs[1].glyph_id);
3437 try std.testing.expectEqual(@as(u32, 96), run.glyphs[2].glyph_id);
3438 try std.testing.expectEqual(@as(u32, 2), run.glyphs[2].source_start);
3439 try std.testing.expectEqual(@as(u32, 4), run.glyphs[2].source_end);
3440 try std.testing.expectEqual(@as(i32, 1920), run.total_x_advance);
3441 }
3442
3443 test "shapeRun selects GSUB lookups by script tag" {
3444 const bytes = try test_font.createWithGsubLigature(std.testing.allocator);
3445 defer std.testing.allocator.free(bytes);
3446 try fixture_binary.replaceLayoutScript(bytes, "GSUB", "latn");
3447
3448 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3449 defer shaper.deinit();
3450 shaper.setPixelHeightScale(20);
3451
3452 var ctx = Context.init(std.testing.allocator, .{});
3453 defer ctx.deinit();
3454
3455 var output = try Output.init(std.testing.allocator, .{
3456 .max_glyphs = 256,
3457 .max_ligature_carets = 256,
3458 });
3459 defer output.deinit(std.testing.allocator);
3460
3461 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "fi" } }, &output);
3462 try std.testing.expectEqual(@as(usize, 2), output.run().glyphs.len);
3463 try std.testing.expectEqual(@as(u32, 'f' - 31), output.run().glyphs[0].glyph_id);
3464
3465 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "fi" }, .script = font.tag("latn") }, &output);
3466 const run = output.run();
3467
3468 try std.testing.expectEqual(@as(usize, 1), run.glyphs.len);
3469 try std.testing.expectEqual(@as(u32, 96), run.glyphs[0].glyph_id);
3470 try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].source_start);
3471 try std.testing.expectEqual(@as(u32, 2), run.glyphs[0].source_end);
3472 }
3473
3474 test "shapeRun selects GSUB lookups by ordered script tags" {
3475 const bytes = try test_font.createWithGsubLigature(std.testing.allocator);
3476 defer std.testing.allocator.free(bytes);
3477 try fixture_binary.replaceLayoutScript(bytes, "GSUB", "dev2");
3478
3479 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3480 defer shaper.deinit();
3481 shaper.setPixelHeightScale(20);
3482
3483 var ctx = Context.init(std.testing.allocator, .{});
3484 defer ctx.deinit();
3485
3486 var output = try Output.init(std.testing.allocator, .{
3487 .max_glyphs = 256,
3488 .max_ligature_carets = 256,
3489 });
3490 defer output.deinit(std.testing.allocator);
3491
3492 const script_tags = unicode_data.scriptOpenTypeTags(.devanagari);
3493 try ctx.shapeRun(.{
3494 .font = &shaper,
3495 .text = .{ .utf8 = "fi" },
3496 .script = font.tag("deva"),
3497 .script_tags = script_tags.slice(),
3498 }, &output);
3499 const run = output.run();
3500
3501 try std.testing.expectEqual(@as(usize, 1), run.glyphs.len);
3502 try std.testing.expectEqual(@as(u32, 96), run.glyphs[0].glyph_id);
3503 }
3504
3505 test "shapeRun applies GSUB default multiple substitutions" {
3506 const bytes = try test_font.createWithGsubMultipleSubstitution(std.testing.allocator);
3507 defer std.testing.allocator.free(bytes);
3508
3509 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3510 defer shaper.deinit();
3511 shaper.setPixelHeightScale(20);
3512
3513 var ctx = Context.init(std.testing.allocator, .{});
3514 defer ctx.deinit();
3515
3516 var output = try Output.init(std.testing.allocator, .{
3517 .max_glyphs = 256,
3518 .max_ligature_carets = 256,
3519 });
3520 defer output.deinit(std.testing.allocator);
3521
3522 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A!" } }, &output);
3523 const run = output.run();
3524 const map = run.clusterMap();
3525
3526 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
3527 try std.testing.expectEqual(@as(usize, 2), run.clusters.len);
3528 try std.testing.expectEqual(@as(u32, 'X' - 31), run.glyphs[0].glyph_id);
3529 try std.testing.expectEqual(@as(u32, 'Y' - 31), run.glyphs[1].glyph_id);
3530 try std.testing.expectEqual(@as(u32, '!' - 31), run.glyphs[2].glyph_id);
3531 try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].source_start);
3532 try std.testing.expectEqual(@as(u32, 1), run.glyphs[0].source_end);
3533 try std.testing.expectEqual(@as(u32, 0), run.glyphs[1].source_start);
3534 try std.testing.expectEqual(@as(u32, 1), run.glyphs[1].source_end);
3535 try std.testing.expect(!run.glyphs[0].flags.unsafe_to_break);
3536 try std.testing.expect(run.glyphs[0].flags.unsafe_to_concat);
3537 try std.testing.expect(run.glyphs[1].flags.unsafe_to_concat);
3538 try std.testing.expectEqual(SourceRange{ .start = 0, .end = 1 }, map.clusterSourceRange(0).?);
3539 try std.testing.expectEqual(GlyphSpan{ .start = 0, .end = 2 }, map.clusterGlyphSpan(0).?);
3540 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(0).?);
3541 try std.testing.expectEqual(true, map.selectableAsUnitOnly(0).?);
3542 try std.testing.expectEqual(false, map.breakRequiresReshaping(0).?);
3543 try std.testing.expectEqual(@as(i32, 1920), run.total_x_advance);
3544 }
3545
3546 test "shapeRun applies GSUB reverse chaining substitutions from run end" {
3547 const bytes = try test_font.createWithGsubReverseChainingSingleSubstitution(std.testing.allocator);
3548 defer std.testing.allocator.free(bytes);
3549
3550 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3551 defer shaper.deinit();
3552 shaper.setPixelHeightScale(20);
3553
3554 var ctx = Context.init(std.testing.allocator, .{});
3555 defer ctx.deinit();
3556
3557 var output = try Output.init(std.testing.allocator, .{
3558 .max_glyphs = 256,
3559 .max_ligature_carets = 256,
3560 });
3561 defer output.deinit(std.testing.allocator);
3562
3563 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AAA" } }, &output);
3564 const run = output.run();
3565
3566 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
3567 try std.testing.expectEqual(@as(u32, 'A' - 31), run.glyphs[0].glyph_id);
3568 try std.testing.expectEqual(@as(u32, 'B' - 31), run.glyphs[1].glyph_id);
3569 try std.testing.expectEqual(@as(u32, 'A' - 31), run.glyphs[2].glyph_id);
3570 try std.testing.expectEqual(@as(i32, 1920), run.total_x_advance);
3571 }
3572
3573 test "shapeRun preserves GDEF glyph classes in output glyphs" {
3574 const bytes = try test_font.createWithGdefGlyphClasses(std.testing.allocator);
3575 defer std.testing.allocator.free(bytes);
3576
3577 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3578 defer shaper.deinit();
3579 shaper.setPixelHeightScale(20);
3580
3581 var ctx = Context.init(std.testing.allocator, .{});
3582 defer ctx.deinit();
3583
3584 var output = try Output.init(std.testing.allocator, .{
3585 .max_glyphs = 256,
3586 .max_ligature_carets = 256,
3587 });
3588 defer output.deinit(std.testing.allocator);
3589
3590 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A^_!" } }, &output);
3591 const run = output.run();
3592
3593 try std.testing.expectEqual(@as(usize, 4), run.glyphs.len);
3594 try std.testing.expectEqual(GlyphClass.base, run.glyphs[0].glyph_class);
3595 try std.testing.expectEqual(GlyphClass.mark, run.glyphs[1].glyph_class);
3596 try std.testing.expectEqual(GlyphClass.component, run.glyphs[2].glyph_class);
3597 try std.testing.expectEqual(GlyphClass.unknown, run.glyphs[3].glyph_class);
3598 }
3599
3600 test "shapeRun refreshes GDEF glyph class after ligature substitution" {
3601 const bytes = try test_font.createWithGsubLigatureAndGdefGlyphClasses(std.testing.allocator);
3602 defer std.testing.allocator.free(bytes);
3603
3604 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3605 defer shaper.deinit();
3606 shaper.setPixelHeightScale(20);
3607
3608 var ctx = Context.init(std.testing.allocator, .{});
3609 defer ctx.deinit();
3610
3611 var output = try Output.init(std.testing.allocator, .{
3612 .max_glyphs = 256,
3613 .max_ligature_carets = 256,
3614 });
3615 defer output.deinit(std.testing.allocator);
3616
3617 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "fi" } }, &output);
3618 const run = output.run();
3619
3620 try std.testing.expectEqual(@as(usize, 1), run.glyphs.len);
3621 try std.testing.expectEqual(@as(u32, 96), run.glyphs[0].glyph_id);
3622 try std.testing.expectEqual(GlyphClass.ligature, run.glyphs[0].glyph_class);
3623 }
3624
3625 test "shapeRun exposes GDEF ligature caret positions" {
3626 const bytes = try test_font.createWithGsubLigatureAndGdefCarets(std.testing.allocator);
3627 defer std.testing.allocator.free(bytes);
3628
3629 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3630 defer shaper.deinit();
3631 shaper.setPixelHeightScale(20);
3632
3633 var ctx = Context.init(std.testing.allocator, .{});
3634 defer ctx.deinit();
3635
3636 var output = try Output.init(std.testing.allocator, .{
3637 .max_glyphs = 256,
3638 .max_ligature_carets = 256,
3639 });
3640 defer output.deinit(std.testing.allocator);
3641
3642 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "fi" } }, &output);
3643 const run = output.run();
3644 const map = run.clusterMap();
3645 const caret = map.clusterLigatureCaret(0, 0).?;
3646
3647 try std.testing.expectEqual(@as(usize, 1), run.glyphs.len);
3648 try std.testing.expectEqual(@as(usize, 1), run.ligature_carets.len);
3649 try std.testing.expectEqual(@as(u16, 1), run.glyphs[0].ligature_caret_count);
3650 try std.testing.expectEqual(@as(usize, 3), map.clusterCaretStopCount(0).?);
3651 try std.testing.expectEqual(@as(u32, 0), map.clusterCaretStop(0, 0).?);
3652 try std.testing.expectEqual(@as(u32, 1), map.clusterCaretStop(0, 1).?);
3653 try std.testing.expectEqual(@as(u32, 2), map.clusterCaretStop(0, 2).?);
3654 try std.testing.expectEqual(@as(usize, 1), map.clusterLigatureCaretCount(0).?);
3655 try std.testing.expectEqual(@as(i32, 320), caret.x_offset);
3656 try std.testing.expectEqual(false, caret.synthesized);
3657 try std.testing.expect(map.clusterLigatureCaret(0, 1) == null);
3658 }
3659
3660 test "shapeRun rejects malformed utf32" {
3661 const bytes = try test_font.create(std.testing.allocator);
3662 defer std.testing.allocator.free(bytes);
3663
3664 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3665 defer shaper.deinit();
3666
3667 var ctx = Context.init(std.testing.allocator, .{});
3668 defer ctx.deinit();
3669
3670 var output = try Output.init(std.testing.allocator, .{
3671 .max_glyphs = 256,
3672 .max_ligature_carets = 256,
3673 });
3674 defer output.deinit(std.testing.allocator);
3675
3676 const surrogate = [_]u32{0xd800};
3677 try std.testing.expectError(error.InvalidUtf32, ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf32 = &surrogate } }, &output));
3678 try std.testing.expectEqual(@as(usize, 0), output.run().glyphs.len);
3679
3680 const too_large = [_]u32{0x110000};
3681 try std.testing.expectError(error.InvalidUtf32, ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf32 = &too_large } }, &output));
3682 try std.testing.expectEqual(@as(usize, 0), output.run().glyphs.len);
3683 }
3684
3685 test "shapeRun validates variation settings" {
3686 const static_bytes = try test_font.create(std.testing.allocator);
3687 defer std.testing.allocator.free(static_bytes);
3688 const variable_bytes = try test_font.createWithVariations(std.testing.allocator);
3689 defer std.testing.allocator.free(variable_bytes);
3690
3691 var static_shaper = Font.initFromBytes(static_bytes.ptr, static_bytes.len) orelse return error.TestUnexpectedResult;
3692 defer static_shaper.deinit();
3693
3694 var variable_shaper = Font.initFromBytes(variable_bytes.ptr, variable_bytes.len) orelse return error.TestUnexpectedResult;
3695 defer variable_shaper.deinit();
3696 variable_shaper.setPixelHeightScale(20);
3697
3698 var ctx = Context.init(std.testing.allocator, .{});
3699 defer ctx.deinit();
3700
3701 var output = try Output.init(std.testing.allocator, .{
3702 .max_glyphs = 256,
3703 .max_ligature_carets = 256,
3704 });
3705 defer output.deinit(std.testing.allocator);
3706
3707 try std.testing.expectError(error.InvalidVariations, ctx.shapeRun(.{
3708 .font = &static_shaper,
3709 .text = .{ .utf8 = "A" },
3710 .variations = &.{.{ .tag = font.tag("wght"), .value = 500.0 }},
3711 }, &output));
3712 try std.testing.expectEqual(@as(usize, 0), output.run().glyphs.len);
3713
3714 try std.testing.expectError(error.InvalidVariations, ctx.shapeRun(.{
3715 .font = &variable_shaper,
3716 .text = .{ .utf8 = "A" },
3717 .variations = &.{.{ .tag = font.tag("opsz"), .value = 12.0 }},
3718 }, &output));
3719 try std.testing.expectEqual(@as(usize, 0), output.run().glyphs.len);
3720
3721 try ctx.shapeRun(.{
3722 .font = &variable_shaper,
3723 .text = .{ .utf8 = "A" },
3724 .variations = &.{.{ .tag = font.tag("wght"), .value = 650.0 }},
3725 }, &output);
3726 const run = output.run();
3727
3728 try std.testing.expectEqual(@as(usize, 1), run.glyphs.len);
3729 try std.testing.expectEqual(@as(u32, 'A' - 31), run.glyphs[0].glyph_id);
3730 try std.testing.expectEqual(@as(i32, 640), run.total_x_advance);
3731 }
3732
3733 test "shapeRun applies GSUB feature variations" {
3734 const bytes = try test_font.createWithGsubFeatureVariations(std.testing.allocator);
3735 defer std.testing.allocator.free(bytes);
3736
3737 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3738 defer shaper.deinit();
3739 shaper.setPixelHeightScale(20);
3740
3741 var ctx = Context.init(std.testing.allocator, .{});
3742 defer ctx.deinit();
3743
3744 var output = try Output.init(std.testing.allocator, .{
3745 .max_glyphs = 256,
3746 .max_ligature_carets = 256,
3747 });
3748 defer output.deinit(std.testing.allocator);
3749
3750 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A" } }, &output);
3751 try std.testing.expectEqual(@as(u32, 'Z' - 31), output.run().glyphs[0].glyph_id);
3752
3753 try ctx.shapeRun(.{
3754 .font = &shaper,
3755 .text = .{ .utf8 = "A" },
3756 .variations = &.{.{ .tag = font.tag("wght"), .value = 900.0 }},
3757 }, &output);
3758 try std.testing.expectEqual(@as(u32, 'B' - 31), output.run().glyphs[0].glyph_id);
3759 }
3760
3761 test "shapeRun applies GSUB required variation alternates" {
3762 const bytes = try test_font.createWithGsubRequiredVariationAlternates(std.testing.allocator);
3763 defer std.testing.allocator.free(bytes);
3764
3765 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3766 defer shaper.deinit();
3767 shaper.setPixelHeightScale(20);
3768
3769 var ctx = Context.init(std.testing.allocator, .{});
3770 defer ctx.deinit();
3771
3772 var output = try Output.init(std.testing.allocator, .{
3773 .max_glyphs = 256,
3774 .max_ligature_carets = 256,
3775 });
3776 defer output.deinit(std.testing.allocator);
3777
3778 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A" } }, &output);
3779 try std.testing.expectEqual(@as(usize, 1), output.run().glyphs.len);
3780 try std.testing.expectEqual(@as(u32, 'Z' - 31), output.run().glyphs[0].glyph_id);
3781
3782 try ctx.shapeRun(.{
3783 .font = &shaper,
3784 .text = .{ .utf8 = "A" },
3785 .variations = &.{.{ .tag = font.tag("wght"), .value = 900.0 }},
3786 }, &output);
3787 try std.testing.expectEqual(@as(usize, 1), output.run().glyphs.len);
3788 try std.testing.expectEqual(@as(u32, 'B' - 31), output.run().glyphs[0].glyph_id);
3789
3790 try ctx.shapeRun(.{
3791 .font = &shaper,
3792 .text = .{ .utf8 = "A" },
3793 .features = &.{.{ .tag = font.tag("rvrn"), .value = 0 }},
3794 }, &output);
3795 try std.testing.expectEqual(@as(usize, 1), output.run().glyphs.len);
3796 try std.testing.expectEqual(@as(u32, 'A' - 31), output.run().glyphs[0].glyph_id);
3797
3798 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "fi" } }, &output);
3799 try std.testing.expectEqual(@as(usize, 2), output.run().glyphs.len);
3800 try std.testing.expectEqual(@as(u32, 'f' - 31), output.run().glyphs[0].glyph_id);
3801 try std.testing.expectEqual(@as(u32, 'i' - 31), output.run().glyphs[1].glyph_id);
3802 }
3803
3804 test "shapeRun applies HVAR variation advances" {
3805 const bytes = try test_font.createWithHorizontalMetricVariations(std.testing.allocator);
3806 defer std.testing.allocator.free(bytes);
3807
3808 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3809 defer shaper.deinit();
3810 shaper.setPixelHeightScale(20);
3811
3812 var ctx = Context.init(std.testing.allocator, .{});
3813 defer ctx.deinit();
3814
3815 var output = try Output.init(std.testing.allocator, .{
3816 .max_glyphs = 256,
3817 .max_ligature_carets = 256,
3818 });
3819 defer output.deinit(std.testing.allocator);
3820
3821 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A" } }, &output);
3822 try std.testing.expectEqual(@as(i32, 640), output.run().total_x_advance);
3823
3824 try ctx.shapeRun(.{
3825 .font = &shaper,
3826 .text = .{ .utf8 = "A" },
3827 .variations = &.{.{ .tag = font.tag("wght"), .value = 650.0 }},
3828 }, &output);
3829 try std.testing.expectEqual(@as(i32, 704), output.run().glyphs[0].x_advance);
3830 try std.testing.expectEqual(@as(i32, 704), output.run().total_x_advance);
3831
3832 try ctx.shapeRun(.{
3833 .font = &shaper,
3834 .text = .{ .utf8 = "A" },
3835 .variations = &.{.{ .tag = font.tag("wght"), .value = 900.0 }},
3836 }, &output);
3837 try std.testing.expectEqual(@as(i32, 768), output.run().glyphs[0].x_advance);
3838 try std.testing.expectEqual(@as(i32, 768), output.run().total_x_advance);
3839 }
3840
3841 test "shapeRun applies VVAR variation advances" {
3842 const bytes = try test_font.createWithVerticalMetricVariations(std.testing.allocator);
3843 defer std.testing.allocator.free(bytes);
3844
3845 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3846 defer shaper.deinit();
3847 shaper.setPixelHeightScale(20);
3848
3849 var ctx = Context.init(std.testing.allocator, .{});
3850 defer ctx.deinit();
3851
3852 var output = try Output.init(std.testing.allocator, .{
3853 .max_glyphs = 256,
3854 .max_ligature_carets = 256,
3855 });
3856 defer output.deinit(std.testing.allocator);
3857
3858 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A" }, .writing_mode = .vertical }, &output);
3859 try std.testing.expectEqual(@as(i32, -896), output.run().total_y_advance);
3860
3861 try ctx.shapeRun(.{
3862 .font = &shaper,
3863 .text = .{ .utf8 = "A" },
3864 .writing_mode = .vertical,
3865 .variations = &.{.{ .tag = font.tag("wght"), .value = 650.0 }},
3866 }, &output);
3867 try std.testing.expectEqual(@as(i32, -960), output.run().glyphs[0].y_advance);
3868 try std.testing.expectEqual(@as(i32, -960), output.run().total_y_advance);
3869
3870 try ctx.shapeRun(.{
3871 .font = &shaper,
3872 .text = .{ .utf8 = "A" },
3873 .writing_mode = .vertical,
3874 .variations = &.{.{ .tag = font.tag("wght"), .value = 900.0 }},
3875 }, &output);
3876 try std.testing.expectEqual(@as(i32, -1024), output.run().glyphs[0].y_advance);
3877 try std.testing.expectEqual(@as(i32, -1024), output.run().total_y_advance);
3878 }
3879
3880 test "shapeRun applies GPOS ValueRecord variation deltas" {
3881 const bytes = try test_font.createWithGposSingleVariationAdjustment(std.testing.allocator);
3882 defer std.testing.allocator.free(bytes);
3883
3884 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3885 defer shaper.deinit();
3886
3887 var ctx = Context.init(std.testing.allocator, .{});
3888 defer ctx.deinit();
3889
3890 var output = try Output.init(std.testing.allocator, .{
3891 .max_glyphs = 256,
3892 .max_ligature_carets = 256,
3893 });
3894 defer output.deinit(std.testing.allocator);
3895
3896 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A" } }, &output);
3897 try std.testing.expectEqual(@as(i32, 1280), output.run().glyphs[0].x_offset);
3898
3899 try ctx.shapeRun(.{
3900 .font = &shaper,
3901 .text = .{ .utf8 = "A" },
3902 .variations = &.{.{ .tag = font.tag("wght"), .value = 650.0 }},
3903 }, &output);
3904 try std.testing.expectEqual(@as(i32, 3840), output.run().glyphs[0].x_offset);
3905
3906 try ctx.shapeRun(.{
3907 .font = &shaper,
3908 .text = .{ .utf8 = "A" },
3909 .variations = &.{.{ .tag = font.tag("wght"), .value = 900.0 }},
3910 }, &output);
3911 try std.testing.expectEqual(@as(i32, 6400), output.run().glyphs[0].x_offset);
3912 }
3913
3914 test "shapeRun applies classic kern pairs to horizontal advances" {
3915 const bytes = try test_font.createWithKern(std.testing.allocator);
3916 defer std.testing.allocator.free(bytes);
3917
3918 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3919 defer shaper.deinit();
3920 shaper.setPixelHeightScale(20);
3921
3922 var ctx = Context.init(std.testing.allocator, .{});
3923 defer ctx.deinit();
3924
3925 var output = try Output.init(std.testing.allocator, .{
3926 .max_glyphs = 256,
3927 .max_ligature_carets = 256,
3928 });
3929 defer output.deinit(std.testing.allocator);
3930
3931 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AV" } }, &output);
3932 const run = output.run();
3933
3934 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
3935 try std.testing.expectEqual(@as(i32, 538), run.glyphs[0].x_advance);
3936 try std.testing.expectEqual(@as(i32, 640), run.glyphs[1].x_advance);
3937 try std.testing.expectEqual(@as(i32, 1178), run.total_x_advance);
3938 try std.testing.expect(!run.glyphs[0].flags.unsafe_to_break);
3939 try std.testing.expect(run.glyphs[0].flags.unsafe_to_concat);
3940 try std.testing.expect(run.glyphs[1].flags.unsafe_to_break);
3941 try std.testing.expect(run.glyphs[1].flags.unsafe_to_concat);
3942 try std.testing.expectEqual(true, run.clusterMap().breakRequiresReshaping(1).?);
3943 }
3944
3945 test "shapeRun disables classic kern fallback with feature settings" {
3946 const bytes = try test_font.createWithKern(std.testing.allocator);
3947 defer std.testing.allocator.free(bytes);
3948
3949 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3950 defer shaper.deinit();
3951 shaper.setPixelHeightScale(20);
3952
3953 var ctx = Context.init(std.testing.allocator, .{});
3954 defer ctx.deinit();
3955
3956 var output = try Output.init(std.testing.allocator, .{
3957 .max_glyphs = 256,
3958 .max_ligature_carets = 256,
3959 });
3960 defer output.deinit(std.testing.allocator);
3961
3962 try ctx.shapeRun(.{
3963 .font = &shaper,
3964 .text = .{ .utf8 = "AV" },
3965 .features = &.{.{ .tag = font.tag("kern"), .value = 0 }},
3966 }, &output);
3967 const run = output.run();
3968
3969 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
3970 try std.testing.expectEqual(@as(i32, 640), run.glyphs[0].x_advance);
3971 try std.testing.expectEqual(@as(i32, 640), run.glyphs[1].x_advance);
3972 try std.testing.expectEqual(@as(i32, 1280), run.total_x_advance);
3973 }
3974
3975 test "shapeRun applies classic kern feature settings by source range" {
3976 const bytes = try test_font.createWithKern(std.testing.allocator);
3977 defer std.testing.allocator.free(bytes);
3978
3979 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
3980 defer shaper.deinit();
3981 shaper.setPixelHeightScale(20);
3982
3983 var ctx = Context.init(std.testing.allocator, .{});
3984 defer ctx.deinit();
3985
3986 var output = try Output.init(std.testing.allocator, .{
3987 .max_glyphs = 256,
3988 .max_ligature_carets = 256,
3989 });
3990 defer output.deinit(std.testing.allocator);
3991
3992 try ctx.shapeRun(.{
3993 .font = &shaper,
3994 .text = .{ .utf8 = "AVAV" },
3995 .features = &.{.{ .tag = font.tag("kern"), .value = 0, .source = .{ .start = 1, .end = 2 } }},
3996 }, &output);
3997 const run = output.run();
3998
3999 try std.testing.expectEqual(@as(usize, 4), run.glyphs.len);
4000 try std.testing.expectEqual(@as(i32, 640), run.glyphs[0].x_advance);
4001 try std.testing.expectEqual(@as(i32, 640), run.glyphs[1].x_advance);
4002 try std.testing.expectEqual(@as(i32, 538), run.glyphs[2].x_advance);
4003 try std.testing.expectEqual(@as(i32, 640), run.glyphs[3].x_advance);
4004 try std.testing.expectEqual(@as(i32, 2458), run.total_x_advance);
4005 }
4006
4007 test "shapeRun applies classic kern pairs across hidden default ignorables" {
4008 const bytes = try test_font.createWithKern(std.testing.allocator);
4009 defer std.testing.allocator.free(bytes);
4010
4011 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4012 defer shaper.deinit();
4013 shaper.setPixelHeightScale(20);
4014
4015 var ctx = Context.init(std.testing.allocator, .{});
4016 defer ctx.deinit();
4017
4018 var output = try Output.init(std.testing.allocator, .{
4019 .max_glyphs = 256,
4020 .max_ligature_carets = 256,
4021 });
4022 defer output.deinit(std.testing.allocator);
4023
4024 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A\u{fe0f}V" } }, &output);
4025 const run = output.run();
4026
4027 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
4028 try std.testing.expect(run.glyphs[1].flags.default_ignorable);
4029 try std.testing.expect(!run.glyphs[1].flags.missing_glyph);
4030 try std.testing.expectEqual(@as(i32, 538), run.glyphs[0].x_advance);
4031 try std.testing.expectEqual(@as(i32, 0), run.glyphs[1].x_advance);
4032 try std.testing.expectEqual(@as(i32, 640), run.glyphs[2].x_advance);
4033 try std.testing.expectEqual(@as(i32, 1178), run.total_x_advance);
4034 try std.testing.expect(run.glyphs[0].flags.unsafe_to_concat);
4035 try std.testing.expect(run.glyphs[2].flags.unsafe_to_concat);
4036 }
4037
4038 test "shapeRun applies GPOS pair adjustments to horizontal advances" {
4039 const bytes = try test_font.createWithGposPairAdjustment(std.testing.allocator);
4040 defer std.testing.allocator.free(bytes);
4041
4042 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4043 defer shaper.deinit();
4044 shaper.setPixelHeightScale(20);
4045
4046 var ctx = Context.init(std.testing.allocator, .{});
4047 defer ctx.deinit();
4048
4049 var output = try Output.init(std.testing.allocator, .{
4050 .max_glyphs = 256,
4051 .max_ligature_carets = 256,
4052 });
4053 defer output.deinit(std.testing.allocator);
4054
4055 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AV" } }, &output);
4056 const run = output.run();
4057
4058 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
4059 try std.testing.expectEqual(@as(i32, 538), run.glyphs[0].x_advance);
4060 try std.testing.expectEqual(@as(i32, 640), run.glyphs[1].x_advance);
4061 try std.testing.expectEqual(@as(i32, 1178), run.total_x_advance);
4062 try std.testing.expect(!run.glyphs[0].flags.unsafe_to_break);
4063 try std.testing.expect(run.glyphs[0].flags.unsafe_to_concat);
4064 try std.testing.expect(run.glyphs[1].flags.unsafe_to_break);
4065 try std.testing.expect(run.glyphs[1].flags.unsafe_to_concat);
4066 try std.testing.expectEqual(true, run.clusterMap().breakRequiresReshaping(1).?);
4067 }
4068
4069 test "shapeRun applies GPOS vertical pair adjustments to vertical advances" {
4070 const bytes = try test_font.createWithGposVerticalPairAdjustment(std.testing.allocator);
4071 defer std.testing.allocator.free(bytes);
4072
4073 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4074 defer shaper.deinit();
4075 shaper.setPixelHeightScale(20);
4076
4077 var ctx = Context.init(std.testing.allocator, .{});
4078 defer ctx.deinit();
4079
4080 var output = try Output.init(std.testing.allocator, .{
4081 .max_glyphs = 256,
4082 .max_ligature_carets = 256,
4083 });
4084 defer output.deinit(std.testing.allocator);
4085
4086 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AV" } }, &output);
4087 const horizontal = output.run();
4088
4089 try std.testing.expectEqual(@as(usize, 2), horizontal.glyphs.len);
4090 try std.testing.expectEqual(@as(i32, 640), horizontal.glyphs[0].x_advance);
4091 try std.testing.expectEqual(@as(i32, 0), horizontal.total_y_advance);
4092
4093 try ctx.shapeRun(.{
4094 .font = &shaper,
4095 .text = .{ .utf8 = "AV" },
4096 .writing_mode = .vertical,
4097 }, &output);
4098 const vertical = output.run();
4099
4100 try std.testing.expectEqual(@as(usize, 2), vertical.glyphs.len);
4101 try std.testing.expectEqual(@as(i32, 0), vertical.glyphs[0].x_advance);
4102 try std.testing.expectEqual(@as(i32, -998), vertical.glyphs[0].y_advance);
4103 try std.testing.expectEqual(@as(i32, -896), vertical.glyphs[1].y_advance);
4104 try std.testing.expectEqual(@as(i32, -1894), vertical.total_y_advance);
4105
4106 try ctx.shapeRun(.{
4107 .font = &shaper,
4108 .text = .{ .utf8 = "AV" },
4109 .writing_mode = .vertical,
4110 .features = &.{.{ .tag = font.tag("vkrn"), .value = 0 }},
4111 }, &output);
4112 const disabled = output.run();
4113
4114 try std.testing.expectEqual(@as(i32, -896), disabled.glyphs[0].y_advance);
4115 try std.testing.expectEqual(@as(i32, -1792), disabled.total_y_advance);
4116 }
4117
4118 test "shapeRun applies GPOS pair adjustments across ignored marks" {
4119 const bytes = try test_font.createWithGposPairIgnoreMarks(std.testing.allocator);
4120 defer std.testing.allocator.free(bytes);
4121
4122 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4123 defer shaper.deinit();
4124 shaper.setPixelHeightScale(20);
4125
4126 var ctx = Context.init(std.testing.allocator, .{});
4127 defer ctx.deinit();
4128
4129 var output = try Output.init(std.testing.allocator, .{
4130 .max_glyphs = 256,
4131 .max_ligature_carets = 256,
4132 });
4133 defer output.deinit(std.testing.allocator);
4134
4135 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A^V" } }, &output);
4136 const run = output.run();
4137
4138 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
4139 try std.testing.expectEqual(GlyphClass.base, run.glyphs[0].glyph_class);
4140 try std.testing.expectEqual(GlyphClass.mark, run.glyphs[1].glyph_class);
4141 try std.testing.expectEqual(GlyphClass.base, run.glyphs[2].glyph_class);
4142 try std.testing.expectEqual(@as(i32, 538), run.glyphs[0].x_advance);
4143 try std.testing.expectEqual(@as(i32, 640), run.glyphs[1].x_advance);
4144 try std.testing.expectEqual(@as(i32, 640), run.glyphs[2].x_advance);
4145 try std.testing.expectEqual(@as(i32, 1818), run.total_x_advance);
4146 try std.testing.expect(!run.glyphs[0].flags.unsafe_to_break);
4147 try std.testing.expect(run.glyphs[0].flags.unsafe_to_concat);
4148 try std.testing.expect(run.glyphs[2].flags.unsafe_to_break);
4149 try std.testing.expect(run.glyphs[2].flags.unsafe_to_concat);
4150 try std.testing.expectEqual(true, run.clusterMap().breakRequiresReshaping(2).?);
4151 }
4152
4153 test "shapeRun keeps unfiltered GPOS pair adjustments adjacent" {
4154 const bytes = try test_font.createWithGposPairAdjustment(std.testing.allocator);
4155 defer std.testing.allocator.free(bytes);
4156
4157 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4158 defer shaper.deinit();
4159 shaper.setPixelHeightScale(20);
4160
4161 var ctx = Context.init(std.testing.allocator, .{});
4162 defer ctx.deinit();
4163
4164 var output = try Output.init(std.testing.allocator, .{
4165 .max_glyphs = 256,
4166 .max_ligature_carets = 256,
4167 });
4168 defer output.deinit(std.testing.allocator);
4169
4170 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A^V" } }, &output);
4171 const run = output.run();
4172
4173 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
4174 try std.testing.expectEqual(@as(i32, 640), run.glyphs[0].x_advance);
4175 try std.testing.expectEqual(@as(i32, 640), run.glyphs[1].x_advance);
4176 try std.testing.expectEqual(@as(i32, 640), run.glyphs[2].x_advance);
4177 try std.testing.expectEqual(@as(i32, 1920), run.total_x_advance);
4178 }
4179
4180 test "shapeRun applies GPOS contextual pair adjustments" {
4181 const bytes = try test_font.createWithGposContextualPairAdjustment(std.testing.allocator);
4182 defer std.testing.allocator.free(bytes);
4183
4184 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4185 defer shaper.deinit();
4186 shaper.setPixelHeightScale(20);
4187
4188 var ctx = Context.init(std.testing.allocator, .{});
4189 defer ctx.deinit();
4190
4191 var output = try Output.init(std.testing.allocator, .{
4192 .max_glyphs = 256,
4193 .max_ligature_carets = 256,
4194 });
4195 defer output.deinit(std.testing.allocator);
4196
4197 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AVC!" } }, &output);
4198 const contextual = output.run();
4199
4200 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
4201 try std.testing.expectEqual(@as(i32, 512), contextual.glyphs[0].x_advance);
4202 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[1].x_advance);
4203 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[2].x_advance);
4204 try std.testing.expectEqual(@as(i32, 2432), contextual.total_x_advance);
4205
4206 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AV!" } }, &output);
4207 const mismatch = output.run();
4208
4209 try std.testing.expectEqual(@as(usize, 3), mismatch.glyphs.len);
4210 try std.testing.expectEqual(@as(i32, 640), mismatch.glyphs[0].x_advance);
4211 try std.testing.expectEqual(@as(i32, 1920), mismatch.total_x_advance);
4212 }
4213
4214 test "shapeRun applies GPOS simple contextual pair adjustments" {
4215 const bytes = try test_font.createWithGposSimpleContextualPairAdjustment(std.testing.allocator);
4216 defer std.testing.allocator.free(bytes);
4217
4218 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4219 defer shaper.deinit();
4220 shaper.setPixelHeightScale(20);
4221
4222 var ctx = Context.init(std.testing.allocator, .{});
4223 defer ctx.deinit();
4224
4225 var output = try Output.init(std.testing.allocator, .{
4226 .max_glyphs = 256,
4227 .max_ligature_carets = 256,
4228 });
4229 defer output.deinit(std.testing.allocator);
4230
4231 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AVC!" } }, &output);
4232 const contextual = output.run();
4233
4234 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
4235 try std.testing.expectEqual(@as(i32, 512), contextual.glyphs[0].x_advance);
4236 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[1].x_advance);
4237 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[2].x_advance);
4238 try std.testing.expectEqual(@as(i32, 2432), contextual.total_x_advance);
4239
4240 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AV!" } }, &output);
4241 const mismatch = output.run();
4242
4243 try std.testing.expectEqual(@as(usize, 3), mismatch.glyphs.len);
4244 try std.testing.expectEqual(@as(i32, 640), mismatch.glyphs[0].x_advance);
4245 try std.testing.expectEqual(@as(i32, 1920), mismatch.total_x_advance);
4246 }
4247
4248 test "shapeRun applies GPOS class contextual pair adjustments" {
4249 const bytes = try test_font.createWithGposClassContextualPairAdjustment(std.testing.allocator);
4250 defer std.testing.allocator.free(bytes);
4251
4252 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4253 defer shaper.deinit();
4254 shaper.setPixelHeightScale(20);
4255
4256 var ctx = Context.init(std.testing.allocator, .{});
4257 defer ctx.deinit();
4258
4259 var output = try Output.init(std.testing.allocator, .{
4260 .max_glyphs = 256,
4261 .max_ligature_carets = 256,
4262 });
4263 defer output.deinit(std.testing.allocator);
4264
4265 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ABC!" } }, &output);
4266 const contextual = output.run();
4267
4268 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
4269 try std.testing.expectEqual(@as(i32, 512), contextual.glyphs[0].x_advance);
4270 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[1].x_advance);
4271 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[2].x_advance);
4272 try std.testing.expectEqual(@as(i32, 2432), contextual.total_x_advance);
4273
4274 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AB!" } }, &output);
4275 const mismatch = output.run();
4276
4277 try std.testing.expectEqual(@as(usize, 3), mismatch.glyphs.len);
4278 try std.testing.expectEqual(@as(i32, 640), mismatch.glyphs[0].x_advance);
4279 try std.testing.expectEqual(@as(i32, 1920), mismatch.total_x_advance);
4280 }
4281
4282 test "shapeRun applies GPOS chained contextual pair adjustments" {
4283 const bytes = try test_font.createWithGposChainedContextualPairAdjustment(std.testing.allocator);
4284 defer std.testing.allocator.free(bytes);
4285
4286 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4287 defer shaper.deinit();
4288 shaper.setPixelHeightScale(20);
4289
4290 var ctx = Context.init(std.testing.allocator, .{});
4291 defer ctx.deinit();
4292
4293 var output = try Output.init(std.testing.allocator, .{
4294 .max_glyphs = 256,
4295 .max_ligature_carets = 256,
4296 });
4297 defer output.deinit(std.testing.allocator);
4298
4299 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AVC!" } }, &output);
4300 const contextual = output.run();
4301
4302 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
4303 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[0].x_advance);
4304 try std.testing.expectEqual(@as(i32, 512), contextual.glyphs[1].x_advance);
4305 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[2].x_advance);
4306 try std.testing.expectEqual(@as(i32, 2432), contextual.total_x_advance);
4307
4308 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "VC!" } }, &output);
4309 const mismatch = output.run();
4310
4311 try std.testing.expectEqual(@as(usize, 3), mismatch.glyphs.len);
4312 try std.testing.expectEqual(@as(i32, 640), mismatch.glyphs[0].x_advance);
4313 try std.testing.expectEqual(@as(i32, 1920), mismatch.total_x_advance);
4314 }
4315
4316 test "shapeRun uses pre context for GPOS chained pair adjustments" {
4317 const bytes = try test_font.createWithGposChainedContextualPairAdjustment(std.testing.allocator);
4318 defer std.testing.allocator.free(bytes);
4319
4320 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4321 defer shaper.deinit();
4322 shaper.setPixelHeightScale(20);
4323
4324 var ctx = Context.init(std.testing.allocator, .{});
4325 defer ctx.deinit();
4326
4327 var output = try Output.init(std.testing.allocator, .{
4328 .max_glyphs = 256,
4329 .max_ligature_carets = 256,
4330 });
4331 defer output.deinit(std.testing.allocator);
4332
4333 try ctx.shapeRun(.{
4334 .font = &shaper,
4335 .text = .{ .utf8 = "VC!" },
4336 .source_offset = 1,
4337 .pre_context = .{ .utf8 = "A" },
4338 }, &output);
4339 const contextual = output.run();
4340
4341 try std.testing.expectEqual(@as(usize, 3), contextual.glyphs.len);
4342 try std.testing.expectEqual(@as(i32, 512), contextual.glyphs[0].x_advance);
4343 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[1].x_advance);
4344 try std.testing.expectEqual(@as(i32, 1792), contextual.total_x_advance);
4345 try std.testing.expectEqual(@as(u32, 1), contextual.glyphs[0].source_start);
4346 try std.testing.expectEqual(@as(u32, 2), contextual.glyphs[0].source_end);
4347 }
4348
4349 test "shapeRun applies GPOS chained simple contextual pair adjustments" {
4350 const bytes = try test_font.createWithGposChainedSimpleContextualPairAdjustment(std.testing.allocator);
4351 defer std.testing.allocator.free(bytes);
4352
4353 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4354 defer shaper.deinit();
4355 shaper.setPixelHeightScale(20);
4356
4357 var ctx = Context.init(std.testing.allocator, .{});
4358 defer ctx.deinit();
4359
4360 var output = try Output.init(std.testing.allocator, .{
4361 .max_glyphs = 256,
4362 .max_ligature_carets = 256,
4363 });
4364 defer output.deinit(std.testing.allocator);
4365
4366 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ABC!" } }, &output);
4367 const contextual = output.run();
4368
4369 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
4370 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[0].x_advance);
4371 try std.testing.expectEqual(@as(i32, 512), contextual.glyphs[1].x_advance);
4372 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[2].x_advance);
4373 try std.testing.expectEqual(@as(i32, 2432), contextual.total_x_advance);
4374
4375 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AB!" } }, &output);
4376 const mismatch = output.run();
4377
4378 try std.testing.expectEqual(@as(usize, 3), mismatch.glyphs.len);
4379 try std.testing.expectEqual(@as(i32, 640), mismatch.glyphs[1].x_advance);
4380 try std.testing.expectEqual(@as(i32, 1920), mismatch.total_x_advance);
4381 }
4382
4383 test "shapeRun applies GPOS chained class contextual pair adjustments" {
4384 const bytes = try test_font.createWithGposChainedClassContextualPairAdjustment(std.testing.allocator);
4385 defer std.testing.allocator.free(bytes);
4386
4387 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4388 defer shaper.deinit();
4389 shaper.setPixelHeightScale(20);
4390
4391 var ctx = Context.init(std.testing.allocator, .{});
4392 defer ctx.deinit();
4393
4394 var output = try Output.init(std.testing.allocator, .{
4395 .max_glyphs = 256,
4396 .max_ligature_carets = 256,
4397 });
4398 defer output.deinit(std.testing.allocator);
4399
4400 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ABC!" } }, &output);
4401 const contextual = output.run();
4402
4403 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
4404 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[0].x_advance);
4405 try std.testing.expectEqual(@as(i32, 512), contextual.glyphs[1].x_advance);
4406 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[2].x_advance);
4407 try std.testing.expectEqual(@as(i32, 2432), contextual.total_x_advance);
4408
4409 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AB!" } }, &output);
4410 const mismatch = output.run();
4411
4412 try std.testing.expectEqual(@as(usize, 3), mismatch.glyphs.len);
4413 try std.testing.expectEqual(@as(i32, 640), mismatch.glyphs[1].x_advance);
4414 try std.testing.expectEqual(@as(i32, 1920), mismatch.total_x_advance);
4415 }
4416
4417 test "shapeRun selects GPOS lookups by script tag" {
4418 const bytes = try test_font.createWithGposPairAdjustment(std.testing.allocator);
4419 defer std.testing.allocator.free(bytes);
4420 try fixture_binary.replaceLayoutScript(bytes, "GPOS", "latn");
4421
4422 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4423 defer shaper.deinit();
4424 shaper.setPixelHeightScale(20);
4425
4426 var ctx = Context.init(std.testing.allocator, .{});
4427 defer ctx.deinit();
4428
4429 var output = try Output.init(std.testing.allocator, .{
4430 .max_glyphs = 256,
4431 .max_ligature_carets = 256,
4432 });
4433 defer output.deinit(std.testing.allocator);
4434
4435 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AV" } }, &output);
4436 try std.testing.expectEqual(@as(i32, 1280), output.run().total_x_advance);
4437
4438 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AV" }, .script = font.tag("latn") }, &output);
4439 const run = output.run();
4440
4441 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
4442 try std.testing.expectEqual(@as(i32, 538), run.glyphs[0].x_advance);
4443 try std.testing.expectEqual(@as(i32, 1178), run.total_x_advance);
4444 }
4445
4446 test "shapeRun selects GPOS lookups by ordered script tags" {
4447 const bytes = try test_font.createWithGposPairAdjustment(std.testing.allocator);
4448 defer std.testing.allocator.free(bytes);
4449 try fixture_binary.replaceLayoutScript(bytes, "GPOS", "bng2");
4450
4451 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4452 defer shaper.deinit();
4453 shaper.setPixelHeightScale(20);
4454
4455 var ctx = Context.init(std.testing.allocator, .{});
4456 defer ctx.deinit();
4457
4458 var output = try Output.init(std.testing.allocator, .{
4459 .max_glyphs = 256,
4460 .max_ligature_carets = 256,
4461 });
4462 defer output.deinit(std.testing.allocator);
4463
4464 const script_tags = unicode_data.scriptOpenTypeTags(.bengali);
4465 try ctx.shapeRun(.{
4466 .font = &shaper,
4467 .text = .{ .utf8 = "AV" },
4468 .script = font.tag("beng"),
4469 .script_tags = script_tags.slice(),
4470 }, &output);
4471 const run = output.run();
4472
4473 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
4474 try std.testing.expectEqual(@as(i32, 538), run.glyphs[0].x_advance);
4475 try std.testing.expectEqual(@as(i32, 1178), run.total_x_advance);
4476 }
4477
4478 test "shapeRun applies GPOS single adjustments" {
4479 const bytes = try test_font.createWithGposSingleAdjustment(std.testing.allocator);
4480 defer std.testing.allocator.free(bytes);
4481
4482 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4483 defer shaper.deinit();
4484 shaper.setPixelHeightScale(20);
4485
4486 var ctx = Context.init(std.testing.allocator, .{});
4487 defer ctx.deinit();
4488
4489 var output = try Output.init(std.testing.allocator, .{
4490 .max_glyphs = 256,
4491 .max_ligature_carets = 256,
4492 });
4493 defer output.deinit(std.testing.allocator);
4494
4495 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A!" } }, &output);
4496 const run = output.run();
4497
4498 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
4499 try std.testing.expectEqual(@as(i32, 589), run.glyphs[0].x_advance);
4500 try std.testing.expectEqual(@as(i32, 26), run.glyphs[0].x_offset);
4501 try std.testing.expectEqual(@as(i32, 38), run.glyphs[0].y_offset);
4502 try std.testing.expectEqual(@as(i32, 640), run.glyphs[1].x_advance);
4503 try std.testing.expectEqual(@as(i32, 1229), run.total_x_advance);
4504
4505 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A!" }, .writing_mode = .vertical }, &output);
4506 const vertical = output.run();
4507
4508 try std.testing.expectEqual(@as(usize, 2), vertical.glyphs.len);
4509 try std.testing.expectEqual(@as(i32, 0), vertical.glyphs[0].x_offset);
4510 try std.testing.expectEqual(@as(i32, 0), vertical.glyphs[0].y_offset);
4511 try std.testing.expectEqual(@as(i32, -1280), vertical.glyphs[0].y_advance);
4512 try std.testing.expectEqual(@as(i32, -2560), vertical.total_y_advance);
4513
4514 try ctx.shapeRun(.{
4515 .font = &shaper,
4516 .text = .{ .utf8 = "A!" },
4517 .writing_mode = .vertical,
4518 .features = &.{.{ .tag = font.tag("dist"), .value = 1 }},
4519 }, &output);
4520 const vertical_enabled = output.run();
4521
4522 try std.testing.expectEqual(@as(usize, 2), vertical_enabled.glyphs.len);
4523 try std.testing.expectEqual(@as(i32, 26), vertical_enabled.glyphs[0].x_offset);
4524 try std.testing.expectEqual(@as(i32, 38), vertical_enabled.glyphs[0].y_offset);
4525 }
4526
4527 test "shapeRun applies GPOS contextual single adjustments" {
4528 const bytes = try test_font.createWithGposContextualSingleAdjustment(std.testing.allocator);
4529 defer std.testing.allocator.free(bytes);
4530
4531 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4532 defer shaper.deinit();
4533 shaper.setPixelHeightScale(20);
4534
4535 var ctx = Context.init(std.testing.allocator, .{});
4536 defer ctx.deinit();
4537
4538 var output = try Output.init(std.testing.allocator, .{
4539 .max_glyphs = 256,
4540 .max_ligature_carets = 256,
4541 });
4542 defer output.deinit(std.testing.allocator);
4543
4544 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ABC!" } }, &output);
4545 const contextual = output.run();
4546
4547 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
4548 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[0].x_advance);
4549 try std.testing.expectEqual(@as(i32, 64), contextual.glyphs[1].x_offset);
4550 try std.testing.expectEqual(@as(i32, 512), contextual.glyphs[1].x_advance);
4551 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[2].x_advance);
4552 try std.testing.expectEqual(@as(i32, 2432), contextual.total_x_advance);
4553
4554 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AB!" } }, &output);
4555 const mismatch = output.run();
4556
4557 try std.testing.expectEqual(@as(usize, 3), mismatch.glyphs.len);
4558 try std.testing.expectEqual(@as(i32, 0), mismatch.glyphs[1].x_offset);
4559 try std.testing.expectEqual(@as(i32, 640), mismatch.glyphs[1].x_advance);
4560 try std.testing.expectEqual(@as(i32, 1920), mismatch.total_x_advance);
4561 }
4562
4563 test "shapeRun applies GPOS simple contextual single adjustments" {
4564 const bytes = try test_font.createWithGposSimpleContextualSingleAdjustment(std.testing.allocator);
4565 defer std.testing.allocator.free(bytes);
4566
4567 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4568 defer shaper.deinit();
4569 shaper.setPixelHeightScale(20);
4570
4571 var ctx = Context.init(std.testing.allocator, .{});
4572 defer ctx.deinit();
4573
4574 var output = try Output.init(std.testing.allocator, .{
4575 .max_glyphs = 256,
4576 .max_ligature_carets = 256,
4577 });
4578 defer output.deinit(std.testing.allocator);
4579
4580 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ABC!" } }, &output);
4581 const contextual = output.run();
4582
4583 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
4584 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[0].x_advance);
4585 try std.testing.expectEqual(@as(i32, 64), contextual.glyphs[1].x_offset);
4586 try std.testing.expectEqual(@as(i32, 512), contextual.glyphs[1].x_advance);
4587 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[2].x_advance);
4588 try std.testing.expectEqual(@as(i32, 2432), contextual.total_x_advance);
4589
4590 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AB!" } }, &output);
4591 const mismatch = output.run();
4592
4593 try std.testing.expectEqual(@as(usize, 3), mismatch.glyphs.len);
4594 try std.testing.expectEqual(@as(i32, 0), mismatch.glyphs[1].x_offset);
4595 try std.testing.expectEqual(@as(i32, 640), mismatch.glyphs[1].x_advance);
4596 try std.testing.expectEqual(@as(i32, 1920), mismatch.total_x_advance);
4597 }
4598
4599 test "shapeRun applies GPOS class contextual single adjustments" {
4600 const bytes = try test_font.createWithGposClassContextualSingleAdjustment(std.testing.allocator);
4601 defer std.testing.allocator.free(bytes);
4602
4603 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4604 defer shaper.deinit();
4605 shaper.setPixelHeightScale(20);
4606
4607 var ctx = Context.init(std.testing.allocator, .{});
4608 defer ctx.deinit();
4609
4610 var output = try Output.init(std.testing.allocator, .{
4611 .max_glyphs = 256,
4612 .max_ligature_carets = 256,
4613 });
4614 defer output.deinit(std.testing.allocator);
4615
4616 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ABC!" } }, &output);
4617 const contextual = output.run();
4618
4619 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
4620 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[0].x_advance);
4621 try std.testing.expectEqual(@as(i32, 64), contextual.glyphs[1].x_offset);
4622 try std.testing.expectEqual(@as(i32, 512), contextual.glyphs[1].x_advance);
4623 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[2].x_advance);
4624 try std.testing.expectEqual(@as(i32, 2432), contextual.total_x_advance);
4625
4626 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AB!" } }, &output);
4627 const mismatch = output.run();
4628
4629 try std.testing.expectEqual(@as(usize, 3), mismatch.glyphs.len);
4630 try std.testing.expectEqual(@as(i32, 0), mismatch.glyphs[1].x_offset);
4631 try std.testing.expectEqual(@as(i32, 640), mismatch.glyphs[1].x_advance);
4632 try std.testing.expectEqual(@as(i32, 1920), mismatch.total_x_advance);
4633 }
4634
4635 test "shapeRun applies GPOS chained contextual single adjustments" {
4636 const bytes = try test_font.createWithGposChainedContextualSingleAdjustment(std.testing.allocator);
4637 defer std.testing.allocator.free(bytes);
4638
4639 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4640 defer shaper.deinit();
4641 shaper.setPixelHeightScale(20);
4642
4643 var ctx = Context.init(std.testing.allocator, .{});
4644 defer ctx.deinit();
4645
4646 var output = try Output.init(std.testing.allocator, .{
4647 .max_glyphs = 256,
4648 .max_ligature_carets = 256,
4649 });
4650 defer output.deinit(std.testing.allocator);
4651
4652 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ABC!" } }, &output);
4653 const contextual = output.run();
4654
4655 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
4656 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[0].x_advance);
4657 try std.testing.expectEqual(@as(i32, 96), contextual.glyphs[1].x_offset);
4658 try std.testing.expectEqual(@as(i32, 480), contextual.glyphs[1].x_advance);
4659 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[2].x_advance);
4660 try std.testing.expectEqual(@as(i32, 2400), contextual.total_x_advance);
4661
4662 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "BC!" } }, &output);
4663 const mismatch = output.run();
4664
4665 try std.testing.expectEqual(@as(usize, 3), mismatch.glyphs.len);
4666 try std.testing.expectEqual(@as(i32, 0), mismatch.glyphs[0].x_offset);
4667 try std.testing.expectEqual(@as(i32, 640), mismatch.glyphs[0].x_advance);
4668 try std.testing.expectEqual(@as(i32, 1920), mismatch.total_x_advance);
4669 }
4670
4671 test "shapeRun uses pre and post context for GPOS chained single adjustments" {
4672 const bytes = try test_font.createWithGposChainedContextualSingleAdjustment(std.testing.allocator);
4673 defer std.testing.allocator.free(bytes);
4674
4675 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4676 defer shaper.deinit();
4677 shaper.setPixelHeightScale(20);
4678
4679 var ctx = Context.init(std.testing.allocator, .{});
4680 defer ctx.deinit();
4681
4682 var output = try Output.init(std.testing.allocator, .{
4683 .max_glyphs = 256,
4684 .max_ligature_carets = 256,
4685 });
4686 defer output.deinit(std.testing.allocator);
4687
4688 try ctx.shapeRun(.{
4689 .font = &shaper,
4690 .text = .{ .utf8 = "B" },
4691 .source_offset = 1,
4692 .pre_context = .{ .utf8 = "A" },
4693 .post_context = .{ .utf8 = "C!" },
4694 }, &output);
4695 const contextual = output.run();
4696
4697 try std.testing.expectEqual(@as(usize, 1), contextual.glyphs.len);
4698 try std.testing.expectEqual(@as(i32, 96), contextual.glyphs[0].x_offset);
4699 try std.testing.expectEqual(@as(i32, 480), contextual.glyphs[0].x_advance);
4700 try std.testing.expectEqual(@as(i32, 480), contextual.total_x_advance);
4701 try std.testing.expectEqual(@as(u32, 1), contextual.glyphs[0].source_start);
4702 try std.testing.expectEqual(@as(u32, 2), contextual.glyphs[0].source_end);
4703 try std.testing.expect(contextual.glyphs[0].flags.unsafe_to_break);
4704 try std.testing.expect(contextual.glyphs[0].flags.unsafe_to_concat);
4705 try std.testing.expectEqual(true, contextual.clusterMap().breakRequiresReshaping(0).?);
4706
4707 try ctx.shapeRun(.{
4708 .font = &shaper,
4709 .text = .{ .utf8 = "B" },
4710 .source_offset = 1,
4711 .pre_context = .{ .utf8 = "A" },
4712 }, &output);
4713 const missing_lookahead = output.run();
4714
4715 try std.testing.expectEqual(@as(usize, 1), missing_lookahead.glyphs.len);
4716 try std.testing.expectEqual(@as(i32, 0), missing_lookahead.glyphs[0].x_offset);
4717 try std.testing.expectEqual(@as(i32, 640), missing_lookahead.glyphs[0].x_advance);
4718 }
4719
4720 test "shapeRun applies GPOS chained simple contextual single adjustments" {
4721 const bytes = try test_font.createWithGposChainedSimpleContextualSingleAdjustment(std.testing.allocator);
4722 defer std.testing.allocator.free(bytes);
4723
4724 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4725 defer shaper.deinit();
4726 shaper.setPixelHeightScale(20);
4727
4728 var ctx = Context.init(std.testing.allocator, .{});
4729 defer ctx.deinit();
4730
4731 var output = try Output.init(std.testing.allocator, .{
4732 .max_glyphs = 256,
4733 .max_ligature_carets = 256,
4734 });
4735 defer output.deinit(std.testing.allocator);
4736
4737 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ABC!" } }, &output);
4738 const contextual = output.run();
4739
4740 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
4741 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[0].x_advance);
4742 try std.testing.expectEqual(@as(i32, 96), contextual.glyphs[1].x_offset);
4743 try std.testing.expectEqual(@as(i32, 480), contextual.glyphs[1].x_advance);
4744 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[2].x_advance);
4745 try std.testing.expectEqual(@as(i32, 2400), contextual.total_x_advance);
4746
4747 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "BC!" } }, &output);
4748 const mismatch = output.run();
4749
4750 try std.testing.expectEqual(@as(usize, 3), mismatch.glyphs.len);
4751 try std.testing.expectEqual(@as(i32, 0), mismatch.glyphs[0].x_offset);
4752 try std.testing.expectEqual(@as(i32, 640), mismatch.glyphs[0].x_advance);
4753 try std.testing.expectEqual(@as(i32, 1920), mismatch.total_x_advance);
4754 }
4755
4756 test "shapeRun applies GPOS chained class contextual single adjustments" {
4757 const bytes = try test_font.createWithGposChainedClassContextualSingleAdjustment(std.testing.allocator);
4758 defer std.testing.allocator.free(bytes);
4759
4760 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4761 defer shaper.deinit();
4762 shaper.setPixelHeightScale(20);
4763
4764 var ctx = Context.init(std.testing.allocator, .{});
4765 defer ctx.deinit();
4766
4767 var output = try Output.init(std.testing.allocator, .{
4768 .max_glyphs = 256,
4769 .max_ligature_carets = 256,
4770 });
4771 defer output.deinit(std.testing.allocator);
4772
4773 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ABC!" } }, &output);
4774 const contextual = output.run();
4775
4776 try std.testing.expectEqual(@as(usize, 4), contextual.glyphs.len);
4777 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[0].x_advance);
4778 try std.testing.expectEqual(@as(i32, 96), contextual.glyphs[1].x_offset);
4779 try std.testing.expectEqual(@as(i32, 480), contextual.glyphs[1].x_advance);
4780 try std.testing.expectEqual(@as(i32, 640), contextual.glyphs[2].x_advance);
4781 try std.testing.expectEqual(@as(i32, 2400), contextual.total_x_advance);
4782
4783 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "BC!" } }, &output);
4784 const mismatch = output.run();
4785
4786 try std.testing.expectEqual(@as(usize, 3), mismatch.glyphs.len);
4787 try std.testing.expectEqual(@as(i32, 0), mismatch.glyphs[0].x_offset);
4788 try std.testing.expectEqual(@as(i32, 640), mismatch.glyphs[0].x_advance);
4789 try std.testing.expectEqual(@as(i32, 1920), mismatch.total_x_advance);
4790 }
4791
4792 test "shapeRun applies GPOS cursive attachments" {
4793 const bytes = try test_font.createWithGposCursiveAttachment(std.testing.allocator);
4794 defer std.testing.allocator.free(bytes);
4795
4796 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4797 defer shaper.deinit();
4798 shaper.setPixelHeightScale(20);
4799
4800 var ctx = Context.init(std.testing.allocator, .{});
4801 defer ctx.deinit();
4802
4803 var output = try Output.init(std.testing.allocator, .{
4804 .max_glyphs = 256,
4805 .max_ligature_carets = 256,
4806 });
4807 defer output.deinit(std.testing.allocator);
4808
4809 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AB!" } }, &output);
4810 const run = output.run();
4811
4812 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
4813 try std.testing.expectEqual(@as(i32, 589), run.glyphs[0].x_advance);
4814 try std.testing.expectEqual(@as(i32, 640), run.glyphs[1].x_advance);
4815 try std.testing.expectEqual(@as(i32, 128), run.glyphs[1].y_offset);
4816 try std.testing.expectEqual(@as(i32, 1869), run.total_x_advance);
4817 try std.testing.expect(run.glyphs[0].flags.unsafe_to_break);
4818 try std.testing.expect(run.glyphs[1].flags.unsafe_to_break);
4819 try std.testing.expectEqual(true, run.clusterMap().breakRequiresReshaping(1).?);
4820
4821 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AB!" }, .writing_mode = .vertical }, &output);
4822 const vertical = output.run();
4823
4824 try std.testing.expectEqual(@as(usize, 3), vertical.glyphs.len);
4825 try std.testing.expectEqual(@as(i32, 0), vertical.glyphs[1].y_offset);
4826
4827 try ctx.shapeRun(.{
4828 .font = &shaper,
4829 .text = .{ .utf8 = "AB!" },
4830 .writing_mode = .vertical,
4831 .features = &.{.{ .tag = font.tag("curs"), .value = 1 }},
4832 }, &output);
4833 const vertical_enabled = output.run();
4834
4835 try std.testing.expectEqual(@as(usize, 3), vertical_enabled.glyphs.len);
4836 try std.testing.expectEqual(@as(i32, 128), vertical_enabled.glyphs[1].y_offset);
4837 }
4838
4839 test "shapeRun applies GPOS cursive attachment source ranges" {
4840 const bytes = try test_font.createWithGposCursiveAttachment(std.testing.allocator);
4841 defer std.testing.allocator.free(bytes);
4842
4843 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4844 defer shaper.deinit();
4845 shaper.setPixelHeightScale(20);
4846
4847 var ctx = Context.init(std.testing.allocator, .{});
4848 defer ctx.deinit();
4849
4850 var output = try Output.init(std.testing.allocator, .{
4851 .max_glyphs = 256,
4852 .max_ligature_carets = 256,
4853 });
4854 defer output.deinit(std.testing.allocator);
4855
4856 try ctx.shapeRun(.{
4857 .font = &shaper,
4858 .text = .{ .utf8 = "ABAB" },
4859 .features = &.{.{ .tag = font.tag("curs"), .value = 0, .source = .{ .start = 1, .end = 2 } }},
4860 }, &output);
4861 const run = output.run();
4862
4863 try std.testing.expectEqual(@as(usize, 4), run.glyphs.len);
4864 try std.testing.expectEqual(@as(i32, 0), run.glyphs[1].y_offset);
4865 try std.testing.expectEqual(@as(i32, 128), run.glyphs[3].y_offset);
4866 }
4867
4868 test "shapeRun applies GPOS mark-to-base attachment" {
4869 const bytes = try test_font.createWithGposMarkToBase(std.testing.allocator);
4870 defer std.testing.allocator.free(bytes);
4871
4872 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4873 defer shaper.deinit();
4874 shaper.setPixelHeightScale(20);
4875
4876 var ctx = Context.init(std.testing.allocator, .{});
4877 defer ctx.deinit();
4878
4879 var output = try Output.init(std.testing.allocator, .{
4880 .max_glyphs = 256,
4881 .max_ligature_carets = 256,
4882 });
4883 defer output.deinit(std.testing.allocator);
4884
4885 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A^!" } }, &output);
4886 const run = output.run();
4887
4888 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
4889 try std.testing.expectEqual(GlyphClass.base, run.glyphs[0].glyph_class);
4890 try std.testing.expectEqual(GlyphClass.mark, run.glyphs[1].glyph_class);
4891 try std.testing.expectEqual(@as(i32, 640), run.glyphs[0].x_advance);
4892 try std.testing.expectEqual(@as(i32, 0), run.glyphs[1].x_advance);
4893 try std.testing.expectEqual(@as(i32, -448), run.glyphs[1].x_offset);
4894 try std.testing.expectEqual(@as(i32, 896), run.glyphs[1].y_offset);
4895 try std.testing.expectEqual(@as(i32, 1280), run.total_x_advance);
4896 try std.testing.expect(run.glyphs[0].attachment == null);
4897 const attachment = run.glyphs[1].attachment.?;
4898 try std.testing.expectEqual(GlyphAttachmentKind.base, attachment.kind);
4899 try std.testing.expectEqual(@as(u32, 0), attachment.target_glyph_index);
4900 try std.testing.expectEqual(@as(u16, 0), attachment.ligature_component);
4901 try std.testing.expect(!run.glyphs[0].flags.unsafe_to_break);
4902 try std.testing.expect(run.glyphs[0].flags.unsafe_to_concat);
4903 try std.testing.expect(run.glyphs[1].flags.unsafe_to_break);
4904 try std.testing.expect(run.glyphs[1].flags.unsafe_to_concat);
4905 try std.testing.expectEqual(true, run.clusterMap().breakRequiresReshaping(1).?);
4906 }
4907
4908 test "shapeRun remaps mark attachments in RTL visual output" {
4909 const bytes = try test_font.createWithGposMarkToBase(std.testing.allocator);
4910 defer std.testing.allocator.free(bytes);
4911
4912 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4913 defer shaper.deinit();
4914 shaper.setPixelHeightScale(20);
4915
4916 var ctx = Context.init(std.testing.allocator, .{});
4917 defer ctx.deinit();
4918
4919 var output = try Output.init(std.testing.allocator, .{
4920 .max_glyphs = 256,
4921 .max_ligature_carets = 256,
4922 });
4923 defer output.deinit(std.testing.allocator);
4924
4925 try ctx.shapeRun(.{
4926 .font = &shaper,
4927 .text = .{ .utf8 = "A^!" },
4928 .direction = .rtl,
4929 }, &output);
4930 const run = output.run();
4931
4932 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
4933 try std.testing.expectEqual(@as(u32, '!' - 31), run.glyphs[0].glyph_id);
4934 try std.testing.expectEqual(GlyphClass.mark, run.glyphs[1].glyph_class);
4935 try std.testing.expectEqual(@as(u32, 'A' - 31), run.glyphs[2].glyph_id);
4936 try std.testing.expectEqual(@as(i32, 192), run.glyphs[1].x_offset);
4937 try std.testing.expectEqual(@as(i32, 896), run.glyphs[1].y_offset);
4938 const attachment = run.glyphs[1].attachment.?;
4939 try std.testing.expectEqual(GlyphAttachmentKind.base, attachment.kind);
4940 try std.testing.expectEqual(@as(u32, 2), attachment.target_glyph_index);
4941 }
4942
4943 test "shapeRun applies GPOS lookups in lookup list order" {
4944 const bytes = try test_font.createWithGposMarkToBaseThenPair(std.testing.allocator);
4945 defer std.testing.allocator.free(bytes);
4946
4947 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4948 defer shaper.deinit();
4949 shaper.setPixelHeightScale(20);
4950
4951 var ctx = Context.init(std.testing.allocator, .{});
4952 defer ctx.deinit();
4953
4954 var output = try Output.init(std.testing.allocator, .{
4955 .max_glyphs = 256,
4956 .max_ligature_carets = 256,
4957 });
4958 defer output.deinit(std.testing.allocator);
4959
4960 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A^V" } }, &output);
4961 const run = output.run();
4962
4963 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
4964 try std.testing.expectEqual(@as(i32, 538), run.glyphs[0].x_advance);
4965 try std.testing.expectEqual(@as(i32, 0), run.glyphs[1].x_advance);
4966 try std.testing.expectEqual(@as(i32, -448), run.glyphs[1].x_offset);
4967 try std.testing.expectEqual(@as(i32, 896), run.glyphs[1].y_offset);
4968 try std.testing.expectEqual(@as(i32, 1178), run.total_x_advance);
4969 }
4970
4971 test "shapeRun applies GPOS mark-to-base source ranges" {
4972 const bytes = try test_font.createWithGposMarkToBase(std.testing.allocator);
4973 defer std.testing.allocator.free(bytes);
4974
4975 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
4976 defer shaper.deinit();
4977 shaper.setPixelHeightScale(20);
4978
4979 var ctx = Context.init(std.testing.allocator, .{});
4980 defer ctx.deinit();
4981
4982 var output = try Output.init(std.testing.allocator, .{
4983 .max_glyphs = 256,
4984 .max_ligature_carets = 256,
4985 });
4986 defer output.deinit(std.testing.allocator);
4987
4988 try ctx.shapeRun(.{
4989 .font = &shaper,
4990 .text = .{ .utf8 = "A^A^" },
4991 .features = &.{.{ .tag = font.tag("mark"), .value = 0, .source = .{ .start = 1, .end = 2 } }},
4992 }, &output);
4993 const run = output.run();
4994
4995 try std.testing.expectEqual(@as(usize, 4), run.glyphs.len);
4996 try std.testing.expectEqual(@as(i32, 0), run.glyphs[1].x_offset);
4997 try std.testing.expectEqual(@as(i32, 0), run.glyphs[1].y_offset);
4998 try std.testing.expectEqual(@as(i32, -448), run.glyphs[3].x_offset);
4999 try std.testing.expectEqual(@as(i32, 896), run.glyphs[3].y_offset);
5000 try std.testing.expect(run.glyphs[1].attachment == null);
5001 try std.testing.expect(run.glyphs[3].attachment != null);
5002 }
5003
5004 test "shapeRun applies GPOS above and below-base mark positioning by default" {
5005 const abvm_bytes = try test_font.createWithGposAboveBaseMarkPositioning(std.testing.allocator);
5006 defer std.testing.allocator.free(abvm_bytes);
5007 const blwm_bytes = try test_font.createWithGposBelowBaseMarkPositioning(std.testing.allocator);
5008 defer std.testing.allocator.free(blwm_bytes);
5009
5010 var abvm_shaper = Font.initFromBytes(abvm_bytes.ptr, abvm_bytes.len) orelse return error.TestUnexpectedResult;
5011 defer abvm_shaper.deinit();
5012 var blwm_shaper = Font.initFromBytes(blwm_bytes.ptr, blwm_bytes.len) orelse return error.TestUnexpectedResult;
5013 defer blwm_shaper.deinit();
5014 abvm_shaper.setPixelHeightScale(20);
5015 blwm_shaper.setPixelHeightScale(20);
5016
5017 var ctx = Context.init(std.testing.allocator, .{});
5018 defer ctx.deinit();
5019
5020 var output = try Output.init(std.testing.allocator, .{
5021 .max_glyphs = 256,
5022 .max_ligature_carets = 256,
5023 });
5024 defer output.deinit(std.testing.allocator);
5025
5026 try ctx.shapeRun(.{ .font = &abvm_shaper, .text = .{ .utf8 = "A^" } }, &output);
5027 const abvm_run = output.run();
5028 try std.testing.expectEqual(@as(usize, 2), abvm_run.glyphs.len);
5029 try std.testing.expectEqual(@as(i32, -448), abvm_run.glyphs[1].x_offset);
5030 try std.testing.expectEqual(@as(i32, 896), abvm_run.glyphs[1].y_offset);
5031
5032 try ctx.shapeRun(.{ .font = &blwm_shaper, .text = .{ .utf8 = "A^" } }, &output);
5033 const blwm_run = output.run();
5034 try std.testing.expectEqual(@as(usize, 2), blwm_run.glyphs.len);
5035 try std.testing.expectEqual(@as(i32, -448), blwm_run.glyphs[1].x_offset);
5036 try std.testing.expectEqual(@as(i32, 896), blwm_run.glyphs[1].y_offset);
5037
5038 try ctx.shapeRun(.{
5039 .font = &abvm_shaper,
5040 .text = .{ .utf8 = "A^" },
5041 .features = &.{.{ .tag = font.tag("abvm"), .value = 0 }},
5042 }, &output);
5043 const disabled_run = output.run();
5044 try std.testing.expectEqual(@as(usize, 2), disabled_run.glyphs.len);
5045 try std.testing.expectEqual(@as(i32, 0), disabled_run.glyphs[1].x_offset);
5046 try std.testing.expectEqual(@as(i32, 0), disabled_run.glyphs[1].y_offset);
5047 }
5048
5049 test "shapeRun applies GPOS mark attachment type filters" {
5050 const bytes = try test_font.createWithGposMarkToBase(std.testing.allocator);
5051 defer std.testing.allocator.free(bytes);
5052 try fixture_binary.setLayoutLookupFlag(bytes, "GPOS", 0, 0x0300);
5053 const gdef_offset = try fixture_binary.tableOffset(bytes, "GDEF");
5054 fixture_binary.writeU16(bytes, gdef_offset + 10, 12);
5055
5056 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5057 defer shaper.deinit();
5058 shaper.setPixelHeightScale(20);
5059
5060 var ctx = Context.init(std.testing.allocator, .{});
5061 defer ctx.deinit();
5062
5063 var output = try Output.init(std.testing.allocator, .{
5064 .max_glyphs = 256,
5065 .max_ligature_carets = 256,
5066 });
5067 defer output.deinit(std.testing.allocator);
5068
5069 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A^" } }, &output);
5070 const run = output.run();
5071
5072 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
5073 try std.testing.expectEqual(@as(i32, -448), run.glyphs[1].x_offset);
5074 try std.testing.expectEqual(@as(i32, 896), run.glyphs[1].y_offset);
5075 }
5076
5077 test "shapeRun applies GPOS mark-to-ligature attachment" {
5078 const bytes = try test_font.createWithGposMarkToLigature(std.testing.allocator);
5079 defer std.testing.allocator.free(bytes);
5080
5081 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5082 defer shaper.deinit();
5083 shaper.setPixelHeightScale(20);
5084
5085 var ctx = Context.init(std.testing.allocator, .{});
5086 defer ctx.deinit();
5087
5088 var output = try Output.init(std.testing.allocator, .{
5089 .max_glyphs = 256,
5090 .max_ligature_carets = 256,
5091 });
5092 defer output.deinit(std.testing.allocator);
5093
5094 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "fi^!" } }, &output);
5095 const run = output.run();
5096
5097 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
5098 try std.testing.expectEqual(@as(u32, 96), run.glyphs[0].glyph_id);
5099 try std.testing.expectEqual(GlyphClass.ligature, run.glyphs[0].glyph_class);
5100 try std.testing.expectEqual(GlyphClass.mark, run.glyphs[1].glyph_class);
5101 try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].source_start);
5102 try std.testing.expectEqual(@as(u32, 2), run.glyphs[0].source_end);
5103 try std.testing.expectEqual(@as(u16, 2), run.glyphs[0].source_codepoint_count);
5104 try std.testing.expectEqual(@as(i32, 640), run.glyphs[0].x_advance);
5105 try std.testing.expectEqual(@as(i32, 0), run.glyphs[1].x_advance);
5106 try std.testing.expectEqual(@as(i32, -346), run.glyphs[1].x_offset);
5107 try std.testing.expectEqual(@as(i32, 832), run.glyphs[1].y_offset);
5108 try std.testing.expectEqual(@as(i32, 1280), run.total_x_advance);
5109 const attachment = run.glyphs[1].attachment.?;
5110 try std.testing.expectEqual(GlyphAttachmentKind.ligature, attachment.kind);
5111 try std.testing.expectEqual(@as(u32, 0), attachment.target_glyph_index);
5112 try std.testing.expectEqual(@as(u16, 1), attachment.ligature_component);
5113 try std.testing.expect(!run.glyphs[0].flags.unsafe_to_break);
5114 try std.testing.expect(run.glyphs[0].flags.unsafe_to_concat);
5115 try std.testing.expect(run.glyphs[1].flags.unsafe_to_break);
5116 try std.testing.expect(run.glyphs[1].flags.unsafe_to_concat);
5117 try std.testing.expectEqual(true, run.clusterMap().breakRequiresReshaping(1).?);
5118 }
5119
5120 test "shapeRun applies GPOS mark-to-ligature source ranges" {
5121 const bytes = try test_font.createWithGposMarkToLigature(std.testing.allocator);
5122 defer std.testing.allocator.free(bytes);
5123
5124 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5125 defer shaper.deinit();
5126 shaper.setPixelHeightScale(20);
5127
5128 var ctx = Context.init(std.testing.allocator, .{});
5129 defer ctx.deinit();
5130
5131 var output = try Output.init(std.testing.allocator, .{
5132 .max_glyphs = 256,
5133 .max_ligature_carets = 256,
5134 });
5135 defer output.deinit(std.testing.allocator);
5136
5137 try ctx.shapeRun(.{
5138 .font = &shaper,
5139 .text = .{ .utf8 = "fi^fi^" },
5140 .features = &.{.{ .tag = font.tag("mark"), .value = 0, .source = .{ .start = 2, .end = 3 } }},
5141 }, &output);
5142 const run = output.run();
5143
5144 try std.testing.expectEqual(@as(usize, 4), run.glyphs.len);
5145 try std.testing.expectEqual(@as(i32, 0), run.glyphs[1].x_offset);
5146 try std.testing.expectEqual(@as(i32, 0), run.glyphs[1].y_offset);
5147 try std.testing.expectEqual(@as(i32, -346), run.glyphs[3].x_offset);
5148 try std.testing.expectEqual(@as(i32, 832), run.glyphs[3].y_offset);
5149 }
5150
5151 test "shapeRun applies GPOS mark-to-mark attachment" {
5152 const bytes = try test_font.createWithGposMarkToMark(std.testing.allocator);
5153 defer std.testing.allocator.free(bytes);
5154
5155 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5156 defer shaper.deinit();
5157 shaper.setPixelHeightScale(20);
5158
5159 var ctx = Context.init(std.testing.allocator, .{});
5160 defer ctx.deinit();
5161
5162 var output = try Output.init(std.testing.allocator, .{
5163 .max_glyphs = 256,
5164 .max_ligature_carets = 256,
5165 });
5166 defer output.deinit(std.testing.allocator);
5167
5168 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "^_!" } }, &output);
5169 const run = output.run();
5170
5171 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
5172 try std.testing.expectEqual(GlyphClass.mark, run.glyphs[0].glyph_class);
5173 try std.testing.expectEqual(GlyphClass.mark, run.glyphs[1].glyph_class);
5174 try std.testing.expectEqual(@as(i32, 0), run.glyphs[0].x_advance);
5175 try std.testing.expectEqual(@as(i32, 0), run.glyphs[1].x_advance);
5176 try std.testing.expectEqual(@as(i32, 26), run.glyphs[1].x_offset);
5177 try std.testing.expectEqual(@as(i32, 384), run.glyphs[1].y_offset);
5178 try std.testing.expectEqual(@as(i32, 640), run.total_x_advance);
5179 const attachment = run.glyphs[1].attachment.?;
5180 try std.testing.expectEqual(GlyphAttachmentKind.mark, attachment.kind);
5181 try std.testing.expectEqual(@as(u32, 0), attachment.target_glyph_index);
5182 try std.testing.expectEqual(@as(u16, 0), attachment.ligature_component);
5183 try std.testing.expect(!run.glyphs[0].flags.unsafe_to_break);
5184 try std.testing.expect(run.glyphs[0].flags.unsafe_to_concat);
5185 try std.testing.expect(run.glyphs[1].flags.unsafe_to_break);
5186 try std.testing.expect(run.glyphs[1].flags.unsafe_to_concat);
5187 try std.testing.expectEqual(true, run.clusterMap().breakRequiresReshaping(1).?);
5188 }
5189
5190 test "shapeRun applies GPOS mark-to-mark source ranges" {
5191 const bytes = try test_font.createWithGposMarkToMark(std.testing.allocator);
5192 defer std.testing.allocator.free(bytes);
5193
5194 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5195 defer shaper.deinit();
5196 shaper.setPixelHeightScale(20);
5197
5198 var ctx = Context.init(std.testing.allocator, .{});
5199 defer ctx.deinit();
5200
5201 var output = try Output.init(std.testing.allocator, .{
5202 .max_glyphs = 256,
5203 .max_ligature_carets = 256,
5204 });
5205 defer output.deinit(std.testing.allocator);
5206
5207 try ctx.shapeRun(.{
5208 .font = &shaper,
5209 .text = .{ .utf8 = "^_^_" },
5210 .features = &.{.{ .tag = font.tag("mkmk"), .value = 0, .source = .{ .start = 1, .end = 2 } }},
5211 }, &output);
5212 const run = output.run();
5213
5214 try std.testing.expectEqual(@as(usize, 4), run.glyphs.len);
5215 try std.testing.expectEqual(@as(i32, 0), run.glyphs[1].x_offset);
5216 try std.testing.expectEqual(@as(i32, 0), run.glyphs[1].y_offset);
5217 try std.testing.expectEqual(@as(i32, 26), run.glyphs[3].x_offset);
5218 try std.testing.expectEqual(@as(i32, 384), run.glyphs[3].y_offset);
5219 }
5220
5221 test "shapeRun prefers GPOS pair adjustments over classic kern fallback" {
5222 const bytes = try test_font.createWithKernAndGposPairAdjustment(std.testing.allocator);
5223 defer std.testing.allocator.free(bytes);
5224
5225 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5226 defer shaper.deinit();
5227 shaper.setPixelHeightScale(20);
5228
5229 var ctx = Context.init(std.testing.allocator, .{});
5230 defer ctx.deinit();
5231
5232 var output = try Output.init(std.testing.allocator, .{
5233 .max_glyphs = 256,
5234 .max_ligature_carets = 256,
5235 });
5236 defer output.deinit(std.testing.allocator);
5237
5238 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AV" } }, &output);
5239 const run = output.run();
5240
5241 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
5242 try std.testing.expectEqual(@as(i32, 538), run.glyphs[0].x_advance);
5243 try std.testing.expectEqual(@as(i32, 1178), run.total_x_advance);
5244 }
5245
5246 test "shapeRun disables GPOS pair positioning with feature settings" {
5247 const bytes = try test_font.createWithKernAndGposPairAdjustment(std.testing.allocator);
5248 defer std.testing.allocator.free(bytes);
5249
5250 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5251 defer shaper.deinit();
5252 shaper.setPixelHeightScale(20);
5253
5254 var ctx = Context.init(std.testing.allocator, .{});
5255 defer ctx.deinit();
5256
5257 var output = try Output.init(std.testing.allocator, .{
5258 .max_glyphs = 256,
5259 .max_ligature_carets = 256,
5260 });
5261 defer output.deinit(std.testing.allocator);
5262
5263 try ctx.shapeRun(.{
5264 .font = &shaper,
5265 .text = .{ .utf8 = "AV" },
5266 .features = &.{.{ .tag = font.tag("kern"), .value = 0 }},
5267 }, &output);
5268 const run = output.run();
5269
5270 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
5271 try std.testing.expectEqual(@as(i32, 640), run.glyphs[0].x_advance);
5272 try std.testing.expectEqual(@as(i32, 640), run.glyphs[1].x_advance);
5273 try std.testing.expectEqual(@as(i32, 1280), run.total_x_advance);
5274 }
5275
5276 test "shapeRun reuses output capacity across runs" {
5277 const bytes = try test_font.create(std.testing.allocator);
5278 defer std.testing.allocator.free(bytes);
5279
5280 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5281 defer shaper.deinit();
5282
5283 var ctx = Context.init(std.testing.allocator, .{});
5284 defer ctx.deinit();
5285
5286 var output = try Output.init(std.testing.allocator, .{
5287 .max_glyphs = 256,
5288 .max_ligature_carets = 256,
5289 });
5290 defer output.deinit(std.testing.allocator);
5291
5292 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "abcdef" } }, &output);
5293 const glyph_capacity = output.glyphCapacity();
5294 const cluster_capacity = output.clusterCapacity();
5295
5296 try ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "ab" } }, &output);
5297 try std.testing.expectEqual(@as(usize, 2), output.run().glyphs.len);
5298 try std.testing.expectEqual(glyph_capacity, output.glyphCapacity());
5299 try std.testing.expectEqual(cluster_capacity, output.clusterCapacity());
5300 }
5301
5302 test "shapeRun emits RTL visual order by cluster" {
5303 const bytes = try test_font.createWithGsubMultipleSubstitution(std.testing.allocator);
5304 defer std.testing.allocator.free(bytes);
5305
5306 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5307 defer shaper.deinit();
5308 shaper.setPixelHeightScale(20);
5309
5310 var ctx = Context.init(std.testing.allocator, .{});
5311 defer ctx.deinit();
5312
5313 var output = try Output.init(std.testing.allocator, .{
5314 .max_glyphs = 256,
5315 .max_ligature_carets = 256,
5316 });
5317 defer output.deinit(std.testing.allocator);
5318
5319 try ctx.shapeRun(.{
5320 .font = &shaper,
5321 .text = .{ .utf8 = "A!" },
5322 .direction = .rtl,
5323 }, &output);
5324 const run = output.run();
5325 const map = run.clusterMap();
5326
5327 try std.testing.expectEqual(Direction.rtl, run.direction);
5328 try std.testing.expectEqual(OutputOrder.visual, run.output_order);
5329 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
5330 try std.testing.expectEqual(@as(usize, 2), run.clusters.len);
5331 try std.testing.expectEqual(@as(u32, '!' - 31), run.glyphs[0].glyph_id);
5332 try std.testing.expectEqual(@as(u32, 'X' - 31), run.glyphs[1].glyph_id);
5333 try std.testing.expectEqual(@as(u32, 'Y' - 31), run.glyphs[2].glyph_id);
5334 try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].cluster_index);
5335 try std.testing.expectEqual(@as(u32, 1), run.glyphs[1].cluster_index);
5336 try std.testing.expectEqual(@as(u32, 1), run.glyphs[2].cluster_index);
5337 try std.testing.expectEqual(SourceRange{ .start = 1, .end = 2 }, map.clusterSourceRange(0).?);
5338 try std.testing.expectEqual(SourceRange{ .start = 0, .end = 1 }, map.clusterSourceRange(1).?);
5339 try std.testing.expectEqual(GlyphSpan{ .start = 1, .end = 3 }, map.clusterGlyphSpan(1).?);
5340 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(0).?);
5341 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(1).?);
5342 try std.testing.expectEqual(@as(i32, 1920), run.total_x_advance);
5343 }
5344
5345 test "shapeRun keeps RTL logical output in source order" {
5346 const bytes = try test_font.create(std.testing.allocator);
5347 defer std.testing.allocator.free(bytes);
5348
5349 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5350 defer shaper.deinit();
5351 shaper.setPixelHeightScale(20);
5352
5353 var ctx = Context.init(std.testing.allocator, .{});
5354 defer ctx.deinit();
5355
5356 var output = try Output.init(std.testing.allocator, .{
5357 .max_glyphs = 256,
5358 .max_ligature_carets = 256,
5359 });
5360 defer output.deinit(std.testing.allocator);
5361
5362 try ctx.shapeRun(.{
5363 .font = &shaper,
5364 .text = .{ .utf8 = "ABC" },
5365 .direction = .rtl,
5366 .output_order = .logical,
5367 }, &output);
5368 const run = output.run();
5369 const map = run.clusterMap();
5370
5371 try std.testing.expectEqual(Direction.rtl, run.direction);
5372 try std.testing.expectEqual(OutputOrder.logical, run.output_order);
5373 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
5374 try std.testing.expectEqual(@as(u32, 'A' - 31), run.glyphs[0].glyph_id);
5375 try std.testing.expectEqual(@as(u32, 'B' - 31), run.glyphs[1].glyph_id);
5376 try std.testing.expectEqual(@as(u32, 'C' - 31), run.glyphs[2].glyph_id);
5377 try std.testing.expectEqual(SourceRange{ .start = 0, .end = 1 }, map.clusterSourceRange(0).?);
5378 try std.testing.expectEqual(SourceRange{ .start = 1, .end = 2 }, map.clusterSourceRange(1).?);
5379 try std.testing.expectEqual(SourceRange{ .start = 2, .end = 3 }, map.clusterSourceRange(2).?);
5380 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(0).?);
5381 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(1).?);
5382 try std.testing.expectEqual(@as(usize, 2), map.byteOffsetToCluster(2).?);
5383 }
5384
5385 test "shapeRun merges combining marks in grapheme cluster modes" {
5386 const bytes = try test_font.create(std.testing.allocator);
5387 defer std.testing.allocator.free(bytes);
5388
5389 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5390 defer shaper.deinit();
5391 shaper.setPixelHeightScale(20);
5392
5393 var ctx = Context.init(std.testing.allocator, .{});
5394 defer ctx.deinit();
5395
5396 var output = try Output.init(std.testing.allocator, .{
5397 .max_glyphs = 256,
5398 .max_ligature_carets = 256,
5399 });
5400 defer output.deinit(std.testing.allocator);
5401
5402 try ctx.shapeRun(.{
5403 .font = &shaper,
5404 .text = .{ .utf8 = "A\u{0301}B" },
5405 .cluster_mode = .monotone_graphemes,
5406 }, &output);
5407 const run = output.run();
5408 const map = run.clusterMap();
5409
5410 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
5411 try std.testing.expectEqual(@as(usize, 2), run.clusters.len);
5412 try std.testing.expectEqual(SourceRange{ .start = 0, .end = 3 }, map.clusterSourceRange(0).?);
5413 try std.testing.expectEqual(SourceRange{ .start = 3, .end = 4 }, map.clusterSourceRange(1).?);
5414 try std.testing.expectEqual(GlyphSpan{ .start = 0, .end = 2 }, map.clusterGlyphSpan(0).?);
5415 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(0).?);
5416 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(1).?);
5417 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(2).?);
5418 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(3).?);
5419 try std.testing.expectEqual(@as(u32, 0), run.glyphs[1].cluster_index);
5420 try std.testing.expectEqual(@as(u32, 0), run.glyphs[1].source_start);
5421 try std.testing.expectEqual(@as(u32, 3), run.glyphs[1].source_end);
5422 try std.testing.expectEqual(@as(u16, 2), run.glyphs[1].source_codepoint_count);
5423 try std.testing.expectEqual(true, map.selectableAsUnitOnly(0).?);
5424 }
5425
5426 test "shapeRun keeps combining marks separate in character cluster modes" {
5427 const bytes = try test_font.create(std.testing.allocator);
5428 defer std.testing.allocator.free(bytes);
5429
5430 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5431 defer shaper.deinit();
5432
5433 var ctx = Context.init(std.testing.allocator, .{});
5434 defer ctx.deinit();
5435
5436 var output = try Output.init(std.testing.allocator, .{
5437 .max_glyphs = 256,
5438 .max_ligature_carets = 256,
5439 });
5440 defer output.deinit(std.testing.allocator);
5441
5442 try ctx.shapeRun(.{
5443 .font = &shaper,
5444 .text = .{ .utf8 = "A\u{0301}" },
5445 .cluster_mode = .characters,
5446 }, &output);
5447 const run = output.run();
5448 const map = run.clusterMap();
5449
5450 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
5451 try std.testing.expectEqual(@as(usize, 2), run.clusters.len);
5452 try std.testing.expectEqual(SourceRange{ .start = 0, .end = 1 }, map.clusterSourceRange(0).?);
5453 try std.testing.expectEqual(SourceRange{ .start = 1, .end = 3 }, map.clusterSourceRange(1).?);
5454 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(0).?);
5455 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(1).?);
5456 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(2).?);
5457 try std.testing.expect(run.glyphs[1].flags.unsafe_to_break);
5458 }
5459
5460 const GraphemeBoundaryCase = struct {
5461 text: []const u32,
5462 ranges: []const SourceRange,
5463 };
5464
5465 test "shapeRun follows Unicode grapheme boundary classes" {
5466 const bytes = try test_font.create(std.testing.allocator);
5467 defer std.testing.allocator.free(bytes);
5468
5469 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5470 defer shaper.deinit();
5471
5472 var ctx = Context.init(std.testing.allocator, .{});
5473 defer ctx.deinit();
5474
5475 var output = try Output.init(std.testing.allocator, .{
5476 .max_glyphs = 256,
5477 .max_ligature_carets = 256,
5478 });
5479 defer output.deinit(std.testing.allocator);
5480
5481 const crlf = [_]u32{ 0x000d, 0x000a, 'A' };
5482 const crlf_ranges = [_]SourceRange{ .{ .start = 0, .end = 8 }, .{ .start = 8, .end = 12 } };
5483 const hangul = [_]u32{ 0x1100, 0x1161, 0x11a8 };
5484 const hangul_ranges = [_]SourceRange{.{ .start = 0, .end = 12 }};
5485 const thai_spacing_mark = [_]u32{ 0x0e01, 0x0e33 };
5486 const thai_spacing_mark_ranges = [_]SourceRange{.{ .start = 0, .end = 8 }};
5487 const prepend = [_]u32{ 0x0600, 'A' };
5488 const prepend_ranges = [_]SourceRange{.{ .start = 0, .end = 8 }};
5489 const plain_zwj = [_]u32{ 'A', 0x200d, 'B' };
5490 const plain_zwj_ranges = [_]SourceRange{ .{ .start = 0, .end = 8 }, .{ .start = 8, .end = 12 } };
5491 const indic_conjunct = [_]u32{ 0x0915, 0x094d, 0x0937 };
5492 const indic_conjunct_ranges = [_]SourceRange{.{ .start = 0, .end = 12 }};
5493
5494 const cases = [_]GraphemeBoundaryCase{
5495 .{ .text = &crlf, .ranges = &crlf_ranges },
5496 .{ .text = &hangul, .ranges = &hangul_ranges },
5497 .{ .text = &thai_spacing_mark, .ranges = &thai_spacing_mark_ranges },
5498 .{ .text = &prepend, .ranges = &prepend_ranges },
5499 .{ .text = &plain_zwj, .ranges = &plain_zwj_ranges },
5500 .{ .text = &indic_conjunct, .ranges = &indic_conjunct_ranges },
5501 };
5502
5503 for (cases) |case| {
5504 try ctx.shapeRun(.{
5505 .font = &shaper,
5506 .text = .{ .utf32 = case.text },
5507 .cluster_mode = .graphemes,
5508 }, &output);
5509 const map = output.run().clusterMap();
5510 try std.testing.expectEqual(case.ranges.len, output.run().clusters.len);
5511 for (case.ranges, 0..) |range, cluster_index| {
5512 try std.testing.expectEqual(range, map.clusterSourceRange(cluster_index).?);
5513 }
5514 }
5515 }
5516
5517 test "shapeRun leaves leading combining marks alone by default" {
5518 const bytes = try test_font.create(std.testing.allocator);
5519 defer std.testing.allocator.free(bytes);
5520
5521 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5522 defer shaper.deinit();
5523 shaper.setPixelHeightScale(20);
5524
5525 var ctx = Context.init(std.testing.allocator, .{});
5526 defer ctx.deinit();
5527
5528 var output = try Output.init(std.testing.allocator, .{
5529 .max_glyphs = 256,
5530 .max_ligature_carets = 256,
5531 });
5532 defer output.deinit(std.testing.allocator);
5533
5534 try ctx.shapeRun(.{
5535 .font = &shaper,
5536 .text = .{ .utf8 = "\u{0301}A" },
5537 .cluster_mode = .monotone_graphemes,
5538 }, &output);
5539 const run = output.run();
5540 const map = run.clusterMap();
5541
5542 try std.testing.expectEqual(@as(usize, 2), run.glyphs.len);
5543 try std.testing.expectEqual(@as(usize, 2), run.clusters.len);
5544 try std.testing.expectEqual(@as(u32, 0), run.glyphs[0].glyph_id);
5545 try std.testing.expect(run.glyphs[0].flags.missing_glyph);
5546 try std.testing.expect(!run.glyphs[0].flags.synthetic);
5547 try std.testing.expectEqual(SourceRange{ .start = 0, .end = 2 }, map.clusterSourceRange(0).?);
5548 try std.testing.expectEqual(GlyphSpan{ .start = 0, .end = 1 }, map.clusterGlyphSpan(0).?);
5549 }
5550
5551 test "shapeRun inserts dotted circle before leading combining marks when requested" {
5552 const bytes = try test_font.create(std.testing.allocator);
5553 defer std.testing.allocator.free(bytes);
5554
5555 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5556 defer shaper.deinit();
5557 shaper.setPixelHeightScale(20);
5558
5559 var ctx = Context.init(std.testing.allocator, .{});
5560 defer ctx.deinit();
5561
5562 var output = try Output.init(std.testing.allocator, .{
5563 .max_glyphs = 256,
5564 .max_ligature_carets = 256,
5565 });
5566 defer output.deinit(std.testing.allocator);
5567
5568 try ctx.shapeRun(.{
5569 .font = &shaper,
5570 .text = .{ .utf8 = "\u{0301}A" },
5571 .cluster_mode = .monotone_graphemes,
5572 .dotted_circle_policy = .insert,
5573 }, &output);
5574 const run = output.run();
5575 const map = run.clusterMap();
5576
5577 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
5578 try std.testing.expectEqual(@as(usize, 2), run.clusters.len);
5579 try std.testing.expectEqual(@as(u32, 96), run.glyphs[0].glyph_id);
5580 try std.testing.expect(run.glyphs[0].flags.synthetic);
5581 try std.testing.expectEqual(@as(u32, 0), run.glyphs[1].glyph_id);
5582 try std.testing.expect(run.glyphs[1].flags.synthetic);
5583 try std.testing.expect(run.glyphs[1].flags.missing_glyph);
5584 try std.testing.expectEqual(SourceRange{ .start = 0, .end = 2 }, map.clusterSourceRange(0).?);
5585 try std.testing.expectEqual(GlyphSpan{ .start = 0, .end = 2 }, map.clusterGlyphSpan(0).?);
5586 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(0).?);
5587 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(1).?);
5588 try std.testing.expectEqual(true, map.selectableAsUnitOnly(0).?);
5589 }
5590
5591 test "shapeRun merges emoji zwj sequences in grapheme cluster modes" {
5592 const bytes = try test_font.create(std.testing.allocator);
5593 defer std.testing.allocator.free(bytes);
5594
5595 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5596 defer shaper.deinit();
5597
5598 var ctx = Context.init(std.testing.allocator, .{});
5599 defer ctx.deinit();
5600
5601 var output = try Output.init(std.testing.allocator, .{
5602 .max_glyphs = 256,
5603 .max_ligature_carets = 256,
5604 });
5605 defer output.deinit(std.testing.allocator);
5606
5607 const text = [_]u32{ 0x1f469, 0x200d, 0x1f469 };
5608 try ctx.shapeRun(.{
5609 .font = &shaper,
5610 .text = .{ .utf32 = &text },
5611 .cluster_mode = .graphemes,
5612 }, &output);
5613 const run = output.run();
5614 const map = run.clusterMap();
5615
5616 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
5617 try std.testing.expectEqual(@as(usize, 1), run.clusters.len);
5618 try std.testing.expectEqual(SourceRange{ .start = 0, .end = 12 }, map.clusterSourceRange(0).?);
5619 try std.testing.expectEqual(GlyphSpan{ .start = 0, .end = 3 }, map.clusterGlyphSpan(0).?);
5620 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(0).?);
5621 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(4).?);
5622 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(8).?);
5623 try std.testing.expectEqual(@as(?usize, null), map.byteOffsetToCluster(12));
5624 for (run.glyphs) |glyph| {
5625 try std.testing.expectEqual(@as(u32, 0), glyph.cluster_index);
5626 try std.testing.expectEqual(@as(u32, 0), glyph.source_start);
5627 try std.testing.expectEqual(@as(u32, 12), glyph.source_end);
5628 try std.testing.expectEqual(@as(u16, 3), glyph.source_codepoint_count);
5629 }
5630 }
5631
5632 test "shapeRun pairs regional indicators in grapheme cluster modes" {
5633 const bytes = try test_font.create(std.testing.allocator);
5634 defer std.testing.allocator.free(bytes);
5635
5636 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5637 defer shaper.deinit();
5638
5639 var ctx = Context.init(std.testing.allocator, .{});
5640 defer ctx.deinit();
5641
5642 var output = try Output.init(std.testing.allocator, .{
5643 .max_glyphs = 256,
5644 .max_ligature_carets = 256,
5645 });
5646 defer output.deinit(std.testing.allocator);
5647
5648 const text = [_]u32{ 0x1f1fa, 0x1f1f8, 0x1f1e8 };
5649 try ctx.shapeRun(.{
5650 .font = &shaper,
5651 .text = .{ .utf32 = &text },
5652 .cluster_mode = .monotone_graphemes,
5653 }, &output);
5654 const run = output.run();
5655 const map = run.clusterMap();
5656
5657 try std.testing.expectEqual(@as(usize, 3), run.glyphs.len);
5658 try std.testing.expectEqual(@as(usize, 2), run.clusters.len);
5659 try std.testing.expectEqual(SourceRange{ .start = 0, .end = 8 }, map.clusterSourceRange(0).?);
5660 try std.testing.expectEqual(SourceRange{ .start = 8, .end = 12 }, map.clusterSourceRange(1).?);
5661 try std.testing.expectEqual(GlyphSpan{ .start = 0, .end = 2 }, map.clusterGlyphSpan(0).?);
5662 try std.testing.expectEqual(GlyphSpan{ .start = 2, .end = 3 }, map.clusterGlyphSpan(1).?);
5663 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(0).?);
5664 try std.testing.expectEqual(@as(usize, 0), map.byteOffsetToCluster(4).?);
5665 try std.testing.expectEqual(@as(usize, 1), map.byteOffsetToCluster(8).?);
5666 }
5667
5668 test "shapeRun rejects malformed utf8" {
5669 const bytes = try test_font.create(std.testing.allocator);
5670 defer std.testing.allocator.free(bytes);
5671
5672 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5673 defer shaper.deinit();
5674
5675 var ctx = Context.init(std.testing.allocator, .{});
5676 defer ctx.deinit();
5677
5678 var output = try Output.init(std.testing.allocator, .{
5679 .max_glyphs = 256,
5680 .max_ligature_carets = 256,
5681 });
5682 defer output.deinit(std.testing.allocator);
5683
5684 try std.testing.expectError(error.InvalidUtf8, ctx.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = &.{0xff} } }, &output));
5685 try std.testing.expectEqual(@as(usize, 0), output.run().glyphs.len);
5686 }
5687
5688 test "shaping output admits multibyte input by glyph count on cold sealed use" {
5689 const bytes = try test_font.create(std.testing.allocator);
5690 defer std.testing.allocator.free(bytes);
5691 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5692 defer shaper.deinit();
5693 var context = Context.init(std.testing.allocator, .{});
5694 defer context.deinit();
5695 var counting = std.testing.FailingAllocator.init(std.testing.allocator, .{});
5696 const allocator = counting.allocator();
5697 var output = try Output.init(allocator, .{
5698 .max_glyphs = 2,
5699 .max_ligature_carets = 0,
5700 });
5701 defer output.deinit(allocator);
5702 counting.fail_index = counting.alloc_index;
5703 const pointer = output.glyphs.items.ptr;
5704 try context.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "AĆ©" } }, &output);
5705 try std.testing.expectEqual(@as(usize, 2), output.run().glyphs.len);
5706 try std.testing.expectError(error.OutputCapacityExceeded, context.shapeRun(.{
5707 .font = &shaper,
5708 .text = .{ .utf8 = "ABC" },
5709 }, &output));
5710 try std.testing.expectEqual(@as(usize, 0), output.run().glyphs.len);
5711 try std.testing.expectEqual(@as(usize, 0), output.run().clusters.len);
5712 try context.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "B" } }, &output);
5713 try std.testing.expectEqual(@as(usize, 1), output.run().glyphs.len);
5714 try std.testing.expectEqual(pointer, output.glyphs.items.ptr);
5715 try std.testing.expectEqual(@as(usize, 1), counting.alloc_index);
5716 }
5717
5718 test "shaping output rejects font expansion and recovers for an admitted run" {
5719 const bytes = try test_font.createWithGsubMultipleSubstitution(std.testing.allocator);
5720 defer std.testing.allocator.free(bytes);
5721 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5722 defer shaper.deinit();
5723 var context = Context.init(std.testing.allocator, .{});
5724 defer context.deinit();
5725 var output = try Output.init(std.testing.allocator, .{
5726 .max_glyphs = 1,
5727 .max_ligature_carets = 0,
5728 });
5729 defer output.deinit(std.testing.allocator);
5730 try std.testing.expectError(error.OutputCapacityExceeded, context.shapeRun(.{
5731 .font = &shaper,
5732 .text = .{ .utf8 = "A" },
5733 }, &output));
5734 try std.testing.expectEqual(@as(usize, 0), output.run().glyphs.len);
5735 try context.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "!" } }, &output);
5736 try std.testing.expectEqual(@as(usize, 1), output.run().glyphs.len);
5737 var expanded = try Output.init(std.testing.allocator, .{
5738 .max_glyphs = 2,
5739 .max_ligature_carets = 0,
5740 });
5741 defer expanded.deinit(std.testing.allocator);
5742 try context.shapeRun(.{ .font = &shaper, .text = .{ .utf8 = "A" } }, &expanded);
5743 try std.testing.expectEqual(@as(usize, 2), expanded.run().glyphs.len);
5744 }
5745
5746 test "shaping output requires a separate ligature caret budget" {
5747 const bytes = try test_font.createWithGsubLigatureAndGdefCarets(std.testing.allocator);
5748 defer std.testing.allocator.free(bytes);
5749 var shaper = Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
5750 defer shaper.deinit();
5751 var context = Context.init(std.testing.allocator, .{});
5752 defer context.deinit();
5753 var output = try Output.init(std.testing.allocator, .{
5754 .max_glyphs = 2,
5755 .max_ligature_carets = 0,
5756 });
5757 defer output.deinit(std.testing.allocator);
5758 const input: Input = .{ .font = &shaper, .text = .{ .utf8 = "fi" } };
5759 try std.testing.expectError(error.OutputCapacityExceeded, context.shapeRun(input, &output));
5760 try std.testing.expectEqual(@as(usize, 0), output.run().glyphs.len);
5761 try std.testing.expectEqual(@as(usize, 0), output.run().ligature_carets.len);
5762 var admitted = try Output.init(std.testing.allocator, .{
5763 .max_glyphs = 2,
5764 .max_ligature_carets = 1,
5765 });
5766 defer admitted.deinit(std.testing.allocator);
5767 try context.shapeRun(input, &admitted);
5768 try std.testing.expectEqual(@as(usize, 1), admitted.run().ligature_carets.len);
5769 }