Add text selection (semi-functional); save as PDF; fix width issues (different vscale/wscale)

This commit is contained in:
bluepython508
2025-11-04 14:03:12 +00:00
parent c023daf180
commit 75754935be
6 changed files with 386 additions and 118 deletions

View File

@@ -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<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)| {
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<ZathuraResult>)
}
.write(ZathuraResult::Unknown);
let res = if res.is_null() {
&mut ZathuraResult::OK
} else {
unsafe { &mut *(res as *mut MaybeUninit<ZathuraResult>) }.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<ZathuraResult>)
}
.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<ZathuraResult>) }.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<Box<FrameItemIterator<'a>>>,
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<Self::Item> {
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
*/

View File

@@ -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<Item = SourceDiagnostic>
fn label(world: &World, span: Span) -> Option<Label<FileId>> {
Some(Label::primary(span.id()?, world.range(span)?))
}
pub struct FrameItemIterator<'a> {
transform: Transform,
recur: Option<Box<FrameItemIterator<'a>>>,
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<Self::Item> {
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))
}
}

View File

@@ -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<zathura_rectangle_s> 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)),
),
)
}
}