diff --git a/include/tile57.h b/include/tile57.h index f842a86..cef603c 100644 --- a/include/tile57.h +++ b/include/tile57.h @@ -1750,6 +1750,41 @@ tile57_status tile57_bake_glyph_sdf(tile57_assets *out, tile57_error *err); * place names and italic hydrography from the SDF text path. Free with * tile57_assets_free. */ tile57_status tile57_bake_glyph_sdf_face(tile57_assets *out, int32_t face, tile57_error *err); +/* tile57_bake_glyph_sdf for named codepoints, out of a font the HOST supplies: + * `font_bytes` is a TrueType file or a collection, of which the first face is + * read, and `codepoints` names the characters to rasterize. + * + * The bundled Noto Sans covers Latin, Greek and Cyrillic, and the prebaked + * sheets carry printable ASCII and Latin-1. A chart naming its features in + * another script needs glyphs no prebaked sheet can hold — CJK alone is some + * 20,000 — so a host bakes the characters its labels actually use, from + * whatever face its platform has for the script, and merges the sheet into the + * atlas it already has. + * + * Same output as tile57_bake_glyph_sdf: only sprite_* filled, at the same em + * size and spread, so the two sheets share one atlas. A codepoint the face has + * no glyph for is left out rather than failing the bake. Free with + * tile57_assets_free. */ +tile57_status tile57_bake_glyph_sdf_codepoints(tile57_assets *out, + const uint8_t *font_bytes, size_t font_len, + const uint32_t *codepoints, size_t count, + tile57_error *err); +/* True when `font_bytes` can draw `codepoint`: the face has a glyph for it AND + * the rasterizer can read that glyph's outlines. + * + * A host picking a face out of its platform's font store has to ask, because + * the two are not the same question. Apple's UI faces cover CJK and are what + * CoreText hands back for Chinese text, but they carry Apple's `cidg` outlines + * rather than glyf or CFF, and no glyph comes out of them. A host that + * installed one would draw blank labels with nothing to say why. Ask with one + * character of the script, then install the first face that answers true. */ +tile57_status tile57_font_covers(const uint8_t *font_bytes, size_t font_len, + uint32_t codepoint, bool *out, tile57_error *err); +/* The same question about the BUNDLED label face, so a host can tell which + * scripts it needs a platform font for and which are already drawn. A chart + * stating four languages may need a face for one of them: asking first stops a + * host from mapping a font for Finnish and leaving Inuktitut blank. */ +tile57_status tile57_label_font_covers(uint32_t codepoint, bool *out, tile57_error *err); void tile57_assets_free(tile57_assets *out); /* ---- chart-style generation --------------------------------------------- diff --git a/src/capi.zig b/src/capi.zig index e668bbc..6aae0ba 100644 --- a/src/capi.zig +++ b/src/capi.zig @@ -2209,6 +2209,76 @@ export fn tile57_bake_glyph_sdf_face(out: ?*CAssets, face: i32, err: ?*CError) c return bakeGlyphSdf(out, face, err); } +/// An SDF sheet for named codepoints, out of a font the HOST supplies. See +/// tile57.h. +export fn tile57_bake_glyph_sdf_codepoints( + out: ?*CAssets, + font_bytes: ?[*]const u8, + font_len: usize, + codepoints: ?[*]const u32, + count: usize, + err: ?*CError, +) callconv(.c) c_int { + const o = out orelse return failWith(err, .badarg, "out must not be null"); + o.* = .{}; + const fb = font_bytes orelse return failWith(err, .badarg, "font_bytes must not be null"); + if (font_len == 0) return failWith(err, .badarg, "font_len must not be zero"); + const cps_c = codepoints orelse return failWith(err, .badarg, "codepoints must not be null"); + if (count == 0) return failWith(err, .badarg, "count must not be zero"); + + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const a = arena.allocator(); + const cps = a.alloc(u21, count) catch |e| return fail(err, e); + for (cps_c[0..count], cps) |src, *dst| { + if (src > 0x10FFFF) return failWith(err, .badarg, "codepoint above the Unicode range"); + dst.* = @intCast(src); + } + var atlas = glyph_sdf.build(a, fb[0..font_len], cps, 32.0, 6) catch |e| return fail(err, e); + // Every codepoint came back with neither ink nor an advance. Either the + // face draws none of them, or its outlines are in a format the rasterizer + // cannot read. Say so: a host that took an empty sheet as success would + // record the characters as served and never ask for them again. + if (atlas.glyphs.count() == 0) + return failWith(err, .unsupported, "the face draws none of those codepoints"); + return finishGlyphSdf(o, a, &atlas, err); +} + +/// True when the BUNDLED label face can draw `codepoint`. See tile57.h. +export fn tile57_label_font_covers(codepoint: u32, out: ?*bool, err: ?*CError) callconv(.c) c_int { + const o = out orelse return failWith(err, .badarg, "out must not be null"); + o.* = false; + if (codepoint > 0x10FFFF) return failWith(err, .badarg, "codepoint above the Unicode range"); + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const one = [_]u21{@intCast(codepoint)}; + const ft = @import("render").font; + var atlas = glyph_sdf.build(arena.allocator(), ft.notosans, &one, 32.0, 6) catch |e| return fail(err, e); + o.* = atlas.glyphs.count() > 0; + return OK; +} + +/// True when `font_bytes` can draw `codepoint`. See tile57.h. +export fn tile57_font_covers( + font_bytes: ?[*]const u8, + font_len: usize, + codepoint: u32, + out: ?*bool, + err: ?*CError, +) callconv(.c) c_int { + const o = out orelse return failWith(err, .badarg, "out must not be null"); + o.* = false; + const fb = font_bytes orelse return failWith(err, .badarg, "font_bytes must not be null"); + if (font_len == 0) return failWith(err, .badarg, "font_len must not be zero"); + if (codepoint > 0x10FFFF) return failWith(err, .badarg, "codepoint above the Unicode range"); + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const one = [_]u21{@intCast(codepoint)}; + var atlas = glyph_sdf.build(arena.allocator(), fb[0..font_len], &one, 32.0, 6) catch |e| return fail(err, e); + o.* = atlas.glyphs.count() > 0; + return OK; +} + fn bakeGlyphSdf(out: ?*CAssets, face: i32, err: ?*CError) c_int { const o = out orelse return failWith(err, .badarg, "out must not be null"); o.* = .{}; @@ -2223,9 +2293,16 @@ fn bakeGlyphSdf(out: ?*CAssets, face: i32, err: ?*CError) c_int { }; const cps = glyph_sdf.defaultCodepoints(a) catch |e| return fail(err, e); var atlas = glyph_sdf.build(a, font, cps, 32.0, 6) catch |e| return fail(err, e); + return finishGlyphSdf(o, a, &atlas, err); +} + +/// Encode a built atlas into *out. `a` is the arena the atlas lives in; the two +/// buffers that leave are duped into gpa, which is what tile57_assets_free +/// releases. +fn finishGlyphSdf(o: *CAssets, a: std.mem.Allocator, atlas: *glyph_sdf.Atlas, err: ?*CError) c_int { const png = (atlas.encodePng(a) catch |e| return fail(err, e)) orelse return failWith(err, .internal, "glyph atlas PNG encode produced nothing"); - const json = glyphMetricsJson(a, &atlas) catch |e| return fail(err, e); + const json = glyphMetricsJson(a, atlas) catch |e| return fail(err, e); o.sprite_png = (gpa.dupe(u8, png) catch |e| { tile57_assets_free(o); return fail(err, e); diff --git a/src/sprite/glyph.zig b/src/sprite/glyph.zig index d103cd2..e97ae69 100644 --- a/src/sprite/glyph.zig +++ b/src/sprite/glyph.zig @@ -9,6 +9,14 @@ const Allocator = std.mem.Allocator; // C glue (src/sprite/svgraster.c): stb_truetype SDF + the shared PNG encoder. extern fn tg_glyph_sdf(font: [*]const u8, font_len: c_int, cp: c_int, em_px: f32, pad: c_int, onedge: c_int, dist_scale: f32, w: *c_int, h: *c_int, xoff: *c_int, yoff: *c_int, advance: *f32) callconv(.c) ?[*]u8; extern fn tg_glyph_free(p: ?[*]u8) callconv(.c) void; +extern fn tg_glyph_present(font: [*]const u8, font_len: c_int, cp: c_int) callconv(.c) c_int; + +/// True when `font` has a glyph of its own for `cp`. A codepoint the face does +/// not map resolves to .notdef, which has an advance and usually a box, so the +/// metrics a rasterizer returns cannot answer this on their own. +pub fn present(font: []const u8, cp: u21) bool { + return tg_glyph_present(font.ptr, @intCast(font.len), @intCast(cp)) != 0; +} extern fn tg_png_encode(rgba: [*]const u8, w: c_int, h: c_int, out_len: *c_int) callconv(.c) ?[*]u8; extern fn tg_svg_free(p: ?*anyopaque) callconv(.c) void; @@ -90,6 +98,16 @@ pub fn build(a: Allocator, font: []const u8, cps: []const u21, em_px: f32, pad: .h = @as(f32, @floatFromInt(h)) / em_px, .advance = adv / em_px, }; + // A codepoint the face does not map resolves to .notdef, whose box + // would be baked as if it were the character. And a face whose + // outlines the rasterizer cannot read at all (Apple's variable UI + // faces carry `cidg` rather than glyf or CFF) yields neither ink nor + // an advance, which a host would pack into its atlas and lay out as + // nothing — a blank label that says the glyph arrived. Skip both: an + // absent codepoint is what "this face cannot draw it" means. A space + // has a glyph of its own and advances, so it survives. + if (!present(font, cp)) continue; + if (sdf == null and adv <= 0) continue; try cells.append(a, .{ .cp = cp, .w = @intCast(@max(w, 0)), .h = @intCast(@max(h, 0)), .sdf = sdf, .gi = gi }); } diff --git a/src/sprite/svgraster.c b/src/sprite/svgraster.c index 834ed31..f56a64b 100644 --- a/src/sprite/svgraster.c +++ b/src/sprite/svgraster.c @@ -47,6 +47,19 @@ unsigned char *tg_glyph_sdf(const unsigned char *font, int font_len, int cp, } void tg_glyph_free(unsigned char *p) { stbtt_FreeSDF(p, NULL); } +// Whether the face has a glyph of its own for `cp`. stbtt_FindGlyphIndex +// answers 0 for a codepoint the cmap does not map, which is .notdef — a real +// glyph with a real advance and, in most faces, a visible box. Metrics alone +// therefore cannot tell a character the face draws from one it does not, and a +// caller that trusted them would bake a row of tofu and record it as the text. +// 1 when the face has the glyph, 0 when it does not or cannot be read. +int tg_glyph_present(const unsigned char *font, int font_len, int cp) { + (void)font_len; + stbtt_fontinfo f; + if (!stbtt_InitFont(&f, font, stbtt_GetFontOffsetForIndex(font, 0))) return 0; + return stbtt_FindGlyphIndex(&f, cp) != 0; +} + // Rasterize a flattened SVG (viewBox normalized to "0 0 W H") at `scale` device // px per user unit. Forces even-odd winding on every shape — the S-101 danger // glyphs are compound paths whose inner subpath is a hole, and nonzero winding