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(|_| ())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user