Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,135 @@
|
||||
//! The shim's text and object calls (PDF-03, PDF-04).
|
||||
|
||||
use std::{
|
||||
ffi::{c_char, c_int},
|
||||
io, ptr,
|
||||
};
|
||||
|
||||
use crate::ffi::{Context, Document, Mupdf, Open, checked, page, text};
|
||||
|
||||
unsafe extern "C" {
|
||||
fn ely_text_blocks(ctx: *mut Context, doc: *mut Document, page: c_int, data: *mut *mut u8, len: *mut usize) -> c_int;
|
||||
fn ely_objects(ctx: *mut Context, doc: *mut Document, page: c_int, data: *mut *mut u8, len: *mut usize) -> c_int;
|
||||
fn ely_put_text(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
page: c_int,
|
||||
block: c_int,
|
||||
expected: *const c_char,
|
||||
text: *const c_char,
|
||||
font: *const c_char,
|
||||
size: f32,
|
||||
rgb: c_int,
|
||||
bx: *const f32,
|
||||
password: *const c_char,
|
||||
scale: f32,
|
||||
missing: *mut c_int,
|
||||
grew: *mut c_int,
|
||||
pixels: *mut *mut u8,
|
||||
w: *mut c_int,
|
||||
h: *mut c_int,
|
||||
) -> c_int;
|
||||
fn ely_place(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
page: c_int,
|
||||
index: c_int,
|
||||
t: *const f32,
|
||||
drop: c_int,
|
||||
path: *const c_char,
|
||||
) -> c_int;
|
||||
fn ely_add_image(ctx: *mut Context, doc: *mut Document, page: c_int, path: *const c_char, rect: *const f32) -> c_int;
|
||||
}
|
||||
|
||||
/// New text and where it goes.
|
||||
pub struct Writing {
|
||||
/// The block replaced, or none for new text.
|
||||
pub block: Option<usize>,
|
||||
pub expected: String,
|
||||
pub text: String,
|
||||
pub font: String,
|
||||
pub size: f32,
|
||||
pub rgb: u32,
|
||||
/// Page space: left, top, width.
|
||||
pub bx: [f32; 3],
|
||||
}
|
||||
|
||||
/// What a text write did.
|
||||
pub enum Wrote {
|
||||
Done,
|
||||
/// A character the font cannot draw; nothing changed.
|
||||
Missing(char),
|
||||
/// The would-be result: BGRA, size, and whether it grew.
|
||||
Preview(Vec<u8>, (u32, u32), bool),
|
||||
}
|
||||
|
||||
fn number(value: usize) -> io::Result<c_int> {
|
||||
c_int::try_from(value).map_err(|_| io::Error::other("a number too large"))
|
||||
}
|
||||
|
||||
impl Mupdf {
|
||||
pub fn text_blocks(&self, doc: &Open, at: usize) -> io::Result<Vec<u8>> {
|
||||
let at = page(at)?;
|
||||
self.owned(|data, len| unsafe { ely_text_blocks(self.ctx, doc.0, at, data, len) })
|
||||
}
|
||||
|
||||
pub fn objects(&self, doc: &Open, at: usize) -> io::Result<Vec<u8>> {
|
||||
let at = page(at)?;
|
||||
self.owned(|data, len| unsafe { ely_objects(self.ctx, doc.0, at, data, len) })
|
||||
}
|
||||
|
||||
/// Writes `w` on page `at`; a positive `scale` only previews.
|
||||
pub fn put_text(&self, doc: &Open, at: usize, w: &Writing, password: &str, scale: f32) -> io::Result<Wrote> {
|
||||
let block = w.block.map_or(Ok(-1), number)?;
|
||||
let (expected, words, font) = (text(&w.expected)?, text(&w.text)?, text(&w.font)?);
|
||||
let password = text(password)?;
|
||||
let rgb = c_int::try_from(w.rgb & 0xff_ffff).map_err(io::Error::other)?;
|
||||
let (mut missing, mut grew, mut pixels, mut width, mut height) = (0, 0, ptr::null_mut(), 0, 0);
|
||||
let status = checked(unsafe {
|
||||
ely_put_text(
|
||||
self.ctx,
|
||||
doc.0,
|
||||
page(at)?,
|
||||
block,
|
||||
expected.as_ptr(),
|
||||
words.as_ptr(),
|
||||
font.as_ptr(),
|
||||
w.size,
|
||||
rgb,
|
||||
w.bx.as_ptr(),
|
||||
password.as_ptr(),
|
||||
scale,
|
||||
&mut missing,
|
||||
&mut grew,
|
||||
&mut pixels,
|
||||
&mut width,
|
||||
&mut height,
|
||||
)
|
||||
})?;
|
||||
if status == 2 {
|
||||
let missing = u32::try_from(missing).ok().and_then(char::from_u32);
|
||||
return Ok(Wrote::Missing(missing.ok_or_else(|| io::Error::other("a missing glyph out of range"))?));
|
||||
}
|
||||
if scale <= 0.0 {
|
||||
return Ok(Wrote::Done);
|
||||
}
|
||||
let size = (u32::try_from(width).map_err(io::Error::other)?, u32::try_from(height).map_err(io::Error::other)?);
|
||||
let bytes = unsafe { std::slice::from_raw_parts(pixels, size.0 as usize * size.1 as usize * 4) }.to_vec();
|
||||
self.free(pixels);
|
||||
Ok(Wrote::Preview(bytes, size, grew != 0))
|
||||
}
|
||||
|
||||
/// Moves draw `index` by page-space `t`, swaps or drops it.
|
||||
pub fn place(&self, doc: &Open, at: usize, index: usize, t: [f32; 6], drop: bool, swap: Option<&str>) -> io::Result<()> {
|
||||
let swap = swap.map(text).transpose()?;
|
||||
let path = swap.as_ref().map_or(ptr::null(), |path| path.as_ptr());
|
||||
let index = number(index)?;
|
||||
checked(unsafe { ely_place(self.ctx, doc.0, page(at)?, index, t.as_ptr(), c_int::from(drop), path) }).map(|_| ())
|
||||
}
|
||||
|
||||
/// Draws the image at `path` into page-space `rect`.
|
||||
pub fn add_image(&self, doc: &Open, at: usize, path: &str, rect: [f32; 4]) -> io::Result<()> {
|
||||
let path = text(path)?;
|
||||
checked(unsafe { ely_add_image(self.ctx, doc.0, page(at)?, path.as_ptr(), rect.as_ptr()) }).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
//! The shim's page edits, undo and saves.
|
||||
|
||||
use std::{
|
||||
ffi::{c_char, c_int},
|
||||
io,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use crate::ffi::{Context, Document, Mupdf, Open, checked, page, text};
|
||||
|
||||
unsafe extern "C" {
|
||||
fn ely_journal(ctx: *mut Context, doc: *mut Document) -> c_int;
|
||||
fn ely_undo(ctx: *mut Context, doc: *mut Document, redo: c_int) -> c_int;
|
||||
fn ely_state(ctx: *mut Context, doc: *mut Document, at: *mut c_int, count: *mut c_int) -> c_int;
|
||||
fn ely_delete_pages(ctx: *mut Context, doc: *mut Document, pages: *const c_int, n: c_int) -> c_int;
|
||||
fn ely_move_pages(ctx: *mut Context, doc: *mut Document, pages: *const c_int, n: c_int, to: c_int) -> c_int;
|
||||
fn ely_rotate_pages(ctx: *mut Context, doc: *mut Document, pages: *const c_int, n: c_int, by: c_int) -> c_int;
|
||||
fn ely_crop_pages(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
pages: *const c_int,
|
||||
n: c_int,
|
||||
margins: *const f32,
|
||||
) -> c_int;
|
||||
fn ely_insert_blank(ctx: *mut Context, doc: *mut Document, at: c_int, w: f32, h: f32) -> c_int;
|
||||
fn ely_insert_file(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
at: c_int,
|
||||
path: *const c_char,
|
||||
count: *mut c_int,
|
||||
) -> c_int;
|
||||
fn ely_save(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
raw: *const c_char,
|
||||
out: *const c_char,
|
||||
password: *const c_char,
|
||||
) -> c_int;
|
||||
fn ely_extract(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
pages: *const c_int,
|
||||
n: c_int,
|
||||
raw: *const c_char,
|
||||
out: *const c_char,
|
||||
password: *const c_char,
|
||||
) -> c_int;
|
||||
}
|
||||
|
||||
/// Page numbers as the shim takes them.
|
||||
fn numbers(pages: &[usize]) -> io::Result<(Vec<c_int>, c_int)> {
|
||||
let pages = pages.iter().map(|at| page(*at)).collect::<io::Result<Vec<_>>>()?;
|
||||
let n = c_int::try_from(pages.len()).map_err(|_| io::Error::other("too many pages"))?;
|
||||
Ok((pages, n))
|
||||
}
|
||||
|
||||
/// The scratch copy a save writes first, removed after.
|
||||
pub(crate) fn raw(out: &Path) -> std::path::PathBuf {
|
||||
let mut name = out.as_os_str().to_owned();
|
||||
name.push(".raw");
|
||||
name.into()
|
||||
}
|
||||
|
||||
pub(crate) fn utf8(path: &Path) -> io::Result<&str> {
|
||||
path.to_str().ok_or_else(|| io::Error::other("a path that is not UTF-8"))
|
||||
}
|
||||
|
||||
impl Mupdf {
|
||||
/// Starts the undo history; every edit becomes a step.
|
||||
pub fn journal(&self, doc: &Open) -> io::Result<()> {
|
||||
checked(unsafe { ely_journal(self.ctx, doc.0) }).map(|_| ())
|
||||
}
|
||||
|
||||
/// MuPDF's undo position and step count.
|
||||
pub fn state(&self, doc: &Open) -> io::Result<(usize, usize)> {
|
||||
let (mut at, mut count) = (0, 0);
|
||||
checked(unsafe { ely_state(self.ctx, doc.0, &mut at, &mut count) })?;
|
||||
let size = |n: c_int| usize::try_from(n).map_err(io::Error::other);
|
||||
Ok((size(at)?, size(count)?))
|
||||
}
|
||||
|
||||
pub fn undo(&self, doc: &Open, redo: bool) -> io::Result<()> {
|
||||
checked(unsafe { ely_undo(self.ctx, doc.0, c_int::from(redo)) }).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn delete_pages(&self, doc: &Open, pages: &[usize]) -> io::Result<()> {
|
||||
let (pages, n) = numbers(pages)?;
|
||||
checked(unsafe { ely_delete_pages(self.ctx, doc.0, pages.as_ptr(), n) }).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn move_pages(&self, doc: &Open, pages: &[usize], to: usize) -> io::Result<()> {
|
||||
let (pages, n) = numbers(pages)?;
|
||||
checked(unsafe { ely_move_pages(self.ctx, doc.0, pages.as_ptr(), n, page(to)?) }).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn rotate_pages(&self, doc: &Open, pages: &[usize], by: i32) -> io::Result<()> {
|
||||
let (pages, n) = numbers(pages)?;
|
||||
checked(unsafe { ely_rotate_pages(self.ctx, doc.0, pages.as_ptr(), n, by) }).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn crop_pages(&self, doc: &Open, pages: &[usize], margins: [f32; 4]) -> io::Result<()> {
|
||||
let (pages, n) = numbers(pages)?;
|
||||
checked(unsafe { ely_crop_pages(self.ctx, doc.0, pages.as_ptr(), n, margins.as_ptr()) }).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn insert_blank(&self, doc: &Open, at: usize, (w, h): (f32, f32)) -> io::Result<()> {
|
||||
checked(unsafe { ely_insert_blank(self.ctx, doc.0, page(at)?, w, h) }).map(|_| ())
|
||||
}
|
||||
|
||||
/// Inserts the pages of `path`; how many came.
|
||||
pub fn insert_file(&self, doc: &Open, at: usize, path: &str) -> io::Result<usize> {
|
||||
let (path, mut count) = (text(path)?, 0);
|
||||
checked(unsafe { ely_insert_file(self.ctx, doc.0, page(at)?, path.as_ptr(), &mut count) })?;
|
||||
usize::try_from(count).map_err(io::Error::other)
|
||||
}
|
||||
|
||||
/// Writes a compacted copy to `out`.
|
||||
pub fn save(&self, doc: &Open, out: &Path, password: &str) -> io::Result<()> {
|
||||
let scratch = raw(out);
|
||||
let (raw_c, out_c, password) = (text(utf8(&scratch)?)?, text(utf8(out)?)?, text(password)?);
|
||||
let saved = checked(unsafe { ely_save(self.ctx, doc.0, raw_c.as_ptr(), out_c.as_ptr(), password.as_ptr()) });
|
||||
clean(&scratch);
|
||||
saved.map(|_| ())
|
||||
}
|
||||
|
||||
/// Writes only `pages` to `out`; the document stays.
|
||||
pub fn extract(&self, doc: &Open, pages: &[usize], out: &Path, password: &str) -> io::Result<()> {
|
||||
let (pages, n) = numbers(pages)?;
|
||||
let scratch = raw(out);
|
||||
let (raw_c, out_c, password) = (text(utf8(&scratch)?)?, text(utf8(out)?)?, text(password)?);
|
||||
let done = checked(unsafe {
|
||||
ely_extract(self.ctx, doc.0, pages.as_ptr(), n, raw_c.as_ptr(), out_c.as_ptr(), password.as_ptr())
|
||||
});
|
||||
clean(&scratch);
|
||||
done.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes a scratch copy; one never written is fine.
|
||||
pub(crate) fn clean(scratch: &Path) {
|
||||
match std::fs::remove_file(scratch) {
|
||||
Ok(()) => {}
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
|
||||
Err(error) => log::warn!("pdf-worker: the scratch copy stays: {error}"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
//! The C shim's calls; each failure leaves a message behind.
|
||||
|
||||
use std::{
|
||||
ffi::{CStr, CString, c_char, c_int, c_void},
|
||||
io, ptr,
|
||||
};
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Context {
|
||||
_private: [u8; 0],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct Document {
|
||||
_private: [u8; 0],
|
||||
}
|
||||
|
||||
unsafe extern "C" {
|
||||
fn ely_error() -> *const c_char;
|
||||
fn ely_init() -> *mut Context;
|
||||
fn ely_open(ctx: *mut Context, path: *const c_char, out: *mut *mut Document) -> c_int;
|
||||
fn ely_close(ctx: *mut Context, doc: *mut Document);
|
||||
fn ely_needs_password(ctx: *mut Context, doc: *mut Document) -> c_int;
|
||||
fn ely_authenticate(ctx: *mut Context, doc: *mut Document, password: *const c_char) -> c_int;
|
||||
fn ely_count_pages(ctx: *mut Context, doc: *mut Document, out: *mut c_int) -> c_int;
|
||||
fn ely_page_bounds(ctx: *mut Context, doc: *mut Document, page: c_int, out: *mut f32) -> c_int;
|
||||
fn ely_render(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
page: c_int,
|
||||
scale: f32,
|
||||
rotate: c_int,
|
||||
tile: *const c_int,
|
||||
dark: c_int,
|
||||
bgra: *mut u8,
|
||||
) -> c_int;
|
||||
fn ely_text(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
page: c_int,
|
||||
data: *mut *mut u8,
|
||||
len: *mut usize,
|
||||
) -> c_int;
|
||||
fn ely_search(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
page: c_int,
|
||||
needle: *const c_char,
|
||||
data: *mut *mut u8,
|
||||
len: *mut usize,
|
||||
) -> c_int;
|
||||
fn ely_outline(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
data: *mut *mut u8,
|
||||
len: *mut usize,
|
||||
) -> c_int;
|
||||
fn ely_free(ctx: *mut Context, data: *mut c_void);
|
||||
fn ely_script_font(data: *const u8, len: usize);
|
||||
}
|
||||
|
||||
/// Great Vibes (OFL), for typed signatures.
|
||||
static SCRIPT: &[u8] = include_bytes!("../../../fonts/GreatVibes-Regular.ttf");
|
||||
|
||||
/// Hands MuPDF the script face, alive while the worker is.
|
||||
pub fn script_font() {
|
||||
unsafe { ely_script_font(SCRIPT.as_ptr(), SCRIPT.len()) }
|
||||
}
|
||||
|
||||
fn error() -> io::Error {
|
||||
let text = unsafe { CStr::from_ptr(ely_error()) };
|
||||
io::Error::other(text.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
pub(crate) fn checked(code: c_int) -> io::Result<c_int> {
|
||||
match code {
|
||||
-1 => Err(error()),
|
||||
other => Ok(other),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn text(value: &str) -> io::Result<CString> {
|
||||
CString::new(value).map_err(|_| io::Error::other("text with a NUL byte"))
|
||||
}
|
||||
|
||||
pub(crate) fn page(number: usize) -> io::Result<c_int> {
|
||||
c_int::try_from(number).map_err(|_| io::Error::other("a page number too large"))
|
||||
}
|
||||
|
||||
/// The one MuPDF context; the worker is single threaded.
|
||||
pub struct Mupdf {
|
||||
pub(crate) ctx: *mut Context,
|
||||
}
|
||||
|
||||
/// An open document, closed by `Mupdf::close`.
|
||||
pub struct Open(pub(crate) *mut Document);
|
||||
|
||||
impl Mupdf {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
let ctx = unsafe { ely_init() };
|
||||
match ctx.is_null() {
|
||||
true => Err(error()),
|
||||
false => Ok(Mupdf { ctx }),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open(&self, path: &str) -> io::Result<Open> {
|
||||
let path = text(path)?;
|
||||
let mut doc = ptr::null_mut();
|
||||
checked(unsafe { ely_open(self.ctx, path.as_ptr(), &mut doc) })?;
|
||||
Ok(Open(doc))
|
||||
}
|
||||
|
||||
pub fn close(&self, doc: Open) {
|
||||
unsafe { ely_close(self.ctx, doc.0) }
|
||||
}
|
||||
|
||||
pub fn locked(&self, doc: &Open) -> io::Result<bool> {
|
||||
checked(unsafe { ely_needs_password(self.ctx, doc.0) }).map(|locked| locked != 0)
|
||||
}
|
||||
|
||||
/// MuPDF's access bits; 0 is refused, 4 the owner.
|
||||
pub fn unlock(&self, doc: &Open, password: &str) -> io::Result<u32> {
|
||||
let password = text(password)?;
|
||||
checked(unsafe { ely_authenticate(self.ctx, doc.0, password.as_ptr()) }).map(|access| access as u32)
|
||||
}
|
||||
|
||||
pub fn pages(&self, doc: &Open) -> io::Result<usize> {
|
||||
let mut count = 0;
|
||||
checked(unsafe { ely_count_pages(self.ctx, doc.0, &mut count) })?;
|
||||
usize::try_from(count).map_err(io::Error::other)
|
||||
}
|
||||
|
||||
pub fn bounds(&self, doc: &Open, number: usize) -> io::Result<[f32; 4]> {
|
||||
let mut out = [0f32; 4];
|
||||
checked(unsafe { ely_page_bounds(self.ctx, doc.0, page(number)?, out.as_mut_ptr()) })?;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&self,
|
||||
doc: &Open,
|
||||
number: usize,
|
||||
(scale, rotate): (f32, i32),
|
||||
tile: [i32; 4],
|
||||
dark: bool,
|
||||
) -> io::Result<Vec<u8>> {
|
||||
let (w, h) = (usize::try_from(tile[2]), usize::try_from(tile[3]));
|
||||
let (Ok(w), Ok(h)) = (w, h) else {
|
||||
return Err(io::Error::other("a tile of negative size"));
|
||||
};
|
||||
let mut pixels = vec![0u8; w * h * 4];
|
||||
let code = unsafe {
|
||||
ely_render(
|
||||
self.ctx,
|
||||
doc.0,
|
||||
page(number)?,
|
||||
scale,
|
||||
rotate,
|
||||
tile.as_ptr(),
|
||||
c_int::from(dark),
|
||||
pixels.as_mut_ptr(),
|
||||
)
|
||||
};
|
||||
checked(code)?;
|
||||
Ok(pixels)
|
||||
}
|
||||
|
||||
/// Frees memory the shim handed over.
|
||||
pub(crate) fn free(&self, data: *mut u8) {
|
||||
unsafe { ely_free(self.ctx, data.cast()) }
|
||||
}
|
||||
|
||||
/// Takes a buffer the shim made, then frees it.
|
||||
pub(crate) fn owned(&self, call: impl FnOnce(*mut *mut u8, *mut usize) -> c_int) -> io::Result<Vec<u8>> {
|
||||
let (mut data, mut len) = (ptr::null_mut(), 0usize);
|
||||
checked(call(&mut data, &mut len))?;
|
||||
let bytes = match data.is_null() {
|
||||
true => Vec::new(),
|
||||
false => unsafe { std::slice::from_raw_parts(data, len) }.to_vec(),
|
||||
};
|
||||
unsafe { ely_free(self.ctx, data.cast()) };
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub fn text(&self, doc: &Open, number: usize) -> io::Result<Vec<u8>> {
|
||||
let number = page(number)?;
|
||||
self.owned(|data, len| unsafe { ely_text(self.ctx, doc.0, number, data, len) })
|
||||
}
|
||||
|
||||
pub fn outline(&self, doc: &Open) -> io::Result<Vec<u8>> {
|
||||
self.owned(|data, len| unsafe { ely_outline(self.ctx, doc.0, data, len) })
|
||||
}
|
||||
|
||||
/// Each hit as its quad count, then its quads.
|
||||
pub fn search(&self, doc: &Open, number: usize, needle: &str) -> io::Result<Vec<u8>> {
|
||||
let (needle, number) = (text(needle)?, page(number)?);
|
||||
self.owned(|data, len| unsafe { ely_search(self.ctx, doc.0, number, needle.as_ptr(), data, len) })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
//! The PDF worker: MuPDF behind framed stdin and stdout.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{self, BufReader, BufWriter},
|
||||
};
|
||||
|
||||
use ely_ipc::{Pack, Unpack, read_frame, write_frame};
|
||||
use ely_pdf_engine::proto::{may, tag};
|
||||
|
||||
mod content;
|
||||
mod edit;
|
||||
mod ffi;
|
||||
mod marks;
|
||||
mod ocr;
|
||||
mod output;
|
||||
mod redact;
|
||||
mod secure;
|
||||
mod sign;
|
||||
mod structure;
|
||||
|
||||
use ffi::{Mupdf, Open};
|
||||
|
||||
/// An open document, its password and undo steps.
|
||||
struct Doc {
|
||||
open: Open,
|
||||
/// Kept in memory only, to reopen saved copies.
|
||||
password: String,
|
||||
/// Opened with the owner password, or unprotected.
|
||||
owner: bool,
|
||||
/// What MuPDF warned at open; kept as the repair report.
|
||||
warnings: String,
|
||||
repair: Option<String>,
|
||||
/// The repaired copy standing in; removed on close.
|
||||
copy: Option<std::path::PathBuf>,
|
||||
/// MuPDF's steps: an edit's id, or none for a save.
|
||||
steps: Vec<Option<u64>>,
|
||||
/// Steps done, as MuPDF counts them.
|
||||
at: usize,
|
||||
}
|
||||
|
||||
impl Doc {
|
||||
/// Equal marks, equal documents.
|
||||
fn mark(&self) -> String {
|
||||
self.steps[..self.at].iter().rev().flatten().next().map_or_else(|| "0".into(), u64::to_string)
|
||||
}
|
||||
|
||||
/// Matches MuPDF's steps; a new last step is `made`.
|
||||
fn sync(&mut self, mupdf: &Mupdf, made: Option<u64>) -> io::Result<()> {
|
||||
let (at, count) = mupdf.state(&self.open)?;
|
||||
if at > self.at {
|
||||
self.steps.truncate(self.at);
|
||||
self.steps.extend((self.at..at).map(|step| made.filter(|_| step + 1 == at)));
|
||||
}
|
||||
// Steps MuPDF keeps but no edit made are never redone.
|
||||
self.steps.resize(count, None);
|
||||
self.at = at;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Undoes or redoes MuPDF steps until one edit is passed.
|
||||
fn step(&mut self, mupdf: &Mupdf, redo: bool) -> io::Result<String> {
|
||||
let able = match redo {
|
||||
true => self.steps[self.at..].iter().any(Option::is_some),
|
||||
false => self.steps[..self.at].iter().any(Option::is_some),
|
||||
};
|
||||
if !able {
|
||||
return Err(io::Error::other(format!("nothing to {}", if redo { "redo" } else { "undo" })));
|
||||
}
|
||||
loop {
|
||||
mupdf.undo(&self.open, redo)?;
|
||||
let passed = match redo {
|
||||
true => {
|
||||
self.at += 1;
|
||||
self.steps[self.at - 1]
|
||||
}
|
||||
false => {
|
||||
self.at -= 1;
|
||||
self.steps[self.at]
|
||||
}
|
||||
};
|
||||
if passed.is_some() {
|
||||
return Ok(self.mark());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Worker {
|
||||
mupdf: Mupdf,
|
||||
docs: HashMap<u64, Doc>,
|
||||
next: u64,
|
||||
/// Edits made so far; each step's id.
|
||||
edits: u64,
|
||||
}
|
||||
|
||||
/// Sorted distinct page numbers.
|
||||
fn pages(ask: &mut Unpack) -> io::Result<Vec<usize>> {
|
||||
let count = ask.u64()? as usize;
|
||||
(0..count).map(|_| Ok(ask.u64()? as usize)).collect()
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
fn doc(&self, id: u64) -> io::Result<&Open> {
|
||||
Ok(&self.record(id)?.open)
|
||||
}
|
||||
|
||||
fn record(&self, id: u64) -> io::Result<&Doc> {
|
||||
self.docs
|
||||
.get(&id)
|
||||
.ok_or_else(|| io::Error::other(format!("no document {id}")))
|
||||
}
|
||||
|
||||
/// Runs one edit on `id`; it becomes the next step.
|
||||
fn edited(&mut self, id: u64, edit: impl FnOnce(&Mupdf, &Open) -> io::Result<()>) -> io::Result<String> {
|
||||
let doc = self.docs.get_mut(&id).ok_or_else(|| io::Error::other(format!("no document {id}")))?;
|
||||
if let Err(error) = edit(&self.mupdf, &doc.open) {
|
||||
// A failed edit may have dropped MuPDF's redo steps.
|
||||
doc.sync(&self.mupdf, None)?;
|
||||
return Err(error);
|
||||
}
|
||||
self.edits += 1;
|
||||
doc.sync(&self.mupdf, Some(self.edits))?;
|
||||
Ok(doc.mark())
|
||||
}
|
||||
|
||||
/// Writes or previews text; a missing glyph changes nothing.
|
||||
fn put_text(&mut self, id: u64, ask: &mut Unpack) -> io::Result<Vec<u8>> {
|
||||
let at = ask.u64()? as usize;
|
||||
let block = usize::try_from(ask.i64()?).ok();
|
||||
let (expected, text, font) = (ask.str()?, ask.str()?, ask.str()?);
|
||||
let size = f32::from_bits(ask.u64()? as u32);
|
||||
let rgb = ask.u64()? as u32;
|
||||
let mut bx = [0f32; 3];
|
||||
for value in &mut bx {
|
||||
*value = f32::from_bits(ask.u64()? as u32);
|
||||
}
|
||||
let scale = f32::from_bits(ask.u64()? as u32);
|
||||
let writing = content::Writing { block, expected, text, font, size, rgb, bx };
|
||||
let doc = self.docs.get_mut(&id).ok_or_else(|| io::Error::other(format!("no document {id}")))?;
|
||||
let wrote = self.mupdf.put_text(&doc.open, at, &writing, &doc.password, scale);
|
||||
if matches!(wrote, Ok(content::Wrote::Done)) {
|
||||
self.edits += 1;
|
||||
}
|
||||
let made = matches!(wrote, Ok(content::Wrote::Done)).then_some(self.edits);
|
||||
doc.sync(&self.mupdf, made)?;
|
||||
Ok(match wrote? {
|
||||
content::Wrote::Done => Pack::new().u64(0).str(&doc.mark()).bytes(),
|
||||
content::Wrote::Missing(c) => Pack::new().u64(2).u64(u64::from(c)).bytes(),
|
||||
content::Wrote::Preview(pixels, (w, h), grew) => {
|
||||
Pack::new().u64(1).u64(u64::from(w)).u64(u64::from(h)).u64(u64::from(grew)).blob(&pixels).bytes()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn edit(&mut self, kind: u8, ask: &mut Unpack) -> io::Result<Vec<u8>> {
|
||||
let id = ask.u64()?;
|
||||
self.guard(id, kind)?;
|
||||
let mark = match kind {
|
||||
tag::HISTORY => self.record(id)?.mark(),
|
||||
tag::UNDO => {
|
||||
let redo = ask.u64()? != 0;
|
||||
let doc = self.docs.get_mut(&id).ok_or_else(|| io::Error::other(format!("no document {id}")))?;
|
||||
doc.step(&self.mupdf, redo)?
|
||||
}
|
||||
tag::DELETE => {
|
||||
let pages = pages(ask)?;
|
||||
self.edited(id, |mupdf, doc| mupdf.delete_pages(doc, &pages))?
|
||||
}
|
||||
tag::MOVE => {
|
||||
let (pages, to) = (pages(ask)?, ask.u64()? as usize);
|
||||
self.edited(id, |mupdf, doc| mupdf.move_pages(doc, &pages, to))?
|
||||
}
|
||||
tag::ROTATE => {
|
||||
let (pages, by) = (pages(ask)?, i32::try_from(ask.i64()?).map_err(io::Error::other)?);
|
||||
self.edited(id, |mupdf, doc| mupdf.rotate_pages(doc, &pages, by))?
|
||||
}
|
||||
tag::CROP => {
|
||||
let pages = pages(ask)?;
|
||||
let mut margins = [0f32; 4];
|
||||
for side in &mut margins {
|
||||
*side = f32::from_bits(ask.u64()? as u32);
|
||||
}
|
||||
self.edited(id, |mupdf, doc| mupdf.crop_pages(doc, &pages, margins))?
|
||||
}
|
||||
tag::BLANK => {
|
||||
let at = ask.u64()? as usize;
|
||||
let size = (f32::from_bits(ask.u64()? as u32), f32::from_bits(ask.u64()? as u32));
|
||||
self.edited(id, |mupdf, doc| mupdf.insert_blank(doc, at, size))?
|
||||
}
|
||||
tag::INSERT => {
|
||||
let (at, path, mut count) = (ask.u64()? as usize, ask.str()?, 0);
|
||||
let mark = self.edited(id, |mupdf, doc| {
|
||||
count = mupdf.insert_file(doc, at, &path)?;
|
||||
Ok(())
|
||||
})?;
|
||||
return Ok(Pack::new().u64(count as u64).str(&mark).bytes());
|
||||
}
|
||||
tag::EXTRACT => {
|
||||
let (pages, out) = (pages(ask)?, ask.str()?);
|
||||
let doc = self.docs.get_mut(&id).ok_or_else(|| io::Error::other(format!("no document {id}")))?;
|
||||
let done = self.mupdf.extract(&doc.open, &pages, out.as_ref(), &doc.password);
|
||||
doc.sync(&self.mupdf, None)?;
|
||||
done?;
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
tag::TEXT_BLOCKS => return self.mupdf.text_blocks(&self.record(id)?.open, ask.u64()? as usize),
|
||||
tag::OBJECTS => return self.mupdf.objects(&self.record(id)?.open, ask.u64()? as usize),
|
||||
tag::PUT_TEXT => return self.put_text(id, ask),
|
||||
tag::PLACE => {
|
||||
let at = ask.u64()? as usize;
|
||||
let (index, mut t) = (ask.u64()? as usize, [0f32; 6]);
|
||||
for value in &mut t {
|
||||
*value = f32::from_bits(ask.u64()? as u32);
|
||||
}
|
||||
let (drop, swap) = (ask.u64()? != 0, ask.str()?);
|
||||
let swap = Some(swap.as_str()).filter(|path| !path.is_empty());
|
||||
self.edited(id, |mupdf, doc| mupdf.place(doc, at, index, t, drop, swap))?
|
||||
}
|
||||
tag::ADD_IMAGE => {
|
||||
let (at, path, mut rect) = (ask.u64()? as usize, ask.str()?, [0f32; 4]);
|
||||
for value in &mut rect {
|
||||
*value = f32::from_bits(ask.u64()? as u32);
|
||||
}
|
||||
self.edited(id, |mupdf, doc| mupdf.add_image(doc, at, &path, rect))?
|
||||
}
|
||||
tag::ANNOTS => return self.mupdf.annots(&self.record(id)?.open, ask.u64()? as usize),
|
||||
tag::OCR_PAGE => {
|
||||
let (at, lang, datadir) = (ask.u64()? as usize, ask.str()?, ask.str()?);
|
||||
let dpi = f32::from_bits(ask.u64()? as u32);
|
||||
let lines = self.mupdf.ocr_page(&self.record(id)?.open, at, &lang, &datadir, dpi)?;
|
||||
log::info!("pdf-worker: OCR read page {at} of document {id}");
|
||||
return Ok(lines);
|
||||
}
|
||||
tag::TEXT_LAYER => {
|
||||
let (json, replace) = (ask.str()?, ask.u64()? != 0);
|
||||
self.edited(id, |mupdf, doc| mupdf.text_layer(doc, &json, replace))?
|
||||
}
|
||||
tag::OUTLINE_EDIT => {
|
||||
let json = ask.str()?;
|
||||
self.edited(id, |mupdf, doc| mupdf.outline_edit(doc, &json))?
|
||||
}
|
||||
tag::LINKS => return self.mupdf.links(&self.record(id)?.open, ask.u64()? as usize),
|
||||
tag::LINK_EDIT => {
|
||||
let (at, json) = (ask.u64()? as usize, ask.str()?);
|
||||
self.edited(id, |mupdf, doc| mupdf.link_edit(doc, at, &json))?
|
||||
}
|
||||
tag::METADATA => return self.mupdf.metadata(&self.record(id)?.open),
|
||||
tag::SET_METADATA => {
|
||||
let json = ask.str()?;
|
||||
self.edited(id, |mupdf, doc| mupdf.set_metadata(doc, &json))?
|
||||
}
|
||||
tag::ATTACHMENTS => return self.mupdf.attachments(&self.record(id)?.open),
|
||||
tag::ATTACHMENT => return self.mupdf.attachment(&self.record(id)?.open, &ask.str()?),
|
||||
tag::ATTACH => {
|
||||
let (name, path) = (ask.str()?, ask.str()?);
|
||||
self.edited(id, |mupdf, doc| mupdf.attach(doc, &name, &path))?
|
||||
}
|
||||
tag::DETACH => {
|
||||
let name = ask.str()?;
|
||||
self.edited(id, |mupdf, doc| mupdf.detach(doc, &name))?
|
||||
}
|
||||
tag::REDACT => {
|
||||
let (json, out) = (ask.str()?, ask.str()?);
|
||||
let record = self.record(id)?;
|
||||
let report = self.mupdf.redact(&record.open, &json, out.as_ref(), &record.password)?;
|
||||
log::info!("pdf-worker: wrote a redacted copy of document {id}");
|
||||
return Ok(report);
|
||||
}
|
||||
tag::CLEAN_SCAN => {
|
||||
let scratch = ask.str()?;
|
||||
let record = self.record(id)?;
|
||||
return self.mupdf.clean_scan(&record.open, scratch.as_ref(), &record.password);
|
||||
}
|
||||
tag::CLEAN => {
|
||||
let (mask, out) = (ask.u64()? as u32, ask.str()?);
|
||||
let record = self.record(id)?;
|
||||
let left = self.mupdf.clean(&record.open, mask, out.as_ref(), &record.password)?;
|
||||
log::info!("pdf-worker: wrote a cleaned copy of document {id}");
|
||||
return Ok(left);
|
||||
}
|
||||
tag::FIELD_NAMES => return self.mupdf.field_names(&self.record(id)?.open),
|
||||
tag::ANNOTATE => {
|
||||
let json = ask.str()?;
|
||||
self.edited(id, |mupdf, doc| mupdf.annotate(doc, &json))?
|
||||
}
|
||||
tag::CHANGE_ANNOT | tag::CHANGE_FIELD | tag::FILL | tag::DELETE_ANNOT => {
|
||||
let (at, object, value) = (ask.u64()? as usize, ask.u64()?, ask.str()?);
|
||||
if kind == tag::DELETE_ANNOT {
|
||||
let needed = if value == "field" { may::EDIT | may::ANNOTATE } else { may::ANNOTATE };
|
||||
secure::refuse(needed, self.allowed(id)?)?;
|
||||
}
|
||||
self.edited(id, |mupdf, doc| match kind {
|
||||
tag::CHANGE_ANNOT => mupdf.change_annot(doc, at, object, &value),
|
||||
tag::CHANGE_FIELD => mupdf.change_field(doc, at, object, &value),
|
||||
tag::FILL => mupdf.fill(doc, at, object, &value),
|
||||
_ => match value.as_str() {
|
||||
"field" | "annot" => mupdf.delete_annot(doc, at, object, value == "field"),
|
||||
other => Err(io::Error::other(format!("no object kind {other}"))),
|
||||
},
|
||||
})?
|
||||
}
|
||||
tag::ADD_FIELD => {
|
||||
let (at, json) = (ask.u64()? as usize, ask.str()?);
|
||||
self.edited(id, |mupdf, doc| mupdf.add_field(doc, at, &json))?
|
||||
}
|
||||
tag::SAVE => {
|
||||
let out = ask.str()?;
|
||||
let doc = self.docs.get_mut(&id).ok_or_else(|| io::Error::other(format!("no document {id}")))?;
|
||||
let done = self.mupdf.save(&doc.open, out.as_ref(), &doc.password);
|
||||
doc.sync(&self.mupdf, None)?;
|
||||
done?;
|
||||
log::info!("pdf-worker: saved document {id}");
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
tag::PERMISSIONS | tag::OWNER | tag::PROTECT => return self.secure(kind, id, ask),
|
||||
tag::SIGNATURES | tag::SIGN | tag::SIGNED => return self.signing(kind, id, ask),
|
||||
tag::CHECK_PICTURES => return self.check_pictures(id),
|
||||
tag::EXPORT_IMAGE | tag::EXPORT_TEXT | tag::OPTIMIZE | tag::PRINT_COPY | tag::REPAIR_REPORT => return self.output(kind, id, ask),
|
||||
other => return Err(io::Error::other(format!("no request {other}"))),
|
||||
};
|
||||
Ok(Pack::new().str(&mark).bytes())
|
||||
}
|
||||
|
||||
fn answer(&mut self, kind: u8, payload: &[u8]) -> io::Result<Vec<u8>> {
|
||||
// A document request names the state it was made for.
|
||||
let stamped;
|
||||
let payload = match kind {
|
||||
tag::OPEN | tag::HELLO => payload,
|
||||
_ => {
|
||||
stamped = self.current(payload)?;
|
||||
&stamped
|
||||
}
|
||||
};
|
||||
let mut ask = Unpack::new(payload);
|
||||
match kind {
|
||||
tag::OPEN => {
|
||||
let open = self.mupdf.open(&ask.str()?)?;
|
||||
let prepared = (|| {
|
||||
let locked = self.mupdf.locked(&open)?;
|
||||
// Unprotected files answer 1; owners carry bit 4.
|
||||
let owner = !locked && matches!(self.mupdf.unlock(&open, "")?, access if access == 1 || access & 4 != 0);
|
||||
self.mupdf.journal(&open)?;
|
||||
Ok::<_, io::Error>((locked, owner))
|
||||
})();
|
||||
// A failed start releases what MuPDF opened.
|
||||
let (locked, owner) = match prepared {
|
||||
Ok(prepared) => prepared,
|
||||
Err(error) => {
|
||||
self.mupdf.close(open);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let id = self.next;
|
||||
self.next += 1;
|
||||
let warnings = output::open_warnings();
|
||||
let doc = Doc { open, password: String::new(), owner, warnings, repair: None, copy: None, steps: Vec::new(), at: 0 };
|
||||
self.docs.insert(id, doc);
|
||||
if !locked && let Err(error) = self.repair(id) {
|
||||
self.forget(id)?;
|
||||
return Err(error);
|
||||
}
|
||||
log::info!("pdf-worker: opened document {id}, locked {locked}");
|
||||
Ok(Pack::new().u64(id).u64(u64::from(locked)).bytes())
|
||||
}
|
||||
tag::UNLOCK => {
|
||||
let (id, password) = (ask.u64()?, ask.str()?);
|
||||
let access = self.mupdf.unlock(self.doc(id)?, &password)?;
|
||||
if access != 0 {
|
||||
let doc = self.docs.get_mut(&id).expect("just read");
|
||||
(doc.password, doc.owner) = (password, access & 4 != 0);
|
||||
self.repair(id)?;
|
||||
}
|
||||
Ok(Pack::new().u64(u64::from(access != 0)).bytes())
|
||||
}
|
||||
tag::PAGES => Ok(Pack::new()
|
||||
.u64(self.mupdf.pages(self.doc(ask.u64()?)?)? as u64)
|
||||
.bytes()),
|
||||
tag::BOUNDS => {
|
||||
let doc = self.doc(ask.u64()?)?;
|
||||
let (first, count) = (ask.u64()? as usize, ask.u64()? as usize);
|
||||
let mut out = Pack::new();
|
||||
for number in first..first + count {
|
||||
for value in self.mupdf.bounds(doc, number)? {
|
||||
out = out.u64(u64::from(value.to_bits()));
|
||||
}
|
||||
}
|
||||
Ok(out.bytes())
|
||||
}
|
||||
tag::RENDER => {
|
||||
let id = ask.u64()?;
|
||||
let page = ask.u64()? as usize;
|
||||
let scale = f32::from_bits(ask.u64()? as u32);
|
||||
let rotate = i32::try_from(ask.i64()?).map_err(io::Error::other)?;
|
||||
let mut tile = [0i32; 4];
|
||||
for value in &mut tile {
|
||||
*value = i32::try_from(ask.i64()?).map_err(io::Error::other)?;
|
||||
}
|
||||
let dark = ask.u64()? != 0;
|
||||
self.drawn(id, page, (scale, rotate), tile, dark)
|
||||
}
|
||||
tag::TEXT => {
|
||||
let doc = self.doc(ask.u64()?)?;
|
||||
self.mupdf.text(doc, ask.u64()? as usize)
|
||||
}
|
||||
tag::SEARCH => {
|
||||
let doc = self.doc(ask.u64()?)?;
|
||||
let page = ask.u64()? as usize;
|
||||
self.mupdf.search(doc, page, &ask.str()?)
|
||||
}
|
||||
tag::OUTLINE => self.mupdf.outline(self.doc(ask.u64()?)?),
|
||||
tag::HELLO => Ok(Pack::new().str(ely_pdf_engine::proto::BUILD).bytes()),
|
||||
tag::CLOSE => {
|
||||
self.forget(ask.u64()?)?;
|
||||
Ok(Vec::new())
|
||||
}
|
||||
other => self.edit(other, &mut ask),
|
||||
}
|
||||
}
|
||||
|
||||
/// The request without its revision, if it is still current.
|
||||
fn current(&self, payload: &[u8]) -> io::Result<Vec<u8>> {
|
||||
let mut ask = Unpack::new(payload);
|
||||
let (id, expected) = (ask.u64()?, ask.u64()?);
|
||||
let doc = self.record(id)?;
|
||||
if expected != ely_pdf_engine::proto::ANY && doc.mark() != expected.to_string() {
|
||||
return Err(io::Error::other(format!("{}document {id} is at {}, not {expected}", ely_pdf_engine::proto::STALE, doc.mark())));
|
||||
}
|
||||
Ok([&id.to_le_bytes()[..], ask.rest()].concat())
|
||||
}
|
||||
|
||||
/// Releases document `id` and its repaired copy.
|
||||
fn forget(&mut self, id: u64) -> io::Result<()> {
|
||||
let doc = self.docs.remove(&id).ok_or_else(|| io::Error::other(format!("no document {id}")))?;
|
||||
self.mupdf.close(doc.open);
|
||||
if let Some(copy) = doc.copy {
|
||||
std::fs::remove_file(copy)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
env_logger::Builder::from_env(env_logger::Env::new().filter_or("ELY_LOG", "info")).init();
|
||||
ffi::script_font();
|
||||
// Numbers from another worker process match nothing here.
|
||||
let started = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_err(io::Error::other)?;
|
||||
let prefix = (u64::from(std::process::id()) ^ u64::from(started.subsec_nanos())) & 0xffff_ffff;
|
||||
let mut worker = Worker {
|
||||
mupdf: Mupdf::new()?,
|
||||
docs: HashMap::new(),
|
||||
next: (prefix << 32) | 1,
|
||||
edits: 0,
|
||||
};
|
||||
let (mut input, mut output) = (
|
||||
BufReader::new(io::stdin().lock()),
|
||||
BufWriter::new(io::stdout().lock()),
|
||||
);
|
||||
while let Some((kind, id, payload)) = read_frame(&mut input)? {
|
||||
let written = match worker.answer(kind, &payload) {
|
||||
Ok(reply) => write_frame(&mut output, tag::OK, id, &reply),
|
||||
Err(error) => {
|
||||
// Text may quote the document: reply, never log.
|
||||
log::warn!("pdf-worker: request {kind} failed ({:?})", error.kind());
|
||||
write_frame(&mut output, tag::ERR, id, error.to_string().as_bytes())
|
||||
}
|
||||
};
|
||||
// An oversized answer is refused; the worker lives.
|
||||
if let Err(error) = written {
|
||||
if error.kind() != io::ErrorKind::InvalidInput {
|
||||
return Err(error);
|
||||
}
|
||||
log::warn!("pdf-worker: request {kind} answer too large");
|
||||
write_frame(&mut output, tag::ERR, id, error.to_string().as_bytes())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
//! The shim's annotation and form calls (PDF-05, PDF-06).
|
||||
|
||||
use std::{
|
||||
ffi::{c_char, c_int},
|
||||
io,
|
||||
};
|
||||
|
||||
use crate::ffi::{Context, Document, Mupdf, Open, checked, page, text};
|
||||
|
||||
unsafe extern "C" {
|
||||
fn ely_annots(ctx: *mut Context, doc: *mut Document, page: c_int, data: *mut *mut u8, len: *mut usize) -> c_int;
|
||||
fn ely_annotate(ctx: *mut Context, doc: *mut Document, json: *const c_char) -> c_int;
|
||||
fn ely_change_annot(ctx: *mut Context, doc: *mut Document, page: c_int, id: c_int, json: *const c_char) -> c_int;
|
||||
fn ely_delete_annot(ctx: *mut Context, doc: *mut Document, page: c_int, id: c_int, widget: c_int) -> c_int;
|
||||
fn ely_add_field(ctx: *mut Context, doc: *mut Document, page: c_int, json: *const c_char) -> c_int;
|
||||
fn ely_fill(ctx: *mut Context, doc: *mut Document, page: c_int, id: c_int, value: *const c_char) -> c_int;
|
||||
fn ely_field_names(ctx: *mut Context, doc: *mut Document, data: *mut *mut u8, len: *mut usize) -> c_int;
|
||||
fn ely_change_field(ctx: *mut Context, doc: *mut Document, page: c_int, id: c_int, json: *const c_char) -> c_int;
|
||||
}
|
||||
|
||||
fn id(value: u64) -> io::Result<c_int> {
|
||||
c_int::try_from(value).map_err(|_| io::Error::other("an object number too large"))
|
||||
}
|
||||
|
||||
impl Mupdf {
|
||||
pub fn annots(&self, doc: &Open, at: usize) -> io::Result<Vec<u8>> {
|
||||
let at = page(at)?;
|
||||
self.owned(|data, len| unsafe { ely_annots(self.ctx, doc.0, at, data, len) })
|
||||
}
|
||||
|
||||
pub fn field_names(&self, doc: &Open) -> io::Result<Vec<u8>> {
|
||||
self.owned(|data, len| unsafe { ely_field_names(self.ctx, doc.0, data, len) })
|
||||
}
|
||||
|
||||
pub fn annotate(&self, doc: &Open, json: &str) -> io::Result<()> {
|
||||
let json = text(json)?;
|
||||
checked(unsafe { ely_annotate(self.ctx, doc.0, json.as_ptr()) }).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn change_annot(&self, doc: &Open, at: usize, annot: u64, json: &str) -> io::Result<()> {
|
||||
let json = text(json)?;
|
||||
checked(unsafe { ely_change_annot(self.ctx, doc.0, page(at)?, id(annot)?, json.as_ptr()) }).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn delete_annot(&self, doc: &Open, at: usize, annot: u64, field: bool) -> io::Result<()> {
|
||||
checked(unsafe { ely_delete_annot(self.ctx, doc.0, page(at)?, id(annot)?, c_int::from(field)) }).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn add_field(&self, doc: &Open, at: usize, json: &str) -> io::Result<()> {
|
||||
let json = text(json)?;
|
||||
checked(unsafe { ely_add_field(self.ctx, doc.0, page(at)?, json.as_ptr()) }).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn fill(&self, doc: &Open, at: usize, field: u64, value: &str) -> io::Result<()> {
|
||||
let value = text(value)?;
|
||||
checked(unsafe { ely_fill(self.ctx, doc.0, page(at)?, id(field)?, value.as_ptr()) }).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn change_field(&self, doc: &Open, at: usize, field: u64, json: &str) -> io::Result<()> {
|
||||
let json = text(json)?;
|
||||
checked(unsafe { ely_change_field(self.ctx, doc.0, page(at)?, id(field)?, json.as_ptr()) }).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! The shim's OCR calls (PDF-08).
|
||||
|
||||
use std::{
|
||||
ffi::{c_char, c_int},
|
||||
io,
|
||||
};
|
||||
|
||||
use crate::ffi::{Context, Document, Mupdf, Open, checked, page, text};
|
||||
|
||||
unsafe extern "C" {
|
||||
fn ely_ocr_page(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
page: c_int,
|
||||
lang: *const c_char,
|
||||
datadir: *const c_char,
|
||||
dpi: f32,
|
||||
data: *mut *mut u8,
|
||||
len: *mut usize,
|
||||
) -> c_int;
|
||||
fn ely_text_layer(ctx: *mut Context, doc: *mut Document, json: *const c_char, replace: c_int) -> c_int;
|
||||
}
|
||||
|
||||
impl Mupdf {
|
||||
pub fn ocr_page(&self, doc: &Open, at: usize, lang: &str, datadir: &str, dpi: f32) -> io::Result<Vec<u8>> {
|
||||
let (at, lang, datadir) = (page(at)?, text(lang)?, text(datadir)?);
|
||||
self.owned(|data, len| unsafe { ely_ocr_page(self.ctx, doc.0, at, lang.as_ptr(), datadir.as_ptr(), dpi, data, len) })
|
||||
}
|
||||
|
||||
pub fn text_layer(&self, doc: &Open, json: &str, replace: bool) -> io::Result<()> {
|
||||
let json = text(json)?;
|
||||
checked(unsafe { ely_text_layer(self.ctx, doc.0, json.as_ptr(), c_int::from(replace)) }).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
//! Page images, text, optimized and repaired copies (PDF-13, PDF-14).
|
||||
|
||||
use std::{
|
||||
ffi::{CStr, c_char, c_int},
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use ely_ipc::{Pack, Unpack};
|
||||
use ely_pdf_engine::proto::{may, tag};
|
||||
|
||||
use crate::edit::{clean, raw, utf8};
|
||||
use crate::ffi::{Context, Document, Mupdf, Open, checked, page, text};
|
||||
use crate::secure::refuse;
|
||||
|
||||
unsafe extern "C" {
|
||||
fn ely_export_image(ctx: *mut Context, doc: *mut Document, number: c_int, dpi: f32, path: *const c_char) -> c_int;
|
||||
fn ely_export_text(ctx: *mut Context, doc: *mut Document, path: *const c_char) -> c_int;
|
||||
fn ely_optimize(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
raw: *const c_char,
|
||||
out: *const c_char,
|
||||
password: *const c_char,
|
||||
compress: c_int,
|
||||
from: c_int,
|
||||
to: c_int,
|
||||
) -> c_int;
|
||||
fn ely_repaired(ctx: *mut Context, doc: *mut Document) -> c_int;
|
||||
fn ely_write_repaired(ctx: *mut Context, doc: *mut Document, path: *const c_char) -> c_int;
|
||||
fn ely_open_warnings() -> *const c_char;
|
||||
fn ely_scan(ctx: *mut Context, doc: *mut Document) -> c_int;
|
||||
fn ely_watch_begin(ctx: *mut Context);
|
||||
fn ely_watch_end(ctx: *mut Context) -> c_int;
|
||||
fn ely_check_pictures(ctx: *mut Context, doc: *mut Document) -> c_int;
|
||||
fn ely_print_copy(ctx: *mut Context, doc: *mut Document, raw: *const c_char, out: *const c_char, password: *const c_char) -> c_int;
|
||||
}
|
||||
|
||||
/// What MuPDF warned while opening the last file.
|
||||
pub fn open_warnings() -> String {
|
||||
unsafe { CStr::from_ptr(ely_open_warnings()) }.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
impl Mupdf {
|
||||
fn optimize(&self, doc: &Open, out: &Path, password: &str, compress: bool, (from, to): (u32, u32)) -> io::Result<()> {
|
||||
let scratch = raw(out);
|
||||
let (raw_c, out_c, password) = (text(utf8(&scratch)?)?, text(utf8(out)?)?, text(password)?);
|
||||
let (from, to) = (c_int::try_from(from).map_err(io::Error::other)?, c_int::try_from(to).map_err(io::Error::other)?);
|
||||
let done = checked(unsafe {
|
||||
ely_optimize(self.ctx, doc.0, raw_c.as_ptr(), out_c.as_ptr(), password.as_ptr(), c_int::from(compress), from, to)
|
||||
});
|
||||
clean(&scratch);
|
||||
if done.is_err() {
|
||||
clean(out);
|
||||
}
|
||||
done.map(|_| ())
|
||||
}
|
||||
|
||||
pub fn repaired(&self, doc: &Open) -> io::Result<bool> {
|
||||
checked(unsafe { ely_repaired(self.ctx, doc.0) }).map(|repaired| repaired != 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Worker {
|
||||
/// MuPDF warned on reading: the file is damaged.
|
||||
fn note_damage(&mut self, id: u64) -> io::Result<()> {
|
||||
let doc = self.docs.get_mut(&id).ok_or_else(|| io::Error::other(format!("no document {id}")))?;
|
||||
let report = doc.repair.get_or_insert_with(String::new);
|
||||
report.push_str(&open_warnings());
|
||||
log::warn!("pdf-worker: document {id} is damaged");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A tile, and whether drawing it met damage.
|
||||
pub(crate) fn drawn(&mut self, id: u64, page: usize, view: (f32, i32), tile: [i32; 4], dark: bool) -> io::Result<Vec<u8>> {
|
||||
unsafe { ely_watch_begin(self.mupdf.ctx) };
|
||||
let pixels = self.mupdf.render(self.doc(id)?, page, view, tile, dark);
|
||||
let warned = unsafe { ely_watch_end(self.mupdf.ctx) } != 0;
|
||||
if warned {
|
||||
self.note_damage(id)?;
|
||||
}
|
||||
Ok(Pack::new().u64(u64::from(warned)).blob(&pixels?).bytes())
|
||||
}
|
||||
|
||||
/// Every picture decoded before an overwrite; true if any failed.
|
||||
pub(crate) fn check_pictures(&mut self, id: u64) -> io::Result<Vec<u8>> {
|
||||
let failed = checked(unsafe { ely_check_pictures(self.mupdf.ctx, self.doc(id)?.0) })? != 0;
|
||||
if failed {
|
||||
self.note_damage(id)?;
|
||||
}
|
||||
Ok(Pack::new().u64(u64::from(failed)).bytes())
|
||||
}
|
||||
|
||||
/// A repaired file gives way to a whole copy.
|
||||
pub(crate) fn repair(&mut self, id: u64) -> io::Result<()> {
|
||||
let doc = self.docs.get_mut(&id).ok_or_else(|| io::Error::other(format!("no document {id}")))?;
|
||||
if doc.repair.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
// Late repairs and broken streams show only on reading.
|
||||
let warned = checked(unsafe { ely_scan(self.mupdf.ctx, doc.open.0) })? != 0;
|
||||
doc.warnings.push_str(&open_warnings());
|
||||
if !warned && !self.mupdf.repaired(&doc.open)? {
|
||||
return Ok(());
|
||||
}
|
||||
let copy: PathBuf = std::env::temp_dir().join(format!("ely-pdf-{}-{id}.pdf", std::process::id()));
|
||||
let path = text(utf8(©)?)?;
|
||||
// Any failure here removes the copy and what was opened.
|
||||
let mupdf = &self.mupdf;
|
||||
let fresh = (|| {
|
||||
checked(unsafe { ely_write_repaired(mupdf.ctx, doc.open.0, path.as_ptr()) })?;
|
||||
let fresh = mupdf.open(utf8(©)?)?;
|
||||
let ready = (|| {
|
||||
if mupdf.locked(&fresh)? && mupdf.unlock(&fresh, &doc.password)? == 0 {
|
||||
return Err(io::Error::other("the repaired copy refused the password"));
|
||||
}
|
||||
mupdf.journal(&fresh)
|
||||
})();
|
||||
match ready {
|
||||
Ok(()) => Ok(fresh),
|
||||
Err(error) => {
|
||||
mupdf.close(fresh);
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
})();
|
||||
let fresh = match fresh {
|
||||
Ok(fresh) => fresh,
|
||||
Err(error) => {
|
||||
clean(©);
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
let broken = std::mem::replace(&mut doc.open, fresh);
|
||||
self.mupdf.close(broken);
|
||||
doc.repair = Some(std::mem::take(&mut doc.warnings));
|
||||
doc.copy = Some(copy);
|
||||
log::info!("pdf-worker: document {id} was damaged; a repaired copy stands in");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn output(&mut self, kind: u8, id: u64, ask: &mut Unpack) -> io::Result<Vec<u8>> {
|
||||
let has = self.allowed(id)?;
|
||||
let doc = self.record(id)?;
|
||||
match kind {
|
||||
tag::EXPORT_IMAGE => {
|
||||
refuse(may::COPY, has)?;
|
||||
let (at, dpi, path) = (ask.u64()? as usize, f32::from_bits(ask.u64()? as u32), ask.str()?);
|
||||
let path_c = text(&path)?;
|
||||
checked(unsafe { ely_export_image(self.mupdf.ctx, doc.open.0, page(at)?, dpi, path_c.as_ptr()) })?;
|
||||
}
|
||||
tag::EXPORT_TEXT => {
|
||||
refuse(may::COPY, has)?;
|
||||
let path = text(&ask.str()?)?;
|
||||
checked(unsafe { ely_export_text(self.mupdf.ctx, doc.open.0, path.as_ptr()) })?;
|
||||
}
|
||||
tag::OPTIMIZE => {
|
||||
refuse(may::EDIT, has)?;
|
||||
let (out, compress) = (ask.str()?, ask.u64()? != 0);
|
||||
let (from, to) = (ask.u64()? as u32, ask.u64()? as u32);
|
||||
self.mupdf.optimize(&doc.open, out.as_ref(), &doc.password, compress, (from, to))?;
|
||||
}
|
||||
tag::PRINT_COPY => {
|
||||
refuse(may::PRINT, has)?;
|
||||
let out = ask.str()?;
|
||||
let scratch = raw(out.as_ref());
|
||||
let (raw_c, out_c, password) = (text(utf8(&scratch)?)?, text(&out)?, text(&doc.password)?);
|
||||
let done = checked(unsafe { ely_print_copy(self.mupdf.ctx, doc.open.0, raw_c.as_ptr(), out_c.as_ptr(), password.as_ptr()) });
|
||||
clean(&scratch);
|
||||
done?;
|
||||
}
|
||||
tag::REPAIR_REPORT => {
|
||||
return Ok(match &doc.repair {
|
||||
Some(report) => Pack::new().u64(1).str(report).bytes(),
|
||||
None => Pack::new().u64(0).bytes(),
|
||||
});
|
||||
}
|
||||
other => return Err(io::Error::other(format!("no request {other}"))),
|
||||
}
|
||||
log::info!("pdf-worker: request {kind} done for document {id}");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
//! The shim's removal and cleaning calls (PDF-09, PDF-10).
|
||||
|
||||
use std::{
|
||||
ffi::{c_char, c_int},
|
||||
io,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use crate::edit::{clean, raw, utf8};
|
||||
use crate::ffi::{Context, Document, Mupdf, Open, text};
|
||||
|
||||
unsafe extern "C" {
|
||||
fn ely_redact(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
json: *const c_char,
|
||||
raw: *const c_char,
|
||||
out: *const c_char,
|
||||
password: *const c_char,
|
||||
data: *mut *mut u8,
|
||||
len: *mut usize,
|
||||
) -> c_int;
|
||||
fn ely_clean_scan(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
raw: *const c_char,
|
||||
password: *const c_char,
|
||||
data: *mut *mut u8,
|
||||
len: *mut usize,
|
||||
) -> c_int;
|
||||
fn ely_clean(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
mask: c_int,
|
||||
raw: *const c_char,
|
||||
out: *const c_char,
|
||||
password: *const c_char,
|
||||
data: *mut *mut u8,
|
||||
len: *mut usize,
|
||||
) -> c_int;
|
||||
}
|
||||
|
||||
impl Mupdf {
|
||||
/// Writes `out` with `json`'s regions removed; the check.
|
||||
pub fn redact(&self, doc: &Open, json: &str, out: &Path, password: &str) -> io::Result<Vec<u8>> {
|
||||
let scratch = raw(out);
|
||||
let (json, raw_c, out_c, password) = (text(json)?, text(utf8(&scratch)?)?, text(utf8(out)?)?, text(password)?);
|
||||
let done = self.owned(|data, len| unsafe {
|
||||
ely_redact(self.ctx, doc.0, json.as_ptr(), raw_c.as_ptr(), out_c.as_ptr(), password.as_ptr(), data, len)
|
||||
});
|
||||
clean(&scratch);
|
||||
// A file that failed its check is not left behind.
|
||||
if done.is_err() {
|
||||
clean(out);
|
||||
}
|
||||
done
|
||||
}
|
||||
|
||||
/// Counts on a copy written to `scratch`, then removed.
|
||||
pub fn clean_scan(&self, doc: &Open, scratch: &Path, password: &str) -> io::Result<Vec<u8>> {
|
||||
let (raw_c, password) = (text(utf8(scratch)?)?, text(password)?);
|
||||
let done = self.owned(|data, len| unsafe { ely_clean_scan(self.ctx, doc.0, raw_c.as_ptr(), password.as_ptr(), data, len) });
|
||||
clean(scratch);
|
||||
done
|
||||
}
|
||||
|
||||
/// Writes `out` cleaned of `mask`; what is left.
|
||||
pub fn clean(&self, doc: &Open, mask: u32, out: &Path, password: &str) -> io::Result<Vec<u8>> {
|
||||
let mask = c_int::try_from(mask).map_err(io::Error::other)?;
|
||||
let scratch = raw(out);
|
||||
let (raw_c, out_c, password) = (text(utf8(&scratch)?)?, text(utf8(out)?)?, text(password)?);
|
||||
let done = self.owned(|data, len| unsafe {
|
||||
ely_clean(self.ctx, doc.0, mask, raw_c.as_ptr(), out_c.as_ptr(), password.as_ptr(), data, len)
|
||||
});
|
||||
clean(&scratch);
|
||||
if done.is_err() {
|
||||
clean(out);
|
||||
}
|
||||
done
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
//! Permissions, owner access and protected copies (PDF-11).
|
||||
|
||||
use std::{
|
||||
ffi::{c_char, c_int},
|
||||
io,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use ely_pdf_engine::proto::{may, tag};
|
||||
|
||||
use crate::edit::{clean, raw, utf8};
|
||||
use crate::ffi::{Context, Document, Mupdf, Open, checked, text};
|
||||
|
||||
unsafe extern "C" {
|
||||
fn ely_permissions(ctx: *mut Context, doc: *mut Document) -> c_int;
|
||||
fn ely_protect(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
raw: *const c_char,
|
||||
out: *const c_char,
|
||||
password: *const c_char,
|
||||
protect: c_int,
|
||||
user: *const c_char,
|
||||
owner: *const c_char,
|
||||
may: c_int,
|
||||
) -> c_int;
|
||||
}
|
||||
|
||||
/// What each request needs the document to allow.
|
||||
pub fn needs(kind: u8) -> u32 {
|
||||
match kind {
|
||||
tag::DELETE | tag::MOVE | tag::ROTATE | tag::BLANK | tag::INSERT | tag::OUTLINE_EDIT => may::ASSEMBLE,
|
||||
tag::CROP | tag::PUT_TEXT | tag::PLACE | tag::ADD_IMAGE | tag::TEXT_LAYER | tag::LINK_EDIT => may::EDIT,
|
||||
tag::SET_METADATA | tag::ATTACH | tag::DETACH | tag::REDACT | tag::CLEAN => may::EDIT,
|
||||
tag::ANNOTATE | tag::CHANGE_ANNOT => may::ANNOTATE,
|
||||
tag::ADD_FIELD | tag::CHANGE_FIELD => may::EDIT | may::ANNOTATE,
|
||||
tag::FILL => may::FILL,
|
||||
tag::EXTRACT => may::COPY,
|
||||
tag::PROTECT => may::OWNER,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The bits `needed` lacks in `has`, as an error.
|
||||
pub fn refuse(needed: u32, has: u32) -> io::Result<()> {
|
||||
let missing = needed & !has;
|
||||
match may::NAMES.iter().find(|(bit, _)| missing & bit != 0) {
|
||||
Some((_, name)) => Err(io::Error::other(format!("{}{name}", may::REFUSED))),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
impl Mupdf {
|
||||
/// Ely's permission bits; `owner` adds full access.
|
||||
pub fn permissions(&self, doc: &Open, owner: bool) -> io::Result<u32> {
|
||||
let bits = checked(unsafe { ely_permissions(self.ctx, doc.0) })? as u32;
|
||||
Ok(if owner { bits | may::OWNER } else { bits })
|
||||
}
|
||||
|
||||
/// Writes `out` protected, or unprotected without `lock`.
|
||||
pub fn protect(&self, doc: &Open, out: &Path, password: &str, lock: Option<(&str, &str, u32)>) -> io::Result<()> {
|
||||
let (user, owner, bits) = lock.unwrap_or(("", "", 0));
|
||||
let scratch = raw(out);
|
||||
let (raw_c, out_c, password) = (text(utf8(&scratch)?)?, text(utf8(out)?)?, text(password)?);
|
||||
let (user, owner) = (text(user)?, text(owner)?);
|
||||
let bits = c_int::try_from(bits).map_err(io::Error::other)?;
|
||||
let done = checked(unsafe {
|
||||
ely_protect(
|
||||
self.ctx,
|
||||
doc.0,
|
||||
raw_c.as_ptr(),
|
||||
out_c.as_ptr(),
|
||||
password.as_ptr(),
|
||||
c_int::from(lock.is_some()),
|
||||
user.as_ptr(),
|
||||
owner.as_ptr(),
|
||||
bits,
|
||||
)
|
||||
});
|
||||
clean(&scratch);
|
||||
if done.is_err() {
|
||||
clean(out);
|
||||
}
|
||||
done.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Worker {
|
||||
/// What document `id` allows now.
|
||||
pub(crate) fn allowed(&self, id: u64) -> io::Result<u32> {
|
||||
let doc = self.record(id)?;
|
||||
self.mupdf.permissions(&doc.open, doc.owner)
|
||||
}
|
||||
|
||||
/// Refuses `kind` on `id` when the document forbids it.
|
||||
pub(crate) fn guard(&self, id: u64, kind: u8) -> io::Result<()> {
|
||||
match needs(kind) {
|
||||
0 => Ok(()),
|
||||
needed => refuse(needed, self.allowed(id)?),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn secure(&mut self, kind: u8, id: u64, ask: &mut ely_ipc::Unpack) -> io::Result<Vec<u8>> {
|
||||
match kind {
|
||||
tag::PERMISSIONS => Ok(ely_ipc::Pack::new().u64(u64::from(self.allowed(id)?)).bytes()),
|
||||
tag::OWNER => {
|
||||
let password = ask.str()?;
|
||||
let doc = self.docs.get_mut(&id).ok_or_else(|| io::Error::other(format!("no document {id}")))?;
|
||||
let access = self.mupdf.unlock(&doc.open, &password)?;
|
||||
let owner = access & 4 != 0;
|
||||
match owner {
|
||||
true => (doc.password, doc.owner) = (password, true),
|
||||
// A refused try drops MuPDF's keys; the old ones return.
|
||||
false => _ = self.mupdf.unlock(&doc.open, &doc.password)?,
|
||||
}
|
||||
log::info!("pdf-worker: owner password for document {id}: {}", if owner { "accepted" } else { "refused" });
|
||||
Ok(ely_ipc::Pack::new().u64(u64::from(owner)).bytes())
|
||||
}
|
||||
tag::PROTECT => {
|
||||
let (out, lock) = (ask.str()?, ask.u64()? != 0);
|
||||
let (user, owner, bits) = (ask.str()?, ask.str()?, ask.u64()? as u32);
|
||||
let doc = self.record(id)?;
|
||||
let lock = lock.then_some((user.as_str(), owner.as_str(), bits));
|
||||
self.mupdf.protect(&doc.open, out.as_ref(), &doc.password, lock)?;
|
||||
log::info!("pdf-worker: wrote a {} copy of document {id}", if lock.is_some() { "protected" } else { "unprotected" });
|
||||
Ok(Vec::new())
|
||||
}
|
||||
other => Err(io::Error::other(format!("no request {other}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
//! The shim's signature calls (PDF-12).
|
||||
|
||||
use std::{
|
||||
ffi::{c_char, c_int},
|
||||
io,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use ely_pdf_engine::{Signing, proto::{may, tag}};
|
||||
|
||||
use crate::edit::{clean, utf8};
|
||||
use crate::ffi::{Context, Document, Mupdf, Open, checked, page, text};
|
||||
use crate::secure::refuse;
|
||||
|
||||
unsafe extern "C" {
|
||||
fn ely_signatures(ctx: *mut Context, doc: *mut Document, data: *mut *mut u8, len: *mut usize) -> c_int;
|
||||
fn ely_signed(ctx: *mut Context, doc: *mut Document) -> c_int;
|
||||
fn ely_sign(
|
||||
ctx: *mut Context,
|
||||
doc: *mut Document,
|
||||
raw: *const c_char,
|
||||
password: *const c_char,
|
||||
pfx: *const c_char,
|
||||
pfx_password: *const c_char,
|
||||
number: c_int,
|
||||
object: c_int,
|
||||
rect: *const f32,
|
||||
name: *const c_char,
|
||||
reason: *const c_char,
|
||||
location: *const c_char,
|
||||
) -> c_int;
|
||||
}
|
||||
|
||||
impl Mupdf {
|
||||
pub fn signatures(&self, doc: &Open) -> io::Result<Vec<u8>> {
|
||||
self.owned(|data, len| unsafe { ely_signatures(self.ctx, doc.0, data, len) })
|
||||
}
|
||||
|
||||
/// Writes `out` signed; a failure leaves nothing.
|
||||
pub fn sign(&self, doc: &Open, out: &Path, password: &str, how: &Signing) -> io::Result<()> {
|
||||
let object = match how.object {
|
||||
Some(object) => c_int::try_from(object).map_err(io::Error::other)?,
|
||||
None => -1,
|
||||
};
|
||||
let (out_c, password) = (text(utf8(out)?)?, text(password)?);
|
||||
let (pfx, pfx_password, name) = (text(&how.pfx)?, text(&how.pfx_password)?, text(&how.name)?);
|
||||
let (reason, location) = (text(&how.reason)?, text(&how.location)?);
|
||||
let done = checked(unsafe {
|
||||
ely_sign(
|
||||
self.ctx,
|
||||
doc.0,
|
||||
out_c.as_ptr(),
|
||||
password.as_ptr(),
|
||||
pfx.as_ptr(),
|
||||
pfx_password.as_ptr(),
|
||||
page(how.page)?,
|
||||
object,
|
||||
how.rect.as_ptr(),
|
||||
name.as_ptr(),
|
||||
reason.as_ptr(),
|
||||
location.as_ptr(),
|
||||
)
|
||||
});
|
||||
if done.is_err() {
|
||||
clean(out);
|
||||
}
|
||||
done.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Worker {
|
||||
pub(crate) fn signing(&mut self, kind: u8, id: u64, ask: &mut ely_ipc::Unpack) -> io::Result<Vec<u8>> {
|
||||
match kind {
|
||||
tag::SIGNATURES => {
|
||||
// A file's signatures, not the edited copy's.
|
||||
let path = ask.str()?;
|
||||
let password = self.record(id)?.password.clone();
|
||||
let file = self.mupdf.open(&path)?;
|
||||
let read = match self.mupdf.locked(&file) {
|
||||
Ok(true) if self.mupdf.unlock(&file, &password)? == 0 => Err(io::Error::other("the file refused the document's password")),
|
||||
Ok(_) => self.mupdf.signatures(&file),
|
||||
Err(error) => Err(error),
|
||||
};
|
||||
self.mupdf.close(file);
|
||||
read
|
||||
}
|
||||
tag::SIGNED => {
|
||||
let count = checked(unsafe { ely_signed(self.mupdf.ctx, self.record(id)?.open.0) })?;
|
||||
Ok(ely_ipc::Pack::new().u64(count as u64).bytes())
|
||||
}
|
||||
tag::SIGN => {
|
||||
let out = ask.str()?;
|
||||
let (page, object) = (ask.u64()? as usize, ask.i64()?);
|
||||
let mut rect = [0f32; 4];
|
||||
for value in &mut rect {
|
||||
*value = f32::from_bits(ask.u64()? as u32);
|
||||
}
|
||||
let (name, pfx, pfx_password) = (ask.str()?, ask.str()?, ask.str()?);
|
||||
let (reason, location) = (ask.str()?, ask.str()?);
|
||||
let object = u32::try_from(object).ok();
|
||||
// Filling a field, or adding one to the page.
|
||||
let needed = if object.is_some() { may::FILL } else { may::EDIT | may::ANNOTATE };
|
||||
refuse(needed, self.allowed(id)?)?;
|
||||
let how = Signing { page, object, rect, name, pfx, pfx_password, reason, location };
|
||||
let doc = self.record(id)?;
|
||||
self.mupdf.sign(&doc.open, out.as_ref(), &doc.password, &how)?;
|
||||
log::info!("pdf-worker: wrote a signed copy of document {id}");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
other => Err(io::Error::other(format!("no request {other}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//! The shim's bookmark, link, property and attachment calls (PDF-07).
|
||||
|
||||
use std::{
|
||||
ffi::{c_char, c_int},
|
||||
io,
|
||||
};
|
||||
|
||||
use crate::ffi::{Context, Document, Mupdf, Open, checked, page, text};
|
||||
|
||||
unsafe extern "C" {
|
||||
fn ely_outline_edit(ctx: *mut Context, doc: *mut Document, json: *const c_char) -> c_int;
|
||||
fn ely_links(ctx: *mut Context, doc: *mut Document, page: c_int, data: *mut *mut u8, len: *mut usize) -> c_int;
|
||||
fn ely_link_edit(ctx: *mut Context, doc: *mut Document, page: c_int, json: *const c_char) -> c_int;
|
||||
fn ely_metadata(ctx: *mut Context, doc: *mut Document, data: *mut *mut u8, len: *mut usize) -> c_int;
|
||||
fn ely_set_metadata(ctx: *mut Context, doc: *mut Document, json: *const c_char) -> c_int;
|
||||
fn ely_attachments(ctx: *mut Context, doc: *mut Document, data: *mut *mut u8, len: *mut usize) -> c_int;
|
||||
fn ely_attachment(ctx: *mut Context, doc: *mut Document, name: *const c_char, data: *mut *mut u8, len: *mut usize) -> c_int;
|
||||
fn ely_attach(ctx: *mut Context, doc: *mut Document, name: *const c_char, path: *const c_char) -> c_int;
|
||||
fn ely_detach(ctx: *mut Context, doc: *mut Document, name: *const c_char) -> c_int;
|
||||
}
|
||||
|
||||
impl Mupdf {
|
||||
pub fn outline_edit(&self, doc: &Open, json: &str) -> io::Result<()> {
|
||||
let json = text(json)?;
|
||||
checked(unsafe { ely_outline_edit(self.ctx, doc.0, json.as_ptr()) }).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn links(&self, doc: &Open, at: usize) -> io::Result<Vec<u8>> {
|
||||
let at = page(at)?;
|
||||
self.owned(|data, len| unsafe { ely_links(self.ctx, doc.0, at, data, len) })
|
||||
}
|
||||
|
||||
pub fn link_edit(&self, doc: &Open, at: usize, json: &str) -> io::Result<()> {
|
||||
let json = text(json)?;
|
||||
checked(unsafe { ely_link_edit(self.ctx, doc.0, page(at)?, json.as_ptr()) }).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn metadata(&self, doc: &Open) -> io::Result<Vec<u8>> {
|
||||
self.owned(|data, len| unsafe { ely_metadata(self.ctx, doc.0, data, len) })
|
||||
}
|
||||
|
||||
pub fn set_metadata(&self, doc: &Open, json: &str) -> io::Result<()> {
|
||||
let json = text(json)?;
|
||||
checked(unsafe { ely_set_metadata(self.ctx, doc.0, json.as_ptr()) }).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn attachments(&self, doc: &Open) -> io::Result<Vec<u8>> {
|
||||
self.owned(|data, len| unsafe { ely_attachments(self.ctx, doc.0, data, len) })
|
||||
}
|
||||
|
||||
pub fn attachment(&self, doc: &Open, name: &str) -> io::Result<Vec<u8>> {
|
||||
let name = text(name)?;
|
||||
self.owned(|data, len| unsafe { ely_attachment(self.ctx, doc.0, name.as_ptr(), data, len) })
|
||||
}
|
||||
|
||||
pub fn attach(&self, doc: &Open, name: &str, path: &str) -> io::Result<()> {
|
||||
let (name, path) = (text(name)?, text(path)?);
|
||||
checked(unsafe { ely_attach(self.ctx, doc.0, name.as_ptr(), path.as_ptr()) }).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn detach(&self, doc: &Open, name: &str) -> io::Result<()> {
|
||||
let name = text(name)?;
|
||||
checked(unsafe { ely_detach(self.ctx, doc.0, name.as_ptr()) }).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
//! The app's side: spawn the worker, ask, read the answer.
|
||||
|
||||
use std::{
|
||||
io::{self, BufReader, BufWriter},
|
||||
path::{Path, PathBuf},
|
||||
process::{Child, ChildStdin, ChildStdout, Command, Stdio},
|
||||
};
|
||||
|
||||
use ely_ipc::{Pack, Unpack, check_reply, read_frame, write_frame};
|
||||
|
||||
use crate::proto::{self, Bookmark, Char, tag};
|
||||
|
||||
|
||||
/// The PDF worker process.
|
||||
pub struct Worker {
|
||||
child: Child,
|
||||
stdin: BufWriter<ChildStdin>,
|
||||
stdout: BufReader<ChildStdout>,
|
||||
/// The last request's id; each reply must carry it.
|
||||
asked: u64,
|
||||
/// This worker among all spawned; its documents carry it.
|
||||
session: u64,
|
||||
}
|
||||
|
||||
static SESSIONS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
|
||||
/// An open document: its worker, number and history mark.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Doc {
|
||||
worker: u64,
|
||||
id: u64,
|
||||
/// The history mark this handle was read at.
|
||||
mark: u64,
|
||||
}
|
||||
|
||||
impl Doc {
|
||||
/// This document as of history `mark`, which an edit answered.
|
||||
pub fn at(self, mark: &str) -> io::Result<Doc> {
|
||||
let mark = mark.parse().map_err(|_| io::Error::other(format!("a history mark that is not a number: {mark}")))?;
|
||||
Ok(Doc { mark, ..self })
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `error` refused a request made for an older state.
|
||||
pub fn is_stale(error: &io::Error) -> bool {
|
||||
error.to_string().starts_with(proto::STALE)
|
||||
}
|
||||
|
||||
/// Drawn pixels, BGRA, and whether drawing them met damage.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Tile {
|
||||
pub bgra: Vec<u8>,
|
||||
pub damaged: bool,
|
||||
}
|
||||
|
||||
/// A document just opened: locked until its password is given.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Opened {
|
||||
pub doc: Doc,
|
||||
pub locked: bool,
|
||||
}
|
||||
|
||||
const WORKER: &str = if cfg!(windows) {
|
||||
"ely-pdf-worker.exe"
|
||||
} else {
|
||||
"ely-pdf-worker"
|
||||
};
|
||||
|
||||
/// The worker binary: `ELY_PDF_WORKER`, beside the app, or this build's.
|
||||
fn worker_bin() -> io::Result<PathBuf> {
|
||||
if let Some(bin) = std::env::var_os("ELY_PDF_WORKER") {
|
||||
let bin = PathBuf::from(bin);
|
||||
return match bin.is_file() {
|
||||
true => Ok(bin),
|
||||
false => Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("ELY_PDF_WORKER {} is not there", bin.display()),
|
||||
)),
|
||||
};
|
||||
}
|
||||
let exe = std::env::current_exe()?;
|
||||
let beside = exe.parent().unwrap_or(Path::new(".")).join(WORKER);
|
||||
if beside.is_file() {
|
||||
return Ok(beside);
|
||||
}
|
||||
// Test binaries live one folder below the worker.
|
||||
let above = exe
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.map(|dir| dir.join(WORKER));
|
||||
above.filter(|bin| bin.is_file()).ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"no ely-pdf-worker binary (set ELY_PDF_WORKER)",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
pub fn spawn() -> io::Result<Self> {
|
||||
Self::spawn_from(&worker_bin()?)
|
||||
}
|
||||
|
||||
/// Spawns `bin`; one from other sources is refused.
|
||||
pub fn spawn_from(bin: &Path) -> io::Result<Self> {
|
||||
let mut child = Command::new(bin)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|error| io::Error::new(error.kind(), format!("pdf-worker spawn: {error}")))?;
|
||||
// Its records reach the app log; nothing reads them back.
|
||||
drop(ely_ipc::relay(child.stderr.take().expect("piped stderr"), "pdf-worker"));
|
||||
let stdin = BufWriter::new(child.stdin.take().expect("piped stdin"));
|
||||
let stdout = BufReader::new(child.stdout.take().expect("piped stdout"));
|
||||
let mut worker = Worker {
|
||||
child,
|
||||
stdin,
|
||||
stdout,
|
||||
asked: 0,
|
||||
session: SESSIONS.fetch_add(1, std::sync::atomic::Ordering::SeqCst),
|
||||
};
|
||||
log::info!("pdf: worker from {}", bin.display());
|
||||
let reply = worker.request(tag::HELLO, &[])?;
|
||||
if Unpack::new(&reply).str()? != proto::BUILD {
|
||||
return Err(io::Error::other(format!(
|
||||
"the pdf worker {} is from other sources; run cargo build -p ely-pdf-engine --bins",
|
||||
bin.display()
|
||||
)));
|
||||
}
|
||||
log::info!("pdf: worker up");
|
||||
Ok(worker)
|
||||
}
|
||||
|
||||
/// `doc` as it is now, for tests that chain edits.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn now(&mut self, doc: Doc) -> io::Result<Doc> {
|
||||
let mark = self.history(doc)?;
|
||||
doc.at(&mark)
|
||||
}
|
||||
|
||||
/// Why the worker process ended, if it has.
|
||||
pub fn ended(&mut self) -> Option<String> {
|
||||
match self.child.try_wait() {
|
||||
Ok(status) => status.map(|status| status.to_string()),
|
||||
Err(error) => Some(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// The worker's process id, for resource tests.
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn pid(&self) -> u32 {
|
||||
self.child.id()
|
||||
}
|
||||
|
||||
/// A request aimed at pages or objects `doc` saw.
|
||||
pub(crate) fn on(&self, doc: &Doc) -> io::Result<Pack> {
|
||||
Ok(Pack::new().u64(self.doc_id(doc)?).u64(doc.mark))
|
||||
}
|
||||
|
||||
/// A request on `doc` whatever its state: the whole document.
|
||||
pub(crate) fn whole(&self, doc: &Doc) -> io::Result<Pack> {
|
||||
Ok(Pack::new().u64(self.doc_id(doc)?).u64(proto::ANY))
|
||||
}
|
||||
|
||||
/// `doc`'s number here; another worker's document is refused.
|
||||
fn doc_id(&self, doc: &Doc) -> io::Result<u64> {
|
||||
match doc.worker == self.session {
|
||||
true => Ok(doc.id),
|
||||
false => Err(io::Error::new(io::ErrorKind::NotFound, "a document of another pdf worker")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn request(&mut self, kind: u8, payload: &[u8]) -> io::Result<Vec<u8>> {
|
||||
self.asked += 1;
|
||||
write_frame(&mut self.stdin, kind, self.asked, payload)?;
|
||||
let frame = read_frame(&mut self.stdout)?;
|
||||
if let Some((_, id, _)) = &frame {
|
||||
check_reply(self.asked, *id)?;
|
||||
}
|
||||
match frame {
|
||||
Some((tag::OK, _, reply)) => Ok(reply),
|
||||
Some((tag::ERR, _, reply)) => Err(refusal(String::from_utf8_lossy(&reply).into_owned())),
|
||||
Some((other, _, _)) => Err(io::Error::other(format!("pdf-worker answered tag {other}"))),
|
||||
None => Err(io::Error::other(match self.child.try_wait() {
|
||||
Ok(Some(status)) => format!("the pdf worker exited: {status}"),
|
||||
_ => "the pdf worker closed its pipe".to_string(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens `path`; a locked file waits for `unlock`.
|
||||
pub fn open(&mut self, path: &Path) -> io::Result<Opened> {
|
||||
let path = path
|
||||
.to_str()
|
||||
.ok_or_else(|| io::Error::other("a path that is not UTF-8"))?;
|
||||
let reply = self.request(tag::OPEN, &Pack::new().str(path).bytes())?;
|
||||
let mut reply = Unpack::new(&reply);
|
||||
Ok(Opened {
|
||||
doc: Doc { worker: self.session, id: reply.u64()?, mark: 0 },
|
||||
locked: reply.u64()? != 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Tries `password`; false when it is wrong.
|
||||
pub fn unlock(&mut self, doc: Doc, password: &str) -> io::Result<bool> {
|
||||
let reply = self.request(tag::UNLOCK, &self.whole(&doc)?.str(password).bytes())?;
|
||||
Ok(Unpack::new(&reply).u64()? != 0)
|
||||
}
|
||||
|
||||
pub fn pages(&mut self, doc: Doc) -> io::Result<usize> {
|
||||
let reply = self.request(tag::PAGES, &self.whole(&doc)?.bytes())?;
|
||||
Ok(Unpack::new(&reply).u64()? as usize)
|
||||
}
|
||||
|
||||
/// Page boxes in points, `count` from `first`.
|
||||
pub fn bounds(&mut self, doc: Doc, first: usize, count: usize) -> io::Result<Vec<[f32; 4]>> {
|
||||
let ask = self.on(&doc)?
|
||||
.u64(first as u64)
|
||||
.u64(count as u64)
|
||||
.bytes();
|
||||
let reply = self.request(tag::BOUNDS, &ask)?;
|
||||
let mut reply = Unpack::new(&reply);
|
||||
let mut boxes = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let mut one = [0f32; 4];
|
||||
for value in &mut one {
|
||||
*value = f32::from_bits(reply.u64()? as u32);
|
||||
}
|
||||
boxes.push(one);
|
||||
}
|
||||
match reply.rest().is_empty() {
|
||||
true => Ok(boxes),
|
||||
false => Err(io::Error::other("a bounds reply too long")),
|
||||
}
|
||||
}
|
||||
|
||||
/// A page's device pixels `tile` (x, y, w, h), BGRA.
|
||||
pub fn render(
|
||||
&mut self,
|
||||
doc: Doc,
|
||||
page: usize,
|
||||
(scale, rotate): (f32, i32),
|
||||
tile: [i32; 4],
|
||||
dark: bool,
|
||||
) -> io::Result<Tile> {
|
||||
let mut ask = self.on(&doc)?
|
||||
.u64(page as u64)
|
||||
.u64(u64::from(scale.to_bits()))
|
||||
.i64(i64::from(rotate));
|
||||
for value in tile {
|
||||
ask = ask.i64(i64::from(value));
|
||||
}
|
||||
let reply = self.request(tag::RENDER, &ask.u64(u64::from(dark)).bytes())?;
|
||||
let mut reply = Unpack::new(&reply);
|
||||
let (damaged, bgra) = (reply.u64()? != 0, reply.blob()?);
|
||||
match bgra.len() == tile[2] as usize * tile[3] as usize * 4 {
|
||||
true => Ok(Tile { bgra, damaged }),
|
||||
false => Err(io::Error::other("a tile of the wrong size")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(&mut self, doc: Doc, page: usize) -> io::Result<Vec<Char>> {
|
||||
proto::chars(&self.request(tag::TEXT, &self.on(&doc)?.u64(page as u64).bytes())?)
|
||||
}
|
||||
|
||||
/// Where `needle` is on a page: each hit's quads.
|
||||
pub fn search(&mut self, doc: Doc, page: usize, needle: &str) -> io::Result<Vec<Vec<[f32; 8]>>> {
|
||||
let ask = self.on(&doc)?.u64(page as u64).str(needle).bytes();
|
||||
proto::hits(&self.request(tag::SEARCH, &ask)?)
|
||||
}
|
||||
|
||||
pub fn outline(&mut self, doc: Doc) -> io::Result<Vec<Bookmark>> {
|
||||
let reply = self.request(tag::OUTLINE, &self.whole(&doc)?.bytes())?;
|
||||
proto::outline(&String::from_utf8(reply).map_err(io::Error::other)?)
|
||||
}
|
||||
|
||||
pub fn close(&mut self, doc: Doc) -> io::Result<()> {
|
||||
self.request(tag::CLOSE, &self.whole(&doc)?.bytes())
|
||||
.map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Worker {
|
||||
fn drop(&mut self) {
|
||||
// The worker holds nothing that needs saving.
|
||||
if let Err(error) = self.child.kill() {
|
||||
log::info!("pdf: the worker was already gone: {error}");
|
||||
}
|
||||
if let Err(error) = self.child.wait() {
|
||||
log::warn!("pdf: the worker was not reaped: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A permission refusal is `PermissionDenied`, named by what it needs.
|
||||
fn refusal(text: String) -> io::Error {
|
||||
match text.strip_prefix(crate::proto::may::REFUSED) {
|
||||
Some(needed) => io::Error::new(io::ErrorKind::PermissionDenied, needed.to_string()),
|
||||
None => io::Error::other(text),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
//! Text blocks and drawn objects: read, write, move (PDF-03, PDF-04).
|
||||
|
||||
use std::{io, path::Path};
|
||||
|
||||
use ely_ipc::{Pack, Unpack};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::client::{Doc, Worker};
|
||||
use crate::proto::tag;
|
||||
|
||||
/// A text block as MuPDF reads it, in page points.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct TextBlock {
|
||||
pub rect: [f32; 4],
|
||||
pub text: String,
|
||||
pub font: String,
|
||||
pub size: f32,
|
||||
pub rgb: u32,
|
||||
/// Upright, and every glyph in the page's own content.
|
||||
pub editable: bool,
|
||||
}
|
||||
|
||||
/// An image or form XObject drawn on the page.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Drawn {
|
||||
pub form: bool,
|
||||
pub rect: [f32; 4],
|
||||
}
|
||||
|
||||
/// Text to write, replacing block `block` or new.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Writing {
|
||||
pub block: Option<usize>,
|
||||
/// The block's text as read; a change since refuses.
|
||||
pub expected: String,
|
||||
pub text: String,
|
||||
/// "original", "helvetica", "times", "courier", "cjk-sc" and so on.
|
||||
pub font: String,
|
||||
pub size: f32,
|
||||
pub rgb: u32,
|
||||
/// Page points: left, top, width.
|
||||
pub bx: [f32; 3],
|
||||
}
|
||||
|
||||
/// What writing did, or would do.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Wrote {
|
||||
/// Done; the history mark after it.
|
||||
Done(String),
|
||||
/// The font lacks this character; nothing changed.
|
||||
Missing(char),
|
||||
/// BGRA of the area, its size, whether text grew.
|
||||
Preview(Vec<u8>, (u32, u32), bool),
|
||||
}
|
||||
|
||||
fn rect(value: &Value) -> io::Result<[f32; 4]> {
|
||||
let bad = || io::Error::other(format!("a bad rectangle {value}"));
|
||||
let list = value.as_array().filter(|list| list.len() == 4).ok_or_else(bad)?;
|
||||
let mut out = [0f32; 4];
|
||||
for (at, side) in list.iter().enumerate() {
|
||||
out[at] = side.as_f64().ok_or_else(bad)? as f32;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn list(bytes: &[u8]) -> io::Result<Vec<Value>> {
|
||||
match serde_json::from_slice(bytes).map_err(io::Error::other)? {
|
||||
Value::Array(list) => Ok(list),
|
||||
other => Err(io::Error::other(format!("not a list: {other}"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn block(value: &Value) -> io::Result<TextBlock> {
|
||||
let bad = || io::Error::other(format!("a bad text block {value}"));
|
||||
Ok(TextBlock {
|
||||
rect: rect(&value["rect"])?,
|
||||
text: value["text"].as_str().ok_or_else(bad)?.to_string(),
|
||||
font: value["font"].as_str().ok_or_else(bad)?.to_string(),
|
||||
size: value["size"].as_f64().ok_or_else(bad)? as f32,
|
||||
rgb: value["color"].as_u64().ok_or_else(bad)? as u32,
|
||||
editable: value["editable"].as_bool().ok_or_else(bad)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn floats(ask: Pack, values: &[f32]) -> Pack {
|
||||
values.iter().fold(ask, |ask, value| ask.u64(u64::from(value.to_bits())))
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
pub fn text_blocks(&mut self, doc: Doc, page: usize) -> io::Result<Vec<TextBlock>> {
|
||||
let reply = self.request(tag::TEXT_BLOCKS, &self.on(&doc)?.u64(page as u64).bytes())?;
|
||||
list(&reply)?.iter().map(block).collect()
|
||||
}
|
||||
|
||||
pub fn objects(&mut self, doc: Doc, page: usize) -> io::Result<Vec<Drawn>> {
|
||||
let reply = self.request(tag::OBJECTS, &self.on(&doc)?.u64(page as u64).bytes())?;
|
||||
list(&reply)?
|
||||
.iter()
|
||||
.map(|value| Ok(Drawn { form: value["kind"] == "form", rect: rect(&value["rect"])? }))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Writes `w` on `page`; a `scale` above zero only previews.
|
||||
pub fn put_text(&mut self, doc: Doc, page: usize, w: &Writing, scale: f32) -> io::Result<Wrote> {
|
||||
let block = w.block.map_or(-1, |block| block as i64);
|
||||
let ask = self.on(&doc)?.u64(page as u64).i64(block).str(&w.expected).str(&w.text).str(&w.font);
|
||||
let ask = floats(ask.u64(u64::from(w.size.to_bits())).u64(u64::from(w.rgb)), &w.bx);
|
||||
let reply = self.request(tag::PUT_TEXT, &ask.u64(u64::from(scale.to_bits())).bytes())?;
|
||||
let mut reply = Unpack::new(&reply);
|
||||
match reply.u64()? {
|
||||
0 => Ok(Wrote::Done(reply.str()?)),
|
||||
2 => {
|
||||
let code = u32::try_from(reply.u64()?).map_err(io::Error::other)?;
|
||||
Ok(Wrote::Missing(char::from_u32(code).ok_or_else(|| io::Error::other("a bad missing glyph"))?))
|
||||
}
|
||||
_ => {
|
||||
let size = (reply.u64()? as u32, reply.u64()? as u32);
|
||||
let grew = reply.u64()? != 0;
|
||||
Ok(Wrote::Preview(reply.blob()?, size, grew))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves or scales draw `index` by page-space `t`; the mark.
|
||||
pub fn place(&mut self, doc: Doc, page: usize, index: usize, t: [f32; 6]) -> io::Result<String> {
|
||||
self.placing(doc, page, index, t, false, "")
|
||||
}
|
||||
|
||||
pub fn delete_object(&mut self, doc: Doc, page: usize, index: usize) -> io::Result<String> {
|
||||
self.placing(doc, page, index, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0], true, "")
|
||||
}
|
||||
|
||||
/// Swaps image `index` for `path`, fitted in its box.
|
||||
pub fn replace_image(&mut self, doc: Doc, page: usize, index: usize, path: &Path) -> io::Result<String> {
|
||||
let path = path.to_str().ok_or_else(|| io::Error::other("a path that is not UTF-8"))?;
|
||||
self.placing(doc, page, index, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0], false, path)
|
||||
}
|
||||
|
||||
fn placing(&mut self, doc: Doc, page: usize, index: usize, t: [f32; 6], drop: bool, swap: &str) -> io::Result<String> {
|
||||
let ask = floats(self.on(&doc)?.u64(page as u64).u64(index as u64), &t);
|
||||
let reply = self.request(tag::PLACE, &ask.u64(u64::from(drop)).str(swap).bytes())?;
|
||||
Unpack::new(&reply).str()
|
||||
}
|
||||
|
||||
/// Draws the image at `path` into page-space `rect`; the mark.
|
||||
pub fn add_image(&mut self, doc: Doc, page: usize, path: &Path, rect: [f32; 4]) -> io::Result<String> {
|
||||
let path = path.to_str().ok_or_else(|| io::Error::other("a path that is not UTF-8"))?;
|
||||
let ask = floats(self.on(&doc)?.u64(page as u64).str(path), &rect);
|
||||
Unpack::new(&self.request(tag::ADD_IMAGE, &ask.bytes())?).str()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//! Page edits, undo and saving (PDF-02).
|
||||
|
||||
use std::{io, path::Path};
|
||||
|
||||
use ely_ipc::{Pack, Unpack};
|
||||
|
||||
use crate::client::{Doc, Worker};
|
||||
use crate::proto::tag;
|
||||
|
||||
/// `ask` (the document) then sorted, distinct `pages`.
|
||||
fn with_pages(ask: Pack, pages: &[usize]) -> Pack {
|
||||
pages
|
||||
.iter()
|
||||
.fold(ask.u64(pages.len() as u64), |ask, page| ask.u64(*page as u64))
|
||||
}
|
||||
|
||||
fn utf8(path: &Path) -> io::Result<&str> {
|
||||
path.to_str()
|
||||
.ok_or_else(|| io::Error::other("a path that is not UTF-8"))
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
/// The history mark: equal marks, equal documents.
|
||||
pub fn history(&mut self, doc: Doc) -> io::Result<String> {
|
||||
self.mark(tag::HISTORY, self.whole(&doc)?)
|
||||
}
|
||||
|
||||
fn mark(&mut self, kind: u8, ask: Pack) -> io::Result<String> {
|
||||
Unpack::new(&self.request(kind, &ask.bytes())?).str()
|
||||
}
|
||||
|
||||
/// Undoes, or redoes, one edit.
|
||||
pub fn undo(&mut self, doc: Doc, redo: bool) -> io::Result<String> {
|
||||
self.mark(tag::UNDO, self.whole(&doc)?.u64(u64::from(redo)))
|
||||
}
|
||||
|
||||
pub fn delete_pages(&mut self, doc: Doc, pages: &[usize]) -> io::Result<String> {
|
||||
self.mark(tag::DELETE, with_pages(self.on(&doc)?, pages))
|
||||
}
|
||||
|
||||
/// Moves `pages` before page `to`, keeping their order.
|
||||
pub fn move_pages(&mut self, doc: Doc, pages: &[usize], to: usize) -> io::Result<String> {
|
||||
self.mark(tag::MOVE, with_pages(self.on(&doc)?, pages).u64(to as u64))
|
||||
}
|
||||
|
||||
/// Turns `pages` clockwise by `by` degrees, a multiple of 90.
|
||||
pub fn rotate_pages(&mut self, doc: Doc, pages: &[usize], by: i32) -> io::Result<String> {
|
||||
self.mark(tag::ROTATE, with_pages(self.on(&doc)?, pages).i64(i64::from(by)))
|
||||
}
|
||||
|
||||
/// Crops by margins as seen: top, right, bottom, left points.
|
||||
pub fn crop_pages(&mut self, doc: Doc, pages: &[usize], margins: [f32; 4]) -> io::Result<String> {
|
||||
let ask = margins
|
||||
.iter()
|
||||
.fold(with_pages(self.on(&doc)?, pages), |ask, side| ask.u64(u64::from(side.to_bits())));
|
||||
self.mark(tag::CROP, ask)
|
||||
}
|
||||
|
||||
/// A blank page of `size` points at `at`.
|
||||
pub fn insert_blank(&mut self, doc: Doc, at: usize, (w, h): (f32, f32)) -> io::Result<String> {
|
||||
let ask = self.on(&doc)?.u64(at as u64);
|
||||
self.mark(tag::BLANK, ask.u64(u64::from(w.to_bits())).u64(u64::from(h.to_bits())))
|
||||
}
|
||||
|
||||
/// Inserts every page of `path`; answers count and mark.
|
||||
pub fn insert_file(&mut self, doc: Doc, at: usize, path: &Path) -> io::Result<(usize, String)> {
|
||||
let ask = self.on(&doc)?.u64(at as u64).str(utf8(path)?).bytes();
|
||||
let reply = self.request(tag::INSERT, &ask)?;
|
||||
let mut reply = Unpack::new(&reply);
|
||||
Ok((reply.u64()? as usize, reply.str()?))
|
||||
}
|
||||
|
||||
/// Writes only `pages` to `path`; the document stays.
|
||||
pub fn extract(&mut self, doc: Doc, pages: &[usize], path: &Path) -> io::Result<()> {
|
||||
let ask = with_pages(self.on(&doc)?, pages).str(utf8(path)?).bytes();
|
||||
self.request(tag::EXTRACT, &ask).map(|_| ())
|
||||
}
|
||||
|
||||
/// Writes the document, compacted, to `path`.
|
||||
pub fn save(&mut self, doc: Doc, path: &Path) -> io::Result<()> {
|
||||
let ask = self.whole(&doc)?.str(utf8(path)?).bytes();
|
||||
self.request(tag::SAVE, &ask).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//! MuPDF in its own process: protocol and client (PDF-01).
|
||||
|
||||
mod client;
|
||||
mod content;
|
||||
mod edit;
|
||||
mod marks;
|
||||
mod ocr;
|
||||
mod output;
|
||||
mod redact;
|
||||
mod secure;
|
||||
mod sign;
|
||||
mod structure;
|
||||
pub mod proto;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod sample;
|
||||
|
||||
pub use client::{Doc, Opened, Tile, Worker, is_stale};
|
||||
pub use content::{Drawn, TextBlock, Writing, Wrote};
|
||||
pub use ocr::{OcrLine, tessdata};
|
||||
pub use output::Optimizing;
|
||||
pub use redact::{Cleaning, Reach, Redacted, Region};
|
||||
pub use secure::Lock;
|
||||
pub use sign::{Digest, Signature, Signed, Signing};
|
||||
pub use structure::{Attachment, BookmarkChange, Link, LinkChange, Properties, Target};
|
||||
pub use marks::{Annot, Field, FieldKind, Marks, NewAnnot, NewField, Shape};
|
||||
pub use proto::{Bookmark, Char};
|
||||
@@ -0,0 +1,267 @@
|
||||
//! Annotations and form fields: read, add, change (PDF-05, PDF-06).
|
||||
|
||||
use std::io;
|
||||
|
||||
use ely_ipc::Unpack;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::client::{Doc, Worker};
|
||||
use crate::proto::tag;
|
||||
|
||||
/// An annotation as the page holds it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Annot {
|
||||
/// The object number; stays through edits.
|
||||
pub id: u64,
|
||||
/// The PDF subtype: Highlight, Text, Ink and so on.
|
||||
pub kind: String,
|
||||
pub rect: [f32; 4],
|
||||
pub rgb: Option<u32>,
|
||||
pub contents: String,
|
||||
pub author: String,
|
||||
pub icon: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FieldKind {
|
||||
Text,
|
||||
Checkbox,
|
||||
Radio,
|
||||
Choice,
|
||||
List,
|
||||
Signature,
|
||||
Button,
|
||||
}
|
||||
|
||||
/// One widget of a form field.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Field {
|
||||
pub id: u64,
|
||||
pub kind: FieldKind,
|
||||
pub name: String,
|
||||
pub rect: [f32; 4],
|
||||
pub value: String,
|
||||
/// A button's on state.
|
||||
pub on: String,
|
||||
pub checked: bool,
|
||||
pub readonly: bool,
|
||||
/// Choice values, and what each shows.
|
||||
pub options: Vec<String>,
|
||||
pub labels: Vec<String>,
|
||||
}
|
||||
|
||||
/// What a page holds besides content.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Marks {
|
||||
pub annots: Vec<Annot>,
|
||||
pub fields: Vec<Field>,
|
||||
}
|
||||
|
||||
/// A new annotation's shape, in page points.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Shape {
|
||||
Highlight(Vec<[f32; 4]>),
|
||||
Underline(Vec<[f32; 4]>),
|
||||
StrikeOut(Vec<[f32; 4]>),
|
||||
Note((f32, f32)),
|
||||
Square([f32; 4]),
|
||||
Circle([f32; 4]),
|
||||
Line([f32; 4]),
|
||||
Ink(Vec<Vec<(f32, f32)>>),
|
||||
Stamp([f32; 4], String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct NewAnnot {
|
||||
pub page: usize,
|
||||
pub shape: Shape,
|
||||
pub rgb: Option<u32>,
|
||||
pub contents: String,
|
||||
pub author: String,
|
||||
}
|
||||
|
||||
/// A new field; radios of one name form a group.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct NewField {
|
||||
pub kind: FieldKind,
|
||||
pub name: String,
|
||||
pub rect: [f32; 4],
|
||||
pub options: Vec<String>,
|
||||
}
|
||||
|
||||
impl Field {
|
||||
/// The last part of the name, which a rename sets.
|
||||
pub fn leaf(&self) -> &str {
|
||||
self.name.rfind('.').map_or(self.name.as_str(), |dot| &self.name[dot + 1..])
|
||||
}
|
||||
}
|
||||
|
||||
impl FieldKind {
|
||||
fn name(self) -> &'static str {
|
||||
match self {
|
||||
FieldKind::Text => "text",
|
||||
FieldKind::Checkbox => "checkbox",
|
||||
FieldKind::Radio => "radio",
|
||||
FieldKind::Choice => "choice",
|
||||
FieldKind::List => "list",
|
||||
FieldKind::Signature => "signature",
|
||||
FieldKind::Button => "button",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn spec(annot: &NewAnnot) -> Value {
|
||||
let (kind, geometry) = match &annot.shape {
|
||||
Shape::Highlight(rects) => ("highlight", json!({ "rects": rects })),
|
||||
Shape::Underline(rects) => ("underline", json!({ "rects": rects })),
|
||||
Shape::StrikeOut(rects) => ("strikeout", json!({ "rects": rects })),
|
||||
Shape::Note(at) => ("note", json!({ "at": [at.0, at.1] })),
|
||||
Shape::Square(rect) => ("square", json!({ "rect": rect })),
|
||||
Shape::Circle(rect) => ("circle", json!({ "rect": rect })),
|
||||
Shape::Line(line) => ("line", json!({ "line": line })),
|
||||
Shape::Ink(strokes) => {
|
||||
let flat: Vec<Vec<f32>> = strokes.iter().map(|s| s.iter().flat_map(|p| [p.0, p.1]).collect()).collect();
|
||||
("ink", json!({ "strokes": flat }))
|
||||
}
|
||||
Shape::Stamp(rect, icon) => ("stamp", json!({ "rect": rect, "icon": icon })),
|
||||
};
|
||||
let mut spec = json!({ "page": annot.page, "kind": kind, "contents": annot.contents, "author": annot.author });
|
||||
if let Some(rgb) = annot.rgb {
|
||||
spec["color"] = json!(rgb);
|
||||
}
|
||||
spec.as_object_mut().expect("an object").extend(geometry.as_object().expect("an object").clone());
|
||||
spec
|
||||
}
|
||||
|
||||
fn bad(what: &str, value: &Value) -> io::Error {
|
||||
io::Error::other(format!("a bad {what} {value}"))
|
||||
}
|
||||
|
||||
fn rect(value: &Value) -> io::Result<[f32; 4]> {
|
||||
let list = value.as_array().filter(|list| list.len() == 4).ok_or_else(|| bad("rectangle", value))?;
|
||||
let mut out = [0f32; 4];
|
||||
for (at, side) in list.iter().enumerate() {
|
||||
out[at] = side.as_f64().ok_or_else(|| bad("rectangle", value))? as f32;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn text(value: &Value, key: &str) -> io::Result<String> {
|
||||
value[key].as_str().map(str::to_string).ok_or_else(|| bad(key, value))
|
||||
}
|
||||
|
||||
fn texts(value: &Value, key: &str) -> io::Result<Vec<String>> {
|
||||
let list = value[key].as_array().ok_or_else(|| bad(key, value))?;
|
||||
list.iter().map(|item| item.as_str().map(str::to_string).ok_or_else(|| bad(key, value))).collect()
|
||||
}
|
||||
|
||||
fn annot(value: &Value) -> io::Result<Annot> {
|
||||
let color = value["color"].as_i64().ok_or_else(|| bad("annotation", value))?;
|
||||
Ok(Annot {
|
||||
id: value["id"].as_u64().ok_or_else(|| bad("annotation", value))?,
|
||||
kind: text(value, "kind")?,
|
||||
rect: rect(&value["rect"])?,
|
||||
rgb: u32::try_from(color).ok(),
|
||||
contents: text(value, "contents")?,
|
||||
author: text(value, "author")?,
|
||||
icon: text(value, "icon")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn field(value: &Value) -> io::Result<Field> {
|
||||
let kind = match value["kind"].as_str() {
|
||||
Some("text") => FieldKind::Text,
|
||||
Some("checkbox") => FieldKind::Checkbox,
|
||||
Some("radio") => FieldKind::Radio,
|
||||
Some("choice") => FieldKind::Choice,
|
||||
Some("list") => FieldKind::List,
|
||||
Some("signature") => FieldKind::Signature,
|
||||
Some("button") => FieldKind::Button,
|
||||
_ => return Err(bad("field", value)),
|
||||
};
|
||||
let flag = |key: &str| value[key].as_bool().ok_or_else(|| bad(key, value));
|
||||
Ok(Field {
|
||||
id: value["id"].as_u64().ok_or_else(|| bad("field", value))?,
|
||||
kind,
|
||||
name: text(value, "name")?,
|
||||
rect: rect(&value["rect"])?,
|
||||
value: text(value, "value")?,
|
||||
on: text(value, "on")?,
|
||||
checked: flag("checked")?,
|
||||
readonly: flag("readonly")?,
|
||||
options: texts(value, "options")?,
|
||||
labels: texts(value, "labels")?,
|
||||
})
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
pub fn marks(&mut self, doc: Doc, page: usize) -> io::Result<Marks> {
|
||||
let reply = self.request(tag::ANNOTS, &self.on(&doc)?.u64(page as u64).bytes())?;
|
||||
let value: Value = serde_json::from_slice(&reply).map_err(io::Error::other)?;
|
||||
let list = |key: &str| value[key].as_array().ok_or_else(|| bad(key, &value));
|
||||
Ok(Marks {
|
||||
annots: list("annots")?.iter().map(annot).collect::<io::Result<_>>()?,
|
||||
fields: list("fields")?.iter().map(field).collect::<io::Result<_>>()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Every field name in the form, widgets or not.
|
||||
pub fn field_names(&mut self, doc: Doc) -> io::Result<Vec<String>> {
|
||||
let reply = self.request(tag::FIELD_NAMES, &self.whole(&doc)?.bytes())?;
|
||||
let value: Value = serde_json::from_slice(&reply).map_err(io::Error::other)?;
|
||||
let list = value.as_array().ok_or_else(|| bad("name list", &value))?;
|
||||
list.iter().map(|name| name.as_str().map(str::to_string).ok_or_else(|| bad("field name", name))).collect()
|
||||
}
|
||||
|
||||
/// Adds `annots` as one step; the mark.
|
||||
pub fn annotate(&mut self, doc: Doc, annots: &[NewAnnot]) -> io::Result<String> {
|
||||
let specs = Value::Array(annots.iter().map(spec).collect());
|
||||
Unpack::new(&self.request(tag::ANNOTATE, &self.on(&doc)?.str(&specs.to_string()).bytes())?).str()
|
||||
}
|
||||
|
||||
fn on_object(&mut self, kind: u8, doc: Doc, page: usize, id: u64, value: &str) -> io::Result<String> {
|
||||
let ask = self.on(&doc)?.u64(page as u64).u64(id).str(value);
|
||||
Unpack::new(&self.request(kind, &ask.bytes())?).str()
|
||||
}
|
||||
|
||||
pub fn change_annot(&mut self, doc: Doc, page: usize, id: u64, rgb: Option<u32>, contents: Option<&str>) -> io::Result<String> {
|
||||
let mut change = json!({});
|
||||
if let Some(rgb) = rgb {
|
||||
change["color"] = json!(rgb);
|
||||
}
|
||||
if let Some(contents) = contents {
|
||||
change["contents"] = json!(contents);
|
||||
}
|
||||
self.on_object(tag::CHANGE_ANNOT, doc, page, id, &change.to_string())
|
||||
}
|
||||
|
||||
pub fn delete_annot(&mut self, doc: Doc, page: usize, id: u64) -> io::Result<String> {
|
||||
self.on_object(tag::DELETE_ANNOT, doc, page, id, "annot")
|
||||
}
|
||||
|
||||
pub fn delete_field(&mut self, doc: Doc, page: usize, id: u64) -> io::Result<String> {
|
||||
self.on_object(tag::DELETE_ANNOT, doc, page, id, "field")
|
||||
}
|
||||
|
||||
/// Sets a value; buttons take their on state or Off.
|
||||
pub fn fill(&mut self, doc: Doc, page: usize, id: u64, value: &str) -> io::Result<String> {
|
||||
self.on_object(tag::FILL, doc, page, id, value)
|
||||
}
|
||||
|
||||
pub fn change_field(&mut self, doc: Doc, page: usize, id: u64, name: Option<&str>, options: Option<&[String]>) -> io::Result<String> {
|
||||
let mut change = json!({});
|
||||
if let Some(name) = name {
|
||||
change["name"] = json!(name);
|
||||
}
|
||||
if let Some(options) = options {
|
||||
change["options"] = json!(options);
|
||||
}
|
||||
self.on_object(tag::CHANGE_FIELD, doc, page, id, &change.to_string())
|
||||
}
|
||||
|
||||
pub fn add_field(&mut self, doc: Doc, page: usize, field: &NewField) -> io::Result<String> {
|
||||
let spec = json!({ "kind": field.kind.name(), "name": field.name, "rect": field.rect, "options": field.options });
|
||||
let ask = self.on(&doc)?.u64(page as u64).str(&spec.to_string());
|
||||
Unpack::new(&self.request(tag::ADD_FIELD, &ask.bytes())?).str()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//! OCR: read pages, then lay their text in hidden (PDF-08).
|
||||
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use ely_ipc::Unpack;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::client::{Doc, Worker};
|
||||
use crate::proto::tag;
|
||||
|
||||
/// A line Tesseract read, in page points.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct OcrLine {
|
||||
pub rect: [f32; 4],
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// Beside the app, in its macOS bundle, or this build's.
|
||||
fn beside(dir: &Path) -> PathBuf {
|
||||
[dir.join("tessdata"), dir.join("../Resources/tessdata")]
|
||||
.into_iter()
|
||||
.find(|found| found.is_dir())
|
||||
.unwrap_or_else(|| Path::new(env!("CARGO_MANIFEST_DIR")).join("../../engines/tessdata"))
|
||||
}
|
||||
|
||||
/// The language data: `ELY_TESSDATA`, the app's, or this build's.
|
||||
pub fn tessdata() -> io::Result<PathBuf> {
|
||||
let found = match std::env::var_os("ELY_TESSDATA") {
|
||||
Some(dir) => PathBuf::from(dir),
|
||||
None => {
|
||||
let exe = std::env::current_exe()?;
|
||||
beside(exe.parent().unwrap_or(Path::new(".")))
|
||||
}
|
||||
};
|
||||
match found.join("eng.traineddata").is_file() {
|
||||
true => Ok(found),
|
||||
false => Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("no OCR data in {} (run scripts/fetch-tessdata.sh or set ELY_TESSDATA)", found.display()),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn line(value: &Value) -> io::Result<OcrLine> {
|
||||
let bad = || io::Error::other(format!("a bad OCR line {value}"));
|
||||
let list = value["rect"].as_array().filter(|list| list.len() == 4).ok_or_else(bad)?;
|
||||
let mut rect = [0f32; 4];
|
||||
for (at, side) in list.iter().enumerate() {
|
||||
rect[at] = side.as_f64().ok_or_else(bad)? as f32;
|
||||
}
|
||||
Ok(OcrLine { rect, text: value["text"].as_str().ok_or_else(bad)?.to_string() })
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
/// Reads `page` in `langs` (as "eng+chi_sim") at `dpi`.
|
||||
pub fn ocr_page(&mut self, doc: Doc, page: usize, langs: &str, data: &Path, dpi: f32) -> io::Result<Vec<OcrLine>> {
|
||||
let data = data.to_str().ok_or_else(|| io::Error::other("a data path that is not UTF-8"))?;
|
||||
let ask = self.on(&doc)?.u64(page as u64).str(langs).str(data).u64(u64::from(dpi.to_bits()));
|
||||
let reply: Value = serde_json::from_slice(&self.request(tag::OCR_PAGE, &ask.bytes())?).map_err(io::Error::other)?;
|
||||
reply.as_array().ok_or_else(|| io::Error::other("an OCR reply that is not a list"))?.iter().map(line).collect()
|
||||
}
|
||||
|
||||
/// Lays `pages` in hidden text, one step; the mark.
|
||||
pub fn text_layer(&mut self, doc: Doc, pages: &[(usize, Vec<OcrLine>)], replace: bool) -> io::Result<String> {
|
||||
let spec: Vec<Value> = (pages.iter())
|
||||
.map(|(page, lines)| json!({ "page": page, "lines": lines.iter().map(|l| json!({ "rect": l.rect, "text": l.text })).collect::<Vec<_>>() }))
|
||||
.collect();
|
||||
let ask = self.on(&doc)?.str(&Value::Array(spec).to_string()).u64(u64::from(replace));
|
||||
Unpack::new(&self.request(tag::TEXT_LAYER, &ask.bytes())?).str()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::beside;
|
||||
|
||||
#[test]
|
||||
fn a_bundle_finds_its_own_data() {
|
||||
let app = tempfile::tempdir().expect("a temp dir");
|
||||
let (exe, data) = (app.path().join("Contents/MacOS"), app.path().join("Contents/Resources/tessdata"));
|
||||
std::fs::create_dir_all(&exe).expect("MacOS");
|
||||
assert!(beside(&exe).ends_with("engines/tessdata"));
|
||||
std::fs::create_dir_all(&data).expect("tessdata");
|
||||
assert_eq!(beside(&exe), exe.join("../Resources/tessdata"));
|
||||
std::fs::create_dir(exe.join("tessdata")).expect("beside");
|
||||
assert_eq!(beside(&exe), exe.join("tessdata"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//! Page images, text, optimized copies, repair reports (PDF-13, PDF-14).
|
||||
|
||||
use std::{io, path::Path};
|
||||
|
||||
use ely_ipc::Unpack;
|
||||
|
||||
use crate::client::{Doc, Worker};
|
||||
use crate::proto::tag;
|
||||
|
||||
/// How an optimized copy is written.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Optimizing {
|
||||
pub compress: bool,
|
||||
/// Images above `from` dpi go to `to`; 0 keeps them.
|
||||
pub from: u32,
|
||||
pub to: u32,
|
||||
}
|
||||
|
||||
fn utf8(path: &Path) -> io::Result<&str> {
|
||||
path.to_str().ok_or_else(|| io::Error::other("a path that is not UTF-8"))
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
/// Page `page` as a PNG at `dpi`, white as printed.
|
||||
pub fn export_image(&mut self, doc: Doc, page: usize, dpi: f32, path: &Path) -> io::Result<()> {
|
||||
let ask = self.on(&doc)?.u64(page as u64).u64(u64::from(dpi.to_bits())).str(utf8(path)?);
|
||||
self.request(tag::EXPORT_IMAGE, &ask.bytes()).map(|_| ())
|
||||
}
|
||||
|
||||
/// Every page's text, UTF-8; form feeds part the pages.
|
||||
pub fn export_text(&mut self, doc: Doc, path: &Path) -> io::Result<()> {
|
||||
self.request(tag::EXPORT_TEXT, &self.whole(&doc)?.str(utf8(path)?).bytes()).map(|_| ())
|
||||
}
|
||||
|
||||
pub fn optimize(&mut self, doc: Doc, out: &Path, how: Optimizing) -> io::Result<()> {
|
||||
let ask = self.whole(&doc)?.str(utf8(out)?).u64(u64::from(how.compress)).u64(u64::from(how.from)).u64(u64::from(how.to));
|
||||
self.request(tag::OPTIMIZE, &ask.bytes()).map(|_| ())
|
||||
}
|
||||
|
||||
/// The document as it is now, written to print.
|
||||
pub fn print_copy(&mut self, doc: Doc, out: &Path) -> io::Result<()> {
|
||||
self.request(tag::PRINT_COPY, &self.whole(&doc)?.str(utf8(out)?).bytes()).map(|_| ())
|
||||
}
|
||||
|
||||
/// Decodes every picture; true if any failed.
|
||||
pub fn check_pictures(&mut self, doc: Doc) -> io::Result<bool> {
|
||||
let reply = self.request(tag::CHECK_PICTURES, &self.whole(&doc)?.bytes())?;
|
||||
Ok(Unpack::new(&reply).u64()? != 0)
|
||||
}
|
||||
|
||||
/// MuPDF's warnings if the file was repaired to open.
|
||||
pub fn repair_report(&mut self, doc: Doc) -> io::Result<Option<String>> {
|
||||
let reply = self.request(tag::REPAIR_REPORT, &self.whole(&doc)?.bytes())?;
|
||||
let mut reply = Unpack::new(&reply);
|
||||
Ok(match reply.u64()? {
|
||||
0 => None,
|
||||
_ => Some(reply.str()?),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
//! What the app asks the PDF worker, and the answers.
|
||||
|
||||
use std::io;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
/// Request tags; replies are OK or ERR.
|
||||
pub mod tag {
|
||||
pub const OPEN: u8 = 1;
|
||||
pub const UNLOCK: u8 = 2;
|
||||
pub const PAGES: u8 = 3;
|
||||
pub const BOUNDS: u8 = 4;
|
||||
pub const RENDER: u8 = 5;
|
||||
pub const TEXT: u8 = 6;
|
||||
pub const SEARCH: u8 = 7;
|
||||
pub const OUTLINE: u8 = 8;
|
||||
pub const CLOSE: u8 = 9;
|
||||
/// The worker's build stamp, checked on spawn.
|
||||
pub const HELLO: u8 = 10;
|
||||
/// Edits answer with the history mark after them.
|
||||
pub const HISTORY: u8 = 11;
|
||||
pub const UNDO: u8 = 12;
|
||||
pub const DELETE: u8 = 13;
|
||||
pub const MOVE: u8 = 14;
|
||||
pub const ROTATE: u8 = 15;
|
||||
pub const CROP: u8 = 16;
|
||||
pub const BLANK: u8 = 17;
|
||||
pub const INSERT: u8 = 18;
|
||||
pub const EXTRACT: u8 = 19;
|
||||
pub const SAVE: u8 = 20;
|
||||
pub const TEXT_BLOCKS: u8 = 21;
|
||||
pub const OBJECTS: u8 = 22;
|
||||
/// Writes text; with a scale, only previews it.
|
||||
pub const PUT_TEXT: u8 = 23;
|
||||
pub const PLACE: u8 = 24;
|
||||
pub const ADD_IMAGE: u8 = 25;
|
||||
/// A page's annotations and fields, as JSON.
|
||||
pub const ANNOTS: u8 = 26;
|
||||
pub const ANNOTATE: u8 = 27;
|
||||
pub const CHANGE_ANNOT: u8 = 28;
|
||||
pub const DELETE_ANNOT: u8 = 29;
|
||||
pub const ADD_FIELD: u8 = 30;
|
||||
pub const FILL: u8 = 31;
|
||||
pub const CHANGE_FIELD: u8 = 32;
|
||||
/// Every field name in the form.
|
||||
pub const FIELD_NAMES: u8 = 33;
|
||||
/// Reads one page's lines; changes nothing.
|
||||
pub const OCR_PAGE: u8 = 34;
|
||||
pub const TEXT_LAYER: u8 = 35;
|
||||
pub const OUTLINE_EDIT: u8 = 36;
|
||||
pub const LINKS: u8 = 37;
|
||||
pub const LINK_EDIT: u8 = 38;
|
||||
pub const METADATA: u8 = 39;
|
||||
pub const SET_METADATA: u8 = 40;
|
||||
pub const ATTACHMENTS: u8 = 41;
|
||||
/// An attachment's bytes.
|
||||
pub const ATTACHMENT: u8 = 42;
|
||||
pub const ATTACH: u8 = 43;
|
||||
pub const DETACH: u8 = 44;
|
||||
/// Writes a new file with regions removed, then checks it.
|
||||
pub const REDACT: u8 = 45;
|
||||
pub const CLEAN_SCAN: u8 = 46;
|
||||
pub const CLEAN: u8 = 47;
|
||||
pub const PERMISSIONS: u8 = 48;
|
||||
/// Tries an owner password; a wrong one changes nothing.
|
||||
pub const OWNER: u8 = 49;
|
||||
pub const PROTECT: u8 = 50;
|
||||
/// A file's signature fields and their checks, as JSON.
|
||||
pub const SIGNATURES: u8 = 51;
|
||||
/// Writes a signed copy; the document is untouched.
|
||||
pub const SIGN: u8 = 52;
|
||||
/// How many fields hold a signature; nothing checked.
|
||||
pub const SIGNED: u8 = 53;
|
||||
pub const EXPORT_IMAGE: u8 = 54;
|
||||
pub const EXPORT_TEXT: u8 = 55;
|
||||
/// Writes a copy compressed and downsampled.
|
||||
pub const OPTIMIZE: u8 = 56;
|
||||
/// Writes a copy to print; needs leave to print.
|
||||
pub const PRINT_COPY: u8 = 57;
|
||||
/// What MuPDF warned when it repaired the file.
|
||||
pub const REPAIR_REPORT: u8 = 58;
|
||||
/// Decodes every picture; answers whether any failed.
|
||||
pub const CHECK_PICTURES: u8 = 59;
|
||||
pub const OK: u8 = 100;
|
||||
pub const ERR: u8 = 101;
|
||||
}
|
||||
|
||||
/// What a document allows, as Ely's bits.
|
||||
pub mod may {
|
||||
pub const PRINT: u32 = 1;
|
||||
pub const COPY: u32 = 2;
|
||||
pub const EDIT: u32 = 4;
|
||||
pub const ANNOTATE: u32 = 8;
|
||||
pub const FILL: u32 = 16;
|
||||
pub const ASSEMBLE: u32 = 32;
|
||||
/// Opened with the owner password, or not protected.
|
||||
pub const OWNER: u32 = 64;
|
||||
pub const NAMES: [(u32, &str); 7] = [
|
||||
(PRINT, "print"),
|
||||
(COPY, "copy"),
|
||||
(EDIT, "edit"),
|
||||
(ANNOTATE, "annotate"),
|
||||
(FILL, "fill"),
|
||||
(ASSEMBLE, "assemble"),
|
||||
(OWNER, "owner"),
|
||||
];
|
||||
/// A refusal's error text starts with this.
|
||||
pub const REFUSED: &str = "restricted:";
|
||||
}
|
||||
|
||||
/// The stamp of the sources this was built from.
|
||||
pub const BUILD: &str = env!("ELY_PDF_BUILD");
|
||||
|
||||
/// One character on a page, in reading order.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Char {
|
||||
/// Which text line it is on, counted over the page.
|
||||
pub line: i32,
|
||||
pub c: char,
|
||||
/// x0, y0, x1, y1 in page points.
|
||||
pub rect: [f32; 4],
|
||||
}
|
||||
|
||||
/// Bytes a character takes in a TEXT reply.
|
||||
pub const CHAR_BYTES: usize = 24;
|
||||
|
||||
/// Parses a TEXT reply: line, code point, four box floats.
|
||||
pub fn chars(bytes: &[u8]) -> io::Result<Vec<Char>> {
|
||||
if !bytes.len().is_multiple_of(CHAR_BYTES) {
|
||||
return Err(io::Error::other("a text reply of odd length"));
|
||||
}
|
||||
let word =
|
||||
|at: &[u8], n: usize| <[u8; 4]>::try_from(&at[n * 4..n * 4 + 4]).expect("four bytes");
|
||||
bytes
|
||||
.chunks_exact(CHAR_BYTES)
|
||||
.map(|one| {
|
||||
let code = u32::from_ne_bytes(word(one, 1));
|
||||
Ok(Char {
|
||||
line: i32::from_ne_bytes(word(one, 0)),
|
||||
c: char::from_u32(code)
|
||||
.ok_or_else(|| io::Error::other(format!("no character {code}")))?,
|
||||
rect: [2, 3, 4, 5].map(|n| f32::from_ne_bytes(word(one, n))),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Parses a SEARCH reply: per hit, count then quads.
|
||||
pub fn hits(bytes: &[u8]) -> io::Result<Vec<Vec<[f32; 8]>>> {
|
||||
let short = || io::Error::other("a short search reply");
|
||||
let (mut hits, mut at) = (Vec::new(), 0);
|
||||
let word = |at: &mut usize| -> io::Result<[u8; 4]> {
|
||||
let four = bytes.get(*at..*at + 4).ok_or_else(short)?;
|
||||
*at += 4;
|
||||
Ok(four.try_into().expect("four bytes"))
|
||||
};
|
||||
while at < bytes.len() {
|
||||
let count = usize::try_from(i32::from_ne_bytes(word(&mut at)?)).map_err(|_| short())?;
|
||||
let mut quads = Vec::with_capacity(count);
|
||||
for _ in 0..count {
|
||||
let mut quad = [0f32; 8];
|
||||
for value in &mut quad {
|
||||
*value = f32::from_ne_bytes(word(&mut at)?);
|
||||
}
|
||||
quads.push(quad);
|
||||
}
|
||||
hits.push(quads);
|
||||
}
|
||||
Ok(hits)
|
||||
}
|
||||
|
||||
/// One bookmark and those under it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Bookmark {
|
||||
pub title: String,
|
||||
/// Its page from zero; none for outside links.
|
||||
pub page: Option<usize>,
|
||||
/// Where on that page, in points from the top.
|
||||
pub y: f32,
|
||||
pub down: Vec<Bookmark>,
|
||||
}
|
||||
|
||||
fn bookmark(value: &Value) -> io::Result<Bookmark> {
|
||||
let bad = || io::Error::other(format!("a bad bookmark {value}"));
|
||||
let page = value["page"].as_i64().ok_or_else(bad)?;
|
||||
Ok(Bookmark {
|
||||
title: value["title"].as_str().ok_or_else(bad)?.to_string(),
|
||||
page: usize::try_from(page).ok(),
|
||||
y: value["y"].as_f64().ok_or_else(bad)? as f32,
|
||||
down: bookmarks(&value["down"])?,
|
||||
})
|
||||
}
|
||||
|
||||
fn bookmarks(value: &Value) -> io::Result<Vec<Bookmark>> {
|
||||
let list = value
|
||||
.as_array()
|
||||
.ok_or_else(|| io::Error::other(format!("no bookmark list in {value}")))?;
|
||||
list.iter().map(bookmark).collect()
|
||||
}
|
||||
|
||||
/// Parses an OUTLINE reply.
|
||||
pub fn outline(json: &str) -> io::Result<Vec<Bookmark>> {
|
||||
bookmarks(&serde_json::from_str(json).map_err(io::Error::other)?)
|
||||
}
|
||||
|
||||
/// A request for whatever state the document is in.
|
||||
pub const ANY: u64 = u64::MAX;
|
||||
/// How a refusal for a changed document begins.
|
||||
pub const STALE: &str = "stale: ";
|
||||
@@ -0,0 +1,105 @@
|
||||
//! Removal and cleaning into new files (PDF-09, PDF-10).
|
||||
|
||||
use std::{io, path::Path};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::client::{Doc, Worker};
|
||||
use crate::proto::tag;
|
||||
|
||||
/// A region to remove, in page points.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Region {
|
||||
pub page: usize,
|
||||
pub rect: [f32; 4],
|
||||
}
|
||||
|
||||
/// The written file's check: zeros, and one version.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Redacted {
|
||||
pub regions: usize,
|
||||
/// Characters still inside a region.
|
||||
pub text: usize,
|
||||
/// Pixels inside a region that are not black.
|
||||
pub light: usize,
|
||||
/// Drawings still inside a region.
|
||||
pub objects: usize,
|
||||
pub versions: usize,
|
||||
}
|
||||
|
||||
/// What cleaning reaches, or what is left after.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Reach {
|
||||
/// Info entries, plus one for an XMP stream.
|
||||
pub meta: usize,
|
||||
pub annots: usize,
|
||||
pub files: usize,
|
||||
/// Pages with hidden text or layers.
|
||||
pub hidden: usize,
|
||||
pub scripts: usize,
|
||||
pub hidden_annots: usize,
|
||||
}
|
||||
|
||||
/// What to clean.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Cleaning {
|
||||
pub meta: bool,
|
||||
pub annots: bool,
|
||||
pub files: bool,
|
||||
/// Invisible text, scripts and hidden annotations.
|
||||
pub hidden: bool,
|
||||
}
|
||||
|
||||
impl Cleaning {
|
||||
fn mask(self) -> u64 {
|
||||
u64::from(self.meta) | u64::from(self.annots) << 1 | u64::from(self.files) << 2 | u64::from(self.hidden) << 3
|
||||
}
|
||||
}
|
||||
|
||||
fn count(value: &Value, key: &str) -> io::Result<usize> {
|
||||
let n = value[key].as_u64().ok_or_else(|| io::Error::other(format!("a bad {key} in {value}")))?;
|
||||
usize::try_from(n).map_err(io::Error::other)
|
||||
}
|
||||
|
||||
fn reach(bytes: &[u8]) -> io::Result<Reach> {
|
||||
let value: Value = serde_json::from_slice(bytes).map_err(io::Error::other)?;
|
||||
Ok(Reach {
|
||||
meta: count(&value, "meta")?,
|
||||
annots: count(&value, "annots")?,
|
||||
files: count(&value, "files")?,
|
||||
hidden: count(&value, "hidden")?,
|
||||
scripts: count(&value, "scripts")?,
|
||||
hidden_annots: count(&value, "hidden_annots")?,
|
||||
})
|
||||
}
|
||||
|
||||
fn utf8(path: &Path) -> io::Result<&str> {
|
||||
path.to_str().ok_or_else(|| io::Error::other("a path that is not UTF-8"))
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
/// Writes `out` without `regions`; an unclean result is an error.
|
||||
pub fn redact(&mut self, doc: Doc, regions: &[Region], out: &Path) -> io::Result<Redacted> {
|
||||
let spec: Vec<Value> = regions.iter().map(|r| json!({ "page": r.page, "rect": r.rect })).collect();
|
||||
let ask = self.on(&doc)?.str(&Value::Array(spec).to_string()).str(utf8(out)?);
|
||||
let value: Value = serde_json::from_slice(&self.request(tag::REDACT, &ask.bytes())?).map_err(io::Error::other)?;
|
||||
Ok(Redacted {
|
||||
regions: count(&value, "regions")?,
|
||||
text: count(&value, "text")?,
|
||||
light: count(&value, "light")?,
|
||||
objects: count(&value, "objects")?,
|
||||
versions: count(&value, "versions")?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Counts what cleaning reaches; `scratch` holds a passing copy.
|
||||
pub fn clean_scan(&mut self, doc: Doc, scratch: &Path) -> io::Result<Reach> {
|
||||
reach(&self.request(tag::CLEAN_SCAN, &self.whole(&doc)?.str(utf8(scratch)?).bytes())?)
|
||||
}
|
||||
|
||||
/// Writes `out` cleaned; what is left in it.
|
||||
pub fn clean(&mut self, doc: Doc, what: Cleaning, out: &Path) -> io::Result<Reach> {
|
||||
let ask = self.whole(&doc)?.u64(what.mask()).str(utf8(out)?);
|
||||
reach(&self.request(tag::CLEAN, &ask.bytes())?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
//! Small PDFs written by hand, for tests.
|
||||
|
||||
/// A bookmark: title, page, and those under it.
|
||||
pub type Mark<'a> = (&'a str, usize, &'a [(&'a str, usize)]);
|
||||
|
||||
/// Links, form fields and page labels to add.
|
||||
#[derive(Default)]
|
||||
pub struct Extras<'a> {
|
||||
/// Link annotations: from page, to page.
|
||||
pub links: &'a [(usize, usize)],
|
||||
/// A text field on each of these pages.
|
||||
pub fields: &'a [usize],
|
||||
/// Page labels: first page, style name.
|
||||
pub labels: &'a [(usize, &'a str)],
|
||||
/// Fields leave out their optional /P.
|
||||
pub loose: bool,
|
||||
/// Every page's /UserUnit, if not one.
|
||||
pub unit: Option<f32>,
|
||||
/// Field names start with this, else "field".
|
||||
pub prefix: Option<&'a str>,
|
||||
/// The form's look: red 24 pt; font key, DA token.
|
||||
pub look: Option<(&'a str, &'a str)>,
|
||||
/// The form's default alignment, /Q.
|
||||
pub align: Option<u8>,
|
||||
/// Page one: a form of this text at 300, 400.
|
||||
pub stamp: Option<&'a str>,
|
||||
/// Page one: a 2x2 image, 100x50 at 72, 500.
|
||||
pub picture: bool,
|
||||
/// Fields hang under one parent of this name.
|
||||
pub parent: Option<&'a str>,
|
||||
/// A parent and child field with no widget.
|
||||
pub bare: Option<(&'a str, &'a str)>,
|
||||
/// Page one: a file annotation, name and bytes, no /Size.
|
||||
pub attached: Option<(&'a str, &'a [u8])>,
|
||||
}
|
||||
|
||||
/// A PDF of `pages` (width, height, lines), with bookmarks.
|
||||
pub fn pdf(pages: &[(f32, f32, &[&str])], marks: &[Mark]) -> Vec<u8> {
|
||||
pdf_with(pages, marks, &Extras::default())
|
||||
}
|
||||
|
||||
/// A PDF as `pdf` makes, with `extras`.
|
||||
pub fn pdf_with(pages: &[(f32, f32, &[&str])], marks: &[Mark], extras: &Extras) -> Vec<u8> {
|
||||
let mut objs: Vec<Vec<u8>> = Vec::new();
|
||||
let page_ids: Vec<usize> = (0..pages.len()).map(|n| 4 + n * 2).collect();
|
||||
// Links, then fields, after the outline objects.
|
||||
let outline_objs: usize = marks.iter().map(|(_, _, down)| 1 + down.len()).sum();
|
||||
let first_extra = 4 + pages.len() * 2 + 1 + outline_objs;
|
||||
let field_ids: Vec<usize> = (0..extras.fields.len()).map(|n| first_extra + extras.links.len() + n).collect();
|
||||
let mut catalog = String::from("<< /Type /Catalog /Pages 2 0 R /Outlines 3 0 R");
|
||||
// The parent field comes last of all.
|
||||
let parent_id = first_extra + extras.links.len() + extras.fields.len() + usize::from(extras.stamp.is_some()) + usize::from(extras.picture);
|
||||
let bare_id = parent_id + usize::from(extras.parent.is_some());
|
||||
let attach_id = bare_id + 2 * usize::from(extras.bare.is_some());
|
||||
if !field_ids.is_empty() || extras.bare.is_some() {
|
||||
let mut refs: Vec<String> = match extras.parent {
|
||||
Some(_) => vec![format!("{parent_id} 0 R")],
|
||||
None => field_ids.iter().map(|id| format!("{id} 0 R")).collect(),
|
||||
};
|
||||
refs.extend(extras.bare.map(|_| format!("{bare_id} 0 R")));
|
||||
let font = 4 + pages.len() * 2;
|
||||
let look = extras.look.map_or_else(String::new, |(key, token)| format!(" /DA (/{token} 24 Tf 1 0 0 rg) /DR << /Font << /{key} {font} 0 R >> >>"));
|
||||
let align = extras.align.map_or_else(String::new, |q| format!(" /Q {q}"));
|
||||
catalog.push_str(&format!(" /AcroForm << /Fields [{}]{look}{align} >>", refs.join(" ")));
|
||||
}
|
||||
if !extras.labels.is_empty() {
|
||||
let nums: Vec<String> = extras.labels.iter().map(|(at, style)| format!("{at} << /S /{style} >>")).collect();
|
||||
catalog.push_str(&format!(" /PageLabels << /Nums [{}] >>", nums.join(" ")));
|
||||
}
|
||||
objs.push(format!("{catalog} >>").into_bytes());
|
||||
let kids: Vec<String> = page_ids.iter().map(|id| format!("{id} 0 R")).collect();
|
||||
objs.push(
|
||||
format!(
|
||||
"<< /Type /Pages /Kids [{}] /Count {} >>",
|
||||
kids.join(" "),
|
||||
pages.len()
|
||||
)
|
||||
.into_bytes(),
|
||||
);
|
||||
// Outline objects follow the pages.
|
||||
let mut id = 4 + pages.len() * 2 + 1;
|
||||
let tops: Vec<usize> = (marks.iter())
|
||||
.map(|(_, _, down)| {
|
||||
let top = id;
|
||||
id += 1 + down.len();
|
||||
top
|
||||
})
|
||||
.collect();
|
||||
objs.push(match (tops.first(), tops.last()) {
|
||||
(Some(first), Some(last)) => format!(
|
||||
"<< /Type /Outlines /First {first} 0 R /Last {last} 0 R /Count {} >>",
|
||||
tops.len()
|
||||
)
|
||||
.into_bytes(),
|
||||
_ => b"<< /Type /Outlines /Count 0 >>".to_vec(),
|
||||
});
|
||||
for (n, (w, h, lines)) in pages.iter().enumerate() {
|
||||
let mut stream = String::from("BT /F1 18 Tf 72 0 Td\n");
|
||||
for (at, line) in lines.iter().enumerate() {
|
||||
let y = h - 72.0 - at as f32 * 24.0;
|
||||
stream.push_str(&format!("1 0 0 1 72 {y} Tm ({line}) Tj\n"));
|
||||
}
|
||||
stream.push_str("ET");
|
||||
let font = 4 + pages.len() * 2;
|
||||
let (drawn, xobjects) = match n {
|
||||
0 => xobjects(extras, first_extra + extras.links.len() + extras.fields.len()),
|
||||
_ => (String::new(), String::new()),
|
||||
};
|
||||
stream.push_str(&drawn);
|
||||
let links = (extras.links.iter().enumerate()).filter(|(_, (from, _))| *from == n).map(|(at, _)| first_extra + at);
|
||||
let fields = (extras.fields.iter().enumerate()).filter(|(_, page)| **page == n).map(|(at, _)| field_ids[at]);
|
||||
let attached = extras.attached.filter(|_| n == 0).map(|_| attach_id);
|
||||
let annots: Vec<String> = links.chain(fields).chain(attached).map(|id| format!("{id} 0 R")).collect();
|
||||
let unit = extras.unit.map_or_else(String::new, |unit| format!(" /UserUnit {unit}"));
|
||||
objs.push(format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {w} {h}]{unit} /Contents {} 0 R /Resources << /Font << /F1 {font} 0 R >>{xobjects} >> /Annots [{}] >>", page_ids[n] + 1, annots.join(" ")).into_bytes());
|
||||
objs.push(
|
||||
format!(
|
||||
"<< /Length {} >>\nstream\n{stream}\nendstream",
|
||||
stream.len()
|
||||
)
|
||||
.into_bytes(),
|
||||
);
|
||||
}
|
||||
objs.push(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_vec());
|
||||
// Top marks link to each other; children hang under them.
|
||||
for (at, (title, page, down)) in marks.iter().enumerate() {
|
||||
let top = tops[at];
|
||||
let mut dict = format!(
|
||||
"<< /Title ({title}) /Parent 3 0 R /Dest [{} 0 R /XYZ 0 700 0]",
|
||||
page_ids[*page]
|
||||
);
|
||||
if at > 0 {
|
||||
dict.push_str(&format!(" /Prev {} 0 R", tops[at - 1]));
|
||||
}
|
||||
if at + 1 < tops.len() {
|
||||
dict.push_str(&format!(" /Next {} 0 R", tops[at + 1]));
|
||||
}
|
||||
if !down.is_empty() {
|
||||
dict.push_str(&format!(
|
||||
" /First {} 0 R /Last {} 0 R /Count {}",
|
||||
top + 1,
|
||||
top + down.len(),
|
||||
down.len()
|
||||
));
|
||||
}
|
||||
objs.push(format!("{dict} >>").into_bytes());
|
||||
for (n, (title, page)) in down.iter().enumerate() {
|
||||
let mut dict = format!(
|
||||
"<< /Title ({title}) /Parent {top} 0 R /Dest [{} 0 R /XYZ 0 500 0]",
|
||||
page_ids[*page]
|
||||
);
|
||||
if n > 0 {
|
||||
dict.push_str(&format!(" /Prev {} 0 R", top + n));
|
||||
}
|
||||
if n + 1 < down.len() {
|
||||
dict.push_str(&format!(" /Next {} 0 R", top + n + 2));
|
||||
}
|
||||
objs.push(format!("{dict} >>").into_bytes());
|
||||
}
|
||||
}
|
||||
for (_, to) in extras.links {
|
||||
objs.push(format!("<< /Type /Annot /Subtype /Link /Rect [72 72 200 100] /Border [0 0 0] /Dest [{} 0 R /Fit] >>", page_ids[*to]).into_bytes());
|
||||
}
|
||||
for (n, page) in extras.fields.iter().enumerate() {
|
||||
let owner = if extras.loose { String::new() } else { format!(" /P {} 0 R", page_ids[*page]) };
|
||||
let name = extras.prefix.unwrap_or("field");
|
||||
let parent = extras.parent.map_or_else(String::new, |_| format!(" /Parent {parent_id} 0 R"));
|
||||
objs.push(format!("<< /Type /Annot /Subtype /Widget /FT /Tx /T ({name}{n}) /V (value{n}) /Rect [72 120 300 140]{owner}{parent} >>").into_bytes());
|
||||
}
|
||||
if let Some(text) = extras.stamp {
|
||||
let font = 4 + pages.len() * 2;
|
||||
let content = format!("BT /F1 12 Tf 0 10 Td ({text}) Tj ET");
|
||||
objs.push(format!("<< /Type /XObject /Subtype /Form /BBox [0 0 200 40] /Resources << /Font << /F1 {font} 0 R >> >> /Length {} >>\nstream\n{content}\nendstream", content.len()).into_bytes());
|
||||
}
|
||||
if extras.picture {
|
||||
let mut image = b"<< /Type /XObject /Subtype /Image /Width 2 /Height 2 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Length 12 >>\nstream\n".to_vec();
|
||||
image.extend_from_slice(&[255, 0, 0, 0, 255, 0, 0, 0, 255, 255, 255, 255]);
|
||||
image.extend_from_slice(b"\nendstream");
|
||||
objs.push(image);
|
||||
}
|
||||
if let Some(name) = extras.parent {
|
||||
let kids: Vec<String> = field_ids.iter().map(|id| format!("{id} 0 R")).collect();
|
||||
objs.push(format!("<< /T ({name}) /Kids [{}] >>", kids.join(" ")).into_bytes());
|
||||
}
|
||||
if let Some((parent, child)) = extras.bare {
|
||||
objs.push(format!("<< /T ({parent}) /Kids [{} 0 R] >>", bare_id + 1).into_bytes());
|
||||
objs.push(format!("<< /T ({child}) /FT /Tx /V (Alice) /Parent {bare_id} 0 R >>").into_bytes());
|
||||
}
|
||||
if let Some((name, bytes)) = extras.attached {
|
||||
objs.push(format!("<< /Type /Annot /Subtype /FileAttachment /Rect [72 72 90 90] /FS {} 0 R >>", attach_id + 1).into_bytes());
|
||||
objs.push(format!("<< /Type /Filespec /F ({name}) /UF ({name}) /EF << /F {} 0 R >> >>", attach_id + 2).into_bytes());
|
||||
let mut file = format!("<< /Type /EmbeddedFile /Length {} >>\nstream\n", bytes.len()).into_bytes();
|
||||
file.extend_from_slice(bytes);
|
||||
file.extend_from_slice(b"\nendstream");
|
||||
objs.push(file);
|
||||
}
|
||||
file(&objs)
|
||||
}
|
||||
|
||||
/// Page one's form and image draws, and their resources.
|
||||
fn xobjects(extras: &Extras, first: usize) -> (String, String) {
|
||||
let (mut drawn, mut names, mut id) = (String::new(), String::new(), first);
|
||||
if extras.stamp.is_some() {
|
||||
drawn.push_str("\nq 1 0 0 1 300 400 cm /Fm1 Do Q");
|
||||
names.push_str(&format!(" /Fm1 {id} 0 R"));
|
||||
id += 1;
|
||||
}
|
||||
if extras.picture {
|
||||
drawn.push_str("\nq 100 0 0 50 72 500 cm /Im1 Do Q");
|
||||
names.push_str(&format!(" /Im1 {id} 0 R"));
|
||||
}
|
||||
match names.is_empty() {
|
||||
true => (drawn, names),
|
||||
false => (drawn, format!(" /XObject <<{names} >>")),
|
||||
}
|
||||
}
|
||||
|
||||
/// One page of `size` points: a gray picture only.
|
||||
pub fn scan((w, h): (f32, f32), (pw, ph): (u32, u32), gray: &[u8]) -> Vec<u8> {
|
||||
let content = format!("q {w} 0 0 {h} 0 0 cm /Im1 Do Q");
|
||||
let mut image = format!("<< /Type /XObject /Subtype /Image /Width {pw} /Height {ph} /ColorSpace /DeviceGray /BitsPerComponent 8 /Length {} >>\nstream\n", gray.len()).into_bytes();
|
||||
image.extend_from_slice(gray);
|
||||
image.extend_from_slice(b"\nendstream");
|
||||
let objs: Vec<Vec<u8>> = vec![
|
||||
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
||||
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {w} {h}] /Contents 4 0 R /Resources << /XObject << /Im1 5 0 R >> >> >>").into_bytes(),
|
||||
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()).into_bytes(),
|
||||
image,
|
||||
];
|
||||
file(&objs)
|
||||
}
|
||||
|
||||
/// Numbered objects as a whole PDF file.
|
||||
fn file(objs: &[Vec<u8>]) -> Vec<u8> {
|
||||
let mut body = b"%PDF-1.7\n".to_vec();
|
||||
let mut offsets = Vec::new();
|
||||
for (n, obj) in objs.iter().enumerate() {
|
||||
offsets.push(body.len());
|
||||
body.extend_from_slice(format!("{} 0 obj\n", n + 1).as_bytes());
|
||||
body.extend_from_slice(obj);
|
||||
body.extend_from_slice(b"\nendobj\n");
|
||||
}
|
||||
let xref = body.len();
|
||||
body.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
|
||||
for offset in offsets {
|
||||
body.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
|
||||
}
|
||||
body.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
|
||||
body
|
||||
}
|
||||
|
||||
/// One Letter page drawing `content`; `objects` are numbered from 5.
|
||||
pub fn one_page(catalog: &str, page: &str, content: &str, objects: &[String]) -> Vec<u8> {
|
||||
let mut objs: Vec<Vec<u8>> = vec![
|
||||
format!("<< /Type /Catalog /Pages 2 0 R{catalog} >>").into_bytes(),
|
||||
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
||||
format!("<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R{page} >>").into_bytes(),
|
||||
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()).into_bytes(),
|
||||
];
|
||||
objs.extend(objects.iter().map(|obj| obj.clone().into_bytes()));
|
||||
file(&objs)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//! Permissions, owner access and protected copies (PDF-11).
|
||||
|
||||
use std::{io, path::Path};
|
||||
|
||||
use ely_ipc::Unpack;
|
||||
|
||||
use crate::client::{Doc, Worker};
|
||||
use crate::proto::tag;
|
||||
|
||||
/// A protected copy's passwords and what it allows.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Lock {
|
||||
/// Empty opens without asking.
|
||||
pub user: String,
|
||||
pub owner: String,
|
||||
/// `proto::may` bits.
|
||||
pub allow: u32,
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
/// `proto::may` bits for `doc` now.
|
||||
pub fn permissions(&mut self, doc: Doc) -> io::Result<u32> {
|
||||
let reply = self.request(tag::PERMISSIONS, &self.whole(&doc)?.bytes())?;
|
||||
Ok(Unpack::new(&reply).u64()? as u32)
|
||||
}
|
||||
|
||||
/// Tries an owner password; false keeps the old access.
|
||||
pub fn owner(&mut self, doc: Doc, password: &str) -> io::Result<bool> {
|
||||
let reply = self.request(tag::OWNER, &self.whole(&doc)?.str(password).bytes())?;
|
||||
Ok(Unpack::new(&reply).u64()? != 0)
|
||||
}
|
||||
|
||||
/// Writes `out` with `lock`, or with no protection.
|
||||
pub fn protect(&mut self, doc: Doc, out: &Path, lock: Option<&Lock>) -> io::Result<()> {
|
||||
let out = out.to_str().ok_or_else(|| io::Error::other("a path that is not UTF-8"))?;
|
||||
let (user, owner, allow) = lock.map_or(("", "", 0), |lock| (lock.user.as_str(), lock.owner.as_str(), lock.allow));
|
||||
let ask = self.whole(&doc)?.str(out).u64(u64::from(lock.is_some())).str(user).str(owner).u64(u64::from(allow));
|
||||
self.request(tag::PROTECT, &ask.bytes()).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
//! Signatures: listed with their checks, and new ones (PDF-12).
|
||||
|
||||
use std::{io, path::Path};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::client::{Doc, Worker};
|
||||
use crate::proto::tag;
|
||||
|
||||
/// Whether the signed bytes are as signed.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Digest {
|
||||
Intact,
|
||||
Changed,
|
||||
/// No certificate, or a signature that will not read.
|
||||
#[default]
|
||||
Unreadable,
|
||||
}
|
||||
|
||||
/// One signature field, signed or empty.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Signature {
|
||||
/// None for a field drawn on no page.
|
||||
pub page: Option<usize>,
|
||||
pub object: u32,
|
||||
pub rect: [f32; 4],
|
||||
pub name: String,
|
||||
pub signed: Option<Signed>,
|
||||
}
|
||||
|
||||
/// What a signed field says and how it checks.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Signed {
|
||||
pub digest: Digest,
|
||||
/// Updates were saved after it.
|
||||
pub later: bool,
|
||||
/// Those updates go past what it allows.
|
||||
pub broken: bool,
|
||||
pub signer: String,
|
||||
pub common: String,
|
||||
/// The PDF date the signer claims.
|
||||
pub date: String,
|
||||
pub reason: String,
|
||||
pub location: String,
|
||||
/// DER, the signer's first.
|
||||
pub certs: Vec<Vec<u8>>,
|
||||
/// Why it could not be read.
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// A new signature: a field, a certificate, words.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Signing {
|
||||
pub page: usize,
|
||||
/// An empty field's object, or a new field at `rect`.
|
||||
pub object: Option<u32>,
|
||||
pub rect: [f32; 4],
|
||||
pub name: String,
|
||||
/// A PKCS #12 file and its password.
|
||||
pub pfx: String,
|
||||
pub pfx_password: String,
|
||||
pub reason: String,
|
||||
pub location: String,
|
||||
}
|
||||
|
||||
fn bad(what: &str, value: &Value) -> io::Error {
|
||||
io::Error::other(format!("a bad {what} in {value}"))
|
||||
}
|
||||
|
||||
fn text(value: &Value, key: &str) -> io::Result<String> {
|
||||
value[key].as_str().map(str::to_string).ok_or_else(|| bad(key, value))
|
||||
}
|
||||
|
||||
fn hex(value: &Value) -> io::Result<Vec<u8>> {
|
||||
let digits = value.as_str().filter(|s| s.len() % 2 == 0).ok_or_else(|| bad("certificate", value))?;
|
||||
(0..digits.len()).step_by(2).map(|at| u8::from_str_radix(&digits[at..at + 2], 16).map_err(io::Error::other)).collect()
|
||||
}
|
||||
|
||||
fn signed(value: &Value) -> io::Result<Signed> {
|
||||
let digest = match value["digest"].as_str() {
|
||||
Some("ok") => Digest::Intact,
|
||||
Some("changed") => Digest::Changed,
|
||||
Some("no_certificate" | "unreadable") => Digest::Unreadable,
|
||||
_ => return Err(bad("digest", value)),
|
||||
};
|
||||
if let Some(error) = value["error"].as_str() {
|
||||
return Ok(Signed { digest, error: Some(error.into()), ..Signed::default() });
|
||||
}
|
||||
let certs = value["certs"].as_array().ok_or_else(|| bad("certs", value))?.iter().map(hex).collect::<io::Result<_>>()?;
|
||||
Ok(Signed {
|
||||
digest,
|
||||
later: value["later"].as_i64().ok_or_else(|| bad("later", value))? != 0,
|
||||
broken: value["broken"].as_i64().ok_or_else(|| bad("broken", value))? != 0,
|
||||
signer: text(value, "signer")?,
|
||||
common: text(value, "common")?,
|
||||
date: text(value, "date")?,
|
||||
reason: text(value, "reason")?,
|
||||
location: text(value, "location")?,
|
||||
certs,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn signature(value: &Value) -> io::Result<Signature> {
|
||||
let rect = value["rect"].as_array().filter(|r| r.len() == 4).ok_or_else(|| bad("rect", value))?;
|
||||
let mut out = [0f32; 4];
|
||||
for (at, side) in rect.iter().enumerate() {
|
||||
out[at] = side.as_f64().ok_or_else(|| bad("rect", value))? as f32;
|
||||
}
|
||||
Ok(Signature {
|
||||
page: usize::try_from(value["page"].as_i64().ok_or_else(|| bad("page", value))?).ok(),
|
||||
object: value["object"].as_u64().and_then(|n| u32::try_from(n).ok()).ok_or_else(|| bad("object", value))?,
|
||||
rect: out,
|
||||
name: text(value, "name")?,
|
||||
signed: match value["signed"].as_bool().ok_or_else(|| bad("signed", value))? {
|
||||
true => Some(signed(value)?),
|
||||
false => None,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
/// Every signature field in the file at `path`, checked.
|
||||
pub fn signatures(&mut self, doc: Doc, path: &Path) -> io::Result<Vec<Signature>> {
|
||||
let path = path.to_str().ok_or_else(|| io::Error::other("a path that is not UTF-8"))?;
|
||||
let reply = self.request(tag::SIGNATURES, &self.whole(&doc)?.str(path).bytes())?;
|
||||
let value: Value = serde_json::from_slice(&reply).map_err(io::Error::other)?;
|
||||
value.as_array().ok_or_else(|| bad("list", &value))?.iter().map(signature).collect()
|
||||
}
|
||||
|
||||
/// Fields holding a signature, counted without checking.
|
||||
pub fn signed(&mut self, doc: Doc) -> io::Result<usize> {
|
||||
let reply = self.request(tag::SIGNED, &self.whole(&doc)?.bytes())?;
|
||||
Ok(ely_ipc::Unpack::new(&reply).u64()? as usize)
|
||||
}
|
||||
|
||||
/// Writes `out` signed by an update; `doc` is untouched.
|
||||
pub fn sign(&mut self, doc: Doc, out: &Path, how: &Signing) -> io::Result<()> {
|
||||
let out = out.to_str().ok_or_else(|| io::Error::other("a path that is not UTF-8"))?;
|
||||
let object = how.object.map_or(-1, i64::from);
|
||||
let mut ask = self.on(&doc)?.str(out).u64(how.page as u64).i64(object);
|
||||
for side in how.rect {
|
||||
ask = ask.u64(u64::from(side.to_bits()));
|
||||
}
|
||||
let ask = ask.str(&how.name).str(&how.pfx).str(&how.pfx_password).str(&how.reason).str(&how.location);
|
||||
self.request(tag::SIGN, &ask.bytes()).map(|_| ())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
//! Bookmarks, links, properties, attachments (PDF-07).
|
||||
|
||||
use std::{io, path::Path};
|
||||
|
||||
use ely_ipc::Unpack;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::client::{Doc, Worker};
|
||||
use crate::proto::tag;
|
||||
|
||||
/// Where a bookmark or link goes.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Target {
|
||||
/// A page and a point down it.
|
||||
Page(usize, f32),
|
||||
/// An http, https or mailto address.
|
||||
Web(String),
|
||||
}
|
||||
|
||||
/// A bookmark change; paths count children from the top.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum BookmarkChange {
|
||||
/// Before the bookmark at `path`, or last past the end.
|
||||
Add { path: Vec<usize>, title: String, page: usize, y: f32 },
|
||||
/// `expect` is its title as read; another refuses.
|
||||
Edit { path: Vec<usize>, expect: String, title: Option<String>, to: Option<(usize, f32)> },
|
||||
Delete { path: Vec<usize>, expect: String },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Link {
|
||||
pub rect: [f32; 4],
|
||||
pub uri: String,
|
||||
/// The page inside the document, if any.
|
||||
pub page: Option<usize>,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
/// A link change; `expect` is the address as read.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum LinkChange {
|
||||
Add { rect: [f32; 4], to: Target },
|
||||
Edit { index: usize, expect: String, to: Target },
|
||||
Delete { index: usize, expect: String },
|
||||
}
|
||||
|
||||
/// The document's properties; none when the file has none.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Properties {
|
||||
pub title: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub subject: Option<String>,
|
||||
pub keywords: Option<String>,
|
||||
pub creator: Option<String>,
|
||||
pub producer: Option<String>,
|
||||
pub created: Option<String>,
|
||||
pub modified: Option<String>,
|
||||
}
|
||||
|
||||
/// An embedded file; dates in seconds, if known.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Attachment {
|
||||
/// Names it for reading and removing.
|
||||
pub key: String,
|
||||
pub name: String,
|
||||
/// The page whose annotation holds it, if any.
|
||||
pub page: Option<usize>,
|
||||
pub size: u64,
|
||||
pub created: Option<i64>,
|
||||
pub modified: Option<i64>,
|
||||
pub mime: String,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
fn bad(what: &str, value: &Value) -> io::Error {
|
||||
io::Error::other(format!("a bad {what} {value}"))
|
||||
}
|
||||
|
||||
fn target(spec: &mut Value, to: &Target) {
|
||||
match to {
|
||||
Target::Page(page, y) => {
|
||||
spec["page"] = json!(page);
|
||||
spec["y"] = json!(y);
|
||||
}
|
||||
Target::Web(uri) => spec["uri"] = json!(uri),
|
||||
}
|
||||
}
|
||||
|
||||
fn parsed(bytes: &[u8]) -> io::Result<Value> {
|
||||
serde_json::from_slice(bytes).map_err(io::Error::other)
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
pub fn change_bookmark(&mut self, doc: Doc, change: &BookmarkChange) -> io::Result<String> {
|
||||
let spec = match change {
|
||||
BookmarkChange::Add { path, title, page, y } => json!({ "op": "add", "path": path, "title": title, "page": page, "y": y }),
|
||||
BookmarkChange::Edit { path, expect, title, to } => {
|
||||
let mut spec = json!({ "op": "edit", "path": path, "expect": expect });
|
||||
if let Some(title) = title {
|
||||
spec["title"] = json!(title);
|
||||
}
|
||||
if let Some((page, y)) = to {
|
||||
target(&mut spec, &Target::Page(*page, *y));
|
||||
}
|
||||
spec
|
||||
}
|
||||
BookmarkChange::Delete { path, expect } => json!({ "op": "delete", "path": path, "expect": expect }),
|
||||
};
|
||||
let ask = self.on(&doc)?.str(&spec.to_string());
|
||||
Unpack::new(&self.request(tag::OUTLINE_EDIT, &ask.bytes())?).str()
|
||||
}
|
||||
|
||||
pub fn links(&mut self, doc: Doc, page: usize) -> io::Result<Vec<Link>> {
|
||||
let reply = parsed(&self.request(tag::LINKS, &self.on(&doc)?.u64(page as u64).bytes())?)?;
|
||||
let list = reply.as_array().ok_or_else(|| bad("link list", &reply))?;
|
||||
list.iter()
|
||||
.map(|value| {
|
||||
let rect = value["rect"].as_array().filter(|r| r.len() == 4).ok_or_else(|| bad("link", value))?;
|
||||
let mut out = [0f32; 4];
|
||||
for (at, side) in rect.iter().enumerate() {
|
||||
out[at] = side.as_f64().ok_or_else(|| bad("link", value))? as f32;
|
||||
}
|
||||
Ok(Link {
|
||||
rect: out,
|
||||
uri: value["uri"].as_str().ok_or_else(|| bad("link", value))?.to_string(),
|
||||
page: usize::try_from(value["page"].as_i64().ok_or_else(|| bad("link", value))?).ok(),
|
||||
y: value["y"].as_f64().ok_or_else(|| bad("link", value))? as f32,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn change_link(&mut self, doc: Doc, page: usize, change: &LinkChange) -> io::Result<String> {
|
||||
let spec = match change {
|
||||
LinkChange::Add { rect, to } => {
|
||||
let mut spec = json!({ "op": "add", "rect": rect });
|
||||
target(&mut spec, to);
|
||||
spec
|
||||
}
|
||||
LinkChange::Edit { index, expect, to } => {
|
||||
let mut spec = json!({ "op": "edit", "index": index, "expect": expect });
|
||||
target(&mut spec, to);
|
||||
spec
|
||||
}
|
||||
LinkChange::Delete { index, expect } => json!({ "op": "delete", "index": index, "expect": expect }),
|
||||
};
|
||||
let ask = self.on(&doc)?.u64(page as u64).str(&spec.to_string());
|
||||
Unpack::new(&self.request(tag::LINK_EDIT, &ask.bytes())?).str()
|
||||
}
|
||||
|
||||
pub fn properties(&mut self, doc: Doc) -> io::Result<Properties> {
|
||||
let reply = parsed(&self.request(tag::METADATA, &self.whole(&doc)?.bytes())?)?;
|
||||
let field = |key: &str| match &reply[key] {
|
||||
Value::Null => Ok(None),
|
||||
Value::String(text) => Ok(Some(text.clone())),
|
||||
other => Err(bad(key, other)),
|
||||
};
|
||||
Ok(Properties {
|
||||
title: field("Title")?,
|
||||
author: field("Author")?,
|
||||
subject: field("Subject")?,
|
||||
keywords: field("Keywords")?,
|
||||
creator: field("Creator")?,
|
||||
producer: field("Producer")?,
|
||||
created: field("CreationDate")?,
|
||||
modified: field("ModDate")?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets Title, Author, Subject or Keywords, one step.
|
||||
pub fn set_properties(&mut self, doc: Doc, changes: &[(&str, &str)]) -> io::Result<String> {
|
||||
let spec: serde_json::Map<String, Value> = changes.iter().map(|(key, value)| (key.to_string(), json!(value))).collect();
|
||||
let ask = self.whole(&doc)?.str(&Value::Object(spec).to_string());
|
||||
Unpack::new(&self.request(tag::SET_METADATA, &ask.bytes())?).str()
|
||||
}
|
||||
|
||||
pub fn attachments(&mut self, doc: Doc) -> io::Result<Vec<Attachment>> {
|
||||
let reply = parsed(&self.request(tag::ATTACHMENTS, &self.whole(&doc)?.bytes())?)?;
|
||||
let list = reply.as_array().ok_or_else(|| bad("attachment list", &reply))?;
|
||||
list.iter()
|
||||
.map(|value| {
|
||||
let text = |key: &str| value[key].as_str().map(str::to_string).ok_or_else(|| bad(key, value));
|
||||
let date = |key: &str| value[key].as_i64().map(|at| (at >= 0).then_some(at)).ok_or_else(|| bad(key, value));
|
||||
Ok(Attachment {
|
||||
key: text("key")?,
|
||||
name: text("name")?,
|
||||
page: usize::try_from(value["page"].as_i64().ok_or_else(|| bad("page", value))?).ok(),
|
||||
size: value["size"].as_u64().ok_or_else(|| bad("size", value))?,
|
||||
created: date("created")?,
|
||||
modified: date("modified")?,
|
||||
mime: text("type")?,
|
||||
note: text("note")?,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn attachment(&mut self, doc: Doc, key: &str) -> io::Result<Vec<u8>> {
|
||||
self.request(tag::ATTACHMENT, &self.on(&doc)?.str(key).bytes())
|
||||
}
|
||||
|
||||
/// Embeds the file at `path` as `name`; the mark.
|
||||
pub fn attach(&mut self, doc: Doc, name: &str, path: &Path) -> io::Result<String> {
|
||||
let path = path.to_str().ok_or_else(|| io::Error::other("a path that is not UTF-8"))?;
|
||||
Unpack::new(&self.request(tag::ATTACH, &self.whole(&doc)?.str(name).str(path).bytes())?).str()
|
||||
}
|
||||
|
||||
pub fn detach(&mut self, doc: Doc, key: &str) -> io::Result<String> {
|
||||
Unpack::new(&self.request(tag::DETACH, &self.on(&doc)?.str(key).bytes())?).str()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user