diff --git a/.gitignore b/.gitignore index 93bb4c5..7ae39b5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ /.direnv *.mgc *.typ +*.pdf /result diff --git a/Cargo.lock b/Cargo.lock index 2665834..7772d56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -628,6 +628,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -637,6 +638,15 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -977,6 +987,18 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "hayro-write" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc05d8b4bc878b9aee48d980ecb25ed08f1dd9fad6da5ab4d9b7c56ec03a0cf6" +dependencies = [ + "flate2", + "hayro-syntax", + "log", + "pdf-writer", +] + [[package]] name = "heck" version = "0.5.0" @@ -1278,6 +1300,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" +[[package]] +name = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + [[package]] name = "indexmap" version = "2.12.0" @@ -1290,6 +1318,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" + [[package]] name = "itertools" version = "0.13.0" @@ -1314,6 +1348,52 @@ dependencies = [ "mutate_once", ] +[[package]] +name = "krilla" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "199be5f63da6e19b71051fd5276258a8e55449ac48e2e7492c68238f38ca9f3b" +dependencies = [ + "base64", + "bumpalo", + "comemo", + "flate2", + "float-cmp 0.10.0", + "gif", + "hayro-write", + "image-webp", + "imagesize 0.14.0", + "once_cell", + "pdf-writer", + "png 0.17.16", + "rayon", + "rustc-hash", + "rustybuzz", + "siphasher", + "skrifa", + "smallvec", + "subsetter", + "tiny-skia-path", + "xmp-writer", + "yoke 0.8.0", + "zune-jpeg", +] + +[[package]] +name = "krilla-svg" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3eec075c9507dfdbfb4b9bc3b2aeac074ed422b61bcfd93517616d6b3d19c3" +dependencies = [ + "flate2", + "fontdb", + "krilla", + "png 0.17.16", + "resvg", + "tiny-skia", + "usvg", +] + [[package]] name = "kurbo" version = "0.11.3" @@ -1369,6 +1449,15 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -1657,6 +1746,18 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pdf-writer" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92a79477295a713c2ed425aa82a8b5d20cec3fdee203706cbe6f3854880c1c81" +dependencies = [ + "bitflags 2.10.0", + "itoa", + "memchr", + "ryu", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2315,7 +2416,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" dependencies = [ - "float-cmp", + "float-cmp 0.9.0", ] [[package]] @@ -2339,6 +2440,18 @@ dependencies = [ "syn", ] +[[package]] +name = "subsetter" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6895a12ac5599bb6057362f00e8a3cf1daab4df33f553a55690a44e4fed8d0" +dependencies = [ + "kurbo 0.12.0", + "rustc-hash", + "skrifa", + "write-fonts", +] + [[package]] name = "svgtypes" version = "0.15.3" @@ -2888,6 +3001,32 @@ dependencies = [ "syn", ] +[[package]] +name = "typst-pdf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99160d071220909c1993d7530abe94c0bceffd6e10be7bb629eae2f73d596f26" +dependencies = [ + "az", + "bytemuck", + "comemo", + "ecow", + "image", + "indexmap", + "infer", + "krilla", + "krilla-svg", + "rustc-hash", + "serde", + "smallvec", + "typst-assets", + "typst-library", + "typst-macros", + "typst-syntax", + "typst-timing", + "typst-utils", +] + [[package]] name = "typst-realize" version = "0.14.0" @@ -3158,7 +3297,7 @@ dependencies = [ "data-url", "flate2", "fontdb", - "imagesize", + "imagesize 0.13.0", "kurbo 0.11.3", "log", "pico-args", @@ -3462,6 +3601,19 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "write-fonts" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "886614b5ce857341226aa091f3c285e450683894acaaa7887f366c361efef79d" +dependencies = [ + "font-types", + "indexmap", + "kurbo 0.12.0", + "log", + "read-fonts", +] + [[package]] name = "writeable" version = "0.5.5" @@ -3496,6 +3648,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "xmp-writer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9e2f4a404d9ebffc0a9832cf4f50907220ba3d7fffa9099261a5cab52f2dd7" + [[package]] name = "yaml-rust" version = "0.4.5" @@ -3562,8 +3720,10 @@ dependencies = [ "codespan-reporting", "pkg-config", "system-deps", + "ttf-parser", "typst", "typst-kit", + "typst-pdf", "typst-render", ] @@ -3676,6 +3836,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zlib-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" + [[package]] name = "zune-core" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 485654f..117acbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,8 @@ typst-render = "0.14.0" cairo-rs = "0.21.2" typst-kit = { version = "0.14.0", features = ["embed-fonts", "vendor-openssl"] } codespan-reporting = "0.13.1" +typst-pdf = "0.14.0" +ttf-parser = "0.25.1" [build-dependencies] bindgen = "0.72.1" diff --git a/src/lib.rs b/src/lib.rs index 386a787..4bd1c53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,20 +1,28 @@ use std::{ - ffi::{CStr, CString, OsStr, c_void}, + cmp::{max, min}, + ffi::{CStr, OsStr, c_void}, + fs, iter, mem::MaybeUninit, os::unix::ffi::OsStrExt, path::Path, slice, }; -use ::typst::layout::{Frame, FrameItem, GroupItem, Page, Point, Transform}; +use ::typst::{ + layout::{Abs, FrameItem, Page, Point, Rect}, + text::{Glyph, TextItem}, +}; use cairo::{Format, ImageSurface}; -use crate::zathura::{ - ZathuraResult, cairo_t, girara_list_append, girara_list_new_with_free, 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_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, +use crate::{ + typst::FrameItemIterator, + zathura::{ + ZathuraResult, cairo_t, girara_list_append, girara_list_new_with_free, 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_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; @@ -35,7 +43,7 @@ pub static zathura_plugin_6_7: zathura::zathura_plugin_definition_s = document_open: Some(document_open), document_free: Some(document_free), document_index_generate: None, - document_save_as: None, + document_save_as: Some(document_save_as), document_attachments_get: None, document_attachment_save: None, document_get_information: None, @@ -82,6 +90,24 @@ unsafe extern "C" fn document_free(_: *mut zathura_document_s, data: *mut c_void 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) }; @@ -122,12 +148,14 @@ unsafe extern "C" fn page_render_cairo( 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 + if vscale > wscale { vscale } else { wscale } }; let format = surface.format(); - let pixels_rgba = typst_render::render(typst_page, scale).take(); + 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 @@ -143,10 +171,14 @@ unsafe extern "C" fn page_render_cairo( } 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()); + .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(); @@ -154,26 +186,94 @@ unsafe extern "C" fn page_render_cairo( 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)| { + dbg!(&item.text[glyph.range()]); + 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 mut res_ = ZathuraResult::Unknown; - let res = unsafe { - &mut *(if res.is_null() { &raw mut res_ } else { res } as *mut MaybeUninit) - } - .write(ZathuraResult::Unknown); + let res = if res.is_null() { + &mut ZathuraResult::OK + } else { + unsafe { &mut *(res as *mut MaybeUninit) }.write(ZathuraResult::Unknown) + }; let typst_page: &Page = unsafe { &*(data as *mut _) }; + let rect = Rect::from(rect); let text = FrameItemIterator::new(&typst_page.frame) - .filter(|(Point { x, y }, _)| { - (rect.x1..=rect.x2).contains(&x.to_pt()) && (rect.y1..=rect.y2).contains(&y.to_pt()) - }) - .filter_map(|(_, item)| { + .filter_map(|(point, item)| { if let FrameItem::Text(item) = item { - Some(item.text.to_string()) + 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 } @@ -199,33 +299,26 @@ unsafe extern "C" fn page_get_selection( rect: zathura_rectangle_s, res: *mut ZathuraResult, ) -> *mut girara_list_t { - let mut res_ = ZathuraResult::Unknown; - let res = unsafe { - &mut *(if res.is_null() { &raw mut res_ } else { res } as *mut MaybeUninit) - } - .write(ZathuraResult::Unknown); - let typst_page: &Page = unsafe { &*(data as *mut _) }; - let min = |x, y| if x < y { x } else { y }; - let max = |x, y| if x > y { x } else { y }; - - let rect = zathura_rectangle_s { - x1: min(rect.x1, rect.x2), - y1: min(rect.y1, rect.y2), - x2: max(rect.x1, rect.x2), - y2: max(rect.y1, rect.y2), + let res = if res.is_null() { + &mut ZathuraResult::OK + } else { + unsafe { &mut *(res as *mut MaybeUninit) }.write(ZathuraResult::Unknown) }; + let typst_page: &Page = unsafe { &*(data as *mut _) }; - let rects = FrameItemIterator::new(&typst_page.frame) - .filter(|(Point { x, y }, _)| { - (rect.x1..=rect.x2).contains(&x.to_pt()) && (rect.y1..=rect.y2).contains(&y.to_pt()) - }) - .filter_map(|(_, item)| { - if let FrameItem::Text(item) = item { - Some(item.bbox()) - } else { - None - } - }); + 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 + } + }); unsafe extern "C" fn drop_rect(data: *mut c_void) { drop(unsafe { Box::from_raw(data as *mut zathura_rectangle_s) }); @@ -251,61 +344,7 @@ unsafe extern "C" fn page_get_selection( } } -struct FrameItemIterator<'a> { - transform: Transform, - recur: Option>>, - current: slice::Iter<'a, (Point, FrameItem)>, -} - -impl<'a> FrameItemIterator<'a> { - fn new(root: &'a Frame) -> Self { - Self::new_at(Default::default(), root) - } - - fn new_at(transform: Transform, root: &'a Frame) -> Self { - Self { - transform, - recur: None, - current: root.items(), - } - } -} - -impl<'a> Iterator for FrameItemIterator<'a> { - type Item = (Point, &'a FrameItem); - - fn next(&mut self) -> Option { - if let Some((position, item)) = self.recur.as_mut().and_then(|r| r.next()) { - return Some((position.transform(self.transform), item)); - } - self.recur = None; - let (position, item) = self.current.next()?; - if let FrameItem::Group(GroupItem { - frame, transform, .. - }) = item - { - self.recur = Some(Box::new(Self::new_at( - transform - .invert() - .unwrap() - .post_concat(Transform::translate(position.x, position.y)), - frame, - ))); - } - Some((position.transform(self.transform), item)) - } -} - // TODO: render warnings // TODO: PDF as attachment // TODO: link/... handling // TODO: better caching -// 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 -*/ diff --git a/src/typst.rs b/src/typst.rs index 130446d..5104871 100644 --- a/src/typst.rs +++ b/src/typst.rs @@ -1,10 +1,5 @@ use std::{ - collections::{HashMap, hash_map::Entry}, - hash::Hash, - io::{self, ErrorKind}, - iter, - path::{Path, PathBuf}, - sync::{LazyLock, Mutex}, + collections::{hash_map::Entry, HashMap}, hash::Hash, io::{self, ErrorKind}, iter, path::{Path, PathBuf}, slice, sync::{LazyLock, Mutex} }; use codespan_reporting::{ @@ -13,12 +8,7 @@ use codespan_reporting::{ term::{Config, DisplayStyle, emit_to_write_style, termcolor::Ansi}, }; use typst::{ - Library, LibraryExt, WorldExt, - diag::{FileError, FileResult, SourceDiagnostic, Warned}, - foundations::{Bytes, Datetime}, - syntax::{FileId, Lines, Source, Span, VirtualPath}, - text::{Font, FontBook}, - utils::LazyHash, + diag::{FileError, FileResult, SourceDiagnostic, Warned}, foundations::{Bytes, Datetime}, layout::{Frame, FrameItem, GroupItem, Point, Transform}, syntax::{FileId, Lines, Source, Span, VirtualPath}, text::{Font, FontBook}, utils::LazyHash, Library, LibraryExt, WorldExt }; use typst_kit::{ download::{Downloader, ProgressSink}, @@ -293,3 +283,49 @@ fn diagnostics(world: &World, errors: impl IntoIterator fn label(world: &World, span: Span) -> Option> { Some(Label::primary(span.id()?, world.range(span)?)) } + + +pub struct FrameItemIterator<'a> { + transform: Transform, + recur: Option>>, + current: slice::Iter<'a, (Point, FrameItem)>, +} + +impl<'a> FrameItemIterator<'a> { + pub fn new(root: &'a Frame) -> Self { + Self::new_at(Default::default(), root) + } + + fn new_at(transform: Transform, root: &'a Frame) -> Self { + Self { + transform, + recur: None, + current: root.items(), + } + } +} + +impl<'a> Iterator for FrameItemIterator<'a> { + type Item = (Point, &'a FrameItem); + + fn next(&mut self) -> Option { + if let Some((position, item)) = self.recur.as_mut().and_then(|r| r.next()) { + return Some((position.transform(self.transform), item)); + } + self.recur = None; + let (position, item) = self.current.next()?; + if let FrameItem::Group(GroupItem { + frame, transform, .. + }) = item + { + self.recur = Some(Box::new(Self::new_at( + transform + .invert() + .unwrap() + .post_concat(Transform::translate(position.x, position.y)), + frame, + ))); + } + Some((position.transform(self.transform), item)) + } +} diff --git a/src/zathura.rs b/src/zathura.rs index c65995d..b499719 100644 --- a/src/zathura.rs +++ b/src/zathura.rs @@ -1,5 +1,6 @@ #![allow(warnings)] pub use cairo::ffi::*; +use typst::layout::{Abs, Point, Rect}; include!(concat!(env!("OUT_DIR"), "/bindings.rs")); @@ -21,3 +22,26 @@ pub enum ZathuraResult { /// The provided password is invalid InvalidPassword = 5, } + +impl From for Rect { + fn from(rect: zathura_rectangle_s) -> Self { + fn min(a: f64, b: f64) -> f64 { + if a < b { a } else { b } + } + + fn max(a: f64, b: f64) -> f64 { + if a > b { a } else { b } + } + + Rect::new( + Point::new( + Abs::pt(min(rect.x1, rect.x2)), + Abs::pt(min(rect.y1, rect.y2)), + ), + Point::new( + Abs::pt(max(rect.x1, rect.x2)), + Abs::pt(max(rect.y1, rect.y2)), + ), + ) + } +}