makepad/libs/win_resource/src/ico.rs
Admin f03c22e47d libs: the AI stack, flowgraph, git, audio_route, win_resource and the other libraries; Flow, the asset library and VJ move to Stage
Library changes outside the platform's own dependencies (those came with the platform commits above). libs/asset and libs/flow move to the Stage repository with the apps that use them.

Covers work ebf442a94..2b1a41df4 for libs:

- wm: the Android super-app compiles tiles on the phone against the host engine
- cargo-makepad: the super-app APK is packed by `android dyn-pack`, in Rust
- cef: a page's audio can be captured instead of played
- flow: the flow-ui app and the flowgraph canvas return to the workspace, with staged progress in libs/flow
- svg: a stroke that bends tighter than its half width collapses its inner edge to the corner
- the accent a name is written with stops deciding whether search finds it
- ai stems: LaneDemixer, VocalsModel and the platform Stage origin/main needs
- The regex engine accepts \b and \B, reports the earliest match so a scan can resume after it, and can decide word boundaries on ASCII so a code sear
- workspace: Scope's settings carry the code map's tab width and the directories each prepared project hides from its map
- cef, video, platform: what Stage's browser needs from the shared layers
- platform, wm: work builds again on iOS, tvOS and wasm, and the window manager is warning-free on Windows
- ci: tools/ci, a Splash runtime that watches a branch and runs every ci.splash it finds
- stitch: the crate says what the rest of the repository says about its license
- ci: a run is one target dir and one cargo batch per target, and the wall draws
- apps: what the CI box found
- Remove Flow, asset and VJ applications moved into Stage
- tests: the workspace suite, run as a whole for the first time, passes outside the example UI tests
- libs: no warnings in the workspace check on any row
- tests: the last three failures of the CI box's night
- gif: the crate's doc examples compile
- cef: a hosted browser survives its host's exit
- tests: the three binaries over ten seconds in release come under it
- git: the memory ledger can take what it is asked for
- script: re-entrant dispatch, thread index validation, Any-based handle downcasts, UTF-8 previews; regex: never_loop fix
- fix(text): enable ttf-parser gvar-alloc for many-tuple variable fonts
- platform, widgets, wm: a touch that is taken away is cancelled, never released
- platform, wm: hosted apps ask the WM for what they cannot do themselves
- wm on Android: apps build on the phone
- task, cargo-makepad, widgets, audio: the task manager chooses its columns, graphs every process' network and disk traffic and installs itself as a D
- flowgraph: ordered ports take many wires, sockets can override their icon, and the canvas embeds as a viewport
- cef: captured frames and audio carry callback clocks and a navigation epoch
- git: timings read a portable clock
- ai: the hub's rig-fixture tests find the asset library without the asset client
- ai llm: a Metal main buffer is filled in 32 MB chunks
- diskmap: a scan lists each macOS directory in one getattrlistbulk call and classifies files without allocating or locking
- platform, widgets, audio_route, ai: window crossfades and whole-frame presents, caption controls that click, themed menus, a stereo-pair audio tap,
- builder: the Builder TUI matches the new terminal design, Windows executables carry app icons, and every app has one
- sheets, score, git, home: saves cannot cut a file short or overwrite another one, and checkouts cannot write outside the repository
- git: worktree status matches git on real repositories and can be cancelled
- cuda: builds link only the CUDA toolkit they are given, never a system install
- audio_route: one API on macOS, Windows and Linux, monitoring by default and processing only when asked, plus shaders that discard a value compile to
- platform, widgets, ai: a restyle recompiles nothing it already has, shows only complete frames, and a window can own its caption
- audio_route example: print the samples that reached the processor each second
- widgets, builder: the big widget families are features, all on by default, and the Builder takes none of them
- ai hub: a local chat with no model says so once, plainly
- ai: Claude Desktop drives any app with the F10 panel
- wm: children open no console windows on Windows
- ai: Connect hands the .mcpb to Claude Desktop itself
- civil-time: one local wall clock for every app, and it knows the zone on Windows
- ai-speech: makepad-ai-sfx (and with it ai-h3) is built only for IndexTTS
- script + wasm_bridge: no build script that reruns on every file
- build: the AI stack, csg, Scope and Source Library join the one workspace
- ai chat: the chat's backends are the CLI, MCP and cloud providers; the local model is the `localai` feature
- widgets: every widget family is a crate of its own under widgets/families, and makepad-widgets links, re-exports and registers the ones its features
- apps, examples, libs: each crate builds only the widget families it uses, and the design overlay is each app's own default feature
- livepipe: takes makepad-widgets without its default families, now that the AI stack is in the one workspace
- ai chat: a build without local AI starts on the first provider that answers here, not on "No model"
- thiserror: RUSTC_BOOTSTRAP no longer turns on the unstable generic member access API
- builder: makepad-builder.exe declares itself like a well-formed Windows program and stops a stalled build through a job object instead of taskkill
- builder + ai-cuda: the CUDA kernel progress shows in the Builder's Compile row, not scribbled over its screen
- repo: no third-party comic archive, generated models or sample asset in the tree
- builder: the Windows Builder ships as source, runs in its own console, and apps compile with Rust's GNU toolchain or Microsoft's
- win_resource: PNG decode and encode on makepad-fast-inflate instead of zune-png
- builder: one folder layout everywhere, makepad-builder.zip with only the files a Windows build reads, a Rust download over parallel connections, and

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 20:10:57 +02:00

