535 lines
18 KiB
Rust
535 lines
18 KiB
Rust
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, Numbering},
|
|
text::{Glyph, TextItem},
|
|
};
|
|
use cairo::{Format, ImageSurface};
|
|
|
|
use crate::{
|
|
typst::FrameItemIterator,
|
|
zathura::{
|
|
GiraraList, ZathuraResult, cairo_t, girara_list_t, zathura_document_get_data,
|
|
zathura_document_get_path, zathura_document_information_entry_list_new,
|
|
zathura_document_information_entry_new, zathura_document_information_entry_t,
|
|
zathura_document_information_type_e_ZATHURA_DOCUMENT_INFORMATION_AUTHOR,
|
|
zathura_document_information_type_e_ZATHURA_DOCUMENT_INFORMATION_CREATION_DATE,
|
|
zathura_document_information_type_e_ZATHURA_DOCUMENT_INFORMATION_KEYWORDS,
|
|
zathura_document_information_type_e_ZATHURA_DOCUMENT_INFORMATION_PRODUCER,
|
|
zathura_document_information_type_e_ZATHURA_DOCUMENT_INFORMATION_TITLE, 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,
|
|
},
|
|
};
|
|
|
|
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: Some(document_get_information),
|
|
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: Some(page_get_label),
|
|
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::<TypstDocument>::from_raw(data as _) });
|
|
}
|
|
ZathuraResult::OK
|
|
}
|
|
|
|
unsafe extern "C" fn document_get_information(
|
|
_: *mut zathura_document_s,
|
|
doc: *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 *(doc as *mut TypstDocument) };
|
|
let i = &doc.doc.info;
|
|
let mut lst = unsafe {
|
|
GiraraList::<zathura_document_information_entry_t>::from_raw(
|
|
zathura_document_information_entry_list_new(),
|
|
)
|
|
};
|
|
[
|
|
(
|
|
zathura_document_information_type_e_ZATHURA_DOCUMENT_INFORMATION_TITLE,
|
|
i.title.clone().unwrap_or_default().to_string(),
|
|
),
|
|
(
|
|
zathura_document_information_type_e_ZATHURA_DOCUMENT_INFORMATION_AUTHOR,
|
|
i.author.join(", "),
|
|
),
|
|
(
|
|
zathura_document_information_type_e_ZATHURA_DOCUMENT_INFORMATION_KEYWORDS,
|
|
i.keywords.join(", "),
|
|
),
|
|
(
|
|
zathura_document_information_type_e_ZATHURA_DOCUMENT_INFORMATION_CREATION_DATE,
|
|
i.date.unwrap_or_default().map_or(String::new(), |d| {
|
|
d.display(Default::default())
|
|
.expect("Defaults do the right thing")
|
|
.to_string()
|
|
}),
|
|
),
|
|
(
|
|
zathura_document_information_type_e_ZATHURA_DOCUMENT_INFORMATION_PRODUCER,
|
|
"Typst (via zathura-plugin-typst)".to_owned(),
|
|
),
|
|
]
|
|
.into_iter()
|
|
.filter(|(_, st)| !st.is_empty())
|
|
.map(|(ty, st)| {
|
|
unsafe extern "C" {
|
|
safe fn calloc(n: usize, s: usize) -> *mut c_void;
|
|
}
|
|
let st_m = calloc(st.len() + 1, 1);
|
|
unsafe { slice::from_raw_parts_mut(st_m as *mut u8, st.len()) }
|
|
.copy_from_slice(st.as_bytes());
|
|
(ty, st_m as *const i8)
|
|
})
|
|
.for_each(|(ty, st)| {
|
|
unsafe { lst.append_allocated(zathura_document_information_entry_new(ty, st)) };
|
|
});
|
|
*res = ZathuraResult::OK;
|
|
lst.into_raw()
|
|
}
|
|
|
|
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<Rect> {
|
|
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<impl Iterator<Item = (Point, Rect, &Glyph)>> {
|
|
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::<String>();
|
|
|
|
*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::<GiraraList<zathura_rectangle_s>>()
|
|
.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()
|
|
}
|
|
}
|
|
|
|
unsafe extern "C" fn page_get_label(
|
|
zpage: *mut zathura_page_t,
|
|
page: *mut c_void,
|
|
out: *mut *mut i8,
|
|
) -> ZathuraResult {
|
|
let doc = unsafe {
|
|
&mut *(zathura_document_get_data(zathura_page_get_document(zpage)) as *mut TypstDocument)
|
|
};
|
|
let page = unsafe { &*(page as *mut Page) };
|
|
let numbering = match page.numbering.clone() {
|
|
Some(Numbering::Pattern(numbering)) => numbering,
|
|
_ => "1".parse().expect("1 is a valid numbering style"),
|
|
};
|
|
|
|
let label = numbering.apply(&[page.number]);
|
|
let label = doc
|
|
.cstring_cache
|
|
.entry(label.to_string())
|
|
.or_insert_with_key(|k| CString::new(k.to_owned()).expect("It's a string"))
|
|
.as_ptr();
|
|
|
|
unsafe { out.write(label as _) };
|
|
ZathuraResult::OK
|
|
}
|
|
|
|
// TODO: render warnings
|
|
// TODO: better caching
|