use std::sync::Arc;
use core_foundation::base::{CFRelease, CFTypeRef};
use core_foundation::dictionary::CFDictionaryRef;
use core_graphics::base::CGFloat;
use core_graphics::data_provider::CGDataProvider;
use core_graphics::geometry::{CGPoint, CGRect, CGSize};
use core_graphics::image::CGImage;
use core_graphics::sys::{CGContextRef, CGDataProviderRef, CGImageRef};
use foreign_types::ForeignType;
use super::backend::{ns_pop_clip, ns_push_clip};
use crate::backend::ImagePaintResult;
use crate::event::Rect;
use crate::image_cache::ImageCache;
use crate::primitives::image::{Image, ImageSource};
enum CGImageSource {}
type CGImageSourceRef = *mut CGImageSource;
fn decode_image(source: &ImageSource) -> Option<CGImage> {
let bytes: Vec<u8> = match source {
ImageSource::Path(path) => std::fs::read(path).ok()?,
ImageSource::Bytes(bytes) => bytes.clone(),
};
if bytes.is_empty() {
return None;
}
let provider = CGDataProvider::from_buffer(Arc::new(bytes));
let source_ref =
unsafe { CGImageSourceCreateWithDataProvider(provider.as_ptr(), std::ptr::null()) };
if source_ref.is_null() {
return None;
}
let image_ref = unsafe { CGImageSourceCreateImageAtIndex(source_ref, 0, std::ptr::null()) };
unsafe { CFRelease(source_ref as CFTypeRef) };
if image_ref.is_null() {
return None;
}
Some(unsafe { CGImage::from_ptr(image_ref) })
}
unsafe fn paint_cg_image(
ctx: CGContextRef,
rect: Rect,
image: &Image,
cg_image: &CGImage,
) -> ImagePaintResult {
debug_assert!(
!ctx.is_null(),
"macos::image: about to paint a decoded image with a null CGContextRef",
);
let dest = image.layout(rect).bounds;
ns_push_clip(ctx, rect);
CGContextTranslateCTM(ctx, dest.x as f64, (dest.y + dest.height) as f64);
CGContextScaleCTM(ctx, 1.0, -1.0);
let local_rect = CGRect::new(
&CGPoint::new(0.0, 0.0),
&CGSize::new(dest.width as f64, dest.height as f64),
);
CGContextDrawImage(ctx, local_rect, cg_image.as_ptr());
ns_pop_clip(ctx);
ImagePaintResult::Painted
}
pub unsafe fn draw_image(ctx: CGContextRef, rect: Rect, image: &Image) -> ImagePaintResult {
if rect.width <= 0.0 || rect.height <= 0.0 {
return ImagePaintResult::Unsupported;
}
let Some(cg_image) = decode_image(&image.source) else {
return ImagePaintResult::Unsupported;
};
paint_cg_image(ctx, rect, image, &cg_image)
}
pub(crate) unsafe fn draw_image_cached(
ctx: CGContextRef,
rect: Rect,
image: &Image,
cache: &mut ImageCache<CGImage>,
scale: f32,
) -> ImagePaintResult {
if rect.width <= 0.0 || rect.height <= 0.0 {
return ImagePaintResult::Unsupported;
}
let source = &image.source;
let Some(cg_image) = cache.get_or_decode(source, 0, 0, scale, || decode_image(source)) else {
return ImagePaintResult::Unsupported;
};
paint_cg_image(ctx, rect, image, cg_image)
}
#[link(name = "ImageIO", kind = "framework")]
extern "C" {
fn CGImageSourceCreateWithDataProvider(
provider: CGDataProviderRef,
options: CFDictionaryRef,
) -> CGImageSourceRef;
fn CGImageSourceCreateImageAtIndex(
isrc: CGImageSourceRef,
index: usize,
options: CFDictionaryRef,
) -> CGImageRef;
}
extern "C" {
fn CGContextDrawImage(c: CGContextRef, rect: CGRect, image: CGImageRef);
fn CGContextTranslateCTM(c: CGContextRef, tx: CGFloat, ty: CGFloat);
fn CGContextScaleCTM(c: CGContextRef, sx: CGFloat, sy: CGFloat);
}
#[cfg(test)]
mod tests {
use super::super::headless::BitmapSurface;
use super::*;
use crate::primitives::image::ImageFit;
use crate::types::WidgetId;
const W: u32 = 200;
const H: u32 = 200;
fn tiny_png_bytes() -> Vec<u8> {
#[allow(clippy::same_item_push)]
let raw = {
let mut raw = Vec::new();
for _ in 0..4 {
raw.push(0u8);
for _ in 0..4 {
raw.extend_from_slice(&[0xff, 0x00, 0x00]);
}
}
raw
};
png_from_rows(&raw)
}
fn two_tone_png_bytes() -> Vec<u8> {
let mut raw = Vec::new();
for row in 0..4u32 {
raw.push(0u8); let colour: [u8; 3] = if row < 2 {
[0xff, 0x00, 0x00] } else {
[0x00, 0x00, 0xff] };
for _ in 0..4 {
raw.extend_from_slice(&colour);
}
}
png_from_rows(&raw)
}
fn png_from_rows(raw: &[u8]) -> Vec<u8> {
let mut png = Vec::new();
png.extend_from_slice(b"\x89PNG\r\n\x1a\n");
fn chunk(out: &mut Vec<u8>, kind: &[u8; 4], data: &[u8]) {
out.extend_from_slice(&(data.len() as u32).to_be_bytes());
let mut body = Vec::with_capacity(4 + data.len());
body.extend_from_slice(kind);
body.extend_from_slice(data);
out.extend_from_slice(&body);
out.extend_from_slice(&crc32(&body).to_be_bytes());
}
let mut ihdr = Vec::new();
ihdr.extend_from_slice(&4u32.to_be_bytes());
ihdr.extend_from_slice(&4u32.to_be_bytes());
ihdr.extend_from_slice(&[8, 2, 0, 0, 0]);
chunk(&mut png, b"IHDR", &ihdr);
let idat = zlib_store(raw);
chunk(&mut png, b"IDAT", &idat);
chunk(&mut png, b"IEND", &[]);
png
}
fn zlib_store(data: &[u8]) -> Vec<u8> {
let mut out = vec![0x78, 0x01]; let len = data.len() as u16;
out.push(1); out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(&(!len).to_le_bytes());
out.extend_from_slice(data);
out.extend_from_slice(&adler32(data).to_be_bytes());
out
}
fn adler32(data: &[u8]) -> u32 {
let mut a: u32 = 1;
let mut b: u32 = 0;
for &byte in data {
a = (a + byte as u32) % 65521;
b = (b + a) % 65521;
}
(b << 16) | a
}
fn crc32(data: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFF_FFFF;
for &byte in data {
crc ^= byte as u32;
for _ in 0..8 {
let mask = (crc & 1).wrapping_neg();
crc = (crc >> 1) ^ (0xEDB8_8320 & mask);
}
}
!crc
}
fn image(source: ImageSource, fit: ImageFit) -> Image {
Image {
id: WidgetId::new("logo"),
source,
intrinsic_size: Some((4, 4)),
fit,
fallback_text: "[LOGO]".into(),
}
}
fn cache() -> ImageCache<CGImage> {
ImageCache::with_capacity(4)
}
#[test]
fn draw_image_from_bytes_paints_real_pixels() {
let surface = BitmapSurface::new(W, H);
surface.fill(1.0, 1.0, 1.0, 1.0);
let img = image(ImageSource::Bytes(tiny_png_bytes()), ImageFit::Fill);
let rect = Rect::new(20.0, 20.0, 60.0, 60.0);
let result = unsafe { draw_image(surface.context_ptr(), rect, &img) };
assert_eq!(result, ImagePaintResult::Painted);
let (r, g, b, _) = surface.pixel(50, 50);
assert_eq!((r, g, b), (255, 0, 0), "decoded PNG should be red");
let (r, g, b, _) = surface.pixel(5, 5);
assert_eq!(
(r, g, b),
(255, 255, 255),
"painting must not bleed outside the target rect"
);
}
#[test]
fn draw_image_from_missing_path_reports_unsupported() {
let surface = BitmapSurface::new(W, H);
let img = image(
ImageSource::Path("/nonexistent/does-not-exist.png".into()),
ImageFit::Contain,
);
let result =
unsafe { draw_image(surface.context_ptr(), Rect::new(0.0, 0.0, 40.0, 40.0), &img) };
assert_eq!(result, ImagePaintResult::Unsupported);
}
#[test]
fn draw_image_from_corrupt_bytes_reports_unsupported() {
let surface = BitmapSurface::new(W, H);
let img = image(ImageSource::Bytes(vec![0, 1, 2, 3]), ImageFit::Contain);
let result =
unsafe { draw_image(surface.context_ptr(), Rect::new(0.0, 0.0, 40.0, 40.0), &img) };
assert_eq!(result, ImagePaintResult::Unsupported);
}
#[test]
fn zero_size_rect_reports_unsupported_without_decoding() {
let surface = BitmapSurface::new(W, H);
let img = image(ImageSource::Bytes(tiny_png_bytes()), ImageFit::Contain);
let result =
unsafe { draw_image(surface.context_ptr(), Rect::new(0.0, 0.0, 0.0, 0.0), &img) };
assert_eq!(result, ImagePaintResult::Unsupported);
}
#[test]
fn draw_image_cached_from_bytes_paints_real_pixels() {
let surface = BitmapSurface::new(W, H);
surface.fill(1.0, 1.0, 1.0, 1.0);
let img = image(ImageSource::Bytes(tiny_png_bytes()), ImageFit::Fill);
let rect = Rect::new(20.0, 20.0, 60.0, 60.0);
let result =
unsafe { draw_image_cached(surface.context_ptr(), rect, &img, &mut cache(), 1.0) };
assert_eq!(result, ImagePaintResult::Painted);
let (r, g, b, _) = surface.pixel(50, 50);
assert_eq!((r, g, b), (255, 0, 0), "decoded PNG should be red");
}
#[test]
fn draw_image_cached_from_missing_path_reports_unsupported() {
let surface = BitmapSurface::new(W, H);
let img = image(
ImageSource::Path("/nonexistent/does-not-exist.png".into()),
ImageFit::Contain,
);
let result = unsafe {
draw_image_cached(
surface.context_ptr(),
Rect::new(0.0, 0.0, 40.0, 40.0),
&img,
&mut cache(),
1.0,
)
};
assert_eq!(result, ImagePaintResult::Unsupported);
}
#[test]
fn draw_image_cached_zero_size_rect_reports_unsupported_without_decoding() {
let surface = BitmapSurface::new(W, H);
let img = image(ImageSource::Bytes(tiny_png_bytes()), ImageFit::Contain);
let result = unsafe {
draw_image_cached(
surface.context_ptr(),
Rect::new(0.0, 0.0, 0.0, 0.0),
&img,
&mut cache(),
1.0,
)
};
assert_eq!(result, ImagePaintResult::Unsupported);
}
#[test]
fn cached_decode_survives_source_becoming_unreadable_on_disk() {
let path = std::env::temp_dir().join(format!(
"quadraui_macos_image_cache_test_{}_{}.png",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system clock")
.as_nanos()
));
std::fs::write(&path, tiny_png_bytes()).expect("write tiny png fixture");
let surface = BitmapSurface::new(W, H);
let mut shared_cache = cache();
let img = image(ImageSource::Path(path.clone()), ImageFit::Contain);
let first = unsafe {
draw_image_cached(
surface.context_ptr(),
Rect::new(0.0, 0.0, 40.0, 40.0),
&img,
&mut shared_cache,
1.0,
)
};
assert_eq!(
first,
ImagePaintResult::Painted,
"first decode should succeed"
);
std::fs::write(&path, b"not a png").expect("corrupt fixture on disk");
let second = unsafe {
draw_image_cached(
surface.context_ptr(),
Rect::new(0.0, 0.0, 80.0, 80.0),
&img,
&mut shared_cache,
1.0,
)
};
assert_eq!(
second,
ImagePaintResult::Painted,
"second draw must hit the cache rather than re-decoding the now-corrupt file"
);
let _ = std::fs::remove_file(&path);
}
#[test]
fn cover_fit_is_clipped_to_rect() {
let surface = BitmapSurface::new(W, H);
surface.fill(1.0, 1.0, 1.0, 1.0);
let img = image(ImageSource::Bytes(tiny_png_bytes()), ImageFit::Cover);
let rect = Rect::new(90.0, 50.0, 10.0, 30.0);
let result = unsafe { draw_image(surface.context_ptr(), rect, &img) };
assert_eq!(result, ImagePaintResult::Painted);
for (x, y) in [(85u32, 65u32), (105, 65)] {
let (r, g, b, _) = surface.pixel(x, y);
assert_eq!(
(r, g, b),
(255, 255, 255),
"Cover fit must not bleed past the clip rect at ({x}, {y})"
);
}
let (r, g, b, _) = surface.pixel(95, 65);
assert_eq!(
(r, g, b),
(255, 0, 0),
"expected the image inside the clip rect"
);
}
#[test]
fn draw_image_orientation_matches_raster_row_order() {
let surface = BitmapSurface::new(W, H);
surface.fill(1.0, 1.0, 1.0, 1.0);
let img = image(ImageSource::Bytes(two_tone_png_bytes()), ImageFit::Fill);
let rect = Rect::new(20.0, 20.0, 60.0, 60.0);
let result = unsafe { draw_image(surface.context_ptr(), rect, &img) };
assert_eq!(result, ImagePaintResult::Painted);
let (r, g, b, _) = surface.pixel(50, 25);
assert_eq!(
(r, g, b),
(255, 0, 0),
"top of the painted rect should show the raster's top-half colour (red) — \
a mirrored/upside-down paint would show blue here instead"
);
let (r, g, b, _) = surface.pixel(50, 75);
assert_eq!(
(r, g, b),
(0, 0, 255),
"bottom of the painted rect should show the raster's bottom-half colour (blue) — \
a mirrored/upside-down paint would show red here instead"
);
}
}