use std::{ cmp::{max, min}, ffi::{CStr, CString, OsStr, c_void}, fs, os::unix::ffi::OsStrExt, path::Path, ptr::{self, NonNull}, slice, }; use ::typst::{ Document, layout::{Abs, FrameItem, Page, Point, Rect}, model::Destination, text::{Glyph, TextItem}, }; use cairo::{Format, ImageSurface}; use crate::{ typst::FrameItemIterator, zathura::{ cairo_t, girara_list_t, zathura_document_get_data, zathura_document_get_path, zathura_document_s, zathura_document_set_data, zathura_document_set_number_of_pages, zathura_link_destination_type_e_ZATHURA_LINK_DESTINATION_UNKNOWN, zathura_link_destination_type_e_ZATHURA_LINK_DESTINATION_XYZ, zathura_link_free, zathura_link_new, zathura_link_target_s, zathura_link_type_e_ZATHURA_LINK_GOTO_DEST, zathura_link_type_e_ZATHURA_LINK_GOTO_REMOTE, zathura_link_type_e_ZATHURA_LINK_URI, zathura_page_get_document, zathura_page_get_index, zathura_page_set_data, zathura_page_set_height, zathura_page_set_width, zathura_page_t, zathura_rectangle_s, GiraraList, ZathuraResult }, }; mod typst; mod zathura; use typst::TypstDocument; #[allow(non_upper_case_globals)] #[unsafe(no_mangle)] pub static zathura_plugin_6_7: zathura::zathura_plugin_definition_s = zathura::zathura_plugin_definition_s { name: c"zathura-typst".as_ptr(), version: zathura::zathura_plugin_version_s { major: 0, minor: 1, rev: 0, }, functions: zathura::zathura_plugin_functions_s { document_open: Some(document_open), document_free: Some(document_free), document_index_generate: None, // TODO? document_save_as: Some(document_save_as), document_attachments_get: None, document_attachment_save: None, document_get_information: None, // TODO page_init: Some(page_init), page_clear: Some(page_clear), page_search_text: None, // TODO? page_links_get: Some(page_links_get), page_form_fields_get: None, page_images_get: None, // TODO? page_image_get_cairo: None, page_get_text: Some(page_get_text), page_get_selection: Some(page_get_selection), page_render_cairo: Some(page_render_cairo), page_get_label: None, page_get_signatures: None, }, mime_types_size: 1, mime_types: { static MIMETYPES: &[&u8] = &[c"text/vnd.typst".to_bytes_with_nul().first().unwrap()]; MIMETYPES.as_ptr() as *mut _ }, }; unsafe extern "C" fn document_open(doc: *mut zathura_document_s) -> ZathuraResult { let path = unsafe { Path::new(OsStr::from_bytes( CStr::from_ptr(zathura_document_get_path(doc)).to_bytes(), )) }; let document = Box::new(TypstDocument::load(path)); unsafe { zathura_document_set_number_of_pages(doc, document.doc.pages.len() as u32); zathura_document_set_data(doc, Box::into_raw(document) as _); } ZathuraResult::OK } unsafe extern "C" fn document_free(_: *mut zathura_document_s, data: *mut c_void) -> ZathuraResult { if !data.is_null() { drop(unsafe { Box::::from_raw(data as _) }); } ZathuraResult::OK } unsafe extern "C" fn document_save_as( _: *mut zathura_document_s, data: *mut c_void, dst: *const i8, ) -> ZathuraResult { let typst_doc: &TypstDocument = unsafe { &*(data as *mut _) }; let dst = Path::new(OsStr::from_bytes(unsafe { CStr::from_ptr(dst) }.to_bytes())); let Ok(bytes) = typst_pdf::pdf(&typst_doc.doc, &Default::default()) else { return ZathuraResult::Unknown; }; let Ok(()) = fs::write(dst, bytes) else { return ZathuraResult::InvalidArguments; }; ZathuraResult::OK } unsafe extern "C" fn page_init(page: *mut zathura_page_t) -> ZathuraResult { let pageno = unsafe { zathura_page_get_index(page) }; let doc = unsafe { zathura_page_get_document(page) }; let typst_doc = unsafe { &mut *(zathura_document_get_data(doc) as *mut TypstDocument) }; let typst_page: &Page = &typst_doc.doc.pages[pageno as usize]; unsafe { zathura_page_set_data(page, typst_page as *const _ as _); zathura_page_set_width(page, typst_page.frame.width().to_pt()); zathura_page_set_height(page, typst_page.frame.height().to_pt()); }; ZathuraResult::OK } unsafe extern "C" fn page_clear(_: *mut zathura_page_t, _: *mut c_void) -> ZathuraResult { ZathuraResult::OK } unsafe extern "C" fn page_render_cairo( _page: *mut zathura_page_t, typst_page: *mut c_void, cairo: *mut cairo_t, _printing: bool, ) -> ZathuraResult { let typst_page: &Page = unsafe { &*(typst_page as *mut _) }; let context = unsafe { cairo::Context::from_raw_none(cairo) }; let surface = context.target(); if surface.status().is_err() { return ZathuraResult::Unknown; } let Ok(surface) = ImageSurface::try_from(surface) else { return ZathuraResult::InvalidArguments; }; let scale = { let wscale = surface.width() as f32 / typst_page.frame.width().to_pt() as f32; let vscale = surface.height() as f32 / typst_page.frame.height().to_pt() as f32; if vscale > wscale { vscale } else { wscale } }; let format = surface.format(); let pixels_rgba = typst_render::render(typst_page, scale); let render_width = pixels_rgba.width(); let pixels_rgba = pixels_rgba.take(); let (pixels_rgba, _) = pixels_rgba.as_chunks::<4>(); // This will always be exact anyway let buf = unsafe { std::slice::from_raw_parts_mut( cairo::ffi::cairo_image_surface_get_data(surface.to_raw_none()), surface.stride() as usize * surface.height() as usize, ) }; if !matches!(format, Format::ARgb32 | Format::Rgb24) { return ZathuraResult::Unknown; } pixels_rgba .chunks_exact(render_width as _) .zip(buf.chunks_mut(surface.stride() as _)) .for_each(|(row, dst)| { row.iter() .zip(dst.chunks_exact_mut(4)) .for_each(|(&[r, g, b, a], dst)| { dst.copy_from_slice(&u32::from_be_bytes([a, r, g, b]).to_ne_bytes()); }) }); surface.mark_dirty(); ZathuraResult::OK } fn rect_intersection(a: Rect, b: Rect) -> Option { Some(Rect::new( Point::new(max(a.min.x, b.min.x), max(a.min.y, b.min.y)), Point::new(min(a.max.x, b.max.x), min(a.max.y, b.max.y)), )) .filter(|r| r.size().all(|c| c.to_raw() >= 0.)) } fn rect_union(a: Rect, b: Rect) -> Rect { Rect::new( Point::new(min(a.min.x, b.min.x), min(a.min.y, b.min.y)), Point::new(max(a.max.x, b.max.x), max(a.max.y, b.max.y)), ) } fn rect_norm(a: Rect) -> Rect { Rect::new( Point::new(min(a.min.x, a.max.x), min(a.min.y, a.max.y)), Point::new(max(a.min.x, a.max.x), max(a.min.y, a.max.y)), ) } fn selected_glyphs( selection: Rect, point: Point, item: &TextItem, ) -> Option> { let bbox = rect_norm(item.bbox()); if bbox.size().any(|d| d.to_raw().is_infinite()) { return None; } let inter = rect_intersection( Rect::new(point + bbox.min, point + bbox.max), rect_norm(selection), )?; Some( item.glyphs .iter() .scan(point, move |cursor, glyph| { Some((*cursor, { *cursor += Point::new(glyph.x_advance.at(item.size), glyph.y_advance.at(item.size)); glyph })) }) .filter_map(move |(point, glyph)| { let bbox = item .font .ttf() .glyph_bounding_box(ttf_parser::GlyphId(glyph.id))?; let base = point + Point::new(glyph.x_offset.at(item.size), glyph.y_offset.at(item.size)); let bbox = rect_norm(Rect::new( base + Point::new( item.font.to_em(bbox.x_min).at(item.size), item.font.to_em(bbox.y_min).at(item.size), ), base + Point::new( item.font.to_em(bbox.x_max).at(item.size), item.font.to_em(bbox.y_max).at(item.size), ), )); rect_intersection(inter, bbox).map(|_| (point, bbox, glyph)) }), ) } unsafe extern "C" fn page_get_text( _page: *mut zathura_page_t, data: *mut c_void, rect: zathura_rectangle_s, res: *mut ZathuraResult, ) -> *mut i8 { let res = if let Some(mut r) = NonNull::new(res) { unsafe { r.write(ZathuraResult::Unknown); r.as_mut() } } else { &mut ZathuraResult::OK }; let typst_page: &Page = unsafe { &*(data as *mut _) }; let rect = Rect::from(rect); let text = FrameItemIterator::new(&typst_page.frame) .filter_map(|(point, item)| { if let FrameItem::Text(item) = item { let mut ranges = selected_glyphs(rect, point, item)?.map(|(_, _, glyph)| glyph.range()); Some(item.text[ranges.next()?.start..ranges.last()?.end].to_owned()) } else { None } }) .collect::(); *res = ZathuraResult::OK; unsafe { let str = slice::from_raw_parts_mut( cairo::glib::ffi::g_malloc0_n(text.len() + 1, 1) as *mut u8, text.len() + 1, ); str[..text.len()].copy_from_slice(text.as_bytes()); str.as_mut_ptr() as _ } } unsafe extern "C" fn page_get_selection( _page: *mut zathura_page_t, data: *mut c_void, rect: zathura_rectangle_s, res: *mut ZathuraResult, ) -> *mut girara_list_t { let res = if let Some(mut r) = NonNull::new(res) { unsafe { r.write(ZathuraResult::Unknown); r.as_mut() } } else { &mut ZathuraResult::OK }; let typst_page: &Page = unsafe { &*(data as *mut _) }; let rect = Rect::from(rect); let rects = FrameItemIterator::new(&typst_page.frame).filter_map(|(point, item)| { if let FrameItem::Text(item) = item { let bbox = selected_glyphs(rect, point, item)?.fold( Rect::new(Point::splat(Abs::inf()), Point::splat(-Abs::inf())), |acc, (_, bbox, _)| rect_union(acc, bbox), ); Some(bbox) } else { None } }); *res = ZathuraResult::OK; rects .map(zathura_rectangle_s::from) .collect::>() .into_raw() } unsafe extern "C" fn page_links_get( zpage: *mut zathura_page_t, page: *mut c_void, res: *mut ZathuraResult, ) -> *mut girara_list_t { let res = if let Some(mut r) = NonNull::new(res) { unsafe { r.write(ZathuraResult::Unknown); r.as_mut() } } else { &mut ZathuraResult::OK }; let doc = unsafe { &mut (*(zathura_document_get_data(zathura_page_get_document(zpage)) as *mut TypstDocument)) }; let page: &Page = unsafe { &*(page as *const _) }; let links = FrameItemIterator::new(&page.frame) .filter_map(|(point, item)| { if let FrameItem::Link(dst, size) = item { Some((Rect::from_pos_size(point, *size), dst)) } else { None } }) .map(|(rect, dst)| { let (ty, target) = match dst { Destination::Url(url) => ( if url.starts_with("file://") { zathura_link_type_e_ZATHURA_LINK_GOTO_REMOTE } else { zathura_link_type_e_ZATHURA_LINK_URI }, zathura_link_target_s { destination_type: zathura_link_destination_type_e_ZATHURA_LINK_DESTINATION_UNKNOWN, value: doc .cstring_cache .entry(url.to_string()) .or_insert_with(|| { CString::new(url.as_bytes()).expect("URL shouldn't contain NUL") }) .as_ptr() as *mut i8, page_number: 0, left: -1., right: -1., top: -1., bottom: -1., zoom: 0., }, ), Destination::Position(position) => ( zathura_link_type_e_ZATHURA_LINK_GOTO_DEST, zathura_link_target_s { destination_type: zathura_link_destination_type_e_ZATHURA_LINK_DESTINATION_XYZ, value: ptr::null_mut(), page_number: position.page.get() as u32 - 1, left: position.point.x.to_pt(), right: -1., top: position.point.y.to_pt(), bottom: -1., zoom: 0., }, ), Destination::Location(location) => { let position = doc.doc.introspector().position(*location); ( zathura_link_type_e_ZATHURA_LINK_GOTO_DEST, zathura_link_target_s { destination_type: zathura_link_destination_type_e_ZATHURA_LINK_DESTINATION_XYZ, value: ptr::null_mut(), page_number: position.page.get() as u32 - 1, left: position.point.x.to_pt(), right: -1., top: position.point.y.to_pt(), bottom: -1., zoom: 0., }, ) } }; unsafe { zathura_link_new(ty, rect.into(), target) } }); unsafe { let mut links_ = GiraraList::new_with_free(zathura_link_free); links_.extend_allocated(links); *res = ZathuraResult::OK; links_.into_raw() } } // TODO: render warnings // TODO: better caching