diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api/assets.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api/assets.rs index 2f84d383..b68b8c7d 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api/assets.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api/assets.rs @@ -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" + ); + } + } } diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/api/mod.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/api/mod.rs index e79233e1..91afbfdc 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/api/mod.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/api/mod.rs @@ -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) diff --git a/src/trx-client/trx-frontend/trx-frontend-http/src/status.rs b/src/trx-client/trx-frontend/trx-frontend-http/src/status.rs index 39f34f3e..6bae90a2 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/src/status.rs +++ b/src/trx-client/trx-frontend/trx-frontend-http/src/status.rs @@ -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 {