From 47f440f6a8ab89cac5d6ad0a825555682e7e985b Mon Sep 17 00:00:00 2001 From: Leon Matthes Date: Wed, 15 Jan 2025 17:39:31 +0100 Subject: [PATCH] QImage: Add conversion from image::RgbaImage Because the image crate changes versions somewhat frequently, the features in CXX-Qt are added with explicit image crate versions. --- CHANGELOG.md | 1 + crates/cxx-qt-lib/Cargo.toml | 10 +- crates/cxx-qt-lib/src/gui/qimage.cpp | 2 + crates/cxx-qt-lib/src/gui/qimage.rs | 198 +++++++++++++++++++++++++++ 4 files changed, 210 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c4b62060..213527ef3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `QDateTime::from_string` to parse `QDateTime` from a `QString`. - Support for further types: `QUuid` +- Allow creating a `QImage` from an `image::RgbaImage`. ### Fixed diff --git a/crates/cxx-qt-lib/Cargo.toml b/crates/cxx-qt-lib/Cargo.toml index d6b534b44..ad9e35a24 100644 --- a/crates/cxx-qt-lib/Cargo.toml +++ b/crates/cxx-qt-lib/Cargo.toml @@ -31,13 +31,19 @@ time = { version = "0.3.20", optional = true } url = { version = "2.3", optional = true } uuid = { version = "1.1.0", optional = true } serde = { version = "1", features=["derive"], optional = true } +# Note: The image crate is not yet at 1.0 and a new version is released regularly. +# To avoid a breaking change at each update, we don't specify a default version, and make the versions explicit. +# Once 1.0 is released, we can add a dependency on `image`, which would then be `image = "1"` +# Also, disable the default features, we don't need any, so they would be included unnecessarily and raise the MSRV unnecessarily. +imagev24 = { version = "0.24", optional=true, package="image", default-features=false } +imagev25 = { version = "0.25", optional=true, package="image", default-features=false } [build-dependencies] cxx-qt-build.workspace = true qt-build-utils.workspace = true [features] -full = ["qt_full", "serde", "url", "uuid", "time", "rgb", "http", "chrono", "bytes"] +full = ["qt_full", "serde", "url", "uuid", "time", "rgb", "http", "chrono", "bytes", "imagev24", "imagev25"] default = [] qt_full = ["qt_gui", "qt_qml", "qt_quickcontrols"] @@ -53,6 +59,8 @@ time = ["dep:time"] url = ["dep:url"] serde = ["dep:serde"] uuid = ["dep:uuid"] +imagev24 = ["dep:imagev24"] +imagev25 = ["dep:imagev25"] link_qt_object_files = ["cxx-qt-build/link_qt_object_files"] [lints] diff --git a/crates/cxx-qt-lib/src/gui/qimage.cpp b/crates/cxx-qt-lib/src/gui/qimage.cpp index 532f28ecd..c4a0f3818 100644 --- a/crates/cxx-qt-lib/src/gui/qimage.cpp +++ b/crates/cxx-qt-lib/src/gui/qimage.cpp @@ -36,6 +36,8 @@ assert_alignment_and_size(QImage, { }); #endif +static_assert(std::is_same_v); + namespace rust { namespace cxxqtlib1 { diff --git a/crates/cxx-qt-lib/src/gui/qimage.rs b/crates/cxx-qt-lib/src/gui/qimage.rs index 67e5a5247..9f255b2c0 100644 --- a/crates/cxx-qt-lib/src/gui/qimage.rs +++ b/crates/cxx-qt-lib/src/gui/qimage.rs @@ -83,6 +83,8 @@ mod ffi { include!("cxx-qt-lib/qsizef.h"); #[allow(dead_code)] type QSizeF = crate::QSizeF; + type QImageCleanupFunction = super::QImageCleanupFunction; + type uchar; /// Returns true if all the colors in the image are shades of gray #[rust_name = "all_gray"] @@ -236,6 +238,7 @@ mod ffi { include!("cxx-qt-lib/common.h"); type QImageFormat; type QImageInvertMode; + type c_void = crate::c_void; #[doc(hidden)] #[rust_name = "qimage_init_default"] @@ -261,6 +264,31 @@ mod ffi { #[rust_name = "qimage_eq"] fn operatorEq(a: &QImage, b: &QImage) -> bool; } + + #[namespace = "rust::cxxqtlib1"] + extern "C++" { + #[doc(hidden)] + #[rust_name = "qimage_init_from_raw_parts_mut"] + unsafe fn construct( + data: *mut uchar, + width: i32, + height: i32, + format: QImageFormat, + image_cleanup_function: QImageCleanupFunction, + image_cleanup_data: *mut c_void, + ) -> QImage; + + #[doc(hidden)] + #[rust_name = "qimage_init_from_raw_parts"] + unsafe fn construct( + data: *const uchar, + width: i32, + height: i32, + format: QImageFormat, + image_cleanup_function: QImageCleanupFunction, + image_cleanup_data: *mut c_void, + ) -> QImage; + } } pub use ffi::{QImageFormat, QImageInvertMode}; @@ -316,6 +344,17 @@ impl Drop for QImage { } } +// Static assertions on the C++ side assert that this type is equal to: +// void(*)(void*) +#[repr(transparent)] +struct QImageCleanupFunction(extern "C" fn(*mut ffi::c_void)); + +unsafe impl ExternType for QImageCleanupFunction { + type Id = type_id!("QImageCleanupFunction"); + + type Kind = cxx::kind::Trivial; +} + impl QImage { /// Convert raw image data to a [`QImage`]. /// @@ -332,6 +371,73 @@ impl QImage { None } } + + /// Constructs a QImage from an existing memory buffer. + /// + /// # Safety + /// For details on safety see the [Qt documentation](https://doc.qt.io/qt-6/qimage.html#QImage-7) + pub unsafe fn from_raw_parts( + data: *const ffi::uchar, + width: i32, + height: i32, + format: QImageFormat, + image_cleanup_function: extern "C" fn(x: *mut ffi::c_void), + image_cleanup_data: *mut ffi::c_void, + ) -> Self { + ffi::qimage_init_from_raw_parts( + data, + width, + height, + format, + QImageCleanupFunction(image_cleanup_function), + image_cleanup_data, + ) + } + + /// Constructs a QImage from an existing mutable memory buffer. + /// + /// # Safety + /// For details on safety see the [Qt documentation](https://doc.qt.io/qt-6/qimage.html#QImage-8) + pub unsafe fn from_raw_parts_mut( + data: *mut ffi::uchar, + width: i32, + height: i32, + format: QImageFormat, + image_cleanup_function: extern "C" fn(x: *mut ffi::c_void), + image_cleanup_data: *mut ffi::c_void, + ) -> Self { + ffi::qimage_init_from_raw_parts_mut( + data, + width, + height, + format, + QImageCleanupFunction(image_cleanup_function), + image_cleanup_data, + ) + } + + /// Constructs a QImage from the raw data inside a `Vec`. + /// + /// # Safety + /// This function is unsafe because it requires that the data matches the given QImageFormat, + /// width and height. + /// + /// It is a convenience function around [Self::from_raw_parts_mut]. + pub unsafe fn from_raw_bytes( + data: Vec, + width: i32, + height: i32, + format: QImageFormat, + ) -> Self { + extern "C" fn delete_boxed_vec(boxed_vec: *mut ffi::c_void) { + drop(Box::new(boxed_vec as *mut Vec)); + } + let data = Box::new(data); + let bytes = data.as_ptr() as *mut ffi::uchar; + let raw_box = Box::into_raw(data) as *mut ffi::c_void; + QImage::from_raw_parts_mut(bytes, width, height, format, delete_boxed_vec, raw_box) + } + /// Returns a number that identifies the contents of this QImage object. pub fn cache_key(&self) -> i64 { ffi::qimage_cache_key(self) @@ -347,9 +453,40 @@ impl QImage { } } +#[cfg(any(feature = "imagev24", feature = "imagev25"))] +macro_rules! from_image { + ($crt:ident) => { + impl From<$crt::RgbaImage> for QImage { + fn from(image: $crt::RgbaImage) -> QImage { + let width = image.width() as i32; + let height = image.height() as i32; + // SAFETY: The RgbaImage has the same format as RGBA8888 and the number of + // pixels is correct for the images width and height, which is guaranteed by + // RgbaImage. + unsafe { + QImage::from_raw_bytes( + image.into_raw(), + width, + height, + QImageFormat::Format_RGBA8888, + ) + } + } + } + }; +} + +#[cfg(feature = "imagev24")] +from_image!(imagev24); + +#[cfg(feature = "imagev25")] +from_image!(imagev25); + #[cfg(test)] mod tests { use super::*; + use crate::QColor; + #[test] fn test_default_values() { let default_image = QImage::default(); @@ -378,4 +515,65 @@ mod tests { assert_eq!(qimage.height(), qimage2.height()); assert_eq!(qimage.format(), qimage2.format()); } + + #[test] + fn test_from_raw_bytes() { + let bytes: Vec = vec![ + 0x01, 0x02, 0x03, 0x04, // Pixel 1 + 0x05, 0x06, 0x07, 0x08, // Pixel 2 + ]; + + let qimage = unsafe { QImage::from_raw_bytes(bytes, 2, 1, QImageFormat::Format_RGBA8888) }; + assert_eq!(qimage.width(), 2); + assert_eq!(qimage.height(), 1); + assert_eq!( + qimage.pixel_color(0, 0), + QColor::from_rgba(0x01, 0x02, 0x03, 0x04), + ); + assert_eq!( + qimage.pixel_color(1, 0), + QColor::from_rgba(0x05, 0x06, 0x07, 0x08), + ); + } + + #[cfg(any(feature = "imagev24", feature = "imagev25"))] + macro_rules! test_image_crate { + ($crt:ident: $test_name:ident) => { + #[test] + fn $test_name() { + use $crt::{Rgba, RgbaImage}; + + // Create a sample RgbaImage + let width = 2; + let height = 2; + let mut rgba_image = RgbaImage::new(width, height); + rgba_image.put_pixel(0, 0, Rgba([255, 0, 0, 255])); // Red pixel + rgba_image.put_pixel(1, 0, Rgba([0, 255, 0, 255])); // Green pixel + rgba_image.put_pixel(0, 1, Rgba([0, 0, 255, 255])); // Blue pixel + rgba_image.put_pixel(1, 1, Rgba([255, 255, 0, 255])); // Yellow pixel + + // Convert RgbaImage to QImage + let qimage: QImage = rgba_image.into(); + + // Verify the conversion + assert_eq!(qimage.width(), width as i32); + assert_eq!(qimage.height(), height as i32); + + // Verify the pixel data + assert_eq!(qimage.pixel_color(0, 0), QColor::from_rgba(255, 0, 0, 255)); // Red pixel + assert_eq!(qimage.pixel_color(1, 0), QColor::from_rgba(0, 255, 0, 255)); // Green pixel + assert_eq!(qimage.pixel_color(0, 1), QColor::from_rgba(0, 0, 255, 255)); // Blue pixel + assert_eq!( + qimage.pixel_color(1, 1), + QColor::from_rgba(255, 255, 0, 255) + ); // Yellow pixel + } + }; + } + + #[cfg(feature = "imagev24")] + test_image_crate!(imagev24: test_imagev24_to_qimage); + + #[cfg(feature = "imagev25")] + test_image_crate!(imagev25: test_imagev25_to_qimage); }