Initial Functional Plugin

This commit is contained in:
bluepython508
2025-10-30 11:08:35 +00:00
commit 01474ad6a0
12 changed files with 4155 additions and 0 deletions

167
src/lib.rs Normal file
View File

@@ -0,0 +1,167 @@
use std::{
ffi::{CStr, OsStr, c_void},
os::unix::ffi::OsStrExt,
path::Path,
};
use cairo::{Format, ImageSurface};
use crate::zathura::{
ZathuraResult, cairo_t, zathura_document_get_data, zathura_document_get_path,
zathura_document_s, zathura_document_set_data, zathura_document_set_number_of_pages,
zathura_page_get_document, zathura_page_get_index, zathura_page_set_height,
zathura_page_set_width, zathura_page_t,
};
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,
document_save_as: None,
document_attachments_get: None,
document_attachment_save: None,
document_get_information: None,
page_init: Some(page_init),
page_clear: Some(page_clear),
page_search_text: None,
page_links_get: None,
page_form_fields_get: None,
page_images_get: None,
page_image_get_cairo: None,
page_get_text: None,
page_get_selection: None,
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 Ok(document) = TypstDocument::load(path) else {
return ZathuraResult::InvalidArguments;
};
let document = Box::new(document);
unsafe {
zathura_document_set_number_of_pages(doc, document.doc.pages.len() as _);
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 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 = &mut typst_doc.doc.pages[pageno as usize];
unsafe {
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,
_: *mut c_void,
cairo: *mut cairo_t,
_printing: bool,
) -> 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 = &mut typst_doc.doc.pages[pageno as usize];
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 wscale < vscale { wscale } else { vscale } // min, but floats aren't Ord
};
let format = surface.format();
let pixels_rgba = typst_render::render(typst_page, scale).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
.iter()
.zip(buf.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
}
// TODO: handle compile errors
// TODO: render warnings
// TODO: text/link/... handling
// TODO: nixify compilation of magicdb (file -Cm ~/.magic:/nix/store/vi7ya34k19nid2m0dmkljqip5572g0bi-file-5.45/share/misc/magic.mgc)
/*
0 string/cW /*\ doc:\ typst\ */ typst text document
!:ext typ
!:mime text/vnd.typst
0 string #import typst text document
!:ext typ
!:mime text/vnd.typst
*/

147
src/typst.rs Normal file
View File

@@ -0,0 +1,147 @@
use std::{
collections::{HashMap, hash_map::Entry},
hash::Hash,
io::{self, ErrorKind},
path::{Path, PathBuf},
sync::{LazyLock, Mutex},
};
use typst::{
Library, LibraryExt,
diag::{FileError, FileResult, SourceDiagnostic, Warned},
foundations::{Bytes, Datetime},
syntax::{FileId, Source, Span, VirtualPath},
text::{Font, FontBook},
utils::LazyHash,
};
use typst_kit::{
download::{Downloader, ProgressSink},
fonts::{FontSearcher, Fonts},
package::PackageStorage,
};
static PACKAGE_SOURCE: LazyLock<Mutex<PackageStorage>> = LazyLock::new(|| {
Mutex::new(PackageStorage::new(
None,
None,
Downloader::new("zathura-typst-plugin v0.1.0"),
))
});
static FONTS: LazyLock<Fonts> = LazyLock::new(|| FontSearcher::new().search());
struct World {
root: PathBuf,
main: FileId,
source_cache: Mutex<HashMap<FileId, Source>>,
file_cache: Mutex<HashMap<FileId, Bytes>>,
}
fn map_or_insert_with_err<K: Hash + Eq, V, E>(
map: &mut HashMap<K, V>,
key: K,
f: impl FnOnce() -> Result<V, E>,
) -> Result<&mut V, E> {
match map.entry(key) {
Entry::Occupied(entry) => Ok(entry.into_mut()),
Entry::Vacant(entry) => Ok(entry.insert(f()?)),
}
}
impl typst::World for World {
fn library(&self) -> &LazyHash<Library> {
static LIBRARY: LazyLock<LazyHash<Library>> =
LazyLock::new(|| LazyHash::new(Library::default()));
&LIBRARY
}
fn book(&self) -> &LazyHash<FontBook> {
static BOOK: LazyLock<LazyHash<FontBook>> =
LazyLock::new(|| LazyHash::new(FONTS.book.clone()));
&BOOK
}
fn main(&self) -> FileId {
self.main
}
fn source(&self, id: FileId) -> FileResult<Source> {
map_or_insert_with_err(&mut self.source_cache.lock().unwrap(), id, || {
Ok(Source::new(id, self.file(id)?.to_str()?.to_string()))
})
.cloned()
}
fn file(&self, id: FileId) -> FileResult<Bytes> {
map_or_insert_with_err(&mut self.file_cache.lock().unwrap(), id, || {
let root = if let Some(package) = id.package() {
&PACKAGE_SOURCE
.lock()
.unwrap()
.prepare_package(package, &mut ProgressSink)?
} else {
&self.root
};
let path = id
.vpath()
.resolve(root)
.ok_or_else(|| FileError::AccessDenied)?;
Ok(Bytes::new(
std::fs::read(&path).map_err(|e| FileError::from_io(e, &path))?,
))
})
.cloned()
}
fn font(&self, index: usize) -> Option<Font> {
FONTS.fonts.get(index)?.get()
}
fn today(&self, _offset: Option<i64>) -> Option<Datetime> {
None
}
}
impl World {
fn new(file: &Path) -> Result<Self, io::Error> {
let root = file
.canonicalize()?
.parent()
.ok_or(ErrorKind::NotFound)?
.to_owned();
Ok(Self {
main: FileId::new(
None,
VirtualPath::within_root(file, &root)
.expect("Root should contain path by construction"),
),
root: root.to_path_buf(),
source_cache: Default::default(),
file_cache: Default::default(),
})
}
}
pub struct TypstDocument {
pub doc: typst::layout::PagedDocument,
pub warnings: Vec<SourceDiagnostic>,
}
impl TypstDocument {
pub fn load(path: &Path) -> Result<Self, Vec<SourceDiagnostic>> {
let Warned { output, warnings } = typst::compile(
&World::new(path)
.map_err(|e| vec![SourceDiagnostic::error(Span::detached(), e.to_string())])?,
);
match output {
Ok(doc) => Ok(Self {
doc,
warnings: warnings.to_vec(),
}),
Err(mut errors) => {
errors.extend(warnings);
Err(errors.to_vec())
}
}
}
}

23
src/zathura.rs Normal file
View File

@@ -0,0 +1,23 @@
#![allow(warnings)]
pub use cairo::ffi::*;
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
unsafe impl Sync for zathura_plugin_definition_s {}
pub type zathura_plugin_error_e = ZathuraResult;
#[repr(u32)]
pub enum ZathuraResult {
/// No error occurred
OK = 0,
/// An unknown error occurred
Unknown = 1,
/// Out of memory
OutOfMemory = 2,
/// The called function has not been implemented
NotImplemented = 3,
/// Invalid arguments have been passed
InvalidArguments = 4,
/// The provided password is invalid
InvalidPassword = 5,
}