Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
212 lines
6.9 KiB
Rust
212 lines
6.9 KiB
Rust
//! 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;
|
|
/// A folder's HTML page as a new PDF; no document.
|
|
pub const HTML_PDF: u8 = 60;
|
|
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: ";
|