From b2b0ca7eb3041c8ec284951e7e977059a2333745 Mon Sep 17 00:00:00 2001 From: Jeremy Collins Date: Tue, 14 Jul 2026 13:43:33 -0400 Subject: [PATCH] feat(render): load an external colour profile via TILE57_COLORPROFILE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The render path always used the colour profile embedded in the library. Add a TILE57_COLORPROFILE override — read via the portrayal C shim, mirroring the TILE57_S101_RULES on-disk rules override — so a host can recolour charts from an on-disk colorProfile.xml with no rebuild (e.g. a monochrome "ink" profile for e-ink). sharedColors() (the viewer's render path) and the `tile57 png`/`pdf` tool both honour it; an unset/unreadable path falls back to the embedded profile. --- src/chart.zig | 15 ++++++++++++++- src/portray/lua_shim.c | 23 +++++++++++++++++++++++ tools/render.zig | 13 ++++++++++++- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/chart.zig b/src/chart.zig index 98db632..708231e 100644 --- a/src/chart.zig +++ b/src/chart.zig @@ -56,7 +56,14 @@ var shared_colors_err: ?anyerror = null; fn sharedColors() !*render.resolve.Colors { while (colors_state.load(.acquire) != 2) { if (colors_state.cmpxchgStrong(@as(u8, 0), @as(u8, 1), .acquire, .monotonic) == null) { - if (render.resolve.Colors.init(gpa, embedded_assets.colorprofile[0].bytes)) |c| { + // TILE57_COLORPROFILE (via the C shim) overrides the embedded profile — + // its bytes are process-lifetime, so the Colors' token keys may slice in. + var ov_len: usize = 0; + const profile_xml: []const u8 = if (tg_colorprofile_override(&ov_len)) |p| + p[0..ov_len] + else + embedded_assets.colorprofile[0].bytes; + if (render.resolve.Colors.init(gpa, profile_xml)) |c| { shared_colors = c; } else |e| { shared_colors_err = e; @@ -74,6 +81,12 @@ fn sharedColors() !*render.resolve.Colors { // dir from TILE57_S101_RULES or null. Provided by the portrayal C shim. extern fn tg_env_rules() callconv(.c) ?[*:0]const u8; +// TILE57_COLORPROFILE override, also via the C shim: a colorProfile.xml the host +// points at to recolour the chart (e.g. a monochrome "ink" profile) without a +// rebuild. Returns the file's bytes (process-lifetime; NULL -> use the embedded +// profile). See sharedColors(). +extern fn tg_colorprofile_override(len: *usize) callconv(.c) ?[*]const u8; + /// Backend / on-disk format. `auto` sniffs PMTiles first, then S-57. pub const Format = enum { auto, pmtiles, s57 }; diff --git a/src/portray/lua_shim.c b/src/portray/lua_shim.c index 125a431..38d4c37 100644 --- a/src/portray/lua_shim.c +++ b/src/portray/lua_shim.c @@ -80,6 +80,29 @@ const char *tile57_diag_lua_version(void) { return LUA_VERSION; } * Used only as a fallback when a caller's rules_dir argument is NULL. */ const char *tg_env_rules(void) { return getenv("TILE57_S101_RULES"); } +/* TILE57_COLORPROFILE override: if the env var names a readable colorProfile.xml, + * slurp it whole and return its bytes with *len set; NULL when unset/unreadable so + * the caller falls back to the embedded profile. Mirrors the TILE57_S101_RULES + * on-disk rules override, but hands back BYTES (Zig's Colors.init parses a slice). + * The buffer is intentionally never freed: it backs a process-lifetime colour table + * whose token keys slice INTO it, so it must outlive every render (one leak/process). */ +const char *tg_colorprofile_override(size_t *len) { + const char *path = getenv("TILE57_COLORPROFILE"); + if (!path || !*path) return NULL; + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + if (fseek(f, 0, SEEK_END) != 0) { fclose(f); return NULL; } + long n = ftell(f); + if (n <= 0 || fseek(f, 0, SEEK_SET) != 0) { fclose(f); return NULL; } + char *buf = (char *)malloc((size_t)n); + if (!buf) { fclose(f); return NULL; } + size_t got = fread(buf, 1, (size_t)n, f); + fclose(f); + if (got != (size_t)n) { free(buf); return NULL; } + *len = got; + return buf; +} + /* Suppress the per-cell "[s101] portrayed …" stderr summary. Set once before a * parallel bake (many threads would otherwise garble the progress output); read * read-only by tg_portray_run thereafter. */ diff --git a/tools/render.zig b/tools/render.zig index 6b3eddf..fb7d70d 100644 --- a/tools/render.zig +++ b/tools/render.zig @@ -8,6 +8,10 @@ const catalog_embed = @import("catalog"); // embedded portrayal assets (colour p const common = @import("common.zig"); const Flags = common.Flags; const usageErr = common.usageErr; + +// TILE57_COLORPROFILE override via the portrayal C shim (mirrors the decl in +// src/chart.zig — env/IO live in C). NULL -> the embedded profile. +extern fn tg_colorprofile_override(len: *usize) callconv(.c) ?[*]const u8; const resolveRulesDir = common.resolveRulesDir; // tile57 png|pdf -o [flags] (one tile) @@ -159,7 +163,14 @@ pub fn run(io: std.Io, a: std.mem.Allocator, args: []const [:0]const u8, output: } defer if (!from_bundle) cell.deinit(); - var colors = try render.resolve.Colors.init(a, catalog_embed.colorprofile[0].bytes); + // Honour the TILE57_COLORPROFILE override (same knob the viewer's render path + // uses via chart.zig sharedColors) so `tile57 png` previews a custom profile. + var ov_len: usize = 0; + const profile_xml: []const u8 = if (tg_colorprofile_override(&ov_len)) |p| + p[0..ov_len] + else + catalog_embed.colorprofile[0].bytes; + var colors = try render.resolve.Colors.init(a, profile_xml); m.data_quality = dq; m.size_scale = size_scale; const settings = m;