221 lines
9.3 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Windows icon images: the per-size `RT_ICON` payloads (32-bit DIB, or PNG
//! for 256 px), the `RT_GROUP_ICON` directory that names them, and the .ico
//! file format for reading existing icons.
use crate::icns::{decode_icns, SourceImage};
use crate::image::{Image, PNG_SIGNATURE};
/// Sizes Explorer, the taskbar and Alt-Tab ask for at 100–200 % scaling.
pub const ICON_SIZES: [u32; 7] = [16, 24, 32, 48, 64, 128, 256];
#[derive(Clone, Debug, PartialEq)]
pub struct IconImage {
pub width: u32,
pub height: u32,
/// A PNG file, or a BITMAPINFOHEADER DIB with doubled height followed by
/// the colour pixels and the 1-bit AND mask.
pub data: Vec<u8>,
}
impl IconImage {
pub fn is_png(&self) -> bool {
self.data.starts_with(PNG_SIGNATURE)
}
pub fn decode(&self) -> Result<Image, String> {
if self.is_png() { Image::decode_png(&self.data) } else { decode_dib(&self.data) }
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct GroupEntry {
pub width: u32,
pub height: u32,
pub bit_count: u16,
pub bytes: u32,
pub id: u16,
}
/// Decode an icon source by content: `.icns`, a single PNG, or `.ico`.
pub fn load_source(bytes: &[u8]) -> Result<Vec<SourceImage>, String> {
let images = if bytes.starts_with(b"icns") {
decode_icns(bytes)?
} else if bytes.starts_with(PNG_SIGNATURE) {
vec![SourceImage { image: Image::decode_png(bytes)?, png: Some(bytes.to_vec()) }]
} else if bytes.starts_with(&[0, 0, 1, 0]) {
let mut images = Vec::new();
for icon in parse_ico(bytes)? {
let png = icon.is_png().then(|| icon.data.clone());
images.push(SourceImage { image: icon.decode()?, png });
}
images
} else {
return Err("unsupported icon source: expected .icns, .png or .ico".into());
};
if images.is_empty() {
return Err("icon source holds no pictures we can read".into());
}
Ok(images)
}
/// The icon's pictures for each of `ICON_SIZES` up to the largest source.
/// Exact source sizes are used as they are; others are reduced from the
/// smallest larger source. 256 px is stored as PNG, the rest as DIBs, which
/// every Windows version and icon consumer reads.
pub fn icon_images(sources: &[SourceImage]) -> Result<Vec<IconImage>, String> {
let side = |s: &SourceImage| s.image.width.min(s.image.height);
let largest = sources.iter().map(side).max().ok_or("icon source holds no pictures")?;
let mut sizes: Vec<u32> = ICON_SIZES.iter().copied().filter(|&s| s <= largest).collect();
if sizes.is_empty() {
sizes.push(largest);
}
let mut images = Vec::new();
for size in sizes {
let exact = sources.iter().find(|s| s.image.width == size && s.image.height == size);
if size >= 256 {
if let Some(png) = exact.and_then(|s| s.png.clone()) {
images.push(IconImage { width: size, height: size, data: png });
continue;
}
}
let image = match exact {
Some(source) => source.image.clone(),
None => {
let from = sources.iter().filter(|s| side(s) >= size).min_by_key(|s| side(s)).unwrap();
from.image.resample(size, size)
}
};
let data = if size >= 256 { image.encode_png()? } else { encode_dib(&image) };
images.push(IconImage { width: size, height: size, data });
}
Ok(images)
}
pub fn encode_dib(image: &Image) -> Vec<u8> {
let (w, h) = (image.width as usize, image.height as usize);
let mask_stride = (w + 31) / 32 * 4;
let mut out = Vec::with_capacity(40 + w * h * 4 + mask_stride * h);
for v in [40u32, w as u32, (h * 2) as u32] { out.extend_from_slice(&v.to_le_bytes()); }
out.extend_from_slice(&1u16.to_le_bytes());
out.extend_from_slice(&32u16.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
out.extend_from_slice(&((w * h * 4 + mask_stride * h) as u32).to_le_bytes());
out.extend_from_slice(&[0; 16]);
for y in (0..h).rev() {
for p in image.rgba[y * w * 4..(y + 1) * w * 4].chunks_exact(4) {
out.extend_from_slice(&[p[2], p[1], p[0], p[3]]);
}
}
// The AND mask marks fully transparent pixels for consumers that ignore
// the alpha channel.
for y in (0..h).rev() {
let mut row = vec![0u8; mask_stride];
for x in 0..w {
if image.rgba[(y * w + x) * 4 + 3] == 0 { row[x / 8] |= 0x80 >> (x % 8); }
}
out.extend_from_slice(&row);
}
out
}
/// Read a 32- or 24-bit icon DIB (the formats `encode_dib` and common icon
/// editors write).
pub fn decode_dib(data: &[u8]) -> Result<Image, String> {
let u32_at = |at: usize| data.get(at..at + 4).map(|b| u32::from_le_bytes(b.try_into().unwrap()));
let header = u32_at(0).ok_or("DIB truncated")? as usize;
let w = u32_at(4).ok_or("DIB truncated")? as i32;
let h2 = u32_at(8).ok_or("DIB truncated")? as i32;
let bits = data.get(14..16).map(|b| u16::from_le_bytes(b.try_into().unwrap())).ok_or("DIB truncated")?;
let compression = u32_at(16).ok_or("DIB truncated")?;
if header < 40 || w <= 0 || h2 <= 0 || w > 1024 || h2 > 2048 || compression != 0 || !(bits == 32 || bits == 24) {
return Err(format!("unsupported icon DIB: {w}x{h2}, {bits} bit, compression {compression}"));
}
let (w, h) = (w as usize, h2 as usize / 2);
let stride = (w * bits as usize / 8 + 3) / 4 * 4;
let mask_stride = (w + 31) / 32 * 4;
let pixels = data.get(header..header + stride * h).ok_or("DIB pixels truncated")?;
let mask = data.get(header + stride * h..header + stride * h + mask_stride * h);
let mut rgba = vec![0u8; w * h * 4];
for y in 0..h {
let row = &pixels[(h - 1 - y) * stride..];
for x in 0..w {
let p = &row[x * bits as usize / 8..];
let alpha = if bits == 32 {
p[3]
} else {
let masked = mask.map_or(false, |m| m[(h - 1 - y) * mask_stride + x / 8] & (0x80 >> (x % 8)) != 0);
if masked { 0 } else { 255 }
};
rgba[(y * w + x) * 4..(y * w + x + 1) * 4].copy_from_slice(&[p[2], p[1], p[0], alpha]);
}
}
Image::new(w as u32, h as u32, rgba)
}
fn dir_dimension(v: u32) -> u8 {
if v >= 256 { 0 } else { v as u8 }
}
/// `GRPICONDIR`: the `RT_GROUP_ICON` payload naming `RT_ICON` resources
/// `first_id..`.
pub fn group_icon(images: &[IconImage], first_id: u16) -> Vec<u8> {
let mut out = Vec::with_capacity(6 + 14 * images.len());
for v in [0u16, 1, images.len() as u16] { out.extend_from_slice(&v.to_le_bytes()); }
for (i, image) in images.iter().enumerate() {
out.extend_from_slice(&[dir_dimension(image.width), dir_dimension(image.height), 0, 0]);
out.extend_from_slice(&1u16.to_le_bytes());
out.extend_from_slice(&32u16.to_le_bytes());
out.extend_from_slice(&(image.data.len() as u32).to_le_bytes());
out.extend_from_slice(&(first_id + i as u16).to_le_bytes());
}
out
}
pub fn parse_group_icon(data: &[u8]) -> Result<Vec<GroupEntry>, String> {
let u16_at = |at: usize| data.get(at..at + 2).map(|b| u16::from_le_bytes(b.try_into().unwrap())).ok_or("group icon truncated");
if u16_at(0)? != 0 || u16_at(2)? != 1 {
return Err("not a group icon directory".into());
}
let count = u16_at(4)? as usize;
(0..count).map(|i| {
let e = data.get(6 + i * 14..6 + (i + 1) * 14).ok_or("group icon entry truncated")?;
let dim = |b: u8| if b == 0 { 256 } else { b as u32 };
Ok(GroupEntry {
width: dim(e[0]),
height: dim(e[1]),
bit_count: u16::from_le_bytes([e[6], e[7]]),
bytes: u32::from_le_bytes(e[8..12].try_into().unwrap()),
id: u16::from_le_bytes([e[12], e[13]]),
})
}).collect()
}
pub fn write_ico(images: &[IconImage]) -> Vec<u8> {
let mut out = Vec::new();
for v in [0u16, 1, images.len() as u16] { out.extend_from_slice(&v.to_le_bytes()); }
let mut offset = 6 + 16 * images.len();
for image in images {
out.extend_from_slice(&[dir_dimension(image.width), dir_dimension(image.height), 0, 0]);
out.extend_from_slice(&1u16.to_le_bytes());
out.extend_from_slice(&32u16.to_le_bytes());
out.extend_from_slice(&(image.data.len() as u32).to_le_bytes());
out.extend_from_slice(&(offset as u32).to_le_bytes());
offset += image.data.len();
}
for image in images { out.extend_from_slice(&image.data); }
out
}
pub fn parse_ico(bytes: &[u8]) -> Result<Vec<IconImage>, String> {
let u16_at = |at: usize| bytes.get(at..at + 2).map(|b| u16::from_le_bytes(b.try_into().unwrap())).ok_or(".ico truncated");
if u16_at(0)? != 0 || u16_at(2)? != 1 {
return Err("not an .ico file".into());
}
let count = u16_at(4)? as usize;
(0..count).map(|i| {
let e = bytes.get(6 + i * 16..6 + (i + 1) * 16).ok_or(".ico entry truncated")?;
let size = u32::from_le_bytes(e[8..12].try_into().unwrap()) as usize;
let offset = u32::from_le_bytes(e[12..16].try_into().unwrap()) as usize;
let data = bytes.get(offset..offset.checked_add(size).ok_or(".ico entry overflows")?).ok_or(".ico image outside the file")?.to_vec();
let dim = |b: u8| if b == 0 { 256 } else { b as u32 };
Ok(IconImage { width: dim(e[0]), height: dim(e[1]), data })
}).collect()
}