Mercurial > touhou
changeset 792:11bc22bad1bf default tip
python: Replace the image crate with png
We weren’t using any of its features anyway, so the png crate is exactly what
we need, without the many heavy dependencies of image.
https://github.com/image-rs/image-png/pull/670 will eventually make it even
faster to build.
| author | Link Mauve <linkmauve@linkmauve.fr> |
|---|---|
| date | Sat, 17 Jan 2026 22:22:25 +0100 |
| parents | a29122662cde |
| children | |
| files | python/Cargo.toml python/src/lib.rs |
| diffstat | 2 files changed, 51 insertions(+), 26 deletions(-) [+] |
line wrap: on
line diff
--- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -16,9 +16,9 @@ touhou-formats = "*" touhou-utils = { version = "*", path = "../utils" } pyo3 = "0.27" -image = { version = "0.25", default-features = false, features = ["png"] } glob = "0.3.3" kira = "0.11.0" +png = "0.18.0" [features] default = []
--- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -1,4 +1,4 @@ -use image::{DynamicImage, GenericImageView, ImageFormat}; +use png::{BitDepth, ColorType, Transformations}; use pyo3::exceptions::{PyIOError, PyKeyError}; use pyo3::prelude::*; use pyo3::types::{PyBytes, PyTuple}; @@ -215,15 +215,15 @@ fn get_image(&self, py: Python, name: &str) -> PyResult<Py<Image>> { let vec = self.get_file_internal(name)?; - let inner = Image::from_rgb_png(&vec); - Py::new(py, Image { inner }) + let image = Image::from_rgb_png(&vec); + Py::new(py, image) } fn get_image_with_alpha(&self, py: Python, name: &str, alpha: &str) -> PyResult<Py<Image>> { let rgb = self.get_file_internal(name)?; let alpha = self.get_file_internal(alpha)?; - let inner = Image::from_rgb_and_alpha_png(&rgb, &alpha); - Py::new(py, Image { inner }) + let image = Image::from_rgb_and_alpha_png(&rgb, &alpha); + Py::new(py, image) } } @@ -238,43 +238,68 @@ #[pyclass(module = "libtouhou")] #[derive(Debug)] struct Image { - inner: DynamicImage, + width: u32, + height: u32, + data: Vec<u8>, } #[pymethods] impl Image { #[getter] fn dimensions(&self) -> (u32, u32) { - self.inner.dimensions() + (self.width, self.height) } #[getter] fn pixels(&self) -> &[u8] { - self.inner.as_bytes() + &self.data } } impl Image { - fn from_rgb_png(rgb: &[u8]) -> DynamicImage { - let rgb = image::load_from_memory_with_format(rgb, ImageFormat::Png).unwrap(); - rgb.into_rgba8().into() + fn load_png(data: &[u8], add_alpha: bool) -> (u32, u32, ColorType, Vec<u8>) { + let cursor = std::io::Cursor::new(data); + let mut decoder = png::Decoder::new(cursor); + // Request either rgb8 or rgba8 data. + decoder.set_transformations(if add_alpha { + Transformations::ALPHA + } else { + Transformations::EXPAND + }); + let mut reader = decoder.read_info().unwrap(); + let mut buf = vec![0; reader.output_buffer_size().unwrap()]; + let info = reader.next_frame(&mut buf).unwrap(); + assert_eq!(buf.capacity(), info.buffer_size()); + assert_eq!(info.bit_depth, BitDepth::Eight); + (info.width, info.height, info.color_type, buf) } - fn from_rgb_and_alpha_png(rgb: &[u8], alpha: &[u8]) -> DynamicImage { - let rgb = image::load_from_memory_with_format(rgb, ImageFormat::Png).unwrap(); - let alpha = image::load_from_memory_with_format(alpha, ImageFormat::Png).unwrap(); + fn from_rgb_png(rgb: &[u8]) -> Self { + let (width, height, color_type, data) = Self::load_png(rgb, true); + assert_eq!(color_type, ColorType::Rgba); + Self { width, height, data } + } - let (width, height) = rgb.dimensions(); - let DynamicImage::ImageLuma8(alpha) = alpha.grayscale() else { - panic!("Alpha couldn’t be converted to grayscale!"); - }; - let pixels = rgb - .pixels() - .zip(alpha.pixels()) - .map(|((_x, _y, rgb), luma)| [rgb[0], rgb[1], rgb[2], luma[0]]) - .flatten() - .collect::<Vec<_>>(); - image::RgbaImage::from_vec(width, height, pixels).unwrap().into() + fn from_rgb_and_alpha_png(rgb: &[u8], alpha: &[u8]) -> Self { + let (width, height, color_type, data) = Self::load_png(rgb, false); + if color_type == ColorType::Rgba { + // TODO: Check which should be used, the alpha channel in the primary PNG, or the alpha + // mask in the secondary PNG, in case both are present (such as for Patchouli’s face). + return Self { width, height, data }; + } + + let (alpha_width, alpha_height, color_type, alpha) = Self::load_png(alpha, false); + assert_eq!(color_type, ColorType::Rgb); + assert_eq!(width, alpha_width); + assert_eq!(height, alpha_height); + + let data = data + .as_chunks::<3>().0 + .into_iter() + .zip(alpha.as_chunks::<3>().0) + .flat_map(|([r, g, b], [luma, _, _])| [*r, *g, *b, *luma]) + .collect(); + Self { width, height, data } } }
