changeset 786:7e940ebeb5fd

Replace SDL2_image with the image crate
author Link Mauve <linkmauve@linkmauve.fr>
date Mon, 01 Dec 2025 17:05:48 +0100
parents f73e8524c045
children 7f9b3f5001c2
files README interpreters/src/lib.rs python/Cargo.toml python/src/lib.rs pytouhou/lib/_sdl.pxd pytouhou/lib/sdl.pxd pytouhou/lib/sdl.pyx pytouhou/ui/opengl/texture.pyx pytouhou/ui/sdl/texture.pyx setup.py
diffstat 10 files changed, 97 insertions(+), 76 deletions(-) [+]
line wrap: on
line diff
--- a/README
+++ b/README
@@ -17,7 +17,7 @@
     * A working OpenGL driver
     * libepoxy
     * SDL2
-    * SDL2_image, SDL2_ttf
+    * SDL2_ttf
     * A TTF font file, placed as “font.ttf” in the game directory.
 
 
--- a/interpreters/src/lib.rs
+++ b/interpreters/src/lib.rs
@@ -1,5 +1,4 @@
 #![deny(missing_docs)]
-#![feature(concat_idents)]
 
 //! Crate implementing interpreters for various Touhou formats.
 
--- a/python/Cargo.toml
+++ b/python/Cargo.toml
@@ -16,10 +16,10 @@
 touhou-formats = "*"
 touhou-utils = { version = "*", path = "../utils" }
 pyo3 = "0.27"
-image = { version = "0.25", default-features = false, features = ["png"], optional = true }
+image = { version = "0.25", default-features = false, features = ["png"] }
 glob = "0.3.3"
 kira = "0.11.0"
 
 [features]
 default = []
-glide = ["image"]
+glide = []
--- a/python/src/lib.rs
+++ b/python/src/lib.rs
@@ -1,3 +1,4 @@
+use image::{DynamicImage, GenericImageView, ImageFormat};
 use pyo3::exceptions::{PyIOError, PyKeyError};
 use pyo3::prelude::*;
 use pyo3::types::{PyBytes, PyTuple};
@@ -176,6 +177,11 @@
 
     /// Return the given file as a Vec<u8>.
     fn get_file_internal(&self, name: &str) -> PyResult<Vec<u8>> {
+        let name = if let Some((_, name)) = name.rsplit_once('/') {
+            name
+        } else {
+            name
+        };
         if let Some(archive) = self.known_files.get(name) {
             let mut archive = archive.lock().unwrap();
             let bytes = archive.get_file(name, true)?;
@@ -206,6 +212,19 @@
         let (_, inner) = msg::Msg::from_slice(&vec).unwrap();
         Ok(Py::new(py, PyMsg { inner })?)
     }
+
+    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 })
+    }
+
+    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 })
+    }
 }
 
 impl Loader {
@@ -216,6 +235,49 @@
     }
 }
 
