app icon: calico cat in a polka-dot scarf (owner's ArtCraft drawing), Dock/taskbar/launcher wiring, palette docs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -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) |
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img alt="DesignCraft app icon: a calico cat wearing a polka-dot scarf" src="assets/app-icon/hicolor/256x256/apps/ai.storyteller.designcraft.png" width="128">
|
||||
</p>
|
||||
|
||||
<h1 align="center">DesignCraft</h1>
|
||||
|
||||
<p align="center">
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -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<egui::IconData> {
|
||||
#[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<u16> = 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,
|
||||
|
||||
@@ -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.
|
||||
@@ -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/<size>/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`.
|
||||
|
After Width: | Height: | Size: 295 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 348 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 664 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 717 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 348 KiB |
@@ -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; }
|
||||
|
||||
render "$SVG" 1024 "$DIR/designcraft-1024.png"
|
||||
# Runtime window/Dock icon on macOS (embedded by apps/designcraft); other platforms use hicolor 256.
|
||||
render "$MAC" 512 "$DIR/designcraft-macos-512.png"
|
||||
|
||||
# Linux hicolor theme.
|
||||
for s in 16 24 32 48 64 128 256 512; do
|
||||
mkdir -p "$DIR/hicolor/${s}x${s}/apps"
|
||||
render "$SVG" "$s" "$DIR/hicolor/${s}x${s}/apps/$ID.png"
|
||||
done
|
||||
mkdir -p "$DIR/hicolor/scalable/apps"
|
||||
cp "$DIR/designcraft-small.svg" "$DIR/hicolor/scalable/apps/$ID.svg"
|
||||
|
||||
# Windows .ico.
|
||||
ICO_PNGS=()
|
||||
for s in 16 20 24 32 40 48 64 128 256; do
|
||||
render "$SVG" "$s" "$TMP/ico-$s.png"
|
||||
ICO_PNGS+=("$TMP/ico-$s.png")
|
||||
done
|
||||
(cd "$ROOT" && cargo run -q -p xtask -- ico "$DIR/designcraft.ico" "${ICO_PNGS[@]}")
|
||||
|
||||
# macOS .icns.
|
||||
if command -v iconutil >/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"
|
||||
@@ -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;
|
||||
@@ -0,0 +1,104 @@
|
||||
//! `cargo xtask ico <out.ico> <in.png>…`: 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<u8>]) -> Result<Vec<u8>, 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 <out.ico> <in.png>…".into());
|
||||
};
|
||||
let pngs = inputs.iter().map(|p| std::fs::read(p).map_err(|e| format!("read {p}: {e}"))).collect::<Result<Vec<_>, _>>()?;
|
||||
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<u8> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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 <out.ico> <in.png>...
|
||||
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(())
|
||||
|
||||