lib/filigree/src/render/text.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const canvas_data = @import("canvas.zig");
  3 const fallback = @import("../fallback/root.zig");
  4 const font = @import("../font/root.zig");
  5 const shape = @import("../shape/root.zig");
  6 
  7 const Canvas = canvas_data.Canvas;
  8 const Color = canvas_data.Color;
  9 
 10 pub const DrawOptions = struct {
 11     color: Color = .{},
 12 };
 13 
 14 pub const Metrics = struct {
 15     advance_x: i32,
 16     advance_y: i32,
 17 };
 18 
 19 /// Reuses caller-provisioned output; replaces its prior run.
 20 pub fn measureUtf8(
 21     allocator: std.mem.Allocator,
 22     output: *shape.Output,
 23     shaped_font: *const shape.Font,
 24     text: []const u8,
 25 ) !Metrics {
 26     var context = shape.Context.init(allocator, .{});
 27     defer context.deinit();
 28     try context.shapeRun(.{
 29         .font = shaped_font,
 30         .text = .{ .utf8 = text },
 31     }, output);
 32     return measureGlyphRun(output.run());
 33 }
 34 
 35 /// Reuses caller-provisioned output; replaces its prior run.
 36 pub fn measureFallbackUtf8(
 37     allocator: std.mem.Allocator,
 38     output: *shape.Output,
 39     shaped_font: *const shape.Font,
 40     fallback_candidates: []const fallback.Candidate,
 41     text: []const u8,
 42 ) !Metrics {
 43     var context = try fallback.Context.init(allocator, .{
 44         .script_runs = .{ .max_source_units = text.len },
 45         .output = output.capacity.limits,
 46     });
 47     defer context.deinit();
 48     try context.shapeRun(.{
 49         .base = .{
 50             .font = shaped_font,
 51             .text = .{ .utf8 = text },
 52         },
 53         .candidates = fallback_candidates,
 54     }, output);
 55     return measureGlyphRun(output.run());
 56 }
 57 
 58 pub fn measureGlyphRun(run: shape.GlyphRun) Metrics {
 59     return .{
 60         .advance_x = round26Dot6(run.total_x_advance),
 61         .advance_y = round26Dot6(run.total_y_advance),
 62     };
 63 }
 64 
 65 /// Reuses caller-provisioned output; replaces its prior run.
 66 pub fn drawUtf8(
 67     allocator: std.mem.Allocator,
 68     output: *shape.Output,
 69     canvas: Canvas,
 70     shaped_font: *const shape.Font,
 71     text: []const u8,
 72     pixel_size: i32,
 73     origin_x: i32,
 74     origin_y: i32,
 75     options: DrawOptions,
 76 ) !Metrics {
 77     var context = shape.Context.init(allocator, .{});
 78     defer context.deinit();
 79     try context.shapeRun(.{
 80         .font = shaped_font,
 81         .text = .{ .utf8 = text },
 82     }, output);
 83     return try drawGlyphRun(
 84         allocator,
 85         canvas,
 86         shaped_font.face,
 87         output.run(),
 88         pixel_size,
 89         origin_x,
 90         origin_y,
 91         options,
 92     );
 93 }
 94 
 95 /// Reuses caller-provisioned output; replaces its prior run.
 96 pub fn drawFallbackUtf8(
 97     allocator: std.mem.Allocator,
 98     output: *shape.Output,
 99     canvas: Canvas,
100     shaped_font: *const shape.Font,
101     fallback_candidates: []const fallback.Candidate,
102     text: []const u8,
103     pixel_size: i32,
104     origin_x: i32,
105     origin_y: i32,
106     options: DrawOptions,
107 ) !Metrics {
108     var context = try fallback.Context.init(allocator, .{
109         .script_runs = .{ .max_source_units = text.len },
110         .output = output.capacity.limits,
111     });
112     defer context.deinit();
113     var segments: std.ArrayListUnmanaged(fallback.Segment) = .empty;
114     defer segments.deinit(allocator);
115     const input = fallback.Input{
116         .base = .{
117             .font = shaped_font,
118             .text = .{ .utf8 = text },
119         },
120         .candidates = fallback_candidates,
121     };
122     try context.shapeRunSegmented(input, output, allocator, &segments);
123     return try drawFallbackGlyphRun(
124         allocator,
125         canvas,
126         input,
127         output.run(),
128         segments.items,
129         pixel_size,
130         origin_x,
131         origin_y,
132         options,
133     );
134 }
135 
136 pub fn drawGlyphRun(
137     allocator: std.mem.Allocator,
138     canvas: Canvas,
139     face: font.Face,
140     run: shape.GlyphRun,
141     pixel_size: i32,
142     origin_x: i32,
143     origin_y: i32,
144     options: DrawOptions,
145 ) !Metrics {
146     var pen_x = origin_x * 64;
147     var pen_y = origin_y * 64;
148     for (run.glyphs) |glyph| {
149         var bitmap = try font.glyphBitmapAlloc(allocator, allocator, face, glyph.glyph_id, pixel_size);
150         defer bitmap.deinit(allocator);
151         const dst_x = round26Dot6(pen_x + glyph.x_offset) + bitmap.offset_x;
152         const dst_y = round26Dot6(pen_y + glyph.y_offset) + bitmap.offset_y;
153         canvas_data.blitBitmap(canvas, bitmap, dst_x, dst_y, options.color);
154         pen_x += glyph.x_advance;
155         pen_y += glyph.y_advance;
156     }
157     return measureGlyphRun(run);
158 }
159 
160 fn drawFallbackGlyphRun(
161     allocator: std.mem.Allocator,
162     canvas: Canvas,
163     input: fallback.Input,
164     run: shape.GlyphRun,
165     segments: []const fallback.Segment,
166     pixel_size: i32,
167     origin_x: i32,
168     origin_y: i32,
169     options: DrawOptions,
170 ) !Metrics {
171     var pen_x = origin_x * 64;
172     var pen_y = origin_y * 64;
173     var glyph_index: usize = 0;
174     for (segments) |segment| {
175         const start: usize = @intCast(segment.glyphs.start);
176         const end: usize = @intCast(segment.glyphs.end);
177         while (glyph_index < start and glyph_index < run.glyphs.len) : (glyph_index += 1) {
178             try drawGlyph(allocator, canvas, input.base.font.face, run.glyphs[glyph_index], pixel_size, &pen_x, &pen_y, options);
179         }
180         const face = fallbackFace(input, segment.candidate_index) orelse return error.InvalidFallbackCandidate;
181         while (glyph_index < end and glyph_index < run.glyphs.len) : (glyph_index += 1) {
182             try drawGlyph(allocator, canvas, face, run.glyphs[glyph_index], pixel_size, &pen_x, &pen_y, options);
183         }
184     }
185     while (glyph_index < run.glyphs.len) : (glyph_index += 1) {
186         try drawGlyph(allocator, canvas, input.base.font.face, run.glyphs[glyph_index], pixel_size, &pen_x, &pen_y, options);
187     }
188     return measureGlyphRun(run);
189 }
190 
191 fn fallbackFace(input: fallback.Input, candidate_index: usize) ?font.Face {
192     if (candidate_index == 0) return input.base.font.face;
193     const index = candidate_index - 1;
194     if (index >= input.candidates.len) return null;
195     return input.candidates[index].font.face;
196 }
197 
198 fn drawGlyph(
199     allocator: std.mem.Allocator,
200     canvas: Canvas,
201     face: font.Face,
202     glyph: shape.ShapedGlyph,
203     pixel_size: i32,
204     pen_x: *i32,
205     pen_y: *i32,
206     options: DrawOptions,
207 ) !void {
208     var bitmap = try font.glyphBitmapAlloc(allocator, allocator, face, glyph.glyph_id, pixel_size);
209     defer bitmap.deinit(allocator);
210     const dst_x = round26Dot6(pen_x.* + glyph.x_offset) + bitmap.offset_x;
211     const dst_y = round26Dot6(pen_y.* + glyph.y_offset) + bitmap.offset_y;
212     canvas_data.blitBitmap(canvas, bitmap, dst_x, dst_y, options.color);
213     pen_x.* += glyph.x_advance;
214     pen_y.* += glyph.y_advance;
215 }
216 
217 fn round26Dot6(value: i32) i32 {
218     if (value >= 0) return @divTrunc(value + 32, 64);
219     return @divTrunc(value - 32, 64);
220 }
221 
222 fn hasVisiblePixel(pixels: []const u8) bool {
223     var index: usize = 3;
224     while (index < pixels.len) : (index += 4) {
225         if (pixels[index] != 0) return true;
226     }
227     return false;
228 }
229 
230 test "render draws shaped utf8 into rgba canvas" {
231     const fixtures = @import("../fixture/root.zig");
232     const allocator = std.testing.allocator;
233     const bytes = try fixtures.createWithOutlines(allocator);
234     defer allocator.free(bytes);
235 
236     var shaped_font = shape.Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
237     defer shaped_font.deinit();
238     shaped_font.setPixelHeightScale(20);
239 
240     var pixels = @as([(64 * 32 * 4)]u8, @splat(0));
241     const canvas = try Canvas.init(&pixels, 64, 32, 64 * 4);
242     var output = try shape.Output.init(allocator, .{
243         .max_glyphs = 16,
244         .max_ligature_carets = 16,
245     });
246     defer output.deinit(allocator);
247     const metrics = try drawUtf8(allocator, &output, canvas, &shaped_font, "A", 20, 4, 4, .{});
248 
249     try std.testing.expectEqual(@as(i32, 10), metrics.advance_x);
250     try std.testing.expect(hasVisiblePixel(&pixels));
251 }
252 
253 test "render measures shaped utf8 without a canvas" {
254     const fixtures = @import("../fixture/root.zig");
255     const allocator = std.testing.allocator;
256     const bytes = try fixtures.createWithOutlines(allocator);
257     defer allocator.free(bytes);
258 
259     var shaped_font = shape.Font.initFromBytes(bytes.ptr, bytes.len) orelse return error.TestUnexpectedResult;
260     defer shaped_font.deinit();
261     shaped_font.setPixelHeightScale(20);
262 
263     var output = try shape.Output.init(allocator, .{
264         .max_glyphs = 16,
265         .max_ligature_carets = 16,
266     });
267     defer output.deinit(allocator);
268     const metrics = try measureUtf8(allocator, &output, &shaped_font, "AB");
269     try std.testing.expectEqual(@as(i32, 20), metrics.advance_x);
270     try std.testing.expectEqual(@as(i32, 0), metrics.advance_y);
271 }
272 
273 test "render draws fallback-shaped utf8 into rgba canvas" {
274     const fixtures = @import("../fixture/root.zig");
275     const allocator = std.testing.allocator;
276     const primary_bytes = try testAsciiRangeFont(allocator, 'A', 'A');
277     defer allocator.free(primary_bytes);
278     const fallback_bytes = try fixtures.createWithOutlines(allocator);
279     defer allocator.free(fallback_bytes);
280 
281     var primary_font = shape.Font.initFromBytes(primary_bytes.ptr, primary_bytes.len) orelse return error.TestUnexpectedResult;
282     defer primary_font.deinit();
283     primary_font.setPixelHeightScale(20);
284     var fallback_font = shape.Font.initFromBytes(fallback_bytes.ptr, fallback_bytes.len) orelse return error.TestUnexpectedResult;
285     defer fallback_font.deinit();
286     fallback_font.setPixelHeightScale(20);
287 
288     var pixels = @as([(64 * 32 * 4)]u8, @splat(0));
289     const canvas = try Canvas.init(&pixels, 64, 32, 64 * 4);
290     const candidates = [_]fallback.Candidate{.{ .font = &fallback_font }};
291     var output = try shape.Output.init(allocator, .{
292         .max_glyphs = 16,
293         .max_ligature_carets = 16,
294     });
295     defer output.deinit(allocator);
296     const metrics = try drawFallbackUtf8(
297         allocator,
298         &output,
299         canvas,
300         &primary_font,
301         &candidates,
302         "AB",
303         20,
304         4,
305         4,
306         .{},
307     );
308 
309     try std.testing.expectEqual(@as(i32, 20), metrics.advance_x);
310     try std.testing.expect(hasVisiblePixel(&pixels));
311 }
312 
313 test "render measures fallback-shaped utf8" {
314     const fixtures = @import("../fixture/root.zig");
315     const allocator = std.testing.allocator;
316     const primary_bytes = try testAsciiRangeFont(allocator, 'A', 'A');
317     defer allocator.free(primary_bytes);
318     const fallback_bytes = try fixtures.createWithOutlines(allocator);
319     defer allocator.free(fallback_bytes);
320 
321     var primary_font = shape.Font.initFromBytes(primary_bytes.ptr, primary_bytes.len) orelse return error.TestUnexpectedResult;
322     defer primary_font.deinit();
323     primary_font.setPixelHeightScale(20);
324     var fallback_font = shape.Font.initFromBytes(fallback_bytes.ptr, fallback_bytes.len) orelse return error.TestUnexpectedResult;
325     defer fallback_font.deinit();
326     fallback_font.setPixelHeightScale(20);
327 
328     const candidates = [_]fallback.Candidate{.{ .font = &fallback_font }};
329     var output = try shape.Output.init(allocator, .{
330         .max_glyphs = 16,
331         .max_ligature_carets = 16,
332     });
333     defer output.deinit(allocator);
334     const metrics = try measureFallbackUtf8(allocator, &output, &primary_font, &candidates, "AB");
335     try std.testing.expectEqual(@as(i32, 20), metrics.advance_x);
336     try std.testing.expectEqual(@as(i32, 0), metrics.advance_y);
337 }
338 
339 fn testAsciiRangeFont(allocator: std.mem.Allocator, start_code: u16, end_code: u16) ![]u8 {
340     const fixtures = @import("../fixture/root.zig");
341     const bytes = try fixtures.createWithOutlines(allocator);
342     errdefer allocator.free(bytes);
343     try patchAsciiRange(bytes, start_code, end_code);
344     return bytes;
345 }
346 
347 fn patchAsciiRange(bytes: []u8, start_code: u16, end_code: u16) !void {
348     const fixtures = @import("../fixture/root.zig");
349     const cmap_offset = try fixtures.binary.tableOffset(bytes, "cmap");
350     if (cmap_offset > bytes.len or bytes.len - cmap_offset < 4) return error.InvalidFont;
351     const subtable_count = readU16(bytes, cmap_offset + 2);
352     if (@as(usize, subtable_count) > (bytes.len - cmap_offset - 4) / 8) return error.InvalidFont;
353 
354     for (0..subtable_count) |subtable_index| {
355         const record = cmap_offset + 4 + subtable_index * 8;
356         const subtable_offset = readU32(bytes, record + 4);
357         const subtable_start = cmap_offset + @as(usize, @intCast(subtable_offset));
358         if (subtable_start > bytes.len or bytes.len - subtable_start < 2) return error.InvalidFont;
359         if (readU16(bytes, subtable_start) != 4) continue;
360         if (bytes.len - subtable_start < 40) return error.InvalidFont;
361         writeU16(bytes, subtable_start + 14, end_code);
362         writeU16(bytes, subtable_start + 22, start_code);
363         return;
364     }
365 
366     return error.MissingCmapFormat4;
367 }
368 
369 fn readU16(bytes: []const u8, offset: usize) u16 {
370     return std.mem.readInt(u16, bytes[offset..][0..2], .big);
371 }
372 
373 fn readU32(bytes: []const u8, offset: usize) u32 {
374     return std.mem.readInt(u32, bytes[offset..][0..4], .big);
375 }
376 
377 fn writeU16(bytes: []u8, offset: usize, value: u16) void {
378     std.mem.writeInt(u16, bytes[offset..][0..2], value, .big);
379 }