Skip to content

Commit

Permalink
QImage: Add conversion from image::RgbaImage
Browse files Browse the repository at this point in the history
Because the image crate changes versions somewhat frequently, the
features in CXX-Qt are added with explicit image crate versions.
  • Loading branch information
LeonMatthesKDAB committed Jan 20, 2025
1 parent e84662a commit 47f440f
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 9 additions & 1 deletion crates/cxx-qt-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions crates/cxx-qt-lib/src/gui/qimage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ assert_alignment_and_size(QImage, {
});
#endif

static_assert(std::is_same_v<QImageCleanupFunction, void (*)(void*)>);

namespace rust {
namespace cxxqtlib1 {

Expand Down
198 changes: 198 additions & 0 deletions crates/cxx-qt-lib/src/gui/qimage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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"]
Expand All @@ -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};
Expand Down Expand Up @@ -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`].
///
Expand All @@ -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<u8>`.
///
/// # 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<u8>,
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<u8>));
}
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)
Expand All @@ -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();
Expand Down Expand Up @@ -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<u8> = 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);
}

0 comments on commit 47f440f

Please sign in to comment.