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 = &ltra_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 = &ltra_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 }