Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions include/tile57.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------------------------------------------
Expand Down
79 changes: 78 additions & 1 deletion src/capi.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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.* = .{};
Expand All @@ -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);
Expand Down
18 changes: 18 additions & 0 deletions src/sprite/glyph.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 });
}

Expand Down
13 changes: 13 additions & 0 deletions src/sprite/svgraster.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading