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 }
     }
 }