hyprforge-image
Bounded, orientation-correct picture decoding: measure without decoding, decide how much may safely be decoded, decode that much, and apply EXIF orientation; no GUI toolkit.
What a picture is, before anything draws it.
Measure it without decoding it, work out what may safely be decoded, decode that much, and turn it the right way up. No iced, no Wayland, no async runtime, nothing Hyprland-shaped — so a viewer, a preview pane and a test without a window can all ask the same questions.
The finding this crate exists for
iced decodes an image handle at full resolution with no cap, and
applies EXIF orientation — but only for two of its three handle kinds.
In iced_graphics-0.14.0/src/image.rs, Handle::Path and
Handle::Bytes go through image::open / load_from_memory and are
then rotated per exif::Tag::Orientation. Handle::Rgba is a pure
passthrough of the pixels it is given.
For a wallpaper or a thumbnail that is fine. For a viewer it is not: a
36-megapixel photograph decodes to 144MB and peaks near 300MB once the
renderer has its own premultiplied copy — the allocation profile that
already cost this suite a lock screen, where a failure inside
iced_tiny_skia caches as "no entry" and panics on the next frame.
So a viewer must hand the renderer Handle::Rgba built from pixels it
decoded within a budget of its own — and the moment it does, it takes
on the orientation work too. Two halves, and getting either one alone
is a bug: skip orientation and every portrait phone photograph is
sideways; apply it here and use a path handle elsewhere and the same
picture is turned twice.
Where to start
measure) first, always — it allocates nothing and everything else
depends on its answer. Then budget, which is pure arithmetic and
the only place a cap lives. Then decode::decode_to_fit.
Honest about what the budget bounds
What is retained is capped by the viewport: 56MB for a
36-megapixel photograph in a 2560x1600 window, against 137MB
uncapped. What is transient cannot be capped here, because image
0.25 has no DCT-scaled decode — a JPEG is decoded whole and then
scaled down, and the peak belongs to the source.
That peak was measured rather than assumed, and measuring it changed
the code: it began at 509MB and is 214MB, because the obvious
downscaler turned out to cost more than the decode it followed. The
table and the reasoning are in budget, the line itself in
decode.
It also corrected something this doc used to claim: image's own
Limits does not bound that peak — the measurement is identical
with and without it.
Where this lives
Part of Hyprforge, a suite of native Hyprland desktop
applications. This crate is crates/hyprforge-image there; it is published to
crates.io so the suite's applications can be built from their own
repositories, and its API follows the suite. Issues and pull requests go
to the suite repository.
cargo add hyprforge-image
This README is rendered from the crate's //! documentation by
tools/crate-readme.py; edit src/lib.rs, not this file.
Licence
MIT. See LICENSE.