diff --git a/Cargo.lock b/Cargo.lock index cd88fe87..5595472d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2085,6 +2085,7 @@ dependencies = [ "muda", "rfd", "serde_json", + "winresource", ] [[package]] @@ -3768,6 +3769,15 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "shlex" version = "2.0.1" @@ -4058,6 +4068,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "toml" +version = "1.1.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "1.1.1+spec-1.1.0" @@ -4088,6 +4113,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + [[package]] name = "tracing" version = "0.1.44" @@ -5003,6 +5034,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winresource" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" +dependencies = [ + "toml", + "version_check", +] + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/README.md b/README.md index ac8ee6e7..6177ad71 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@

+

+ LightCraft app icon: an engraved lynx in paper and ink on LightCraft amber (#f2a516) +

+

LightCraft

Your photos. Your pixels. Your machine.

diff --git a/apps/lightcraft/Cargo.toml b/apps/lightcraft/Cargo.toml index 21de0243..e81e79eb 100644 --- a/apps/lightcraft/Cargo.toml +++ b/apps/lightcraft/Cargo.toml @@ -19,6 +19,10 @@ egui = { workspace = true } serde_json = { workspace = true } log = { workspace = true } +[build-dependencies] +# Icon + VERSIONINFO for lightcraft.exe; build.rs only uses it when targeting Windows. +winresource = "0.1" + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rfd = { workspace = true } diff --git a/apps/lightcraft/build.rs b/apps/lightcraft/build.rs new file mode 100644 index 00000000..4947091d --- /dev/null +++ b/apps/lightcraft/build.rs @@ -0,0 +1,28 @@ +//! Windows only: embed the app icon and version info (VERSIONINFO) into `lightcraft.exe`, so +//! Explorer, the taskbar, the Start menu and Alt-Tab show the lynx. +//! +//! On every other target this does nothing. A missing resource compiler is a warning, so a +//! cross-compile from macOS or Linux still links, unless `LIGHTCRAFT_REQUIRE_WINRES=1` turns it +//! into an error (for release builds). + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=../../assets/app-icon/lightcraft.ico"); + println!("cargo:rerun-if-env-changed=LIGHTCRAFT_REQUIRE_WINRES"); + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") { + return; + } + let mut res = winresource::WindowsResource::new(); + res.set_icon("../../assets/app-icon/lightcraft.ico") + .set("ProductName", "LightCraft") + .set("FileDescription", "LightCraft photo library and raw developer") + .set("LegalCopyright", "Copyright (c) the LightCraft authors. MIT OR Apache-2.0.") + .set("OriginalFilename", "lightcraft.exe") + .set("InternalName", "lightcraft"); + if let Err(e) = res.compile() { + if std::env::var_os("LIGHTCRAFT_REQUIRE_WINRES").is_some() { + panic!("embedding Windows resources failed: {e}"); + } + println!("cargo:warning=lightcraft.exe built without icon/version resources: {e}"); + } +} diff --git a/apps/lightcraft/src/main.rs b/apps/lightcraft/src/main.rs index dc8e7e0c..c90ffe95 100644 --- a/apps/lightcraft/src/main.rs +++ b/apps/lightcraft/src/main.rs @@ -21,6 +21,19 @@ mod native_menu; use lightcraft_engine::Session; use lightcraft_ui_egui::{LightcraftApp, Services, UiState}; +/// Reverse-DNS app id: Wayland app id, `.desktop` file and hicolor icon name. +const APP_ID: &str = "ai.storyteller.lightcraft"; + +/// The window, taskbar and Dock icon (assets/app-icon/, regenerated by packaging/icons.sh). macOS +/// gets the version with Apple's transparent margin so it sits right among the other Dock icons. +fn app_icon() -> egui::IconData { + #[cfg(target_os = "macos")] + let png: &[u8] = include_bytes!("../../../assets/app-icon/lightcraft-macos-512.png"); + #[cfg(not(target_os = "macos"))] + let png: &[u8] = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.lightcraft.png"); + eframe::icon_data::from_png_bytes(png).unwrap_or_default() +} + struct App(LightcraftApp, #[cfg(target_os = "macos")] Option); impl eframe::App for App { @@ -251,7 +264,10 @@ fn main() -> eframe::Result { .with_drag_and_drop(true) .with_fullsize_content_view(true) .with_titlebar_shown(false) - .with_title_shown(false), + .with_title_shown(false) + .with_icon(app_icon()) + // Wayland matches the window to packaging/linux/ai.storyteller.lightcraft.desktop by this id + .with_app_id(APP_ID), ..Default::default() }; eframe::run_native( diff --git a/assets/ATTRIBUTION.md b/assets/ATTRIBUTION.md index 33485087..aa215ce8 100644 --- a/assets/ATTRIBUTION.md +++ b/assets/ATTRIBUTION.md @@ -9,6 +9,7 @@ covers a whole directory), or if a licence file referenced in the Licence column | Path | Asset | Creator | Source | Licence | Added | Modifications | |---|---|---|---|---|---|---| | `assets/fonts/Inter-*.ttf` (Regular, Medium, SemiBold) | Inter | Rasmus Andersson | https://github.com/rsms/inter | SIL Open Font License 1.1 (`assets/fonts/OFL-Inter.txt`) | 2026-09-30 | none | +| `assets/app-icon/` (`lightcraft.svg` and the PNG/ICNS/ICO/SVG files derived from it) | LightCraft app icon (lynx) | LightCraft project owner (drawn in ArtCraft) | original work (vectorised from the owner's drawing; see `assets/app-icon/README.md`) | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | 2026-10-02 | vectorised to SVG; rendered to PNG/ICNS/ICO by `packaging/icons.sh` | | `crates/scenes/` (generated images) | Procedural demo photographs | LightCraft contributors | original work (generated at runtime, no source imagery) | MIT OR Apache-2.0 | 2026-09-30 | n/a | | `crates/ui-egui/src/icons.rs` | UI icons drawn as vector paths in code | LightCraft contributors | original work | MIT OR Apache-2.0 | 2026-09-30 | n/a | | `docs/images/*-tetons.jpg`, `docs/images/grid-pd.jpg` (screenshots containing it) | "The Tetons and the Snake River" | Ansel Adams (1942), U.S. National Archives | https://commons.wikimedia.org/wiki/File:Adams_The_Tetons_and_the_Snake_River.jpg | Public domain (U.S. federal government work) | 2026-09-30 | developed in LightCraft; shown inside app screenshots | diff --git a/assets/app-icon/LICENSE.txt b/assets/app-icon/LICENSE.txt new file mode 100644 index 00000000..23da456b --- /dev/null +++ b/assets/app-icon/LICENSE.txt @@ -0,0 +1,13 @@ +LightCraft app icon (lightcraft.svg and every file derived from it in this directory: +lightcraft-small.svg, lightcraft-1024.png, lightcraft-macos-512.png, lightcraft.icns, +lightcraft.ico, hicolor/). + +Original artwork made for this project by the LightCraft project owner in ArtCraft (no +third-party material). Licensed like the rest of LightCraft, under either of + + * Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0) + * MIT license (https://opensource.org/licenses/MIT) + +at your option. + +Regenerate the derived files with packaging/icons.sh. diff --git a/assets/app-icon/README.md b/assets/app-icon/README.md new file mode 100644 index 00000000..2a6dd4b8 --- /dev/null +++ b/assets/app-icon/README.md @@ -0,0 +1,49 @@ +# LightCraft app icon + +A **lynx**, frontal head-and-shoulders portrait with an alert gaze, in the style of an engraving: the +Crafting Apps "owl template" (full-bleed colour field, tight portrait, the figure in paper with ink line +work and a crisp ink contour, no roundel or border). + +## Palette (exactly three colours) + +| Colour | Hex | Used for | +|---|---|---| +| Ink | `#0b0b0c` | line work and the contour | +| Paper | `#efe9dc` | the figure (the lynx) | +| LightCraft amber | `#f2a516` | the full-bleed field (the app colour) | + +## Geometry + +`viewBox="0 0 512 512"`, a rounded-square tile with `rx=112`, clipped; the colour fills the whole tile. +macOS files (`.icns`, `lightcraft-macos-512.png`) add Apple's transparent margin (an 824/1024 body); +Windows and Linux files use the full-bleed tile so the lynx reads at 16-48 px. + +## Provenance + +The owner's original drawing, made in ArtCraft (2880 px, keyed to the three colours), vectorised with +craftrules `assets/logo-options/_tools/vectorize_tile.py`. The source drawing stays in craftrules at +`assets/app-icons/lightcraft/source.png`; `lightcraft.svg` here is the canonical artwork. Licence: +`LICENSE.txt` (MIT OR Apache-2.0, like the repo). + +## Files + +| File | What | +|---|---| +| `lightcraft.svg` | canonical vector, traced at 2048 px (about 770 KB) | +| `lightcraft-small.svg` | lighter vector, traced at 1024 px (about 360 KB) | +| `lightcraft-1024.png` | 1024 px render (store listings, docs) | +| `lightcraft-macos-512.png` | runtime window/Dock icon on macOS (embedded by `apps/lightcraft/src/main.rs`) | +| `lightcraft.icns` | macOS bundle icon (`CFBundleIconFile`) | +| `lightcraft.ico` | Windows icon, 16-256 px, embedded in `lightcraft.exe` by `apps/lightcraft/build.rs` | +| `hicolor/x/apps/ai.storyteller.lightcraft.png` | Linux icon theme, 16-512 px; the 256 px one is also the runtime window icon on Windows and Linux | +| `hicolor/scalable/apps/ai.storyteller.lightcraft.svg` | Linux scalable icon (the small SVG) | + +The app id is `ai.storyteller.lightcraft`: the Wayland app id, the `.desktop` file +(`packaging/linux/ai.storyteller.lightcraft.desktop`, `Icon=ai.storyteller.lightcraft`) and the hicolor icon +name. To install on Linux, copy `hicolor/` into `/usr/share/icons/hicolor/` (or `~/.local/share/icons/hicolor/`) +and the `.desktop` file into `applications/`. + +## Regenerate + +Edit `lightcraft.svg` (or `lightcraft-small.svg`), then run `packaging/icons.sh` (needs `resvg`; `iconutil` +on macOS for the `.icns`; the `.ico` is packed by `cargo xtask ico`). Every output is committed. diff --git a/assets/app-icon/hicolor/128x128/apps/ai.storyteller.lightcraft.png b/assets/app-icon/hicolor/128x128/apps/ai.storyteller.lightcraft.png new file mode 100644 index 00000000..f9da957a Binary files /dev/null and b/assets/app-icon/hicolor/128x128/apps/ai.storyteller.lightcraft.png differ diff --git a/assets/app-icon/hicolor/16x16/apps/ai.storyteller.lightcraft.png b/assets/app-icon/hicolor/16x16/apps/ai.storyteller.lightcraft.png new file mode 100644 index 00000000..037a2c43 Binary files /dev/null and b/assets/app-icon/hicolor/16x16/apps/ai.storyteller.lightcraft.png differ diff --git a/assets/app-icon/hicolor/24x24/apps/ai.storyteller.lightcraft.png b/assets/app-icon/hicolor/24x24/apps/ai.storyteller.lightcraft.png new file mode 100644 index 00000000..514eb83f Binary files /dev/null and b/assets/app-icon/hicolor/24x24/apps/ai.storyteller.lightcraft.png differ diff --git a/assets/app-icon/hicolor/256x256/apps/ai.storyteller.lightcraft.png b/assets/app-icon/hicolor/256x256/apps/ai.storyteller.lightcraft.png new file mode 100644 index 00000000..29398ec3 Binary files /dev/null and b/assets/app-icon/hicolor/256x256/apps/ai.storyteller.lightcraft.png differ diff --git a/assets/app-icon/hicolor/32x32/apps/ai.storyteller.lightcraft.png b/assets/app-icon/hicolor/32x32/apps/ai.storyteller.lightcraft.png new file mode 100644 index 00000000..087ad0a6 Binary files /dev/null and b/assets/app-icon/hicolor/32x32/apps/ai.storyteller.lightcraft.png differ diff --git a/assets/app-icon/hicolor/48x48/apps/ai.storyteller.lightcraft.png b/assets/app-icon/hicolor/48x48/apps/ai.storyteller.lightcraft.png new file mode 100644 index 00000000..0ed28ee7 Binary files /dev/null and b/assets/app-icon/hicolor/48x48/apps/ai.storyteller.lightcraft.png differ diff --git a/assets/app-icon/hicolor/512x512/apps/ai.storyteller.lightcraft.png b/assets/app-icon/hicolor/512x512/apps/ai.storyteller.lightcraft.png new file mode 100644 index 00000000..5933c280 Binary files /dev/null and b/assets/app-icon/hicolor/512x512/apps/ai.storyteller.lightcraft.png differ diff --git a/assets/app-icon/hicolor/64x64/apps/ai.storyteller.lightcraft.png b/assets/app-icon/hicolor/64x64/apps/ai.storyteller.lightcraft.png new file mode 100644 index 00000000..2b0c8f54 Binary files /dev/null and b/assets/app-icon/hicolor/64x64/apps/ai.storyteller.lightcraft.png differ diff --git a/assets/app-icon/hicolor/scalable/apps/ai.storyteller.lightcraft.svg b/assets/app-icon/hicolor/scalable/apps/ai.storyteller.lightcraft.svg new file mode 100644 index 00000000..9dc39ee8 --- /dev/null +++ b/assets/app-icon/hicolor/scalable/apps/ai.storyteller.lightcraft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/app-icon/lightcraft-1024.png b/assets/app-icon/lightcraft-1024.png new file mode 100644 index 00000000..20b1799f Binary files /dev/null and b/assets/app-icon/lightcraft-1024.png differ diff --git a/assets/app-icon/lightcraft-macos-512.png b/assets/app-icon/lightcraft-macos-512.png new file mode 100644 index 00000000..54f0b617 Binary files /dev/null and b/assets/app-icon/lightcraft-macos-512.png differ diff --git a/assets/app-icon/lightcraft-small.svg b/assets/app-icon/lightcraft-small.svg new file mode 100644 index 00000000..9dc39ee8 --- /dev/null +++ b/assets/app-icon/lightcraft-small.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/app-icon/lightcraft.icns b/assets/app-icon/lightcraft.icns new file mode 100644 index 00000000..0ceba6fc Binary files /dev/null and b/assets/app-icon/lightcraft.icns differ diff --git a/assets/app-icon/lightcraft.ico b/assets/app-icon/lightcraft.ico new file mode 100644 index 00000000..1aafd923 Binary files /dev/null and b/assets/app-icon/lightcraft.ico differ diff --git a/assets/app-icon/lightcraft.svg b/assets/app-icon/lightcraft.svg new file mode 100644 index 00000000..0fb16be3 --- /dev/null +++ b/assets/app-icon/lightcraft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packaging/icons.sh b/packaging/icons.sh new file mode 100755 index 00000000..cf7895ec --- /dev/null +++ b/packaging/icons.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Regenerate every app icon file from assets/app-icon/lightcraft.svg. +# +# Needs: resvg (brew install resvg / cargo install resvg). On macOS, iconutil also writes the +# .icns. The outputs are committed, so building and packaging never need these tools. +# +# packaging/icons.sh +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DIR="$ROOT/assets/app-icon" +SVG="$DIR/lightcraft.svg" +ID="ai.storyteller.lightcraft" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +command -v resvg >/dev/null || { echo "error: resvg not found (brew install resvg)" >&2; exit 1; } + +# The SVG is the full-bleed 512 tile (rx=112). Windows and Linux use it as is, so the lynx reads at +# 16-48 px. macOS icons follow Apple's grid: an 824/1024 body with a transparent margin, made by +# widening the viewBox (512 / 0.805 = 636, so 62 units each side). +MAC="$TMP/macos.svg" +sed 's/viewBox="0 0 512 512"/viewBox="-62 -62 636 636"/' "$SVG" >"$MAC" +grep -q 'viewBox="-62 -62 636 636"' "$MAC" || { echo "error: unexpected viewBox in $SVG" >&2; exit 1; } + +render() { resvg -w "$2" -h "$2" "$1" "$3" /dev/null; then + SET="$TMP/lightcraft.iconset" + mkdir -p "$SET" + for s in 16 32 128 256 512; do + render "$MAC" "$s" "$SET/icon_${s}x${s}.png" + render "$MAC" $((s * 2)) "$SET/icon_${s}x${s}@2x.png" + done + iconutil -c icns -o "$DIR/lightcraft.icns" "$SET" +else + echo "warning: iconutil not found (macOS only); lightcraft.icns not regenerated" >&2 +fi +echo "icons written to $DIR" diff --git a/packaging/linux/ai.storyteller.lightcraft.desktop b/packaging/linux/ai.storyteller.lightcraft.desktop new file mode 100644 index 00000000..5ea3af99 --- /dev/null +++ b/packaging/linux/ai.storyteller.lightcraft.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Type=Application +Name=LightCraft +GenericName=Photo Library and Raw Developer +Comment=Organise photos and develop raw files, non-destructively +Exec=lightcraft %F +Icon=ai.storyteller.lightcraft +Terminal=false +Categories=Graphics;2DGraphics;RasterGraphics;Photography; +Keywords=photo;raw;camera;library;develop; +StartupWMClass=ai.storyteller.lightcraft +MimeType=image/jpeg;image/png;image/tiff;image/webp;image/x-adobe-dng;image/x-sony-arw;image/x-canon-cr2;image/x-canon-cr3;image/x-nikon-nef;image/x-fuji-raf;image/x-olympus-orf;image/x-panasonic-rw2;image/x-pentax-pef; diff --git a/xtask/src/ico.rs b/xtask/src/ico.rs new file mode 100644 index 00000000..233d95f5 --- /dev/null +++ b/xtask/src/ico.rs @@ -0,0 +1,104 @@ +//! `cargo xtask ico …`: pack PNG images into a Windows `.ico`. +//! +//! Vista and later read PNG-compressed icon entries, so each PNG is stored as is. The size comes +//! from the PNG's IHDR chunk; 256 px is written as 0, per the ICO format. + +use std::path::Path; + +/// Width and height from a PNG's IHDR chunk. +pub fn png_size(png: &[u8]) -> Result<(u32, u32), String> { + const SIG: &[u8] = b"\x89PNG\r\n\x1a\n"; + if png.len() < 24 || &png[..8] != SIG || &png[12..16] != b"IHDR" { + return Err("not a PNG file".into()); + } + let w = u32::from_be_bytes([png[16], png[17], png[18], png[19]]); + let h = u32::from_be_bytes([png[20], png[21], png[22], png[23]]); + Ok((w, h)) +} + +/// An ICO file holding `pngs`, which must be square and at most 256 px. +pub fn pack(pngs: &[Vec]) -> Result, String> { + if pngs.is_empty() || pngs.len() > u16::MAX as usize { + return Err("need at least one PNG".into()); + } + let mut out = Vec::new(); + out.extend_from_slice(&0u16.to_le_bytes()); // reserved + out.extend_from_slice(&1u16.to_le_bytes()); // type: icon + out.extend_from_slice(&(pngs.len() as u16).to_le_bytes()); + let mut offset = 6 + 16 * pngs.len(); + for png in pngs { + let (w, h) = png_size(png)?; + if w != h || w == 0 || w > 256 { + return Err(format!("icon images must be square and at most 256 px, got {w}x{h}")); + } + let dim = if w == 256 { 0 } else { w as u8 }; + out.extend_from_slice(&[dim, dim, 0, 0]); // width, height, palette size, reserved + out.extend_from_slice(&1u16.to_le_bytes()); // colour planes + out.extend_from_slice(&32u16.to_le_bytes()); // bits per pixel + out.extend_from_slice(&(png.len() as u32).to_le_bytes()); + out.extend_from_slice(&(offset as u32).to_le_bytes()); + offset += png.len(); + } + for png in pngs { + out.extend_from_slice(png); + } + Ok(out) +} + +pub fn run(args: &[&str]) -> Result<(), String> { + let [out, inputs @ ..] = args else { + return Err("usage: cargo xtask ico …".into()); + }; + let pngs = inputs.iter().map(|p| std::fs::read(p).map_err(|e| format!("read {p}: {e}"))).collect::, _>>()?; + let ico = pack(&pngs)?; + std::fs::write(Path::new(out), &ico).map_err(|e| format!("write {out}: {e}"))?; + println!("{out}: {} images, {} bytes", pngs.len(), ico.len()); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn fake_png(size: u32) -> Vec { + let mut v = b"\x89PNG\r\n\x1a\n\0\0\0\x0dIHDR".to_vec(); + v.extend_from_slice(&size.to_be_bytes()); + v.extend_from_slice(&size.to_be_bytes()); + v.extend_from_slice(&[8, 6, 0, 0, 0, 1, 2, 3, 4]); + v + } + + #[test] + fn packs_directory_and_payloads() { + let a = fake_png(16); + let b = fake_png(256); + let ico = pack(&[a.clone(), b.clone()]).unwrap(); + assert_eq!(&ico[..6], &[0, 0, 1, 0, 2, 0]); + // Entry 0: 16 px at offset 6 + 32. + assert_eq!(&ico[6..8], &[16, 16]); + assert_eq!(u32::from_le_bytes(ico[14..18].try_into().unwrap()), a.len() as u32); + assert_eq!(u32::from_le_bytes(ico[18..22].try_into().unwrap()), 38); + // Entry 1: 256 px stored as 0, right after the first payload. + assert_eq!(&ico[22..24], &[0, 0]); + assert_eq!(u32::from_le_bytes(ico[34..38].try_into().unwrap()), 38 + a.len() as u32); + assert_eq!(&ico[38..38 + a.len()], &a[..]); + assert_eq!(&ico[38 + a.len()..], &b[..]); + } + + #[test] + fn rejects_bad_input() { + assert!(pack(&[]).is_err()); + assert!(pack(&[fake_png(512)]).is_err()); + assert!(pack(&[b"GIF89a".to_vec()]).is_err()); + assert_eq!(png_size(&fake_png(48)).unwrap(), (48, 48)); + } + + #[test] + fn committed_icon_is_valid() { + let path = crate::root().join("assets/app-icon/lightcraft.ico"); + let Ok(ico) = std::fs::read(&path) else { return }; + assert_eq!(&ico[..4], &[0, 0, 1, 0]); + let n = u16::from_le_bytes([ico[4], ico[5]]) as usize; + assert!(n >= 4, "expected several sizes in {}", path.display()); + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 7f4af926..f0c85f77 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -5,6 +5,7 @@ mod assets; mod bench; +mod ico; mod layers; mod parity; mod stats; @@ -21,6 +22,8 @@ commands: bench [FILE] [--strict] [--threshold PCT] run the render benchmark, append to target/bench/history.jsonl, compare CPU time with the previous run (default input: corpus/raw/arw-sony-a7m3-compressed.arw) + ico ... + pack square PNGs (<= 256 px) into a Windows .ico (see packaging/icons.sh) layers enforce the crate dependency layering (plan/architecture.md §3) parity [--write] check docs/parity.md (every cmd:/ctl: id and path it cites exists) and print the @@ -39,6 +42,7 @@ fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); let rest: Vec<&str> = args.iter().skip(1).map(String::as_str).collect(); let result = match args.first().map(String::as_str) { + Some("ico") => ico::run(&rest), Some("layers") => cmd_layers(), Some("assets") => assets::run(&root()), Some("bench") => bench::run(&root(), &rest),