Files
sprout/src/actions/splash.rs

160 lines
5.2 KiB
Rust
Raw Normal View History

use crate::context::SproutContext;
use crate::utils::framebuffer::Framebuffer;
2025-10-05 03:12:00 -07:00
use crate::utils::read_file_contents;
use anyhow::{Context, Result};
2025-10-05 03:12:00 -07:00
use image::imageops::{FilterType, resize};
use image::math::Rect;
use image::{DynamicImage, ImageBuffer, ImageFormat, ImageReader, Rgba};
use serde::{Deserialize, Serialize};
2025-10-05 03:12:00 -07:00
use std::io::Cursor;
use std::rc::Rc;
use std::time::Duration;
use uefi::boot::ScopedProtocol;
use uefi::proto::console::gop::GraphicsOutput;
2025-10-05 03:12:00 -07:00
const DEFAULT_SPLASH_TIME: u32 = 0;
/// The configuration of the splash action.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct SplashConfiguration {
/// The path to the image to display.
/// Currently, only PNG images are supported.
pub image: String,
/// The time to display the splash image without interruption, in seconds.
/// The default value is `0` which will display the image and let everything
/// continue.
#[serde(default = "default_splash_time")]
pub time: u32,
2025-10-05 03:12:00 -07:00
}
fn default_splash_time() -> u32 {
DEFAULT_SPLASH_TIME
2025-10-05 03:12:00 -07:00
}
/// Acquire the [GraphicsOutput]. We will find the first graphics output only.
fn setup_graphics() -> Result<ScopedProtocol<GraphicsOutput>> {
// Grab the handle for the graphics output protocol.
2025-10-05 03:12:00 -07:00
let gop_handle = uefi::boot::get_handle_for_protocol::<GraphicsOutput>()
.context("unable to get graphics output")?;
// Open the graphics output protocol exclusively.
2025-10-05 03:12:00 -07:00
uefi::boot::open_protocol_exclusive::<GraphicsOutput>(gop_handle)
.context("unable to open graphics output")
2025-10-05 03:12:00 -07:00
}
/// Produces a [Rect] that fits the `image` inside the specified `frame`.
/// The output [Rect] should be used to resize the image.
2025-10-05 03:12:00 -07:00
fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect {
// Convert the image dimensions to a [Rect].
2025-10-05 03:12:00 -07:00
let input = Rect {
x: 0,
y: 0,
width: image.width(),
height: image.height(),
};
// Handle the case where the image is zero-sized.
if input.height == 0 || input.width == 0 {
return input;
}
// Calculate the ratio of the image dimensions.
2025-10-05 03:12:00 -07:00
let input_ratio = input.width as f32 / input.height as f32;
// Calculate the ratio of the frame dimensions.
2025-10-05 03:12:00 -07:00
let frame_ratio = frame.width as f32 / frame.height as f32;
// Create [Rect] to store the output dimensions.
2025-10-05 03:12:00 -07:00
let mut output = Rect {
x: 0,
y: 0,
width: frame.width,
height: frame.height,
};
// Handle the case where the output is zero-sized.
if output.height == 0 || output.width == 0 {
return output;
}
2025-10-05 03:12:00 -07:00
if input_ratio < frame_ratio {
output.width = (frame.height as f32 * input_ratio).floor() as u32;
output.height = frame.height;
output.x = frame.x + (frame.width - output.width) / 2;
output.y = frame.y;
} else {
output.width = frame.width;
output.height = (frame.width as f32 / input_ratio).floor() as u32;
output.x = frame.x;
output.y = frame.y + (frame.height - output.height) / 2;
}
output
}
/// Resize the input `image` to fit the `frame`.
2025-10-05 03:12:00 -07:00
fn resize_to_fit(image: &DynamicImage, frame: Rect) -> ImageBuffer<Rgba<u8>, Vec<u8>> {
let image = image.to_rgba8();
resize(&image, frame.width, frame.height, FilterType::Lanczos3)
}
/// Draw the `image` on the screen using [GraphicsOutput].
fn draw(image: DynamicImage) -> Result<()> {
// Acquire the [GraphicsOutput] protocol.
let mut gop = setup_graphics()?;
// Acquire the current screen size.
2025-10-05 03:12:00 -07:00
let (width, height) = gop.current_mode_info().resolution();
// Create a display frame.
2025-10-05 03:12:00 -07:00
let display_frame = Rect {
x: 0,
y: 0,
width: width as _,
height: height as _,
};
// Fit the image to the display frame.
2025-10-05 03:12:00 -07:00
let fit = fit_to_frame(&image, display_frame);
// Resize the image to fit the display frame.
2025-10-05 03:12:00 -07:00
let image = resize_to_fit(&image, fit);
// Create a framebuffer to draw the image on.
let mut framebuffer =
Framebuffer::new(width, height).context("unable to create framebuffer")?;
// Iterate over the pixels in the image and put them on the framebuffer.
2025-10-05 03:12:00 -07:00
for (x, y, pixel) in image.enumerate_pixels() {
let Some(fb) = framebuffer.pixel((x + fit.x) as usize, (fit.y + y) as usize) else {
continue;
};
fb.red = pixel[0];
fb.green = pixel[1];
fb.blue = pixel[2];
}
// Blit the framebuffer to the screen.
framebuffer.blit(&mut gop)?;
Ok(())
2025-10-05 03:12:00 -07:00
}
/// Runs the splash action with the specified `configuration` inside the provided `context`.
pub fn splash(context: Rc<SproutContext>, configuration: &SplashConfiguration) -> Result<()> {
// Stamp the image path value.
2025-10-05 03:12:00 -07:00
let image = context.stamp(&configuration.image);
// Read the image contents.
let image = read_file_contents(Some(context.root().loaded_image_path()?), &image)?;
// Decode the image as a PNG.
2025-10-05 03:12:00 -07:00
let image = ImageReader::with_format(Cursor::new(image), ImageFormat::Png)
.decode()
.context("unable to decode splash image")?;
// Draw the image on the screen.
draw(image)?;
// Sleep for the specified time.
2025-10-05 03:12:00 -07:00
std::thread::sleep(Duration::from_secs(configuration.time as u64));
// Return control to sprout.
Ok(())
2025-10-05 03:12:00 -07:00
}