use crate::hosting::HostingView;
use crate::objc::{self};
#[cfg(target_os = "macos")]
use crate::objc::NSRect;
use crate::views::ViewHandle;
use core::ffi::c_void;
pub struct Rendered {
pub width: u32,
pub height: u32,
pub pixels: Vec<u8>,
}
impl Rendered {
pub fn diff_count(&self, other: &Rendered, tolerance: u8) -> u32 {
if self.width != other.width || self.height != other.height {
return u32::MAX;
}
if self.pixels == other.pixels {
return 0;
}
if tolerance == 0 {
return self
.pixels
.chunks_exact(4)
.zip(other.pixels.chunks_exact(4))
.filter(|(a, b)| a != b)
.count() as u32;
}
let mut n = 0;
for (a, b) in self.pixels.chunks_exact(4).zip(other.pixels.chunks_exact(4)) {
if a.iter().zip(b).any(|(x, y)| x.abs_diff(*y) > tolerance) {
n += 1;
}
}
n
}
pub fn distinct(&self) -> usize {
let pixels = self.pixels.chunks_exact(4);
let mut bits = 12;
let mut table = vec![u32::MAX; 1 << bits];
let mut has_sentinel = false;
let mut count = 0usize;
for px in pixels {
let key = u32::from_le_bytes([px[0], px[1], px[2], px[3]]);
if key == u32::MAX {
if !has_sentinel {
has_sentinel = true;
count += 1;
}
continue;
}
let mut i = ((key as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) >> (64 - bits)) as usize;
loop {
let slot = table[i];
if slot == key {
break;
}
if slot == u32::MAX {
table[i] = key;
count += 1;
if count > (1 << bits) / 2 {
bits += 1;
let mut bigger = vec![u32::MAX; 1 << bits];
for &v in table.iter().filter(|&&v| v != u32::MAX) {
let mut j = ((v as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15)
>> (64 - bits)) as usize;
while bigger[j] != u32::MAX {
j = (j + 1) & ((1 << bits) - 1);
}
bigger[j] = v;
}
table = bigger;
}
break;
}
i = (i + 1) & ((1 << bits) - 1);
}
}
count
}
#[cfg(target_os = "macos")]
pub fn write_png(&self, path: &str) -> bool {
unsafe {
let rep = new_bitmap(self.width, self.height);
let data = objc::send_ret_ptr(rep, objc::sel(c"bitmapData")) as *mut u8;
let row = objc::send_ret_u64(rep, objc::sel(c"bytesPerRow")) as usize;
for y in 0..self.height as usize {
let src = &self.pixels[y * self.width as usize * 4..][..self.width as usize * 4];
core::ptr::copy_nonoverlapping(src.as_ptr(), data.add(y * row), src.len());
}
let ok = write_rep_png(rep, path);
objc::send(rep, objc::sel(c"release"));
ok
}
}
#[cfg(not(target_os = "macos"))]
pub fn write_png(&self, path: &str) -> bool {
crate::cg::write_png(self.width, self.height, &self.pixels, path)
}
}
#[cfg(target_os = "macos")]
pub fn render(view: &ViewHandle, width: u32, height: u32) -> Rendered {
let hv = HostingView::new(view);
let out = render_hosting(&hv, width, height);
drop(hv);
drain();
out
}
pub fn drain() {
pump(0.0);
}
#[cfg(target_os = "macos")]
pub fn render_hosting(hv: &HostingView, width: u32, height: u32) -> Rendered {
unsafe {
let _pool = objc::AutoreleasePool::new();
let app = objc::shared_app();
let _ = app;
let _ = offscreen_window_for(hv, width, height);
hv.set_frame(width as f64, height as f64);
capture_view(hv.as_ptr(), width, height)
}
}
#[cfg(target_os = "macos")]
unsafe fn offscreen_window_for(hv: &HostingView, width: u32, height: u32) -> *mut c_void {
unsafe { hv.offscreen(width as f64, height as f64) }
}
#[cfg(target_os = "macos")]
pub unsafe fn capture_view(view: *mut c_void, width: u32, height: u32) -> Rendered {
unsafe {
let _pool = objc::AutoreleasePool::new();
let bounds = NSRect::new(0.0, 0.0, width as f64, height as f64);
let rep = new_bitmap(width, height);
objc::send_rect_id(
view,
objc::sel(c"cacheDisplayInRect:toBitmapImageRep:"),
bounds,
rep,
);
let data = objc::send_ret_ptr(rep, objc::sel(c"bitmapData"));
let row = objc::send_ret_u64(rep, objc::sel(c"bytesPerRow")) as usize;
let mut pixels = vec![0u8; (width * height * 4) as usize];
for y in 0..height as usize {
core::ptr::copy_nonoverlapping(
data.add(y * row),
pixels.as_mut_ptr().add(y * width as usize * 4),
width as usize * 4,
);
}
objc::send(rep, objc::sel(c"release"));
Rendered { width, height, pixels }
}
}
#[cfg(target_os = "macos")]
pub unsafe fn capture_window(window: *mut c_void, width: u32, height: u32) -> Rendered {
unsafe {
let content = objc::send(window, objc::sel(c"contentView"));
let frame_view = objc::send(content, objc::sel(c"superview"));
let target = if frame_view.is_null() { content } else { frame_view };
capture_view(target, width, height)
}
}
#[cfg(target_os = "macos")]
pub fn render_chrome(view: &ViewHandle, width: u32, height: u32) -> Rendered {
unsafe {
let _pool = objc::AutoreleasePool::new();
let win = titled_window(view, width, height);
pump_until(0.45, || objc::send_ret_bool(win, objc::sel(c"isVisible")));
let out = capture_window(win, width, height);
objc::send(win, objc::sel(c"close"));
pump(0.01);
out
}
}
#[cfg(target_os = "macos")]
pub fn render_sheet(view: &ViewHandle, win_w: u32, win_h: u32, w: u32, h: u32) -> Rendered {
unsafe {
let _pool = objc::AutoreleasePool::new();
let win = titled_window(view, win_w, win_h);
pump_until(0.7, || !objc::send(win, objc::sel(c"attachedSheet")).is_null());
let sheet = objc::send(win, objc::sel(c"attachedSheet"));
let out = if sheet.is_null() {
Rendered { width: w, height: h, pixels: vec![0; (w * h * 4) as usize] }
} else {
capture_window(sheet, w, h)
};
objc::send(win, objc::sel(c"close"));
pump(0.01);
out
}
}
#[cfg(target_os = "macos")]
pub fn render_composited(view: &ViewHandle, width: u32, height: u32) -> Rendered {
unsafe {
let _pool = objc::AutoreleasePool::new();
let win = titled_window(view, width, height);
pump_until(0.7, || objc::send_ret_bool(win, objc::sel(c"isVisible")));
let out = capture_composited(win, width, height);
objc::send(win, objc::sel(c"close"));
pump(0.01);
out
}
}
#[cfg(target_os = "macos")]
pub fn pump_until(limit: f64, mut ready: impl FnMut() -> bool) {
let step = 0.005;
let mut waited = 0.0;
while waited < limit {
pump(step);
waited += step;
if ready() {
pump(0.02);
return;
}
}
}
#[cfg(target_os = "macos")]
pub unsafe fn capture_composited(window: *mut c_void, width: u32, height: u32) -> Rendered {
let _pool = objc::AutoreleasePool::new();
unsafe extern "C" {
fn CGWindowListCreateImage(
bounds: objc::NSRect,
list_option: u32,
window_id: u32,
image_option: u32,
) -> *mut c_void;
}
unsafe {
let id = objc::send_ret_u64(window, objc::sel(c"windowNumber")) as u32;
let cg = CGWindowListCreateImage(
objc::NSRect::new(f64::INFINITY, f64::INFINITY, 0.0, 0.0),
1 << 3,
id,
(1 << 4) | 1,
);
if cg.is_null() {
return Rendered { width, height, pixels: vec![0; (width * height * 4) as usize] };
}
let rep = objc::send_id(
objc::send(objc::class(c"OBJC_CLASS_$_NSBitmapImageRep"), objc::sel(c"alloc")),
objc::sel(c"initWithCGImage:"),
cg,
);
let got_w = objc::send_ret_u64(rep, objc::sel(c"pixelsWide")) as usize;
let got_h = objc::send_ret_u64(rep, objc::sel(c"pixelsHigh")) as usize;
let row = objc::send_ret_u64(rep, objc::sel(c"bytesPerRow")) as usize;
let data = objc::send_ret_ptr(rep, objc::sel(c"bitmapData"));
let mut pixels = vec![0u8; (width * height * 4) as usize];
let copy_w = got_w.min(width as usize);
for y in 0..got_h.min(height as usize) {
core::ptr::copy_nonoverlapping(
data.add(y * row),
pixels.as_mut_ptr().add(y * width as usize * 4),
copy_w * 4,
);
}
Rendered { width, height, pixels }
}
}
#[cfg(target_os = "macos")]
pub fn render_popover(view: &ViewHandle, win_w: u32, win_h: u32, w: u32, h: u32) -> Rendered {
unsafe {
let _pool = objc::AutoreleasePool::new();
let win = titled_window(view, win_w, win_h);
pump_until(0.8, || !find_window(|name| name.contains("Popover")).is_null());
let popover = find_window(|name| name.contains("Popover"));
let out = if popover.is_null() {
Rendered { width: w, height: h, pixels: vec![0; (w * h * 4) as usize] }
} else {
capture_window(popover, w, h)
};
objc::send(win, objc::sel(c"close"));
if !popover.is_null() {
objc::send_id(popover, objc::sel(c"orderOut:"), core::ptr::null_mut());
objc::send(popover, objc::sel(c"close"));
}
pump(0.01);
out
}
}
#[cfg(target_os = "macos")]
unsafe fn find_window(pred: impl Fn(&str) -> bool) -> *mut c_void {
unsafe {
let wins = objc::send(objc::shared_app(), objc::sel(c"windows"));
if wins.is_null() {
return core::ptr::null_mut();
}
let n = objc::send_ret_u64(wins, objc::sel(c"count"));
for i in 0..n {
let w = objc::send_u64_id(wins, objc::sel(c"objectAtIndex:"), i, core::ptr::null_mut());
let cls = objc::send(w, objc::sel(c"className"));
let p = objc::send_ret_ptr(cls, objc::sel(c"UTF8String"));
if p.is_null() {
continue;
}
let name = core::ffi::CStr::from_ptr(p as *const i8).to_string_lossy();
if pred(&name) {
return w;
}
}
core::ptr::null_mut()
}
}
#[cfg(target_os = "macos")]
unsafe fn titled_window(view: &ViewHandle, width: u32, height: u32) -> *mut c_void {
unsafe {
let app = objc::shared_app();
objc::send_i64(app, objc::sel(c"setActivationPolicy:"), 1);
objc::send_bool(
objc::class(c"OBJC_CLASS_$_NSWindow"),
objc::sel(c"setAllowsAutomaticWindowTabbing:"),
false,
);
let hosting = HostingView::new(view);
let bounds = NSRect::new(0.0, 0.0, width as f64, height as f64);
let win = objc::send(objc::class(c"OBJC_CLASS_$_NSWindow"), objc::sel(c"alloc"));
type InitWin =
unsafe extern "C" fn(*mut c_void, *const c_void, NSRect, u64, u64, bool) -> *mut c_void;
let init: InitWin = core::mem::transmute(objc::msg_send_addr());
let win = init(win, objc::sel(c"initWithContentRect:styleMask:backing:defer:"), bounds, 15, 2, false);
let name = objc::nsstring("NSAppearanceNameAqua");
let appearance = objc::send_id(
objc::class(c"OBJC_CLASS_$_NSAppearance"),
objc::sel(c"appearanceNamed:"),
name,
);
if !appearance.is_null() {
objc::send_id(win, objc::sel(c"setAppearance:"), appearance);
}
objc::send_i64(win, objc::sel(c"setTabbingMode:"), 2); objc::send_i64(win, objc::sel(c"setAnimationBehavior:"), 2);
objc::send_id(win, objc::sel(c"setTitle:"), objc::nsstring("parity"));
objc::send_id(win, objc::sel(c"setContentView:"), hosting.as_ptr());
type SetFrame = unsafe extern "C" fn(*mut c_void, *const c_void, NSRect, bool);
let set_frame: SetFrame = core::mem::transmute(objc::msg_send_addr());
set_frame(win, objc::sel(c"setFrame:display:"), bounds, true);
objc::send_id(win, objc::sel(c"orderFront:"), core::ptr::null_mut());
drop(hosting);
win
}
}
#[cfg(target_os = "macos")]
pub unsafe fn during_tracking(delay: f64, interval: f64, body: fn() -> bool) {
#[repr(C)]
struct TimerContext {
version: isize,
info: *mut c_void,
retain: *const c_void,
release: *const c_void,
copy_description: *const c_void,
}
unsafe extern "C" fn fire(timer: *mut c_void, info: *mut c_void) {
unsafe {
let body: fn() -> bool = core::mem::transmute(info);
if body() {
let invalidate: unsafe extern "C" fn(*mut c_void) =
core::mem::transmute(cf(c"CFRunLoopTimerInvalidate"));
invalidate(timer);
}
}
}
unsafe {
type Create = unsafe extern "C" fn(
*const c_void, f64, f64, u64, isize,
unsafe extern "C" fn(*mut c_void, *mut c_void),
*mut TimerContext,
) -> *mut c_void;
type Add = unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void);
type Now = unsafe extern "C" fn() -> f64;
type Main = unsafe extern "C" fn() -> *mut c_void;
let create: Create = core::mem::transmute(cf(c"CFRunLoopTimerCreate"));
let add: Add = core::mem::transmute(cf(c"CFRunLoopAddTimer"));
let now: Now = core::mem::transmute(cf(c"CFAbsoluteTimeGetCurrent"));
let main: Main = core::mem::transmute(cf(c"CFRunLoopGetMain"));
let mut ctx = TimerContext {
version: 0,
info: body as *mut c_void,
retain: core::ptr::null(),
release: core::ptr::null(),
copy_description: core::ptr::null(),
};
let timer = create(core::ptr::null(), now() + delay, interval, 0, 0, fire, &mut ctx);
let modes = *(cf(c"kCFRunLoopCommonModes") as *const *mut c_void);
add(main(), timer, modes);
}
}
#[cfg(target_os = "macos")]
fn cf(name: &core::ffi::CStr) -> *mut c_void {
let p = unsafe { libc::dlsym((-2isize) as *mut c_void, name.as_ptr()) };
assert!(!p.is_null(), "CoreFoundation: {}", name.to_string_lossy());
p
}
#[cfg(target_os = "macos")]
pub unsafe fn post_key(key_code: u16, characters: &str) {
unsafe {
type Make = unsafe extern "C" fn(
*mut c_void, *const c_void, u64, objc::NSPoint, u64, f64, i64,
*mut c_void, *mut c_void, *mut c_void, bool, u16,
) -> *mut c_void;
let make: Make = core::mem::transmute(objc::msg_send_addr());
let chars = objc::nsstring(characters);
for kind in [10u64, 11] {
let event = make(
objc::class(c"OBJC_CLASS_$_NSEvent"),
objc::sel(c"keyEventWithType:location:modifierFlags:timestamp:windowNumber:context:characters:charactersIgnoringModifiers:isARepeat:keyCode:"),
kind,
objc::NSPoint { x: 0.0, y: 0.0 },
0,
uptime(),
0,
core::ptr::null_mut(),
chars,
chars,
false,
key_code,
);
if !event.is_null() {
objc::send_id_bool(objc::shared_app(), objc::sel(c"postEvent:atStart:"), event, true);
}
}
}
}
#[cfg(target_os = "macos")]
pub fn pump_events(seconds: f64) {
unsafe {
let app = objc::shared_app();
let deadline = uptime() + seconds;
while uptime() < deadline {
type Next = unsafe extern "C" fn(
*mut c_void, *const c_void, u64, *mut c_void, *mut c_void, bool,
) -> *mut c_void;
let next: Next = core::mem::transmute(objc::msg_send_addr());
type DateFn = unsafe extern "C" fn(*mut c_void, *const c_void, f64) -> *mut c_void;
let date: DateFn = core::mem::transmute(objc::msg_send_addr());
let until = date(
objc::class(c"OBJC_CLASS_$_NSDate"),
objc::sel(c"dateWithTimeIntervalSinceNow:"),
0.01,
);
let event = next(
app,
objc::sel(c"nextEventMatchingMask:untilDate:inMode:dequeue:"),
u64::MAX,
until,
objc::nsstring("kCFRunLoopDefaultMode"),
true,
);
if !event.is_null() {
objc::send_id(app, objc::sel(c"sendEvent:"), event);
}
}
}
}
pub fn uptime() -> f64 {
unsafe {
let info = objc::send(
objc::class(c"OBJC_CLASS_$_NSProcessInfo"),
objc::sel(c"processInfo"),
);
objc::send_ret_f64(info, objc::sel(c"systemUptime"))
}
}
#[cfg(target_os = "macos")]
pub fn activate(timeout: f64) -> bool {
unsafe {
let app = objc::shared_app();
objc::send_i64(app, objc::sel(c"setActivationPolicy:"), 0);
objc::send_bool(app, objc::sel(c"activateIgnoringOtherApps:"), true);
if objc::responds(app, objc::sel(c"activate")) {
objc::send(app, objc::sel(c"activate"));
}
let running = objc::send(
objc::class(c"OBJC_CLASS_$_NSRunningApplication"),
objc::sel(c"currentApplication"),
);
if !running.is_null() {
objc::send_u64(running, objc::sel(c"activateWithOptions:"), 1 | 2);
}
let deadline = uptime() + timeout;
while uptime() < deadline {
if objc::send(app, objc::sel(c"isActive")) as usize & 1 == 1 {
return true;
}
pump_events(0.05);
}
false
}
}
#[cfg(target_os = "macos")]
pub fn deactivate(timeout: f64) -> bool {
unsafe {
let app = objc::shared_app();
objc::send_i64(app, objc::sel(c"setActivationPolicy:"), 2);
objc::send_id(app, objc::sel(c"hide:"), core::ptr::null_mut());
objc::send(app, objc::sel(c"deactivate"));
let deadline = uptime() + timeout;
while uptime() < deadline {
if objc::send(app, objc::sel(c"isActive")) as usize & 1 == 0 {
return true;
}
pump_events(0.05);
}
false
}
}
#[cfg(target_os = "macos")]
pub fn pump_events_until(limit: f64, mut ready: impl FnMut() -> bool) {
let step = 0.005;
let mut waited = 0.0;
while waited < limit {
pump_events(step);
waited += step;
if ready() {
pump_events(0.02);
return;
}
}
}
#[cfg(target_os = "macos")]
pub unsafe fn post_mouse(window: *mut c_void, kind: u64, x: f64, y: f64, clicks: i64) {
unsafe {
let event = mouse_event(window, kind, x, y, clicks);
if !event.is_null() {
objc::send_id_bool(objc::shared_app(), objc::sel(c"postEvent:atStart:"), event, false);
}
}
}
#[cfg(target_os = "macos")]
pub unsafe fn mouse_event_for(
window: *mut c_void,
kind: u64,
x: f64,
y: f64,
clicks: i64,
) -> *mut c_void {
unsafe { mouse_event(window, kind, x, y, clicks) }
}
#[cfg(target_os = "macos")]
unsafe fn mouse_event(window: *mut c_void, kind: u64, x: f64, y: f64, clicks: i64) -> *mut c_void {
unsafe {
let number = objc::send_ret_u64(window, objc::sel(c"windowNumber")) as i64;
type Make = unsafe extern "C" fn(
*mut c_void, *const c_void, u64, objc::NSPoint, u64, f64, i64, *mut c_void, i64, i64, f32,
) -> *mut c_void;
let make: Make = core::mem::transmute(objc::msg_send_addr());
make(
objc::class(c"OBJC_CLASS_$_NSEvent"),
objc::sel(c"mouseEventWithType:location:modifierFlags:timestamp:windowNumber:context:eventNumber:clickCount:pressure:"),
kind,
objc::NSPoint { x, y },
0,
uptime(),
number,
core::ptr::null_mut(),
0,
clicks,
if kind == 2 || kind == 4 { 0.0 } else { 1.0 },
)
}
}
#[cfg(target_os = "macos")]
pub unsafe fn send_mouse(window: *mut c_void, kind: u64, x: f64, y: f64, clicks: i64) {
unsafe {
let event = mouse_event(window, kind, x, y, clicks);
if !event.is_null() {
objc::send_id(window, objc::sel(c"sendEvent:"), event);
}
}
}
#[cfg(target_os = "macos")]
pub unsafe fn send_key(window: *mut c_void, key_code: u16, characters: &str, flags: u64) {
unsafe {
type Make = unsafe extern "C" fn(
*mut c_void, *const c_void, u64, objc::NSPoint, u64, f64, i64,
*mut c_void, *mut c_void, *mut c_void, bool, u16,
) -> *mut c_void;
let make: Make = core::mem::transmute(objc::msg_send_addr());
let chars = objc::nsstring(characters);
let number = objc::send_ret_u64(window, objc::sel(c"windowNumber")) as i64;
for kind in [10u64, 11] {
let event = make(
objc::class(c"OBJC_CLASS_$_NSEvent"),
objc::sel(c"keyEventWithType:location:modifierFlags:timestamp:windowNumber:context:characters:charactersIgnoringModifiers:isARepeat:keyCode:"),
kind,
objc::NSPoint { x: 0.0, y: 0.0 },
flags,
uptime(),
number,
core::ptr::null_mut(),
chars,
chars,
false,
key_code,
);
if !event.is_null() {
objc::send_id(window, objc::sel(c"sendEvent:"), event);
}
}
}
}
#[cfg(not(target_os = "macos"))]
pub fn render(view: &ViewHandle, width: u32, height: u32) -> Rendered {
let hv = HostingView::new(view);
render_hosting(&hv, width, height)
}
#[cfg(not(target_os = "macos"))]
pub fn render_hosting(hv: &HostingView, width: u32, height: u32) -> Rendered {
unsafe {
let _pool = objc::AutoreleasePool::new();
let window = hv.offscreen(width as f64, height as f64);
hv.set_frame(width as f64, height as f64);
objc::send(window, objc::sel(c"layoutIfNeeded"));
capture_view(hv.as_ptr(), width, height)
}
}
#[cfg(not(target_os = "macos"))]
pub unsafe fn capture_view(view: *mut c_void, width: u32, height: u32) -> Rendered {
unsafe {
let layer = objc::send(view, objc::sel(c"layer"));
assert!(!layer.is_null(), "the view has no layer to render");
let pixels = crate::cg::capture_layer(layer, width, height);
Rendered { width, height, pixels }
}
}
pub fn pump(seconds: f64) {
unsafe {
let loop_ = objc::send(
objc::class(c"OBJC_CLASS_$_NSRunLoop"),
objc::sel(c"currentRunLoop"),
);
type DateFn = unsafe extern "C" fn(*mut c_void, *const c_void, f64) -> *mut c_void;
let f: DateFn = core::mem::transmute(objc::msg_send_addr());
let until = f(
objc::class(c"OBJC_CLASS_$_NSDate"),
objc::sel(c"dateWithTimeIntervalSinceNow:"),
seconds,
);
objc::send_id(loop_, objc::sel(c"runUntilDate:"), until);
}
}
pub fn render_png(view: &ViewHandle, width: u32, height: u32, path: &str) -> bool {
let img = render(view, width, height);
img.write_png(path)
}
#[cfg(target_os = "macos")]
unsafe fn new_bitmap(width: u32, height: u32) -> *mut c_void {
let alloc = unsafe {
objc::send(objc::class(c"OBJC_CLASS_$_NSBitmapImageRep"), objc::sel(c"alloc"))
};
#[allow(clippy::too_many_arguments)]
type Init = unsafe extern "C" fn(
*mut c_void,
*const c_void,
*mut u8, i64, i64, i64, i64, bool, bool, *mut c_void, i64, i64, ) -> *mut c_void;
let init: Init = unsafe { core::mem::transmute(objc::msg_send_addr()) };
let space = unsafe { objc::nsstring("NSDeviceRGBColorSpace") };
let rep = unsafe {
init(
alloc,
objc::sel(c"initWithBitmapDataPlanes:pixelsWide:pixelsHigh:bitsPerSample:samplesPerPixel:hasAlpha:isPlanar:colorSpaceName:bytesPerRow:bitsPerPixel:"),
core::ptr::null_mut(),
width as i64,
height as i64,
8,
4,
true,
false,
space,
0,
0,
)
};
assert!(!rep.is_null(), "NSBitmapImageRep creation failed");
rep
}
#[cfg(target_os = "macos")]
unsafe fn write_rep_png(rep: *mut c_void, path: &str) -> bool {
unsafe {
let props =
objc::send(objc::class(c"OBJC_CLASS_$_NSDictionary"), objc::sel(c"dictionary"));
let data =
objc::send_u64_id(rep, objc::sel(c"representationUsingType:properties:"), 4, props);
if data.is_null() {
return false;
}
objc::send_id_bool(
data,
objc::sel(c"writeToFile:atomically:"),
objc::nsstring(path),
true,
)
}
}
pub fn render_image(view: &ViewHandle, width: u32, height: u32) -> Rendered {
unsafe {
let _pool = objc::AutoreleasePool::new();
let framed = crate::views::frame(view, width as f64, height as f64);
let meta = crate::abi::generic_metadata2(
crate::resolve::image_renderer_ma(),
crate::resolve::anyview_meta(),
crate::resolve::anyview_wt(),
);
assert!(!meta.is_null(), "ImageRenderer<AnyView> metadata");
let mut content: u64 = framed.as_ptr() as u64;
let renderer: *mut c_void;
core::arch::asm!("blr {f}", f = in(reg) crate::resolve::image_renderer_init(),
in("x0") &mut content as *mut u64,
in("x20") meta,
lateout("x0") renderer,
lateout("x1") _, lateout("x2") _, lateout("x3") _, lateout("x4") _,
lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x8") _,
lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
);
assert!(!renderer.is_null(), "ImageRenderer creation failed");
core::arch::asm!("blr {f}", f = in(reg) crate::resolve::image_renderer_scale_set(),
in("d0") 1.0f64,
in("x20") renderer,
lateout("x0") _, lateout("x1") _, lateout("x2") _, lateout("x3") _,
lateout("x4") _, lateout("x5") _, lateout("x6") _, lateout("x7") _,
lateout("x8") _, lateout("x9") _, lateout("x10") _, lateout("x11") _,
lateout("x12") _, lateout("x13") _, lateout("x14") _, lateout("x15") _,
lateout("x16") _, lateout("x17") _, lateout("lr") _, lateout("x21") _,
clobber_abi("C"),
);
let cg: *mut c_void;
core::arch::asm!("blr {f}", f = in(reg) crate::resolve::image_renderer_cgimage(),
in("x20") renderer,
lateout("x0") cg,
lateout("x1") _, lateout("x2") _, lateout("x3") _, lateout("x4") _,
lateout("x5") _, lateout("x6") _, lateout("x7") _, lateout("x8") _,
lateout("x9") _, lateout("x10") _, lateout("x11") _, lateout("x12") _,
lateout("x13") _, lateout("x14") _, lateout("x15") _, lateout("x16") _,
lateout("x17") _, lateout("lr") _, lateout("x21") _, clobber_abi("C"),
);
assert!(!cg.is_null(), "ImageRenderer produced no image");
let out = {
#[cfg(target_os = "macos")]
{
let alloc =
objc::send(objc::class(c"OBJC_CLASS_$_NSBitmapImageRep"), objc::sel(c"alloc"));
let rep = objc::send_id(alloc, objc::sel(c"initWithCGImage:"), cg);
assert!(!rep.is_null(), "NSBitmapImageRep from CGImage failed");
let data = objc::send_ret_ptr(rep, objc::sel(c"bitmapData"));
let row = objc::send_ret_u64(rep, objc::sel(c"bytesPerRow")) as usize;
let w = objc::send_ret_u64(rep, objc::sel(c"pixelsWide")) as u32;
let h = objc::send_ret_u64(rep, objc::sel(c"pixelsHigh")) as u32;
let mut pixels = vec![0u8; (w * h * 4) as usize];
for y in 0..h as usize {
core::ptr::copy_nonoverlapping(
data.add(y * row),
pixels.as_mut_ptr().add(y * w as usize * 4),
w as usize * 4,
);
}
objc::send(rep, objc::sel(c"release"));
Rendered { width: w, height: h, pixels }
}
#[cfg(not(target_os = "macos"))]
{
let (w, h, pixels) = crate::cg::cgimage_to_rgba(cg);
Rendered { width: w, height: h, pixels }
}
};
unsafe extern "C" {
fn CGImageRelease(image: *mut c_void);
}
CGImageRelease(cg);
swift_runtime_sys::RuntimeRaw::swift_release(renderer);
out
}
}
pub struct Animation {
pub frames: Vec<Rendered>,
}
impl Animation {
pub fn sweep(
count: usize,
width: u32,
height: u32,
build: impl Fn(f64) -> ViewHandle,
) -> Self {
Self::sweep_with(count, width, height, false, build)
}
pub fn sweep_with(
count: usize,
width: u32,
height: u32,
via_image_renderer: bool,
build: impl Fn(f64) -> ViewHandle,
) -> Self {
let frames = (0..count)
.map(|i| {
let t = if count <= 1 { 0.0 } else { i as f64 / (count - 1) as f64 };
let view = build(t);
if via_image_renderer {
render_image(&view, width, height)
} else {
render(&view, width, height)
}
})
.collect();
Self { frames }
}
pub fn diff_count(&self, other: &Animation, tolerance: u8) -> u32 {
if self.frames.len() != other.frames.len() {
return u32::MAX;
}
self.frames
.iter()
.zip(&other.frames)
.map(|(a, b)| a.diff_count(b, tolerance))
.fold(0u32, |acc, n| acc.saturating_add(n))
}
pub fn frame_deltas(&self) -> Vec<u32> {
self.frames
.windows(2)
.map(|w| w[0].diff_count(&w[1], 0))
.collect()
}
pub fn write_filmstrip(&self, path: &str) -> bool {
let Some(first) = self.frames.first() else { return false };
let (fw, fh) = (first.width as usize, first.height as usize);
let n = self.frames.len();
let gap = 4usize;
let width = fw * n + gap * (n - 1);
let mut pixels = vec![0u8; width * fh * 4];
for (i, frame) in self.frames.iter().enumerate() {
let x0 = i * (fw + gap);
for y in 0..fh {
let src = &frame.pixels[y * fw * 4..(y + 1) * fw * 4];
let dst = (y * width + x0) * 4;
pixels[dst..dst + fw * 4].copy_from_slice(src);
}
}
Rendered { width: width as u32, height: fh as u32, pixels }.write_png(path)
}
pub fn write_apng(&self, path: &str, delay_ms: u16) -> std::io::Result<()> {
std::fs::write(path, encode_apng(&self.frames, delay_ms))
}
}
fn crc32(data: &[u8]) -> u32 {
let mut table = [0u32; 256];
for (i, entry) in table.iter_mut().enumerate() {
let mut c = i as u32;
for _ in 0..8 {
c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 };
}
*entry = c;
}
let mut c = 0xFFFF_FFFFu32;
for &b in data {
c = table[((c ^ b as u32) & 0xFF) as usize] ^ (c >> 8);
}
c ^ 0xFFFF_FFFF
}
fn adler32(data: &[u8]) -> u32 {
let (mut a, mut b) = (1u32, 0u32);
for &byte in data {
a = (a + byte as u32) % 65521;
b = (b + a) % 65521;
}
(b << 16) | a
}
fn zlib_stored(data: &[u8]) -> Vec<u8> {
let mut out = vec![0x78, 0x01]; let mut chunks = data.chunks(65535).peekable();
if data.is_empty() {
out.extend_from_slice(&[0x01, 0x00, 0x00, 0xFF, 0xFF]);
}
while let Some(chunk) = chunks.next() {
let last = chunks.peek().is_none();
out.push(u8::from(last));
let n = chunk.len() as u16;
out.extend_from_slice(&n.to_le_bytes());
out.extend_from_slice(&(!n).to_le_bytes());
out.extend_from_slice(chunk);
}
out.extend_from_slice(&adler32(data).to_be_bytes());
out
}
fn chunk(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(payload.len() + 12);
out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
out.extend_from_slice(kind);
out.extend_from_slice(payload);
let mut crc_input = Vec::with_capacity(payload.len() + 4);
crc_input.extend_from_slice(kind);
crc_input.extend_from_slice(payload);
out.extend_from_slice(&crc32(&crc_input).to_be_bytes());
out
}
fn scanlines(frame: &Rendered) -> Vec<u8> {
let stride = frame.width as usize * 4;
let mut raw = Vec::with_capacity((stride + 1) * frame.height as usize);
for y in 0..frame.height as usize {
raw.push(0);
raw.extend_from_slice(&frame.pixels[y * stride..(y + 1) * stride]);
}
raw
}
fn encode_apng(frames: &[Rendered], delay_ms: u16) -> Vec<u8> {
assert!(!frames.is_empty(), "an APNG needs at least one frame");
let (w, h) = (frames[0].width, frames[0].height);
let mut png = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A];
let mut ihdr = Vec::new();
ihdr.extend_from_slice(&w.to_be_bytes());
ihdr.extend_from_slice(&h.to_be_bytes());
ihdr.extend_from_slice(&[8, 6, 0, 0, 0]); png.extend_from_slice(&chunk(b"IHDR", &ihdr));
let mut actl = Vec::new();
actl.extend_from_slice(&(frames.len() as u32).to_be_bytes());
actl.extend_from_slice(&0u32.to_be_bytes()); png.extend_from_slice(&chunk(b"acTL", &actl));
let mut seq = 0u32;
for (i, frame) in frames.iter().enumerate() {
let mut fctl = Vec::new();
fctl.extend_from_slice(&seq.to_be_bytes());
seq += 1;
fctl.extend_from_slice(&w.to_be_bytes());
fctl.extend_from_slice(&h.to_be_bytes());
fctl.extend_from_slice(&0u32.to_be_bytes()); fctl.extend_from_slice(&0u32.to_be_bytes()); fctl.extend_from_slice(&delay_ms.to_be_bytes());
fctl.extend_from_slice(&1000u16.to_be_bytes()); fctl.extend_from_slice(&[0, 0]); png.extend_from_slice(&chunk(b"fcTL", &fctl));
let data = zlib_stored(&scanlines(frame));
if i == 0 {
png.extend_from_slice(&chunk(b"IDAT", &data));
} else {
let mut fdat = seq.to_be_bytes().to_vec();
seq += 1;
fdat.extend_from_slice(&data);
png.extend_from_slice(&chunk(b"fdAT", &fdat));
}
}
png.extend_from_slice(&chunk(b"IEND", &[]));
png
}