[feat](trx-frontend-http): serve the vendored APRS symbol sprites

Embed the six sheets alongside the other vendored assets and serve them
under /vendor with the same immutable cache headers.

The browser computes a symbol's cell from a 16x6 grid of 24px cells, so
a re-vendored sheet at any other size would shift every station onto a
neighbouring icon -- wrong on every packet, and invisible unless you
know which glyph to expect.  Pin the geometry by parsing each embedded
PNG's IHDR in a test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018huL1ELyr86yVqfAabtioA
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
sjg
2026-08-03 19:55:50 +02:00
co-authored by Claude Opus 5
parent 1bc2c88e2f
commit 02fe492dbf
3 changed files with 102 additions and 0 deletions
@@ -279,6 +279,47 @@ pub(crate) async fn leaflet_layers_2x() -> impl Responder {
.body(status::LEAFLET_LAYERS_2X)
}
// ---------------------------------------------------------------------------
// Vendored APRS symbol sprites
// ---------------------------------------------------------------------------
fn embedded_png(bytes: &'static [u8]) -> HttpResponse {
HttpResponse::Ok()
.insert_header((header::CONTENT_TYPE, "image/png"))
.insert_header((header::CACHE_CONTROL, "public, max-age=604800, immutable"))
.body(bytes)
}
#[get("/vendor/aprs-symbols-24-0.png")]
pub(crate) async fn aprs_symbols_primary() -> impl Responder {
embedded_png(status::APRS_SYMBOLS_PRIMARY)
}
#[get("/vendor/aprs-symbols-24-0-2x.png")]
pub(crate) async fn aprs_symbols_primary_2x() -> impl Responder {
embedded_png(status::APRS_SYMBOLS_PRIMARY_2X)
}
#[get("/vendor/aprs-symbols-24-1.png")]
pub(crate) async fn aprs_symbols_alternate() -> impl Responder {
embedded_png(status::APRS_SYMBOLS_ALTERNATE)
}
#[get("/vendor/aprs-symbols-24-1-2x.png")]
pub(crate) async fn aprs_symbols_alternate_2x() -> impl Responder {
embedded_png(status::APRS_SYMBOLS_ALTERNATE_2X)
}
#[get("/vendor/aprs-symbols-24-2.png")]
pub(crate) async fn aprs_symbols_overlay() -> impl Responder {
embedded_png(status::APRS_SYMBOLS_OVERLAY)
}
#[get("/vendor/aprs-symbols-24-2-2x.png")]
pub(crate) async fn aprs_symbols_overlay_2x() -> impl Responder {
embedded_png(status::APRS_SYMBOLS_OVERLAY_2X)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -307,4 +348,41 @@ mod tests {
);
assert_eq!(generated_content_type("secret.txt"), None);
}
/// Reads the width and height out of a PNG IHDR chunk.
fn png_dimensions(bytes: &[u8]) -> (u32, u32) {
assert_eq!(&bytes[..8], b"\x89PNG\r\n\x1a\n", "not a PNG");
assert_eq!(&bytes[12..16], b"IHDR", "first chunk is not IHDR");
let width = u32::from_be_bytes(bytes[16..20].try_into().expect("IHDR width"));
let height = u32::from_be_bytes(bytes[20..24].try_into().expect("IHDR height"));
(width, height)
}
/// The browser computes sprite cell offsets from a 16x6 grid of 24px cells,
/// so a sheet at any other size would silently shift every APRS symbol.
#[test]
fn aprs_symbol_sheets_match_the_sprite_grid_the_frontend_assumes() {
for (name, bytes) in [
("aprs-symbols-24-0", status::APRS_SYMBOLS_PRIMARY),
("aprs-symbols-24-1", status::APRS_SYMBOLS_ALTERNATE),
("aprs-symbols-24-2", status::APRS_SYMBOLS_OVERLAY),
] {
assert_eq!(
png_dimensions(bytes),
(384, 144),
"{name} is not a 16x6 grid"
);
}
for (name, bytes) in [
("aprs-symbols-24-0-2x", status::APRS_SYMBOLS_PRIMARY_2X),
("aprs-symbols-24-1-2x", status::APRS_SYMBOLS_ALTERNATE_2X),
("aprs-symbols-24-2-2x", status::APRS_SYMBOLS_OVERLAY_2X),
] {
assert_eq!(
png_dimensions(bytes),
(768, 288),
"{name} is not a 2x sheet"
);
}
}
}
@@ -658,6 +658,13 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
.service(assets::leaflet_marker_shadow)
.service(assets::leaflet_layers)
.service(assets::leaflet_layers_2x)
// Vendored APRS symbol sprites
.service(assets::aprs_symbols_primary)
.service(assets::aprs_symbols_primary_2x)
.service(assets::aprs_symbols_alternate)
.service(assets::aprs_symbols_alternate_2x)
.service(assets::aprs_symbols_overlay)
.service(assets::aprs_symbols_overlay_2x)
.service(assets::generated_asset)
// Virtual channels
.service(vchan::list_channels)
@@ -37,6 +37,23 @@ pub const LEAFLET_MARKER_SHADOW: &[u8] = include_bytes!("../assets/web/vendor/ma
pub const LEAFLET_LAYERS: &[u8] = include_bytes!("../assets/web/vendor/layers.png");
pub const LEAFLET_LAYERS_2X: &[u8] = include_bytes!("../assets/web/vendor/layers-2x.png");
// Vendored APRS symbol sprites (https://github.com/hessu/aprs-symbols).
// Each sheet is a 16x6 grid of 24px cells indexed by `symbol code - 0x21`:
// table 0 is the primary ('/') set, table 1 the alternate ('\') set, and
// table 2 the overlay characters drawn on top of an alternate symbol.
pub const APRS_SYMBOLS_PRIMARY: &[u8] =
include_bytes!("../assets/web/vendor/aprs-symbols-24-0.png");
pub const APRS_SYMBOLS_PRIMARY_2X: &[u8] =
include_bytes!("../assets/web/vendor/aprs-symbols-24-0-2x.png");
pub const APRS_SYMBOLS_ALTERNATE: &[u8] =
include_bytes!("../assets/web/vendor/aprs-symbols-24-1.png");
pub const APRS_SYMBOLS_ALTERNATE_2X: &[u8] =
include_bytes!("../assets/web/vendor/aprs-symbols-24-1-2x.png");
pub const APRS_SYMBOLS_OVERLAY: &[u8] =
include_bytes!("../assets/web/vendor/aprs-symbols-24-2.png");
pub const APRS_SYMBOLS_OVERLAY_2X: &[u8] =
include_bytes!("../assets/web/vendor/aprs-symbols-24-2-2x.png");
/// Build version tag used for cache-busting asset URLs and ETag headers.
/// Computed once from `PKG_VERSION` + `CLIENT_BUILD_DATE`.
pub fn build_version_tag() -> &'static str {