diff --git a/ASSETS.md b/ASSETS.md index 6ed97ea2..37ee406d 100644 --- a/ASSETS.md +++ b/ASSETS.md @@ -38,6 +38,22 @@ Generated-in-code art is original and has no file to list: the UI icon set (`cra | `crates/render/tests/golden/feather.png` | DesignCraft contributors (generated) | rendered by DesignCraft's own renderer (`crates/render/tests/effects.rs`, synthetic shapes and bundled fonts) | MIT OR Apache-2.0 | Golden test image for soft effects | | `crates/render/tests/golden/inner-shadow.png` | DesignCraft contributors (generated) | rendered by DesignCraft's own renderer (`crates/render/tests/effects.rs`, synthetic shapes and bundled fonts) | MIT OR Apache-2.0 | Golden test image for soft effects | | `crates/render/tests/golden/outer-glow.png` | DesignCraft contributors (generated) | rendered by DesignCraft's own renderer (`crates/render/tests/effects.rs`, synthetic shapes and bundled fonts) | MIT OR Apache-2.0 | Golden test image for soft effects | +| `assets/app-icon/LICENSE.txt` | (licence text) | this project | — | Licence for the app icon | +| `assets/app-icon/designcraft-1024.png` | DesignCraft project owner (drawn in ArtCraft) | rendered from `assets/app-icon/designcraft.svg` by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) | +| `assets/app-icon/designcraft-macos-512.png` | DesignCraft project owner (drawn in ArtCraft) | rendered from `assets/app-icon/designcraft.svg` by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) | +| `assets/app-icon/designcraft-small.svg` | DesignCraft project owner (drawn in ArtCraft) | owner's ArtCraft drawing, vectorised with craftrules `assets/logo-options/_tools/vectorize_tile.py` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) | +| `assets/app-icon/designcraft.icns` | DesignCraft project owner (drawn in ArtCraft) | rendered from `assets/app-icon/designcraft.svg` by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) | +| `assets/app-icon/designcraft.ico` | DesignCraft project owner (drawn in ArtCraft) | rendered from `assets/app-icon/designcraft.svg` by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) | +| `assets/app-icon/designcraft.svg` | DesignCraft project owner (drawn in ArtCraft) | owner's ArtCraft drawing, vectorised with craftrules `assets/logo-options/_tools/vectorize_tile.py` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) | +| `assets/app-icon/hicolor/128x128/apps/ai.storyteller.designcraft.png` | DesignCraft project owner (drawn in ArtCraft) | rendered from `assets/app-icon/designcraft.svg` by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) | +| `assets/app-icon/hicolor/16x16/apps/ai.storyteller.designcraft.png` | DesignCraft project owner (drawn in ArtCraft) | rendered from `assets/app-icon/designcraft.svg` by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) | +| `assets/app-icon/hicolor/24x24/apps/ai.storyteller.designcraft.png` | DesignCraft project owner (drawn in ArtCraft) | rendered from `assets/app-icon/designcraft.svg` by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) | +| `assets/app-icon/hicolor/256x256/apps/ai.storyteller.designcraft.png` | DesignCraft project owner (drawn in ArtCraft) | rendered from `assets/app-icon/designcraft.svg` by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) | +| `assets/app-icon/hicolor/32x32/apps/ai.storyteller.designcraft.png` | DesignCraft project owner (drawn in ArtCraft) | rendered from `assets/app-icon/designcraft.svg` by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) | +| `assets/app-icon/hicolor/48x48/apps/ai.storyteller.designcraft.png` | DesignCraft project owner (drawn in ArtCraft) | rendered from `assets/app-icon/designcraft.svg` by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) | +| `assets/app-icon/hicolor/512x512/apps/ai.storyteller.designcraft.png` | DesignCraft project owner (drawn in ArtCraft) | rendered from `assets/app-icon/designcraft.svg` by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) | +| `assets/app-icon/hicolor/64x64/apps/ai.storyteller.designcraft.png` | DesignCraft project owner (drawn in ArtCraft) | rendered from `assets/app-icon/designcraft.svg` by `packaging/icons.sh` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) | +| `assets/app-icon/hicolor/scalable/apps/ai.storyteller.designcraft.svg` | DesignCraft project owner (drawn in ArtCraft) | owner's ArtCraft drawing, vectorised with craftrules `assets/logo-options/_tools/vectorize_tile.py` | MIT OR Apache-2.0 (`assets/app-icon/LICENSE.txt`) | App icon: calico cat in a polka-dot scarf (see `assets/app-icon/README.md`) | | `docs/brand/artcraft-logo.svg`, `docs/brand/artcraft-logo.png` | ArtCraft / Storyteller | ArtCraft brand kit (project owner) | ArtCraft trademark — used with the owner's permission; not covered by the code licence | ArtCraft wordmark (README header) | | `docs/brand/artcraft-logo-white.svg`, `docs/brand/artcraft-logo-white.png` | ArtCraft / Storyteller | ArtCraft brand kit (project owner) | ArtCraft trademark — used with the owner's permission; not covered by the code licence | Wordmark for dark backgrounds | | `docs/brand/artcraft-mark.svg`, `docs/brand/artcraft-mark.png` | ArtCraft / Storyteller | ArtCraft brand kit (project owner) | ArtCraft trademark — used with the owner's permission; not covered by the code licence | The mark; its outline is drawn in code by `crates/ui-egui/src/about.rs` (About splash, start screen) | diff --git a/Cargo.lock b/Cargo.lock index bde10898..aa455f8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -853,6 +853,7 @@ dependencies = [ "muda", "rfd", "serde_json", + "winresource", ] [[package]] @@ -4070,10 +4071,12 @@ 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", ] @@ -4107,6 +4110,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" @@ -5152,6 +5161,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 e30965b0..476d40cb 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@

+

+ DesignCraft app icon: a calico cat wearing a polka-dot scarf +

+

DesignCraft

@@ -139,6 +143,7 @@ egui frontend is a separate crate, so the UI can be swapped without touching the - **Status and milestones:** [ROADMAP.md](ROADMAP.md) - **Contributor and agent rules** (clean-room, asset policy, quality gates): [`AGENTS.md`](AGENTS.md) - **Bundled assets:** every one is listed with its licence in [`ASSETS.md`](ASSETS.md) +- **App icon and colour:** a calico cat in a polka-dot scarf on DesignCraft green `#7bb51c`; see [`assets/app-icon/`](assets/app-icon/README.md) ## The Crafting Apps diff --git a/apps/designcraft/Cargo.toml b/apps/designcraft/Cargo.toml index 33841e14..b7d3a2c3 100644 --- a/apps/designcraft/Cargo.toml +++ b/apps/designcraft/Cargo.toml @@ -17,6 +17,10 @@ egui = { workspace = true } serde_json = { workspace = true } log = { workspace = true } +[build-dependencies] +# Icon + VERSIONINFO for designcraft.exe; build.rs only uses it when the target is Windows. +winresource = "0.1" + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rfd = { workspace = true } diff --git a/apps/designcraft/build.rs b/apps/designcraft/build.rs new file mode 100644 index 00000000..0f6b58d9 --- /dev/null +++ b/apps/designcraft/build.rs @@ -0,0 +1,29 @@ +//! Windows only: embed the app icon and version info (VERSIONINFO) into `designcraft.exe`, so it +//! shows in Explorer, the taskbar, the Start menu and Alt-Tab. +//! +//! 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 `DESIGNCRAFT_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/designcraft.ico"); + println!("cargo:rerun-if-env-changed=DESIGNCRAFT_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/designcraft.ico") + .set("ProductName", "DesignCraft") + .set("FileDescription", "DesignCraft page layout") + .set("CompanyName", "Learning Machines LLC") + .set("LegalCopyright", "Copyright (c) the DesignCraft authors. MIT OR Apache-2.0.") + .set("OriginalFilename", "designcraft.exe") + .set("InternalName", "designcraft"); + if let Err(e) = res.compile() { + if std::env::var_os("DESIGNCRAFT_REQUIRE_WINRES").is_some() { + panic!("embedding Windows resources failed: {e}"); + } + println!("cargo:warning=designcraft.exe built without icon/version resources: {e}"); + } +} diff --git a/apps/designcraft/src/main.rs b/apps/designcraft/src/main.rs index e250f7c6..493e4f05 100644 --- a/apps/designcraft/src/main.rs +++ b/apps/designcraft/src/main.rs @@ -99,6 +99,19 @@ fn services() -> Services { } } +/// Desktop app ID: the Linux `.desktop` file name and hicolor icon name (Wayland matches windows by it). +const APP_ID: &str = "ai.storyteller.designcraft"; + +/// The window, Dock, taskbar and Alt-Tab icon. macOS gets Apple's icon grid (transparent margin); +/// elsewhere the full tile. Regenerate with `packaging/icons.sh`. +fn app_icon() -> Option { + #[cfg(target_os = "macos")] + let png: &[u8] = include_bytes!("../../../assets/app-icon/designcraft-macos-512.png"); + #[cfg(not(target_os = "macos"))] + let png: &[u8] = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.designcraft.png"); + eframe::icon_data::from_png_bytes(png).map_err(|e| log::warn!("app icon: {e}")).ok() +} + fn main() -> eframe::Result { let mut control_port: Option = std::env::var("DESIGNCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok()); let mut files = Vec::new(); @@ -115,7 +128,7 @@ fn main() -> eframe::Result { _ => files.push(a), } } - let options = eframe::NativeOptions { + let mut options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_title("DesignCraft") .with_inner_size([1440.0, 900.0]) @@ -126,6 +139,10 @@ fn main() -> eframe::Result { .with_title_shown(false), ..Default::default() }; + options.viewport = options.viewport.with_app_id(APP_ID); + if let Some(icon) = app_icon() { + options.viewport = options.viewport.with_icon(icon); + } eframe::run_native( "DesignCraft", options, diff --git a/assets/app-icon/LICENSE.txt b/assets/app-icon/LICENSE.txt new file mode 100644 index 00000000..2c9c2dda --- /dev/null +++ b/assets/app-icon/LICENSE.txt @@ -0,0 +1,13 @@ +DesignCraft app icon (designcraft.svg, designcraft-small.svg and every file derived from them in +this directory: designcraft-1024.png, designcraft-macos-512.png, designcraft.icns, designcraft.ico, +hicolor/). + +Original artwork made for this project by the project owner in ArtCraft, then vectorised. No +third-party material. Licensed like the rest of DesignCraft, 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..e0ee414a --- /dev/null +++ b/assets/app-icon/README.md @@ -0,0 +1,50 @@ +# DesignCraft app icon + +**A calico cat wearing a polka-dot scarf**, as an engraving-style head-and-shoulders portrait in the +Crafting Apps' owl-template framing: a full-bleed colour field, the cat in three-quarter view, its +shoulders running off the bottom edge. + +## Palette + +Exactly three colours: + +| Colour | Hex | Used for | +|---|---|---| +| Ink | `#0b0b0c` | Line work and contour | +| Paper | `#efe9dc` | The figure (the cat and its scarf) | +| DesignCraft green (app colour) | `#7bb51c` | The full-bleed background field | + +## Geometry + +A 512 × 512 viewBox, clipped to a rounded square with `rx=112`. Windows and Linux use the full tile. The +macOS icons (`.icns`, `designcraft-macos-512.png`) sit on Apple's grid: an 824/1024 body with a +transparent margin. + +## Provenance + +The owner drew the cat in ArtCraft. The 2880 px drawing, keyed to the three-colour palette, lives +outside this repo in craftrules at `assets/app-icons/designcraft/source.png`. It was vectorised with +craftrules' `assets/logo-options/_tools/vectorize_tile.py`: `designcraft.svg` is traced at 2048 px and +`designcraft-small.svg` at 1024 px. Design rules: craftrules `standards/icon-design.md`. + +## Files + +| File | Use | +|---|---| +| `designcraft.svg` | Canonical artwork (about 680 KB) | +| `designcraft-small.svg` | Lighter vector (about 350 KB); also `hicolor/scalable` | +| `designcraft-1024.png` | 1024 px render of the full tile | +| `designcraft-macos-512.png` | Runtime Dock icon on macOS (embedded by `apps/designcraft`) | +| `designcraft.icns` | macOS bundle icon (`CFBundleIconFile`) | +| `designcraft.ico` | Windows exe icon, 16–256 px (embedded by `apps/designcraft/build.rs`) | +| `hicolor//apps/ai.storyteller.designcraft.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.designcraft.svg` | Linux scalable icon | +| `LICENSE.txt` | MIT OR Apache-2.0 | + +The app ID is `ai.storyteller.designcraft`. It's used for the hicolor icon names, the Wayland app ID and +`packaging/linux/ai.storyteller.designcraft.desktop`. + +## Regenerate + +Edit `designcraft.svg` (or `designcraft-small.svg`), then run `packaging/icons.sh`. It needs `resvg`, and +`iconutil` for the `.icns` (macOS only). It packs the `.ico` with `cargo xtask ico`. diff --git a/assets/app-icon/designcraft-1024.png b/assets/app-icon/designcraft-1024.png new file mode 100644 index 00000000..1d5a44cd Binary files /dev/null and b/assets/app-icon/designcraft-1024.png differ diff --git a/assets/app-icon/designcraft-macos-512.png b/assets/app-icon/designcraft-macos-512.png new file mode 100644 index 00000000..46655217 Binary files /dev/null and b/assets/app-icon/designcraft-macos-512.png differ diff --git a/assets/app-icon/designcraft-small.svg b/assets/app-icon/designcraft-small.svg new file mode 100644 index 00000000..80d1dcfe --- /dev/null +++ b/assets/app-icon/designcraft-small.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/app-icon/designcraft.icns b/assets/app-icon/designcraft.icns new file mode 100644 index 00000000..3e32c292 Binary files /dev/null and b/assets/app-icon/designcraft.icns differ diff --git a/assets/app-icon/designcraft.ico b/assets/app-icon/designcraft.ico new file mode 100644 index 00000000..cdbaa533 Binary files /dev/null and b/assets/app-icon/designcraft.ico differ diff --git a/assets/app-icon/designcraft.svg b/assets/app-icon/designcraft.svg new file mode 100644 index 00000000..05d05db4 --- /dev/null +++ b/assets/app-icon/designcraft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/app-icon/hicolor/128x128/apps/ai.storyteller.designcraft.png b/assets/app-icon/hicolor/128x128/apps/ai.storyteller.designcraft.png new file mode 100644 index 00000000..02b2b782 Binary files /dev/null and b/assets/app-icon/hicolor/128x128/apps/ai.storyteller.designcraft.png differ diff --git a/assets/app-icon/hicolor/16x16/apps/ai.storyteller.designcraft.png b/assets/app-icon/hicolor/16x16/apps/ai.storyteller.designcraft.png new file mode 100644 index 00000000..d08780c2 Binary files /dev/null and b/assets/app-icon/hicolor/16x16/apps/ai.storyteller.designcraft.png differ diff --git a/assets/app-icon/hicolor/24x24/apps/ai.storyteller.designcraft.png b/assets/app-icon/hicolor/24x24/apps/ai.storyteller.designcraft.png new file mode 100644 index 00000000..77e03ab8 Binary files /dev/null and b/assets/app-icon/hicolor/24x24/apps/ai.storyteller.designcraft.png differ diff --git a/assets/app-icon/hicolor/256x256/apps/ai.storyteller.designcraft.png b/assets/app-icon/hicolor/256x256/apps/ai.storyteller.designcraft.png new file mode 100644 index 00000000..89a2bad3 Binary files /dev/null and b/assets/app-icon/hicolor/256x256/apps/ai.storyteller.designcraft.png differ diff --git a/assets/app-icon/hicolor/32x32/apps/ai.storyteller.designcraft.png b/assets/app-icon/hicolor/32x32/apps/ai.storyteller.designcraft.png new file mode 100644 index 00000000..bd951404 Binary files /dev/null and b/assets/app-icon/hicolor/32x32/apps/ai.storyteller.designcraft.png differ diff --git a/assets/app-icon/hicolor/48x48/apps/ai.storyteller.designcraft.png b/assets/app-icon/hicolor/48x48/apps/ai.storyteller.designcraft.png new file mode 100644 index 00000000..5bee383b Binary files /dev/null and b/assets/app-icon/hicolor/48x48/apps/ai.storyteller.designcraft.png differ diff --git a/assets/app-icon/hicolor/512x512/apps/ai.storyteller.designcraft.png b/assets/app-icon/hicolor/512x512/apps/ai.storyteller.designcraft.png new file mode 100644 index 00000000..a6c59aca Binary files /dev/null and b/assets/app-icon/hicolor/512x512/apps/ai.storyteller.designcraft.png differ diff --git a/assets/app-icon/hicolor/64x64/apps/ai.storyteller.designcraft.png b/assets/app-icon/hicolor/64x64/apps/ai.storyteller.designcraft.png new file mode 100644 index 00000000..9e3afe37 Binary files /dev/null and b/assets/app-icon/hicolor/64x64/apps/ai.storyteller.designcraft.png differ diff --git a/assets/app-icon/hicolor/scalable/apps/ai.storyteller.designcraft.svg b/assets/app-icon/hicolor/scalable/apps/ai.storyteller.designcraft.svg new file mode 100644 index 00000000..80d1dcfe --- /dev/null +++ b/assets/app-icon/hicolor/scalable/apps/ai.storyteller.designcraft.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..6e06fb9a --- /dev/null +++ b/packaging/icons.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Regenerate every app icon from assets/app-icon/designcraft.svg. +# +# Needs: resvg (brew install resvg / cargo install resvg). On macOS, iconutil also writes the +# .icns. The outputs are committed, so builds 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/designcraft.svg" +ID="ai.storyteller.designcraft" +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 artwork is a full-bleed 512 tile (rx=112). Windows and Linux use it as is. macOS icons sit on +# Apple's grid: an 824/1024 body with a transparent margin, so widen the viewBox to 512*1024/824. +MAC="$TMP/mac.svg" +sed 's/viewBox="0 0 512 512"/viewBox="-62.136 -62.136 636.272 636.272"/' "$SVG" >"$MAC" +grep -q 'viewBox="-62.136' "$MAC" || { echo "error: expected viewBox=\"0 0 512 512\" in $SVG" >&2; exit 1; } + +render() { resvg -w "$2" -h "$2" "$1" "$3" /dev/null; then + SET="$TMP/designcraft.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/designcraft.icns" "$SET" +else + echo "warning: iconutil not found (macOS only); designcraft.icns not regenerated" >&2 +fi +echo "icons written to $DIR" diff --git a/packaging/linux/ai.storyteller.designcraft.desktop b/packaging/linux/ai.storyteller.designcraft.desktop new file mode 100644 index 00000000..cdf1461c --- /dev/null +++ b/packaging/linux/ai.storyteller.designcraft.desktop @@ -0,0 +1,13 @@ +[Desktop Entry] +Type=Application +Name=DesignCraft +GenericName=Page Layout +Comment=Lay out magazines, books and print documents +Exec=designcraft %F +TryExec=designcraft +Icon=ai.storyteller.designcraft +Terminal=false +StartupNotify=true +StartupWMClass=ai.storyteller.designcraft +Categories=Graphics;Publishing; +Keywords=layout;page;desktop publishing;dtp;magazine;book;indesign;idml;typesetting; diff --git a/xtask/src/ico.rs b/xtask/src/ico.rs new file mode 100644 index 00000000..6d99ebfb --- /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/designcraft.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 83c7829b..e39a9411 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -4,6 +4,7 @@ //! invoked through `std::process::Command`. mod assets; +mod ico; mod layers; mod parity; mod stats; @@ -23,6 +24,8 @@ commands: show where test corpora live; --download fetches PngSuite into corpus/pngsuite stats [--exact] count tests and lines per crate (--exact: ask the test harness via `-- --list`) parity recompute the InDesign feature-parity summary in docs/parity.md + ico ... + pack square PNGs (<= 256 px) into a Windows .ico (see packaging/icons.sh) "; fn main() -> ExitCode { @@ -36,6 +39,7 @@ fn main() -> ExitCode { Some("corpus") => cmd_corpus(rest.contains(&"--download")), Some("stats") => stats::run(&root(), rest.contains(&"--exact")), Some("parity") => parity::run(&root()), + Some("ico") => ico::run(&rest), Some("-h" | "--help" | "help") | None => { print!("{USAGE}"); Ok(())