+#[pyclass(module = "libtouhou")]
+#[derive(Debug)]
+struct Image {
+    inner: DynamicImage,
+}
+
+#[pymethods]
+impl Image {
+    #[getter]
+    fn dimensions(&self) -> (u32, u32) {
+        self.inner.dimensions()
+    }
+
+    #[getter]
+    fn pixels(&self) -> &[u8] {
+        self.inner.as_bytes()
+    }
+}
+
+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 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();
+
+        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()
+    }
+}
+
 /// A loader for Touhou files.
 #[pyclass(module = "libtouhou")]
 struct Prng {
@@ -250,6 +312,9 @@
     use super::Loader;
 
     #[pymodule_export]
+    use super::Image;
+
+    #[pymodule_export]
     use super::Prng;
 
     #[pymodule_export]
--- a/pytouhou/lib/_sdl.pxd
+++ b/pytouhou/lib/_sdl.pxd
@@ -146,7 +146,7 @@
 
     void SDL_FreeSurface(SDL_Surface *surface)
     int SDL_BlitSurface(SDL_Surface *src, const SDL_Rect *srcrect, SDL_Surface *dst, SDL_Rect *dstrect)
-    SDL_Surface *SDL_CreateRGBSurface(Uint32 flags, int width, int height, int depth, Uint32 Rmask, Uint32 Gmask, Uint32 Bmask, Uint32 Amask)
+    SDL_Surface *SDL_CreateRGBSurfaceWithFormatFrom(void *pixels, int width, int height, int depth, int pitch, Uint32 fmt)
 
 
 cdef extern from "SDL_rwops.h" nogil:
@@ -157,18 +157,13 @@
     int SDL_RWclose(SDL_RWops *context)
 
 
-cdef extern from "SDL_image.h" nogil:
-    int IMG_INIT_PNG
-
-    int IMG_Init(int flags)
-    void IMG_Quit()
-    SDL_Surface *IMG_LoadPNG_RW(SDL_RWops *src)
-
-
 cdef extern from "SDL_pixels.h" nogil:
     ctypedef struct SDL_Color:
         Uint8 r, g, b, a
 
+    ctypedef enum SDL_PixelFormatEnum:
+        SDL_PIXELFORMAT_ABGR8888
+
 
 cdef extern from "SDL_ttf.h" nogil:
     ctypedef struct TTF_Font:
--- a/pytouhou/lib/sdl.pxd
+++ b/pytouhou/lib/sdl.pxd
@@ -89,6 +89,7 @@
 
 
 cdef class Surface:
+    cdef bytes data
     cdef SDL_Surface *surface
 
     cdef bint blit(self, Surface other) except True
@@ -102,11 +103,9 @@
 
 
 cdef bint init(Uint32 flags) except True
-cdef bint img_init(int flags) except True
 cdef bint ttf_init() except True
 cdef bint gl_set_attribute(SDL_GLattr attr, int value) except True
-cdef Surface load_png(file_)
-cdef Surface create_rgb_surface(int width, int height, int depth, Uint32 rmask=*, Uint32 gmask=*, Uint32 bmask=*, Uint32 amask=*)
+cdef Surface create_rgba_surface(bytes pixels, int width, int height)
 cdef Uint32 get_ticks() nogil
 cdef void delay(Uint32 ms) nogil
 cpdef bint show_simple_message_box(unicode message) except True
--- a/pytouhou/lib/sdl.pyx
+++ b/pytouhou/lib/sdl.pyx
@@ -71,14 +71,12 @@
         IF UNAME_SYSNAME == "Windows":
             SDL_SetMainReady()
         init(SDL_INIT_VIDEO if self.video else 0)
-        img_init(IMG_INIT_PNG)
         ttf_init()
 
         keyboard_state = SDL_GetKeyboardState(NULL)
 
     def __exit__(self, *args):
         TTF_Quit()
-        IMG_Quit()
         SDL_Quit()
 
 
@@ -239,6 +237,9 @@
 
 
 cdef class Surface:
+    def __init__(self, bytes data=None):
+        self.data = data
+
     def __dealloc__(self):
         if self.surface != NULL:
             SDL_FreeSurface(self.surface)
@@ -288,11 +289,6 @@
         raise SDLError()
 
 
-cdef bint img_init(int flags) except True:
-    if IMG_Init(flags) != flags:
-        raise SDLError()
-
-
 cdef bint ttf_init() except True:
     if TTF_Init() < 0:
         raise SDLError()
@@ -303,20 +299,13 @@
         raise SDLError()
 
 
-cdef Surface load_png(file_):
-    data = file_.read()
-    rwops = SDL_RWFromConstMem(<char*>data, len(data))
-    surface = Surface()
-    surface.surface = IMG_LoadPNG_RW(rwops)
-    SDL_RWclose(rwops)
-    if surface.surface == NULL:
-        raise SDLError()
-    return surface
-
-
-cdef Surface create_rgb_surface(int width, int height, int depth, Uint32 rmask=0, Uint32 gmask=0, Uint32 bmask=0, Uint32 amask=0):
-    surface = Surface()
-    surface.surface = SDL_CreateRGBSurface(0, width, height, depth, rmask, gmask, bmask, amask)
+cdef Surface create_rgba_surface(bytes data, int width, int height):
+    surface = Surface(data)
+    cdef char *pixels = <char*>data
+    depth = 32
+    pitch = width * 4
+    fmt = SDL_PIXELFORMAT_ABGR8888
+    surface.surface = SDL_CreateRGBSurfaceWithFormatFrom(pixels, width, height, depth, pitch, fmt)
     if surface.surface == NULL:
         raise SDLError()
     return surface
--- a/pytouhou/ui/opengl/texture.pyx
+++ b/pytouhou/ui/opengl/texture.pyx
@@ -19,7 +19,6 @@
           glGenTextures, glBindTexture, glTexImage2D, GL_TEXTURE_2D, GLuint,
           glPushDebugGroup, GL_DEBUG_SOURCE_APPLICATION, glPopDebugGroup)
 
