Skip to main content

hyprforge_image/
lib.rs

1//! What a picture is, before anything draws it.
2//!
3//! Measure it without decoding it, work out what may safely be decoded,
4//! decode that much, and turn it the right way up. No iced, no Wayland,
5//! no async runtime, nothing Hyprland-shaped — so a viewer, a preview
6//! pane and a test without a window can all ask the same questions.
7//!
8//! # The finding this crate exists for
9//!
10//! iced decodes an image handle at **full resolution with no cap**, and
11//! applies EXIF orientation — but only for two of its three handle kinds.
12//! In `iced_graphics-0.14.0/src/image.rs`, `Handle::Path` and
13//! `Handle::Bytes` go through `image::open` / `load_from_memory` and are
14//! then rotated per `exif::Tag::Orientation`. `Handle::Rgba` is a pure
15//! passthrough of the pixels it is given.
16//!
17//! For a wallpaper or a thumbnail that is fine. For a viewer it is not: a
18//! 36-megapixel photograph decodes to 144MB and peaks near 300MB once the
19//! renderer has its own premultiplied copy — the allocation profile that
20//! already cost this suite a lock screen, where a failure inside
21//! `iced_tiny_skia` caches as "no entry" and panics on the *next* frame.
22//!
23//! So a viewer must hand the renderer `Handle::Rgba` built from pixels it
24//! decoded within a budget of its own — and the moment it does, it takes
25//! on the orientation work too. Two halves, and getting either one alone
26//! is a bug: skip orientation and every portrait phone photograph is
27//! sideways; apply it here *and* use a path handle elsewhere and the same
28//! picture is turned twice.
29//!
30//! # Where to start
31//!
32//! [`measure`](measure()) first, always — it allocates nothing and everything else
33//! depends on its answer. Then [`budget`], which is pure arithmetic and
34//! the only place a cap lives. Then [`decode::decode_to_fit`].
35//!
36//! # Honest about what the budget bounds
37//!
38//! What is *retained* is capped by the viewport: 56MB for a
39//! 36-megapixel photograph in a 2560x1600 window, against 137MB
40//! uncapped. What is *transient* cannot be capped here, because `image`
41//! 0.25 has no DCT-scaled decode — a JPEG is decoded whole and then
42//! scaled down, and the peak belongs to the source.
43//!
44//! That peak was measured rather than assumed, and measuring it changed
45//! the code: it began at 509MB and is 214MB, because the obvious
46//! downscaler turned out to cost more than the decode it followed. The
47//! table and the reasoning are in [`budget`], the line itself in
48//! [`decode`].
49//!
50//! It also corrected something this doc used to claim: `image`'s own
51//! `Limits` does *not* bound that peak — the measurement is identical
52//! with and without it.
53
54pub mod budget;
55pub mod camera;
56pub mod decode;
57pub mod error;
58pub mod format;
59pub mod measure;
60pub mod orientation;
61
62pub use budget::{Budget, DecodePixels, ViewportPixels};
63pub use camera::Camera;
64pub use decode::{decode_to_fit, Decoded};
65pub use error::ImageError;
66pub use measure::{measure, Measured, SourcePixels};
67pub use orientation::Orientation;