# 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](https://github.com/hyprforge-suite/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`.