-from pytouhou.lib.sdl cimport load_png, create_rgb_surface
 from pytouhou.lib.sdl import SDLError
 from pytouhou.formats.thtx import Texture #TODO: perhaps define that elsewhere?
 from pytouhou.game.text cimport NativeText
@@ -105,24 +104,12 @@
 
 
 cdef decode_png(loader, first_name, secondary_name):
-    image_file = load_png(loader.get_file(os.path.basename(first_name)))
-    width, height = image_file.surface.w, image_file.surface.h
-
-    # Support only 32 bits RGBA. Paletted surfaces are awful to work with.
-    #TODO: verify it doesn’t blow up on big-endian systems.
-    new_image = create_rgb_surface(width, height, 32, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000)
-    new_image.blit(image_file)
-
-    if secondary_name:
-        alpha_file = load_png(loader.get_file(os.path.basename(secondary_name)))
-        assert (width == alpha_file.surface.w and height == alpha_file.surface.h)
-
-        new_alpha_file = create_rgb_surface(width, height, 24)
-        new_alpha_file.blit(alpha_file)
-
-        new_image.set_alpha(new_alpha_file)
-
-    return Texture(width, height, -4, new_image.pixels)
+    if secondary_name is not None:
+        image = loader.get_image_with_alpha(first_name, secondary_name)
+    else:
+        image = loader.get_image(first_name)
+    width, height = image.dimensions
+    return Texture(width, height, -4, image.pixels)
 
 
 cdef GLuint load_texture(thtx) except? 65535:
--- a/pytouhou/ui/sdl/texture.pyx
+++ b/pytouhou/ui/sdl/texture.pyx
@@ -12,7 +12,7 @@
 ## GNU General Public License for more details.
 ##
 
-from pytouhou.lib.sdl cimport load_png, create_rgb_surface
+from pytouhou.lib.sdl cimport create_rgba_surface
 from pytouhou.lib.sdl import SDLError
 from pytouhou.game.text cimport NativeText
 
@@ -78,21 +78,9 @@
 
 
 cdef Surface decode_png(loader, first_name, secondary_name):
-    image_file = load_png(loader.get_file(os.path.basename(first_name)))
-    width, height = image_file.surface.w, image_file.surface.h
-
-    # Support only 32 bits RGBA. Paletted surfaces are awful to work with.
-    #TODO: verify it doesn’t blow up on big-endian systems.
-    new_image = create_rgb_surface(width, height, 32, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000)
-    new_image.blit(image_file)
-
     if secondary_name:
-        alpha_file = load_png(loader.get_file(os.path.basename(secondary_name)))
-        assert (width == alpha_file.surface.w and height == alpha_file.surface.h)
-
-        new_alpha_file = create_rgb_surface(width, height, 24)
-        new_alpha_file.blit(alpha_file)
-
-        new_image.set_alpha(new_alpha_file)
-
-    return new_image
+        image = loader.get_image_with_alpha(first_name, secondary_name)
+    else:
+        image = loader.get_image(first_name)
+    width, height = image.dimensions
+    return create_rgba_surface(image.pixels, width, height)
--- a/setup.py
+++ b/setup.py
@@ -17,7 +17,7 @@
 
 COMMAND = 'pkg-config'
 GLFW_LIBRARIES = ['glfw3']
-SDL_LIBRARIES = ['sdl2', 'SDL2_image', 'SDL2_ttf']
+SDL_LIBRARIES = ['sdl2', 'SDL2_ttf']
 GL_LIBRARIES = ['epoxy']
 
 debug = False  # True to generate HTML annotations and display infered types.
@@ -67,7 +67,6 @@
 default_libs = {
     'glfw3': '-lglfw',
     'sdl2': '-lSDL2',
-    'SDL2_image': '-lSDL2_image',
     'SDL2_ttf': '-lSDL2_ttf',
     'epoxy': '-lepoxy'
 }