Files
ElyOffice/crates/ely-pdf-engine/src/bin/ely-pdf-worker/main.rs
T
ZacharyZhang-NYandClaude Opus 5.5 6b676e06e6 R7.4: an unembedded font is no damage (#5)
The warning watch skips MuPDF's non-embedded font notice and its
repeat counts. Text and search reads are watched too: damage they meet
is kept on the document, told by the next draw or picture check, and
fails image and text exports until then.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 13:44:17 -04:00

489 lines
21 KiB
Rust

//! 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>,
/// Damage a read met that no draw has told yet.
found: bool,
/// 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 {
if let Err(error) = mupdf.undo(&self.open, redo) {
// MuPDF may have moved part way; follow it.
self.at = mupdf.state(&self.open)?.0;
return Err(error);
}
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::Unwritable => Pack::new().u64(3).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 | tag::HTML_PDF => 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, found: false, 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 (id, page) = (ask.u64()?, ask.u64()? as usize);
self.read(id, |mupdf, doc| mupdf.text(doc, page))
}
tag::SEARCH => {
let (id, page, needle) = (ask.u64()?, ask.u64()? as usize, ask.str()?);
self.read(id, |mupdf, doc| mupdf.search(doc, page, &needle))
}
tag::OUTLINE => self.mupdf.outline(self.doc(ask.u64()?)?),
tag::HELLO => Ok(Pack::new().str(ely_pdf_engine::proto::BUILD).bytes()),
tag::HTML_PDF => {
let (dir, page, out) = (ask.str()?, ask.str()?, ask.str()?);
self.mupdf.html_pdf(&dir, &page, &out).map(|()| Vec::new())
}
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(())
}