# Images
Guido supports displaying both raster images (PNG, JPEG, GIF, WebP) and SVG vector graphics. Images are rendered as GPU textures and compose seamlessly with container transforms.
## Basic Usage
The `image()` function creates an image widget from a file path:
```rust
# extern crate guido;
# fn main() {
use guido::prelude::*;
// Load a PNG image
container()
.width(32.0)
.height(32.0)
.child(image("./icon.png"));
// Load an SVG (auto-detected by extension)
container()
.width(100.0)
.height(100.0)
.child(image("./logo.svg"))
# ;
# }
```
## Image Sources
You can load images from different sources using `ImageSource`:
```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
# let svg_string = String::new();
# let image_bytes: &[u8] = &[];
# let rgba_bytes: Vec<u8> = Vec::new();
// From file path (raster)
ImageSource::Path("./photo.jpg".into());
// From memory (raster)
ImageSource::Bytes(image_bytes.into());
// From file path (SVG)
ImageSource::SvgPath("./icon.svg".into());
// From memory (SVG)
ImageSource::SvgBytes(svg_string.as_bytes().into());
// Raw pre-decoded RGBA8 pixels (width * height * 4 bytes, row-major)
ImageSource::Rgba { width: 22, height: 22, pixels: rgba_bytes.into() }
# ;
# }
```
When using a string path with `image()`, the file extension determines the type automatically: `.svg` files use SVG rendering, all others use raster decoding.
`ImageSource::Rgba` skips decoding entirely — use it for pixel data that never existed in an encoded format, such as tray icon pixmaps or album art received over D-Bus. Its pixels have straight alpha, as a decoder gives them, not premultiplied.
## Loading in the Background
A raster `Path` or `Bytes` source is decoded on a background thread, not in the
frame that first shows it. A large wallpaper takes a noticeable fraction of a
second to decode, and a frame that waited for it would put nothing on screen
until then. So the first frame goes out without the picture: the box is laid out
at the image's size, read from the file's header, and nothing is drawn in it.
When the decode finishes the image is drawn on the next frame, with nothing from
your application needed to prompt it. Two images showing the same source share
one decode.
An SVG is rasterized at the size it is shown. An icon-sized one is cheap enough to
draw in the frame that shows it, so it appears at once. A large one — anything
past 128 × 128 pixels once HiDPI and the 2× quality are counted — is rasterized
on the same background thread and appears on a later frame the same way. When
its box changes size it needs a new raster, and until that one is ready the
previous one is drawn, stretched, so a large SVG in a resize animation never
blinks out.
The image does not fade itself in. It tells you when it is ready, as a signal,
and a fade is then an `opacity` with a transition on the container around it:
```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
let wallpaper = image("./wallpaper.png").content_fit(ContentFit::Cover);
let ready = wallpaper.ready();
container()
.width(fill())
.height(fill())
.opacity((move || if ready.get() { 1.0 } else { 0.0 }).transition(200.0))
.child(wallpaper)
# ;
# }
```
`ready()` follows the source: when a reactive source changes, it is false until
the new one is decoded. A source that failed to decode is never ready, and says
why once in the log. An SVG is ready from the first frame that draws it: an
icon-sized one is drawn, and usually ready, from the start, and a large one once
its first raster lands. `ImageSource::Rgba` is ready from the start — the way to
have a raster image in the very first frame, if you decoded it yourself.
## Sizing
The box comes from the enclosing container, like every other size in guido; the
image decides only how its pixels land inside it.
```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
// A 32x32 box
container().width(32.0).height(32.0).child(image("./icon.png"));
// Width fixed, height from the aspect ratio (the default fit is Contain)
container().width(200.0).child(image("./banner.png"));
// No box at all - the image reports its intrinsic size
image("./icon.png")
# ;
# }
```
## Content Fit Modes
The `content_fit()` method controls how the image maps into the box it is
given:
| `ContentFit::Contain` | Fit within the box, preserving aspect ratio (default) |
| `ContentFit::Cover` | Fill the box, cropping, preserving aspect ratio |
| `ContentFit::Fill` | Stretch to fill exactly, ignoring aspect ratio |
| `ContentFit::None` | Intrinsic size, ignoring the box |
`Cover` and `Fill` take all the room the container offers; they differ in how
the pixels land. `Contain` takes only the largest rect of the image's own
aspect ratio that fits, so the empty strip is left to the parent's alignment
rather than painted.
Like every declared value, it takes a signal as readily as a value, so an image
can change how it fills its box without being rebuilt:
```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
# let cropped = create_signal(true);
image("./photo.jpg").content_fit(move || {
if cropped.get() {
ContentFit::Cover
} else {
ContentFit::Contain
}
})
# ;
# }
```
```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
# let wallpaper = "./wallpaper.jpg";
// Cover a 200x150 box, cropping what does not fit
container()
.width(200.0)
.height(150.0)
.child(image("./photo.jpg").content_fit(ContentFit::Cover));
// A wallpaper: cover the whole surface
container()
.width(fill())
.height(fill())
.child(image(wallpaper).content_fit(ContentFit::Cover))
# ;
# }
```
## Transform Composition
Images inherit transforms from parent containers, just like text:
```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
// Rotated image
container()
.rotate(15.0)
.child(
container()
.width(32.0)
.height(32.0)
.child(image("./badge.svg"))
);
// Scaled image
container()
.scale(1.5)
.child(
container()
.width(24.0)
.height(24.0)
.child(image("./icon.png"))
);
// Combined transforms
container()
.rotate(45.0)
.scale(2.0)
.child(image("./logo.svg"))
# ;
# }
```
## SVG Quality
SVGs are automatically rasterized at the appropriate scale for crisp rendering:
- HiDPI displays: SVGs render at the display scale factor
- Transforms: When scaled via container transforms, SVGs re-rasterize at the higher resolution
- Quality: A 2x supersampling multiplier ensures smooth edges
This means SVGs stay crisp regardless of how they're scaled or transformed.
## In-Memory SVGs
For dynamically generated or embedded SVGs:
```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
let svg_data = r##"
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="40" fill="#4f46e5" />
</svg>
"##;
container()
.width(48.0)
.height(48.0)
.child(image(ImageSource::SvgBytes(svg_data.as_bytes().into())))
# ;
# }
```
An `Image` carries no box of its own: the container it sits in is what gives it
one, exactly as with a `text`. `content_fit` then decides how the picture uses
that box.
## Tinting
`tint` draws every pixel of an image in one colour, keeping its alpha. It is
how a monochrome icon takes the colour of the text beside it, a hover, or a
theme:
```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
let hovered = create_signal(false);
container()
.width(16.0)
.height(16.0)
.on_hover(move |h| hovered.set(h))
.child(image("./battery.svg").tint(move || {
if hovered.get() { Color::WHITE } else { Color::rgb(0.6, 0.6, 0.6) }
}))
# ;
# }
```
The colour is applied while the image is drawn, from the texture it already
has, so changing it costs a repaint rather than rasterizing the SVG again. The
tint's alpha fades the image as `opacity` would.
## Reactive Images
Image sources can be reactive, allowing dynamic image changes:
```rust
# extern crate guido;
# use guido::prelude::*;
# fn main() {
let icon_source = create_signal(ImageSource::Path("./play.png".into()));
// The image updates when the signal changes
container()
.width(32.0)
.height(32.0)
.child(image(icon_source));
// Change the image on click
container()
.on_click(move || {
icon_source.set(ImageSource::Path("./pause.png".into()));
})
.child(image(icon_source))
# ;
# }
```
## Supported Formats
### Raster Formats
- PNG
- JPEG
- GIF
- WebP
### Vector Formats
- SVG
## Example
Here's a complete example showing various image features:
```rust,no_run
# extern crate guido;
use guido::prelude::*;
fn main() {
App::new().run(|app| {
let svg_icon = r##"
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#3b82f6"/>
</svg>
"##;
let view = container()
.padding(16.0)
.layout(Flex::row().spacing(16.0))
.child(
// PNG image
container()
.width(48.0)
.height(48.0)
.child(image("./logo.png"))
)
.child(
// SVG from memory
container()
.width(32.0)
.height(32.0)
.child(image(ImageSource::SvgBytes(svg_icon.as_bytes().into())))
)
.child(
// Rotated image
container()
.rotate(15.0)
.child(
container()
.width(24.0)
.height(24.0)
.child(image("./icon.svg"))
)
);
app.add_surface(
SurfaceConfig::new()
.height(80)
.background_color(Color::rgb(0.1, 0.1, 0.15)),
move || view,
);
});
}
```
## Performance Notes
- Raster files and bytes are decoded off the frame, once per source, and the
decoded pixels are dropped once they are uploaded: after that the GPU texture
is the image. If the texture is later evicted, the image is decoded again the
next time it is drawn
- Images are cached as GPU textures. Small ones — up to 256 pixels a side once
rasterized — share atlas pages, so a screen full of icons is a few draw calls,
not one per icon
- The cache's budget is 100 MB of images (about width × height × 4 bytes each) and it evicts
the least recently drawn past that, never one drawn in the last second. An application
with other needs sets its own budget with `App::image_cache_budget`. The atlas pages the
small images share are 4 MB each and are not counted: a page is given back once eviction
has emptied it, so the GPU memory held can run a few pages past the budget
- SVGs are re-rasterized when their display size or scale changes — in the frame for an icon,
off it for a large one, which draws the previous raster stretched until the new one lands
- Texture uploads happen once per unique image/scale combination