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:
ZacharyZhang-NY committed 2026-10-08 21:20:08 -04:00
commit b7fca0146d
817 files changed
+143101

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
[package]
name = "ely-pdf-engine"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish.workspace = true
[dependencies]
ely-ipc.workspace = true
log.workspace = true
env_logger.workspace = true
serde_json.workspace = true
[features]
test-support = []
[build-dependencies]
cc = "1"
[dev-dependencies]
ely-pdf-engine = { path = ".", features = ["test-support"] }
tempfile.workspace = true
[lints]
workspace = true
[target.'cfg(target_os = "macos")'.dev-dependencies]
objc2.workspace = true
objc2-foundation = { workspace = true, features = ["NSArray"] }
objc2-pdf-kit = { workspace = true, features = ["PDFPage", "PDFAnnotation", "PDFAnnotationUtilities"] }
+110
View File
@@ -0,0 +1,110 @@
//! Builds the MuPDF shim into the worker binary only.
use std::path::PathBuf;
fn main() {
let root = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("set by cargo")).join("../..");
let mupdf = std::env::var_os("ELY_MUPDF_DIR").map_or_else(|| root.join("engines/mupdf-build"), PathBuf::from);
// One stamp for client and worker; stale workers refused.
let mut stamp: u64 = 0xcbf2_9ce4_8422_2325;
for source in [
"shim/ely_mupdf.c",
"shim/ely_pages.c",
"shim/ely_insert.c",
"shim/ely_text.c",
"shim/ely_write.c",
"shim/ely_place.c",
"shim/ely_annot.c",
"shim/ely_form.c",
"shim/ely_ocr.c",
"shim/ely_outline.c",
"shim/ely_meta.c",
"shim/ely_redact.c",
"shim/ely_alt.c",
"shim/ely_clean.c",
"shim/ely_secure.c",
"shim/ely_sign.c",
"shim/ely_output.c",
"shim/ely_shim.h",
"src/proto.rs",
"src/bin/ely-pdf-worker/main.rs",
"src/bin/ely-pdf-worker/ffi.rs",
"src/bin/ely-pdf-worker/edit.rs",
"src/bin/ely-pdf-worker/content.rs",
"src/edit.rs",
"src/content.rs",
"src/marks.rs",
"src/bin/ely-pdf-worker/marks.rs",
"src/bin/ely-pdf-worker/ocr.rs",
"src/bin/ely-pdf-worker/structure.rs",
"src/bin/ely-pdf-worker/redact.rs",
"src/redact.rs",
"src/structure.rs",
"src/ocr.rs",
"src/bin/ely-pdf-worker/secure.rs",
"src/secure.rs",
"src/bin/ely-pdf-worker/sign.rs",
"src/sign.rs",
"src/bin/ely-pdf-worker/output.rs",
"src/output.rs",
"../ely-ipc/src/lib.rs",
] {
println!("cargo:rerun-if-changed={source}");
for byte in std::fs::read(source).expect("a source file") {
stamp = (stamp ^ u64::from(byte)).wrapping_mul(0x100_0000_01b3);
}
}
println!("cargo:rustc-env=ELY_PDF_BUILD={stamp:016x}");
println!("cargo:rerun-if-env-changed=ELY_MUPDF_DIR");
for lib in ["libmupdf.a", "libmupdf-third.a", "libmupdf-pkcs7.a"] {
println!("cargo:rerun-if-changed={}", mupdf.join(lib).display());
if !mupdf.join(lib).is_file() {
panic!("no {lib} in {} (run scripts/build-mupdf.sh or set ELY_MUPDF_DIR)", mupdf.display());
}
}
cc::Build::new()
.file("shim/ely_mupdf.c")
.file("shim/ely_pages.c")
.file("shim/ely_insert.c")
.file("shim/ely_text.c")
.file("shim/ely_write.c")
.file("shim/ely_place.c")
.file("shim/ely_annot.c")
.file("shim/ely_form.c")
.file("shim/ely_ocr.c")
.file("shim/ely_outline.c")
.file("shim/ely_meta.c")
.file("shim/ely_redact.c")
.file("shim/ely_alt.c")
.file("shim/ely_clean.c")
.file("shim/ely_secure.c")
.file("shim/ely_sign.c")
.file("shim/ely_output.c")
.include(root.join("reference/mupdf/include"))
.include(openssl("includedir"))
.warnings(true)
.cargo_metadata(false)
.compile("ely_mupdf");
let out = PathBuf::from(std::env::var("OUT_DIR").expect("set by cargo"));
let bin = "ely-pdf-worker";
println!("cargo:rustc-link-arg-bin={bin}={}", out.join("libely_mupdf.a").display());
println!("cargo:rustc-link-arg-bin={bin}={}", mupdf.join("libmupdf-pkcs7.a").display());
println!("cargo:rustc-link-arg-bin={bin}={}", mupdf.join("libmupdf.a").display());
println!("cargo:rustc-link-arg-bin={bin}={}", mupdf.join("libmupdf-third.a").display());
// Signing and checking need OpenSSL, linked in whole.
println!("cargo:rustc-link-arg-bin={bin}={}", PathBuf::from(openssl("libdir")).join("libcrypto.a").display());
println!("cargo:rustc-link-arg-bin={bin}=-lm");
// Tesseract inside MuPDF is C++.
let cxx = if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") { "-lc++" } else { "-lstdc++" };
println!("cargo:rustc-link-arg-bin={bin}={cxx}");
}
/// An OpenSSL directory, from pkg-config.
fn openssl(variable: &str) -> String {
let out = std::process::Command::new("pkg-config")
.args([&format!("--variable={variable}"), "libcrypto"])
.output()
.expect("pkg-config, to find OpenSSL");
assert!(out.status.success(), "pkg-config knows no libcrypto");
String::from_utf8(out.stdout).expect("a UTF-8 path").trim().to_string()
}
Binary file not shown.
+93
View File
@@ -0,0 +1,93 @@
Copyright 2015 The Great Vibes Pro Project Authors (https://github.com/googlefonts/great-vibes)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
+86
View File
@@ -0,0 +1,86 @@
/* Alternate text dropped from redacted pages (PDF-09). */
#include "ely_shim.h"
typedef void (bdc_fn)(fz_context *ctx, pdf_processor *proc, const char *tag, pdf_obj *raw, pdf_obj *cooked);
static void drop_said(fz_context *ctx, pdf_obj *dict)
{
if (!pdf_is_dict(ctx, dict))
return;
pdf_dict_del(ctx, dict, PDF_NAME(ActualText));
pdf_dict_del(ctx, dict, PDF_NAME(Alt));
pdf_dict_del(ctx, dict, PDF_NAME(E));
}
static void quiet_bdc(fz_context *ctx, pdf_processor *proc, const char *tag, pdf_obj *raw, pdf_obj *cooked)
{
bdc_fn **out = fz_user_context(ctx);
drop_said(ctx, raw);
drop_said(ctx, cooked);
(*out)(ctx, proc, tag, raw, cooked);
}
/* Each output stream writes marked content through `quiet_bdc`. */
static pdf_processor *quiet_factory(fz_context *ctx, pdf_document *doc, pdf_processor *chain, int struct_parents,
fz_matrix transform, pdf_filter_options *options, void *sopts)
{
bdc_fn **out = fz_user_context(ctx);
if (chain->op_BDC != quiet_bdc)
{
*out = chain->op_BDC;
chain->op_BDC = quiet_bdc;
}
return pdf_new_sanitize_filter(ctx, doc, chain, struct_parents, transform, options, sopts);
}
/* Strips elements on `page`, by own, inherited or kids' `Pg`. */
static int quiet_elem(fz_context *ctx, pdf_obj *elem, int page, int inherited)
{
pdf_obj *kids;
int pg, i, on = 0;
if (!pdf_is_dict(ctx, elem) || pdf_mark_obj(ctx, elem))
return 0;
fz_try(ctx)
{
pg = pdf_dict_gets(ctx, elem, "Pg") ? pdf_to_num(ctx, pdf_dict_gets(ctx, elem, "Pg")) : inherited;
on = pg == page;
kids = pdf_dict_get(ctx, elem, PDF_NAME(K));
if (pdf_is_array(ctx, kids))
for (i = 0; i < pdf_array_len(ctx, kids); i++)
on |= quiet_elem(ctx, pdf_array_get(ctx, kids, i), page, pg);
else
on |= quiet_elem(ctx, kids, page, pg);
if (on)
{
drop_said(ctx, elem);
pdf_dict_del(ctx, elem, PDF_NAME(T));
}
}
fz_always(ctx)
pdf_unmark_obj(ctx, elem);
fz_catch(ctx)
fz_rethrow(ctx);
return on;
}
/* Drops what marked content says on `page`, forms too. */
void ely_drop_alt(fz_context *ctx, pdf_document *pdf, pdf_page *page)
{
pdf_sanitize_filter_options sopts = { 0 };
pdf_filter_factory list[2] = { { quiet_factory, NULL }, { NULL, NULL } };
pdf_filter_options opts = { 0 };
void *before = fz_user_context(ctx);
bdc_fn *out = NULL;
list[0].options = &sopts;
opts.recurse = 1;
opts.filters = list;
fz_set_user_context(ctx, &out);
fz_try(ctx)
pdf_filter_page_contents(ctx, pdf, page, &opts);
fz_always(ctx)
fz_set_user_context(ctx, before);
fz_catch(ctx)
fz_rethrow(ctx);
quiet_elem(ctx, pdf_dict_getp(ctx, pdf_trailer(ctx, pdf), "Root/StructTreeRoot"), pdf_to_num(ctx, page->obj), 0);
}
+461
View File
@@ -0,0 +1,461 @@
/* Annotations: list, add, change, delete (PDF-05). */
#include <math.h>
#include <string.h>
#include "ely_shim.h"
static const char *stamps[] = { "Approved", "AsIs", "Confidential", "Departmental", "Draft", "Experimental",
"Expired", "Final", "ForComment", "ForPublicRelease", "NotApproved", "NotForPublicRelease", "Sold",
"TopSecret", NULL };
void ely_json_string(fz_context *ctx, fz_output *out, const char *s)
{
fz_write_byte(ctx, out, '"');
for (; *s; s++)
{
unsigned char c = (unsigned char)*s;
if (c == '"' || c == '\\')
fz_write_printf(ctx, out, "\\%c", c);
else if (c < 32)
fz_write_printf(ctx, out, "\\u%04x", c);
else
fz_write_byte(ctx, out, c);
}
fz_write_byte(ctx, out, '"');
}
fz_json *ely_member(fz_context *ctx, fz_json *obj, const char *key, int type)
{
fz_json *value = fz_json_object_get(ctx, obj, key);
if (value && value->type != type)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad %s", key);
return value;
}
float ely_number(fz_context *ctx, fz_json *array, int at)
{
fz_json *value = fz_json_array_get(ctx, array, at);
if (!value || value->type != FZ_JSON_NUMBER)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad number list");
return (float)value->u.number;
}
/* Four numbers, at least `least` wide and high. */
fz_rect ely_rect(fz_context *ctx, fz_json *array, float least)
{
fz_rect r;
if (!array || array->type != FZ_JSON_ARRAY || fz_json_array_length(ctx, array) != 4)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad rectangle");
r = fz_make_rect(ely_number(ctx, array, 0), ely_number(ctx, array, 1), ely_number(ctx, array, 2),
ely_number(ctx, array, 3));
if (r.x1 - r.x0 < least || r.y1 - r.y0 < least)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a rectangle too small");
return r;
}
static enum pdf_annot_type kind_of(fz_context *ctx, const char *kind)
{
if (!strcmp(kind, "highlight")) return PDF_ANNOT_HIGHLIGHT;
if (!strcmp(kind, "underline")) return PDF_ANNOT_UNDERLINE;
if (!strcmp(kind, "strikeout")) return PDF_ANNOT_STRIKE_OUT;
if (!strcmp(kind, "note")) return PDF_ANNOT_TEXT;
if (!strcmp(kind, "square")) return PDF_ANNOT_SQUARE;
if (!strcmp(kind, "circle")) return PDF_ANNOT_CIRCLE;
if (!strcmp(kind, "line")) return PDF_ANNOT_LINE;
if (!strcmp(kind, "ink")) return PDF_ANNOT_INK;
if (!strcmp(kind, "stamp")) return PDF_ANNOT_STAMP;
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no annotation kind %s", kind);
}
static int rgb_of(fz_context *ctx, pdf_annot *annot)
{
float c[4], k;
int n;
pdf_annot_color(ctx, annot, &n, c);
if (n == 1)
c[1] = c[2] = c[0];
else if (n == 4)
{
k = 1 - c[3];
c[0] = (1 - c[0]) * k, c[1] = (1 - c[1]) * k, c[2] = (1 - c[2]) * k;
}
else if (n != 3)
return -1;
return ((int)(c[0] * 255 + 0.5f) << 16) | ((int)(c[1] * 255 + 0.5f) << 8) | (int)(c[2] * 255 + 0.5f);
}
static int color_of(fz_context *ctx, fz_json *spec)
{
fz_json *color = ely_member(ctx, spec, "color", FZ_JSON_NUMBER);
if (!color)
return -1;
if (color->u.number < 0 || color->u.number > 0xffffff)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad color");
return (int)color->u.number;
}
static const char *text_of(fz_context *ctx, fz_json *spec, const char *key)
{
fz_json *text = ely_member(ctx, spec, key, FZ_JSON_STRING);
return text ? text->u.string : NULL;
}
static int is_stamp(const char *name)
{
int i;
for (i = 0; name && stamps[i]; i++)
if (!strcmp(stamps[i], name))
return 1;
return 0;
}
/* Throws unless `spec` makes a whole annotation. */
static enum pdf_annot_type check(fz_context *ctx, fz_json *spec, int pages)
{
fz_json *page = ely_member(ctx, spec, "page", FZ_JSON_NUMBER);
fz_json *kind = ely_member(ctx, spec, "kind", FZ_JSON_STRING);
fz_json *list;
enum pdf_annot_type type;
int i, n;
if (!page || page->u.number < 0 || page->u.number >= pages || !kind)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "an annotation with no page or kind");
type = kind_of(ctx, kind->u.string);
color_of(ctx, spec);
text_of(ctx, spec, "contents");
text_of(ctx, spec, "author");
switch (type)
{
case PDF_ANNOT_HIGHLIGHT:
case PDF_ANNOT_UNDERLINE:
case PDF_ANNOT_STRIKE_OUT:
list = ely_member(ctx, spec, "rects", FZ_JSON_ARRAY);
n = list ? fz_json_array_length(ctx, list) : 0;
if (n == 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a mark over nothing");
for (i = 0; i < n; i++)
ely_rect(ctx, fz_json_array_get(ctx, list, i), 0.1f);
break;
case PDF_ANNOT_TEXT:
list = ely_member(ctx, spec, "at", FZ_JSON_ARRAY);
if (!list || fz_json_array_length(ctx, list) != 2)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a note with no place");
ely_number(ctx, list, 0), ely_number(ctx, list, 1);
break;
case PDF_ANNOT_LINE:
list = ely_member(ctx, spec, "line", FZ_JSON_ARRAY);
if (!list || fz_json_array_length(ctx, list) != 4)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a line with no ends");
if (fabsf(ely_number(ctx, list, 2) - ely_number(ctx, list, 0)) +
fabsf(ely_number(ctx, list, 3) - ely_number(ctx, list, 1)) < 2)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a line too short");
break;
case PDF_ANNOT_INK:
list = ely_member(ctx, spec, "strokes", FZ_JSON_ARRAY);
n = list ? fz_json_array_length(ctx, list) : 0;
if (n == 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "ink with no strokes");
for (i = 0; i < n; i++)
{
fz_json *stroke = fz_json_array_get(ctx, list, i);
int k, m = stroke->type == FZ_JSON_ARRAY ? fz_json_array_length(ctx, stroke) : 0;
if (m < 4 || m % 2)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad stroke");
for (k = 0; k < m; k++)
ely_number(ctx, stroke, k);
}
break;
case PDF_ANNOT_STAMP:
if (!is_stamp(text_of(ctx, spec, "icon")))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no such stamp");
ely_rect(ctx, ely_member(ctx, spec, "rect", FZ_JSON_ARRAY), 2);
break;
default:
ely_rect(ctx, ely_member(ctx, spec, "rect", FZ_JSON_ARRAY), 2);
}
return type;
}
static fz_point point_at(fz_context *ctx, fz_json *list, int at)
{
return fz_make_point(ely_number(ctx, list, at), ely_number(ctx, list, at + 1));
}
/* Makes a checked `spec` inside the open step. */
static void make(fz_context *ctx, pdf_document *pdf, fz_json *spec, enum pdf_annot_type type)
{
pdf_page *page = NULL;
pdf_annot *annot = NULL;
fz_point *points = NULL;
fz_var(page);
fz_var(annot);
fz_var(points);
fz_try(ctx)
{
fz_json *list;
int i, k, n, color = color_of(ctx, spec);
page = pdf_load_page(ctx, pdf, (int)ely_member(ctx, spec, "page", FZ_JSON_NUMBER)->u.number);
annot = pdf_create_annot(ctx, page, type);
switch (type)
{
case PDF_ANNOT_HIGHLIGHT:
case PDF_ANNOT_UNDERLINE:
case PDF_ANNOT_STRIKE_OUT:
list = ely_member(ctx, spec, "rects", FZ_JSON_ARRAY);
n = fz_json_array_length(ctx, list);
for (i = 0; i < n; i++)
pdf_add_annot_quad_point(ctx, annot, fz_quad_from_rect(ely_rect(ctx, fz_json_array_get(ctx, list, i), 0.1f)));
break;
case PDF_ANNOT_TEXT:
{
fz_point at = point_at(ctx, ely_member(ctx, spec, "at", FZ_JSON_ARRAY), 0);
pdf_set_annot_rect(ctx, annot, fz_make_rect(at.x, at.y, at.x + 16, at.y + 16));
pdf_set_annot_popup(ctx, annot, fz_make_rect(at.x + 20, at.y, at.x + 220, at.y + 100));
break;
}
case PDF_ANNOT_LINE:
list = ely_member(ctx, spec, "line", FZ_JSON_ARRAY);
pdf_set_annot_line(ctx, annot, point_at(ctx, list, 0), point_at(ctx, list, 2));
break;
case PDF_ANNOT_INK:
list = ely_member(ctx, spec, "strokes", FZ_JSON_ARRAY);
n = fz_json_array_length(ctx, list);
for (i = 0; i < n; i++)
{
fz_json *stroke = fz_json_array_get(ctx, list, i);
int m = fz_json_array_length(ctx, stroke) / 2;
fz_free(ctx, points);
points = NULL;
points = fz_malloc_array(ctx, m, fz_point);
for (k = 0; k < m; k++)
points[k] = point_at(ctx, stroke, k * 2);
pdf_add_annot_ink_list(ctx, annot, m, points);
}
break;
case PDF_ANNOT_STAMP:
pdf_set_annot_icon_name(ctx, annot, text_of(ctx, spec, "icon"));
pdf_set_annot_rect(ctx, annot, ely_rect(ctx, ely_member(ctx, spec, "rect", FZ_JSON_ARRAY), 2));
break;
default:
pdf_set_annot_rect(ctx, annot, ely_rect(ctx, ely_member(ctx, spec, "rect", FZ_JSON_ARRAY), 2));
}
if (color >= 0)
{
float rgb[3] = { (color >> 16) / 255.0f, ((color >> 8) & 255) / 255.0f, (color & 255) / 255.0f };
pdf_set_annot_color(ctx, annot, 3, rgb);
}
if (text_of(ctx, spec, "contents"))
pdf_set_annot_contents(ctx, annot, text_of(ctx, spec, "contents"));
if (text_of(ctx, spec, "author"))
pdf_set_annot_author(ctx, annot, text_of(ctx, spec, "author"));
pdf_update_annot(ctx, annot);
}
fz_always(ctx)
{
fz_free(ctx, points);
pdf_drop_annot(ctx, annot);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
fz_rethrow(ctx);
}
/* A JSON list of annotations, added in one step. */
int ely_annotate(fz_context *ctx, fz_document *doc, const char *json)
{
pdf_document *pdf = NULL;
fz_pool *pool = NULL;
enum pdf_annot_type *types = NULL;
int begun = 0;
fz_var(pdf);
fz_var(pool);
fz_var(types);
fz_var(begun);
fz_try(ctx)
{
fz_json *specs;
int i, n, pages;
pdf = ely_pdf_of(ctx, doc);
pages = pdf_count_pages(ctx, pdf);
pool = fz_new_pool(ctx);
specs = fz_parse_json(ctx, pool, json);
n = specs->type == FZ_JSON_ARRAY ? fz_json_array_length(ctx, specs) : 0;
if (n == 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no annotations to add");
types = fz_malloc_array(ctx, n, enum pdf_annot_type);
for (i = 0; i < n; i++)
types[i] = check(ctx, fz_json_array_get(ctx, specs, i), pages);
pdf_begin_operation(ctx, pdf, "Annotate");
begun = 1;
for (i = 0; i < n; i++)
make(ctx, pdf, fz_json_array_get(ctx, specs, i), types[i]);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
fz_free(ctx, types);
fz_drop_pool(ctx, pool);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* The annotation or widget object `id` on `page`. */
pdf_annot *ely_find_annot(fz_context *ctx, pdf_page *page, int id, int widget)
{
pdf_annot *annot = widget ? pdf_first_widget(ctx, page) : pdf_first_annot(ctx, page);
for (; annot; annot = widget ? pdf_next_widget(ctx, annot) : pdf_next_annot(ctx, annot))
if (pdf_to_num(ctx, pdf_annot_obj(ctx, annot)) == id)
return annot;
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no %s %d on the page", widget ? "field" : "annotation", id);
}
/* Sets an annotation's colour or contents, in one step. */
int ely_change_annot(fz_context *ctx, fz_document *doc, int number, int id, const char *json)
{
pdf_document *pdf = NULL;
pdf_page *page = NULL;
fz_pool *pool = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(pool);
fz_var(begun);
fz_try(ctx)
{
pdf_annot *annot;
fz_json *spec;
const char *contents;
int color;
pdf = ely_pdf_of(ctx, doc);
page = pdf_load_page(ctx, pdf, number);
annot = ely_find_annot(ctx, page, id, 0);
pool = fz_new_pool(ctx);
spec = fz_parse_json(ctx, pool, json);
color = color_of(ctx, spec);
contents = text_of(ctx, spec, "contents");
if (color < 0 && !contents)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no change");
pdf_begin_operation(ctx, pdf, "Change an annotation");
begun = 1;
if (color >= 0)
{
float rgb[3] = { (color >> 16) / 255.0f, ((color >> 8) & 255) / 255.0f, (color & 255) / 255.0f };
pdf_set_annot_color(ctx, annot, 3, rgb);
}
if (contents)
pdf_set_annot_contents(ctx, annot, contents);
pdf_update_annot(ctx, annot);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
fz_drop_pool(ctx, pool);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Unlinks `field`; an emptied parent goes too. */
void ely_unlink_field(fz_context *ctx, pdf_document *pdf, pdf_obj *field)
{
pdf_obj *parent = pdf_dict_get(ctx, field, PDF_NAME(Parent));
pdf_obj *kids = parent ? pdf_dict_get(ctx, parent, PDF_NAME(Kids)) : pdf_dict_getp(ctx, pdf_trailer(ctx, pdf), "Root/AcroForm/Fields");
int at = pdf_array_find(ctx, kids, field);
if (at >= 0)
pdf_array_delete(ctx, kids, at);
if (parent && pdf_array_len(ctx, kids) == 0)
ely_unlink_field(ctx, pdf, parent);
}
/* Deletes annotation or field `id`, in one step. */
int ely_delete_annot(fz_context *ctx, fz_document *doc, int number, int id, int widget)
{
pdf_document *pdf = NULL;
pdf_page *page = NULL;
pdf_obj *field = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(field);
fz_var(begun);
fz_try(ctx)
{
pdf_annot *annot;
pdf = ely_pdf_of(ctx, doc);
page = pdf_load_page(ctx, pdf, number);
annot = ely_find_annot(ctx, page, id, widget);
field = pdf_keep_obj(ctx, pdf_annot_obj(ctx, annot));
pdf_begin_operation(ctx, pdf, widget ? "Delete a field" : "Delete an annotation");
begun = 1;
pdf_delete_annot(ctx, page, annot);
if (widget)
ely_unlink_field(ctx, pdf, field);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
pdf_drop_obj(ctx, field);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
static void write_annot(fz_context *ctx, fz_output *out, pdf_annot *annot)
{
fz_rect r = pdf_bound_annot(ctx, annot);
fz_write_printf(ctx, out, "{\"id\":%d,\"kind\":", pdf_to_num(ctx, pdf_annot_obj(ctx, annot)));
ely_json_string(ctx, out, pdf_string_from_annot_type(ctx, pdf_annot_type(ctx, annot)));
fz_write_printf(ctx, out, ",\"rect\":[%g,%g,%g,%g],\"color\":%d,\"contents\":", r.x0, r.y0, r.x1, r.y1,
rgb_of(ctx, annot));
ely_json_string(ctx, out, pdf_annot_contents(ctx, annot));
fz_write_string(ctx, out, ",\"author\":");
ely_json_string(ctx, out, pdf_annot_has_author(ctx, annot) ? pdf_annot_author(ctx, annot) : "");
fz_write_string(ctx, out, ",\"icon\":");
ely_json_string(ctx, out, pdf_annot_has_icon_name(ctx, annot) ? pdf_annot_icon_name(ctx, annot) : "");
fz_write_byte(ctx, out, '}');
}
/* JSON of a page's annotations and form fields. */
int ely_annots(fz_context *ctx, fz_document *doc, int number, unsigned char **data, size_t *len)
{
pdf_page *page = NULL;
fz_buffer *buf = NULL;
fz_output *out = NULL;
fz_var(page);
fz_var(buf);
fz_var(out);
fz_try(ctx)
{
pdf_annot *annot;
page = pdf_load_page(ctx, ely_pdf_of(ctx, doc), number);
buf = fz_new_buffer(ctx, 1024);
out = fz_new_output_with_buffer(ctx, buf);
fz_write_string(ctx, out, "{\"annots\":[");
for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot))
{
if (annot != pdf_first_annot(ctx, page))
fz_write_byte(ctx, out, ',');
write_annot(ctx, out, annot);
}
fz_write_string(ctx, out, "],\"fields\":[");
ely_write_fields(ctx, out, page);
fz_write_string(ctx, out, "]}");
fz_close_output(ctx, out);
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
{
fz_drop_output(ctx, out);
fz_drop_buffer(ctx, buf);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
+395
View File
@@ -0,0 +1,395 @@
/* Cleaning into a new file, counted before and after (PDF-10). */
#include <string.h>
#include "ely_shim.h"
enum { CLEAN_META = 1, CLEAN_ANNOTS = 2, CLEAN_FILES = 4, CLEAN_HIDDEN = 8, CLEAN_ALL = 15 };
/* What cleaning reaches, counted. */
typedef struct
{
int meta, annots, files, hidden_pages, scripts, hidden_annots;
} reach;
/* The filter running a page, and what it drops. */
typedef struct
{
pdf_processor *top;
int dropped;
} hiding;
static pdf_processor *hide_factory(fz_context *ctx, pdf_document *doc, pdf_processor *chain, int struct_parents,
fz_matrix transform, pdf_filter_options *options, void *sopts)
{
hiding *h = ((pdf_sanitize_filter_options *)sopts)->opaque;
h->top = pdf_new_sanitize_filter(ctx, doc, chain, struct_parents, transform, options, sopts);
/* Layers count as hidden only for a viewer's usage. */
h->top->usage = "View";
return h->top;
}
/* Unseen text: modes 3 and 7, or zero opacity. */
static int hide_text(fz_context *ctx, void *opaque, int *ucs, int len, fz_matrix trm, fz_matrix ctm, fz_rect bbox,
int tr, float ca, float CA)
{
hiding *h = opaque;
int fills = tr == 0 || tr == 2 || tr == 4 || tr == 6, strokes = tr == 1 || tr == 2 || tr == 5 || tr == 6;
int drop = h->top->hidden > 0 || tr == 3 || tr == 7 || ((!fills || ca == 0) && (!strokes || CA == 0));
(void)ctx, (void)ucs, (void)len, (void)trm, (void)ctm, (void)bbox;
h->dropped += drop;
return drop;
}
/* Drawing inside a layer that is off. */
static int hide_cull(fz_context *ctx, void *opaque, fz_rect bbox, fz_cull_type type)
{
hiding *h = opaque;
(void)ctx, (void)bbox, (void)type;
h->dropped += h->top->hidden > 0;
return h->top->hidden > 0;
}
/* Drops hidden text and hidden layers from `page`; how many. */
static int drop_hidden(fz_context *ctx, pdf_document *pdf, pdf_page *page)
{
hiding h = { NULL, 0 };
pdf_sanitize_filter_options sopts = { 0 };
pdf_filter_factory list[2] = { { hide_factory, NULL }, { NULL, NULL } };
pdf_filter_options opts = { 0 };
sopts.opaque = &h;
sopts.text_filter = hide_text;
sopts.culler = hide_cull;
list[0].options = &sopts;
opts.recurse = 1;
opts.filters = list;
pdf_filter_page_contents(ctx, pdf, page, &opts);
if (h.dropped)
ely_drop_alt(ctx, pdf, page);
return h.dropped;
}
static int is_script(fz_context *ctx, pdf_obj *action)
{
return pdf_is_dict(ctx, action) &&
(pdf_name_eq(ctx, pdf_dict_get(ctx, action, PDF_NAME(S)), PDF_NAME(JavaScript)) || pdf_dict_get(ctx, action, PDF_NAME(JS)));
}
/* `action` without its scripts, their Next chains kept. */
static pdf_obj *spliced(fz_context *ctx, pdf_document *pdf, pdf_obj *action, int *n)
{
pdf_obj *out = NULL, *each = NULL;
int i, k;
fz_var(each);
if (is_script(ctx, action))
{
if (pdf_mark_obj(ctx, action))
return NULL;
(*n)++;
fz_try(ctx)
out = spliced(ctx, pdf, pdf_dict_get(ctx, action, PDF_NAME(Next)), n);
fz_always(ctx)
pdf_unmark_obj(ctx, action);
fz_catch(ctx)
fz_rethrow(ctx);
return out;
}
if (!pdf_is_array(ctx, action))
return pdf_keep_obj(ctx, action);
out = pdf_new_array(ctx, pdf, pdf_array_len(ctx, action));
fz_try(ctx)
for (i = 0; i < pdf_array_len(ctx, action); i++)
{
each = spliced(ctx, pdf, pdf_array_get(ctx, action, i), n);
if (pdf_is_array(ctx, each))
for (k = 0; k < pdf_array_len(ctx, each); k++)
pdf_array_push(ctx, out, pdf_array_get(ctx, each, k));
else if (each)
pdf_array_push(ctx, out, each);
pdf_drop_obj(ctx, each);
each = NULL;
}
fz_always(ctx)
pdf_drop_obj(ctx, each);
fz_catch(ctx)
{
pdf_drop_obj(ctx, out);
fz_rethrow(ctx);
}
return out;
}
/* Splices scripts out of `dict[key]`; how many. */
static int splice_key(fz_context *ctx, pdf_document *pdf, pdf_obj *dict, const char *key)
{
pdf_obj *now = pdf_dict_gets(ctx, dict, key), *kept;
int n = 0;
if (!now)
return 0;
kept = spliced(ctx, pdf, now, &n);
if (n == 0)
pdf_drop_obj(ctx, kept);
else if (kept && !(pdf_is_array(ctx, kept) && pdf_array_len(ctx, kept) == 0))
pdf_dict_puts_drop(ctx, dict, key, kept);
else
{
pdf_drop_obj(ctx, kept);
pdf_dict_dels(ctx, dict, key);
}
return n;
}
static int drop_key(fz_context *ctx, pdf_obj *dict, const char *key)
{
int had = pdf_dict_gets(ctx, dict, key) != NULL;
pdf_dict_dels(ctx, dict, key);
return had;
}
/* Cleans `obj` and the direct values inside it. */
static void clean_obj(fz_context *ctx, pdf_document *pdf, pdf_obj *obj, int mask, reach *r)
{
pdf_obj *aa;
int i;
if (pdf_is_dict(ctx, obj))
{
if (mask & CLEAN_META)
r->meta += drop_key(ctx, obj, "Metadata") + drop_key(ctx, obj, "PieceInfo");
if (mask & CLEAN_FILES)
{
r->files += drop_key(ctx, obj, "EF");
drop_key(ctx, obj, "RF");
drop_key(ctx, obj, "AF");
}
if (mask & CLEAN_HIDDEN)
{
r->scripts += splice_key(ctx, pdf, obj, "A") + splice_key(ctx, pdf, obj, "OpenAction") +
splice_key(ctx, pdf, obj, "Next");
aa = pdf_dict_get(ctx, obj, PDF_NAME(AA));
for (i = pdf_dict_len(ctx, aa) - 1; i >= 0; i--)
r->scripts += splice_key(ctx, pdf, aa, pdf_to_name(ctx, pdf_dict_get_key(ctx, aa, i)));
if (pdf_is_dict(ctx, aa) && pdf_dict_len(ctx, aa) == 0)
pdf_dict_del(ctx, obj, PDF_NAME(AA));
}
for (i = 0; i < pdf_dict_len(ctx, obj); i++)
if (!pdf_is_indirect(ctx, pdf_dict_get_val(ctx, obj, i)))
clean_obj(ctx, pdf, pdf_dict_get_val(ctx, obj, i), mask, r);
}
else if (pdf_is_array(ctx, obj))
for (i = 0; i < pdf_array_len(ctx, obj); i++)
if (!pdf_is_indirect(ctx, pdf_array_get(ctx, obj, i)))
clean_obj(ctx, pdf, pdf_array_get(ctx, obj, i), mask, r);
}
/* Every object: metadata, embedded bytes, scripts. */
static void clean_all(fz_context *ctx, pdf_document *pdf, int mask, reach *r)
{
int i;
for (i = 1; i < pdf_xref_len(ctx, pdf); i++)
{
pdf_obj *obj = pdf_new_indirect(ctx, pdf, i, 0);
fz_try(ctx)
clean_obj(ctx, pdf, pdf_resolve_indirect(ctx, obj), mask, r);
fz_always(ctx)
pdf_drop_obj(ctx, obj);
fz_catch(ctx)
fz_rethrow(ctx);
}
}
static int unseen(fz_context *ctx, pdf_obj *annot)
{
return (pdf_dict_get_int(ctx, annot, PDF_NAME(F)) & (PDF_ANNOT_IS_HIDDEN | PDF_ANNOT_IS_INVISIBLE)) != 0;
}
/* Hidden fields go with their values. */
static int drop_hidden_widgets(fz_context *ctx, pdf_page *page)
{
pdf_annot *widget;
int n = 0;
restart:
for (widget = pdf_first_widget(ctx, page); widget; widget = pdf_next_widget(ctx, widget))
if (unseen(ctx, pdf_annot_obj(ctx, widget)))
{
pdf_obj *field = pdf_keep_obj(ctx, pdf_annot_obj(ctx, widget));
fz_try(ctx)
{
pdf_delete_annot(ctx, page, widget);
ely_unlink_field(ctx, page->doc, field);
}
fz_always(ctx)
pdf_drop_obj(ctx, field);
fz_catch(ctx)
fz_rethrow(ctx);
n++;
goto restart;
}
return n;
}
/* Links are not in MuPDF's annotation list. */
static int drop_hidden_links(fz_context *ctx, pdf_page *page)
{
pdf_obj *annots = pdf_dict_get(ctx, page->obj, PDF_NAME(Annots));
int i, n = 0;
for (i = pdf_array_len(ctx, annots) - 1; i >= 0; i--)
{
pdf_obj *annot = pdf_array_get(ctx, annots, i);
if (pdf_name_eq(ctx, pdf_dict_get(ctx, annot, PDF_NAME(Subtype)), PDF_NAME(Link)) && unseen(ctx, annot))
{
pdf_array_delete(ctx, annots, i);
n++;
}
}
return n;
}
static void sweep_page(fz_context *ctx, pdf_document *pdf, pdf_page *page, int mask, reach *r)
{
pdf_annot *annot;
if (mask & CLEAN_HIDDEN)
r->hidden_pages += drop_hidden(ctx, pdf, page) > 0;
restart:
for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot))
{
int file = pdf_annot_type(ctx, annot) == PDF_ANNOT_FILE_ATTACHMENT;
int hidden = (pdf_annot_flags(ctx, annot) & (PDF_ANNOT_IS_HIDDEN | PDF_ANNOT_IS_INVISIBLE)) != 0;
if ((file && (mask & CLEAN_FILES)) || (!file && (mask & CLEAN_ANNOTS)) || (hidden && (mask & CLEAN_HIDDEN)))
{
r->hidden_annots += hidden && !file;
r->annots += !file && !hidden;
pdf_delete_annot(ctx, page, annot);
goto restart;
}
}
if (mask & CLEAN_HIDDEN)
r->hidden_annots += drop_hidden_widgets(ctx, page) + drop_hidden_links(ctx, page);
}
/* Drops what `mask` names from `doc`, counting what was there. */
static reach sweep(fz_context *ctx, fz_document *doc, int mask)
{
pdf_document *pdf = ely_pdf_of(ctx, doc);
pdf_obj *trailer = pdf_trailer(ctx, pdf), *root = pdf_dict_get(ctx, trailer, PDF_NAME(Root));
pdf_obj *names = pdf_dict_get(ctx, root, PDF_NAME(Names)), *tree;
pdf_page *page = NULL;
reach r = { 0 };
int i;
fz_var(page);
if (mask & CLEAN_META)
{
r.meta = pdf_dict_len(ctx, pdf_dict_get(ctx, trailer, PDF_NAME(Info)));
pdf_dict_del(ctx, trailer, PDF_NAME(Info));
}
if ((mask & CLEAN_FILES) && names)
pdf_dict_del(ctx, names, PDF_NAME(EmbeddedFiles));
if (mask & CLEAN_HIDDEN)
{
tree = pdf_load_name_tree(ctx, pdf, PDF_NAME(JavaScript));
r.scripts = pdf_dict_len(ctx, tree);
pdf_drop_obj(ctx, tree);
if (names)
pdf_dict_del(ctx, names, PDF_NAME(JavaScript));
}
clean_all(ctx, pdf, mask, &r);
fz_try(ctx)
{
for (i = 0; i < pdf_count_pages(ctx, pdf); i++)
{
page = pdf_load_page(ctx, pdf, i);
sweep_page(ctx, pdf, page, mask, &r);
fz_drop_page(ctx, (fz_page *)page);
page = NULL;
}
}
fz_always(ctx)
fz_drop_page(ctx, (fz_page *)page);
fz_catch(ctx)
fz_rethrow(ctx);
return r;
}
static void write_reach(fz_context *ctx, fz_buffer *buf, reach r)
{
fz_append_printf(ctx, buf, "{\"meta\":%d,\"annots\":%d,\"files\":%d,\"hidden\":%d,\"scripts\":%d,\"hidden_annots\":%d}",
r.meta, r.annots, r.files, r.hidden_pages, r.scripts, r.hidden_annots);
}
/* Counts by sweeping a throwaway copy of `pdf`. */
static reach count_copy(fz_context *ctx, pdf_document *pdf, const char *raw, const char *password)
{
fz_document *copy = NULL;
reach r;
fz_var(copy);
fz_try(ctx)
{
pdf_save_snapshot(ctx, pdf, raw);
copy = fz_open_document(ctx, raw);
if (fz_needs_password(ctx, copy) && !fz_authenticate_password(ctx, copy, password))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the copy refused its password");
r = sweep(ctx, copy, CLEAN_ALL);
}
fz_always(ctx)
fz_drop_document(ctx, copy);
fz_catch(ctx)
fz_rethrow(ctx);
return r;
}
/* What cleaning would reach, as JSON. */
int ely_clean_scan(fz_context *ctx, fz_document *doc, const char *raw, const char *password, unsigned char **data, size_t *len)
{
fz_buffer *buf = NULL;
fz_var(buf);
fz_try(ctx)
{
buf = fz_new_buffer(ctx, 128);
write_reach(ctx, buf, count_copy(ctx, ely_pdf_of(ctx, doc), raw, password));
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
fz_drop_buffer(ctx, buf);
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
static void apply_clean(fz_context *ctx, fz_document *copy, void *arg)
{
sweep(ctx, copy, *(int *)arg);
}
/* Cleans `mask` into `out`, then counts what is left. */
int ely_clean(fz_context *ctx, fz_document *doc, int mask, const char *raw, const char *out, const char *password,
unsigned char **data, size_t *len)
{
fz_document *written = NULL;
fz_buffer *buf = NULL;
fz_var(written);
fz_var(buf);
fz_try(ctx)
{
reach left;
if (mask <= 0 || mask > CLEAN_ALL)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "nothing chosen to clean");
ely_save_copy(ctx, ely_pdf_of(ctx, doc), raw, out, password, NULL, apply_clean, &mask);
written = fz_open_document(ctx, out);
if (fz_needs_password(ctx, written) && !fz_authenticate_password(ctx, written, password))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the written file refused its password");
left = count_copy(ctx, ely_pdf_of(ctx, written), raw, password);
buf = fz_new_buffer(ctx, 128);
write_reach(ctx, buf, left);
if (((mask & CLEAN_META) && left.meta) || ((mask & CLEAN_ANNOTS) && left.annots) || ((mask & CLEAN_FILES) && left.files) ||
((mask & CLEAN_HIDDEN) && (left.hidden_pages || left.scripts || left.hidden_annots)))
fz_throw(ctx, FZ_ERROR_FORMAT, "the written file still holds what was cleaned");
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
{
fz_drop_buffer(ctx, buf);
fz_drop_document(ctx, written);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
+489
View File
@@ -0,0 +1,489 @@
/* Form fields: list, add, fill, change (PDF-06). */
#include <stdio.h>
#include <string.h>
#include "ely_shim.h"
static const char *kind_name(enum pdf_widget_type type)
{
switch (type)
{
case PDF_WIDGET_TYPE_TEXT: return "text";
case PDF_WIDGET_TYPE_CHECKBOX: return "checkbox";
case PDF_WIDGET_TYPE_RADIOBUTTON: return "radio";
case PDF_WIDGET_TYPE_COMBOBOX: return "choice";
case PDF_WIDGET_TYPE_LISTBOX: return "list";
case PDF_WIDGET_TYPE_SIGNATURE: return "signature";
default: return "button";
}
}
/* The field that names `obj`. */
static pdf_obj *head_of(fz_context *ctx, pdf_obj *obj)
{
while (!pdf_dict_get(ctx, obj, PDF_NAME(T)) && pdf_dict_get(ctx, obj, PDF_NAME(Parent)))
obj = pdf_dict_get(ctx, obj, PDF_NAME(Parent));
return obj;
}
static pdf_obj *fields_of(fz_context *ctx, pdf_document *pdf)
{
pdf_obj *root = pdf_dict_get(ctx, pdf_trailer(ctx, pdf), PDF_NAME(Root));
pdf_obj *form = pdf_dict_get(ctx, root, PDF_NAME(AcroForm));
if (!form)
form = pdf_dict_put_dict(ctx, root, PDF_NAME(AcroForm), 2);
if (!pdf_is_array(ctx, pdf_dict_get(ctx, form, PDF_NAME(Fields))))
pdf_dict_put_array(ctx, form, PDF_NAME(Fields), 4);
return pdf_dict_get(ctx, form, PDF_NAME(Fields));
}
/* A value as text; names and streams too. */
static void write_value(fz_context *ctx, fz_output *out, pdf_obj *v)
{
char *text = NULL;
fz_var(text);
fz_try(ctx)
{
if (pdf_is_name(ctx, v))
ely_json_string(ctx, out, pdf_to_name(ctx, v));
else if (pdf_is_stream(ctx, v))
{
text = pdf_new_utf8_from_pdf_stream_obj(ctx, v);
ely_json_string(ctx, out, text);
}
else
ely_json_string(ctx, out, pdf_to_text_string(ctx, v));
}
fz_always(ctx)
fz_free(ctx, text);
fz_catch(ctx)
fz_rethrow(ctx);
}
static void write_options(fz_context *ctx, fz_output *out, pdf_obj *obj, int export)
{
int i, n = pdf_choice_field_option_count(ctx, obj);
fz_write_byte(ctx, out, '[');
for (i = 0; i < n; i++)
{
if (i)
fz_write_byte(ctx, out, ',');
ely_json_string(ctx, out, pdf_choice_field_option(ctx, obj, export, i));
}
fz_write_byte(ctx, out, ']');
}
void ely_write_fields(fz_context *ctx, fz_output *out, pdf_page *page)
{
pdf_annot *widget;
char *name = NULL;
fz_var(name);
fz_try(ctx)
{
for (widget = pdf_first_widget(ctx, page); widget; widget = pdf_next_widget(ctx, widget))
{
pdf_obj *obj = pdf_annot_obj(ctx, widget);
enum pdf_widget_type type = pdf_widget_type(ctx, widget);
fz_rect r = pdf_bound_widget(ctx, widget);
int button = type == PDF_WIDGET_TYPE_CHECKBOX || type == PDF_WIDGET_TYPE_RADIOBUTTON;
pdf_obj *as = pdf_dict_get(ctx, obj, PDF_NAME(AS));
fz_free(ctx, name);
name = NULL;
name = pdf_load_field_name(ctx, obj);
fz_write_printf(ctx, out, "%s{\"id\":%d,\"kind\":\"%s\",\"name\":", widget == pdf_first_widget(ctx, page) ? "" : ",",
pdf_to_num(ctx, obj), kind_name(type));
ely_json_string(ctx, out, name);
fz_write_printf(ctx, out, ",\"rect\":[%g,%g,%g,%g],\"value\":", r.x0, r.y0, r.x1, r.y1);
write_value(ctx, out, pdf_dict_get_inheritable(ctx, obj, PDF_NAME(V)));
fz_write_string(ctx, out, ",\"on\":");
ely_json_string(ctx, out, button ? pdf_to_name(ctx, pdf_button_field_on_state(ctx, obj)) : "");
fz_write_printf(ctx, out, ",\"checked\":%s,\"readonly\":%s,\"options\":",
button && pdf_is_name(ctx, as) && !pdf_name_eq(ctx, as, PDF_NAME(Off)) ? "true" : "false",
pdf_widget_is_readonly(ctx, widget) ? "true" : "false");
write_options(ctx, out, obj, 1);
fz_write_string(ctx, out, ",\"labels\":");
write_options(ctx, out, obj, 0);
fz_write_byte(ctx, out, '}');
}
}
fz_always(ctx)
fz_free(ctx, name);
fz_catch(ctx)
fz_rethrow(ctx);
}
/* Shows `state` on each button under `node`, else Off. */
static void show_state(fz_context *ctx, pdf_obj *node, pdf_obj *state)
{
pdf_obj *kids = pdf_dict_get(ctx, node, PDF_NAME(Kids));
int i;
if (kids)
{
for (i = 0; i < pdf_array_len(ctx, kids); i++)
show_state(ctx, pdf_array_get(ctx, kids, i), state);
return;
}
pdf_dict_put(ctx, node, PDF_NAME(AS), pdf_dict_get(ctx, pdf_dict_getp(ctx, node, "AP/N"), state) ? state : PDF_NAME(Off));
}
/* Fills field widget `id` with `value`, in one step. */
int ely_fill(fz_context *ctx, fz_document *doc, int number, int id, const char *value)
{
pdf_document *pdf = NULL;
pdf_page *page = NULL;
pdf_obj *state = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(state);
fz_var(begun);
fz_try(ctx)
{
pdf_annot *widget;
pdf_obj *obj;
enum pdf_widget_type type;
pdf = ely_pdf_of(ctx, doc);
page = pdf_load_page(ctx, pdf, number);
widget = ely_find_annot(ctx, page, id, 1);
obj = pdf_annot_obj(ctx, widget);
type = pdf_widget_type(ctx, widget);
if (pdf_widget_is_readonly(ctx, widget))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the field is read only");
if (type == PDF_WIDGET_TYPE_CHECKBOX || type == PDF_WIDGET_TYPE_RADIOBUTTON)
{
state = pdf_new_name(ctx, value);
if (!pdf_name_eq(ctx, state, PDF_NAME(Off)) && !pdf_name_eq(ctx, state, pdf_button_field_on_state(ctx, obj)))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the button has no state %s", value);
}
else if (type != PDF_WIDGET_TYPE_TEXT && type != PDF_WIDGET_TYPE_COMBOBOX && type != PDF_WIDGET_TYPE_LISTBOX)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a %s field cannot be filled", kind_name(type));
pdf_begin_operation(ctx, pdf, "Fill a field");
begun = 1;
if (state)
{
pdf_dict_put(ctx, head_of(ctx, obj), PDF_NAME(V), state);
show_state(ctx, head_of(ctx, obj), state);
}
else if (!pdf_set_field_value(ctx, pdf, obj, value, 1))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the field refused the value");
pdf_update_annot(ctx, widget);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
pdf_drop_obj(ctx, state);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Throws unless `prefix` plus `name` is free for `kind`. */
static pdf_obj *check_name(fz_context *ctx, pdf_obj *fields, const char *prefix, const char *name, const char *kind,
pdf_obj *self)
{
pdf_obj *found = NULL;
char *full = NULL;
if (!name[0] || strchr(name, '.'))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad field name");
fz_var(full);
fz_try(ctx)
{
full = fz_asprintf(ctx, "%s%s", prefix, name);
found = pdf_lookup_field(ctx, fields, full);
}
fz_always(ctx)
fz_free(ctx, full);
fz_catch(ctx)
fz_rethrow(ctx);
if (!found || (self && !pdf_objcmp(ctx, found, self)))
return NULL;
if (!strcmp(kind, "radio") && pdf_field_type(ctx, found) == PDF_WIDGET_TYPE_RADIOBUTTON &&
pdf_dict_get(ctx, found, PDF_NAME(Kids)))
return found;
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a field named %s%s exists", prefix, name);
}
/* The full name of `field`'s parent, with a dot. */
static char *parent_path(fz_context *ctx, pdf_obj *field)
{
pdf_obj *parent = pdf_dict_get(ctx, field, PDF_NAME(Parent));
char *name = parent ? pdf_load_field_name(ctx, parent) : NULL, *path = NULL;
fz_try(ctx)
path = fz_asprintf(ctx, "%s%s", name ? name : "", name && name[0] ? "." : "");
fz_always(ctx)
fz_free(ctx, name);
fz_catch(ctx)
fz_rethrow(ctx);
return path;
}
static void put_options(fz_context *ctx, pdf_obj *field, fz_json *options)
{
pdf_obj *opt = pdf_dict_put_array(ctx, field, PDF_NAME(Opt), 4);
int i, n = fz_json_array_length(ctx, options);
for (i = 0; i < n; i++)
pdf_array_push_text_string(ctx, opt, fz_json_array_get(ctx, options, i)->u.string);
}
static void check_options(fz_context *ctx, fz_json *options)
{
int i, n = options ? fz_json_array_length(ctx, options) : 0;
if (n == 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a choice with no options");
for (i = 0; i < n; i++)
if (fz_json_array_get(ctx, options, i)->type != FZ_JSON_STRING || !fz_json_array_get(ctx, options, i)->u.string[0])
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad option");
}
/* An on state no button in `group` has. */
static pdf_obj *new_state(fz_context *ctx, pdf_obj *group)
{
pdf_obj *kids = pdf_dict_get(ctx, group, PDF_NAME(Kids));
char name[32];
int n, i, taken;
for (n = pdf_array_len(ctx, kids) + 1;; n++)
{
snprintf(name, sizeof name, "Choice%d", n);
for (i = 0, taken = 0; i < pdf_array_len(ctx, kids); i++)
taken |= !strcmp(pdf_to_name(ctx, pdf_button_field_on_state(ctx, pdf_array_get(ctx, kids, i))), name);
if (!taken)
return pdf_new_name(ctx, name);
}
}
/* Adds a field of four kinds, in one step. */
int ely_add_field(fz_context *ctx, fz_document *doc, int number, const char *json)
{
pdf_document *pdf = NULL;
pdf_page *page = NULL;
pdf_annot *widget = NULL;
pdf_obj *state = NULL;
fz_pool *pool = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(widget);
fz_var(state);
fz_var(pool);
fz_var(begun);
fz_try(ctx)
{
fz_json *spec, *options, *kindj, *namej;
const char *kind, *name;
pdf_obj *fields, *group, *obj, *mk;
fz_rect rect;
pdf = ely_pdf_of(ctx, doc);
page = pdf_load_page(ctx, pdf, number);
pool = fz_new_pool(ctx);
spec = fz_parse_json(ctx, pool, json);
kindj = ely_member(ctx, spec, "kind", FZ_JSON_STRING);
namej = ely_member(ctx, spec, "name", FZ_JSON_STRING);
options = ely_member(ctx, spec, "options", FZ_JSON_ARRAY);
if (!kindj || !namej)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a field with no kind or name");
kind = kindj->u.string, name = namej->u.string;
if (strcmp(kind, "text") && strcmp(kind, "checkbox") && strcmp(kind, "radio") && strcmp(kind, "choice"))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no field kind %s", kind);
if (!strcmp(kind, "choice"))
check_options(ctx, options);
rect = ely_rect(ctx, ely_member(ctx, spec, "rect", FZ_JSON_ARRAY), 4);
group = check_name(ctx, pdf_dict_getp(ctx, pdf_trailer(ctx, pdf), "Root/AcroForm/Fields"), "", name, kind, NULL);
pdf_begin_operation(ctx, pdf, "Add a field");
begun = 1;
fields = fields_of(ctx, pdf);
widget = pdf_create_annot_raw(ctx, page, PDF_ANNOT_WIDGET);
obj = pdf_annot_obj(ctx, widget);
pdf_dict_put(ctx, obj, PDF_NAME(P), page->obj);
pdf_dict_put_int(ctx, obj, PDF_NAME(F), PDF_ANNOT_IS_PRINT);
pdf_dict_put_rect(ctx, obj, PDF_NAME(Rect), fz_transform_rect(rect, ely_to_user(ctx, page)));
pdf_dict_put_text_string(ctx, obj, PDF_NAME(DA), "/Helv 0 Tf 0 g");
mk = pdf_dict_put_dict(ctx, obj, PDF_NAME(MK), 1);
pdf_array_push_real(ctx, pdf_dict_put_array(ctx, mk, PDF_NAME(BC), 1), 0);
if (!strcmp(kind, "radio"))
{
if (!group)
{
group = pdf_add_new_dict(ctx, pdf, 4);
pdf_dict_put(ctx, group, PDF_NAME(FT), PDF_NAME(Btn));
pdf_dict_put_int(ctx, group, PDF_NAME(Ff), PDF_BTN_FIELD_IS_RADIO | PDF_BTN_FIELD_IS_NO_TOGGLE_TO_OFF);
pdf_dict_put_text_string(ctx, group, PDF_NAME(T), name);
pdf_dict_put(ctx, group, PDF_NAME(V), PDF_NAME(Off));
pdf_dict_put_array(ctx, group, PDF_NAME(Kids), 2);
pdf_array_push_drop(ctx, fields, group);
}
state = new_state(ctx, group);
pdf_dict_put(ctx, obj, PDF_NAME(Parent), group);
pdf_array_push(ctx, pdf_dict_get(ctx, group, PDF_NAME(Kids)), obj);
}
else
{
pdf_dict_put_text_string(ctx, obj, PDF_NAME(T), name);
pdf_array_push(ctx, fields, obj);
}
if (!strcmp(kind, "text"))
pdf_dict_put(ctx, obj, PDF_NAME(FT), PDF_NAME(Tx));
else if (!strcmp(kind, "choice"))
{
pdf_dict_put(ctx, obj, PDF_NAME(FT), PDF_NAME(Ch));
pdf_dict_put_int(ctx, obj, PDF_NAME(Ff), PDF_CH_FIELD_IS_COMBO);
put_options(ctx, obj, options);
}
else if (!strcmp(kind, "checkbox"))
{
pdf_dict_put(ctx, obj, PDF_NAME(FT), PDF_NAME(Btn));
pdf_dict_put(ctx, obj, PDF_NAME(V), PDF_NAME(Off));
state = pdf_new_name(ctx, "Yes");
}
/* The on look is drawn under its own state. */
if (state)
pdf_dict_put(ctx, obj, PDF_NAME(AS), state);
pdf_annot_request_resynthesis(ctx, widget);
pdf_update_annot(ctx, widget);
if (state)
pdf_dict_put(ctx, obj, PDF_NAME(AS), PDF_NAME(Off));
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
fz_drop_pool(ctx, pool);
pdf_drop_obj(ctx, state);
pdf_drop_annot(ctx, widget);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Renames a field or sets its options. */
int ely_change_field(fz_context *ctx, fz_document *doc, int number, int id, const char *json)
{
pdf_document *pdf = NULL;
pdf_page *page = NULL;
fz_pool *pool = NULL;
char *path = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(pool);
fz_var(path);
fz_var(begun);
fz_try(ctx)
{
pdf_annot *widget;
pdf_obj *obj, *head;
fz_json *spec, *name, *options;
enum pdf_widget_type type;
pdf = ely_pdf_of(ctx, doc);
page = pdf_load_page(ctx, pdf, number);
widget = ely_find_annot(ctx, page, id, 1);
obj = pdf_annot_obj(ctx, widget);
head = head_of(ctx, obj);
type = pdf_widget_type(ctx, widget);
pool = fz_new_pool(ctx);
spec = fz_parse_json(ctx, pool, json);
name = ely_member(ctx, spec, "name", FZ_JSON_STRING);
options = ely_member(ctx, spec, "options", FZ_JSON_ARRAY);
if (!name && !options)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no change");
if (name)
{
path = parent_path(ctx, head);
check_name(ctx, pdf_dict_getp(ctx, pdf_trailer(ctx, pdf), "Root/AcroForm/Fields"), path, name->u.string, "", head);
}
if (options && type != PDF_WIDGET_TYPE_COMBOBOX && type != PDF_WIDGET_TYPE_LISTBOX)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "only a choice has options");
if (options)
check_options(ctx, options);
pdf_begin_operation(ctx, pdf, "Change a field");
begun = 1;
if (name)
pdf_dict_put_text_string(ctx, head, PDF_NAME(T), name->u.string);
if (options)
{
int i, kept = 0;
const char *value = pdf_to_text_string(ctx, pdf_dict_get_inheritable(ctx, obj, PDF_NAME(V)));
for (i = 0; i < fz_json_array_length(ctx, options); i++)
kept |= !strcmp(fz_json_array_get(ctx, options, i)->u.string, value);
if (obj != head)
pdf_dict_del(ctx, obj, PDF_NAME(Opt));
put_options(ctx, head, options);
if (!kept)
pdf_dict_del(ctx, head, PDF_NAME(V));
}
pdf_annot_request_resynthesis(ctx, widget);
pdf_update_annot(ctx, widget);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
fz_drop_pool(ctx, pool);
fz_free(ctx, path);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Writes each named node under `kids`, with its full name. */
static void write_names(fz_context *ctx, fz_output *out, pdf_obj *kids, const char *prefix, int depth, int *first)
{
int i;
if (depth > 32)
fz_throw(ctx, FZ_ERROR_FORMAT, "a field tree too deep");
for (i = 0; i < pdf_array_len(ctx, kids); i++)
{
pdf_obj *node = pdf_array_get(ctx, kids, i);
pdf_obj *t = pdf_dict_get(ctx, node, PDF_NAME(T));
char *name = NULL;
fz_var(name);
fz_try(ctx)
{
name = t ? fz_asprintf(ctx, "%s%s%s", prefix, prefix[0] ? "." : "", pdf_to_text_string(ctx, t)) : fz_strdup(ctx, prefix);
if (t)
{
fz_write_string(ctx, out, *first ? "" : ",");
ely_json_string(ctx, out, name);
*first = 0;
}
write_names(ctx, out, pdf_dict_get(ctx, node, PDF_NAME(Kids)), name, depth + 1, first);
}
fz_always(ctx)
fz_free(ctx, name);
fz_catch(ctx)
fz_rethrow(ctx);
}
}
/* Every field name in the form, widgets or not. */
int ely_field_names(fz_context *ctx, fz_document *doc, unsigned char **data, size_t *len)
{
fz_buffer *buf = NULL;
fz_output *out = NULL;
fz_var(buf);
fz_var(out);
fz_try(ctx)
{
int first = 1;
pdf_document *pdf = ely_pdf_of(ctx, doc);
buf = fz_new_buffer(ctx, 256);
out = fz_new_output_with_buffer(ctx, buf);
fz_write_byte(ctx, out, '[');
write_names(ctx, out, pdf_dict_getp(ctx, pdf_trailer(ctx, pdf), "Root/AcroForm/Fields"), "", 0, &first);
fz_write_byte(ctx, out, ']');
fz_close_output(ctx, out);
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
{
fz_drop_output(ctx, out);
fz_drop_buffer(ctx, buf);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
+364
View File
@@ -0,0 +1,364 @@
/* Pages from another PDF, with their links, notes and fields. */
#include <stdio.h>
#include <string.h>
#include "ely_shim.h"
/* A source link to its own page `page`. */
typedef struct
{
pdf_obj *annot;
pdf_obj *rest;
int page;
} inner_link;
/* Takes page references off the source's annotations, links noted. */
static int unhook(fz_context *ctx, pdf_document *src, inner_link *links, int room)
{
int i, j, n = 0, count = pdf_count_pages(ctx, src);
for (i = 0; i < count; i++)
{
pdf_obj *annots = pdf_dict_get(ctx, pdf_lookup_page_obj(ctx, src, i), PDF_NAME(Annots));
for (j = 0; j < pdf_array_len(ctx, annots); j++)
{
pdf_obj *annot = pdf_array_get(ctx, annots, j);
pdf_obj *page = ely_target(ctx, src, annot);
int to = page ? pdf_lookup_page_number(ctx, src, page) : -1;
pdf_dict_del(ctx, annot, PDF_NAME(P));
if (to < 0 || n == room)
continue;
/* The view after the page: /XYZ, /Fit and so on. */
pdf_obj *dest = pdf_dict_get(ctx, annot, PDF_NAME(Dest));
if (!dest)
dest = pdf_dict_getp(ctx, annot, "A/D");
if (pdf_is_name(ctx, dest) || pdf_is_string(ctx, dest))
dest = pdf_lookup_dest(ctx, src, dest);
if (pdf_is_dict(ctx, dest))
dest = pdf_dict_get(ctx, dest, PDF_NAME(D));
links[n].annot = pdf_keep_obj(ctx, annot);
links[n].rest = pdf_copy_array(ctx, dest);
pdf_array_delete(ctx, links[n].rest, 0);
links[n].page = to;
n++;
pdf_dict_del(ctx, annot, PDF_NAME(Dest));
pdf_dict_del(ctx, annot, PDF_NAME(A));
}
}
return n;
}
/* The top of the field tree `widget` belongs to. */
static pdf_obj *root_field(fz_context *ctx, pdf_obj *widget)
{
pdf_obj *parent;
while ((parent = pdf_dict_get(ctx, widget, PDF_NAME(Parent))) != NULL)
widget = parent;
return widget;
}
/* The source form's default look and its font renames. */
typedef struct
{
pdf_obj *da;
pdf_obj *q;
pdf_obj *renames;
} form_look;
static int hex(int c)
{
return c >= '0' && c <= '9' ? c - '0' : (c | 32) >= 'a' && (c | 32) <= 'f' ? (c | 32) - 'a' + 10 : -1;
}
/* A name token's bytes, #xx escapes decoded. */
static void decode_name(const char *token, size_t len, char *out)
{
size_t i, k = 0;
for (i = 0; i < len; i++)
{
if (token[i] == '#' && i + 2 < len && hex(token[i + 1]) >= 0 && hex(token[i + 2]) >= 0)
{
out[k++] = (char)(hex(token[i + 1]) * 16 + hex(token[i + 2]));
i += 2;
}
else
out[k++] = token[i];
}
out[k] = 0;
}
/* Writes `name` as a name token, odd bytes escaped. */
static void encode_name(fz_context *ctx, fz_buffer *buf, const char *name)
{
const unsigned char *c;
fz_append_byte(ctx, buf, '/');
for (c = (const unsigned char *)name; *c; c++)
if (*c < 0x21 || *c > 0x7e || strchr("#/[]()<>{}%", *c))
fz_append_printf(ctx, buf, "#%02X", *c);
else
fz_append_byte(ctx, buf, *c);
}
/* `name`, or `name` and `n` when n is above one. */
static char *candidate(fz_context *ctx, const char *name, const char *gap, int n)
{
return n == 1 ? fz_strdup(ctx, name) : fz_asprintf(ctx, "%s%s%d", name, gap, n);
}
/* Merges the source's form fonts; taken names get numbered. */
static void merge_fonts(fz_context *ctx, pdf_document *pdf, pdf_graft_map *map, pdf_document *src, form_look *look)
{
pdf_obj *from = pdf_dict_getp(ctx, pdf_trailer(ctx, src), "Root/AcroForm/DR/Font");
pdf_obj *to, *font = NULL;
char *fresh = NULL;
int i, n;
fz_var(font);
fz_var(fresh);
if (!from)
return;
to = pdf_dict_getp(ctx, pdf_trailer(ctx, pdf), "Root/AcroForm/DR/Font");
if (!to)
{
to = pdf_new_dict(ctx, pdf, 4);
pdf_dict_putp_drop(ctx, pdf_trailer(ctx, pdf), "Root/AcroForm/DR/Font", to);
}
fz_try(ctx)
for (i = 0; i < pdf_dict_len(ctx, from); i++)
{
const char *name = pdf_to_name(ctx, pdf_dict_get_key(ctx, from, i));
font = pdf_graft_mapped_object(ctx, map, pdf_dict_get_val(ctx, from, i));
for (n = 1;; n++)
{
pdf_obj *held;
fz_free(ctx, fresh);
fresh = candidate(ctx, name, "_", n);
held = pdf_dict_gets(ctx, to, fresh);
if (!held)
pdf_dict_puts(ctx, to, fresh, font);
if (!held || (pdf_is_indirect(ctx, held) && pdf_to_num(ctx, held) == pdf_to_num(ctx, font)))
break;
}
if (n > 1)
pdf_dict_puts_drop(ctx, look->renames, name, pdf_new_name(ctx, fresh));
pdf_drop_obj(ctx, font);
font = NULL;
}
fz_always(ctx)
{
pdf_drop_obj(ctx, font);
fz_free(ctx, fresh);
}
fz_catch(ctx)
fz_rethrow(ctx);
}
/* `node`'s /DA with renamed fonts renamed. */
static void rename_fonts(fz_context *ctx, pdf_obj *node, pdf_obj *renames)
{
const char *da = pdf_dict_get_text_string(ctx, node, PDF_NAME(DA));
fz_buffer *buf = NULL;
char *name = NULL;
fz_var(buf);
fz_var(name);
if (!*da || !pdf_dict_len(ctx, renames))
return;
fz_try(ctx)
{
unsigned char *data;
size_t len;
buf = fz_new_buffer(ctx, 64);
while (*da)
{
size_t word = 1;
pdf_obj *to;
if (*da != '/')
{
fz_append_byte(ctx, buf, *da++);
continue;
}
while (da[word] && !strchr(" \t\r\n\f/[]()<>{}%", da[word]))
word++;
fz_free(ctx, name);
name = fz_malloc(ctx, word);
decode_name(da + 1, word - 1, name);
to = pdf_dict_gets(ctx, renames, name);
if (to)
encode_name(ctx, buf, pdf_to_name(ctx, to));
else
fz_append_data(ctx, buf, da, word);
da += word;
}
len = fz_buffer_storage(ctx, buf, &data);
pdf_dict_put_string(ctx, node, PDF_NAME(DA), (const char *)data, len);
}
fz_always(ctx)
{
fz_drop_buffer(ctx, buf);
fz_free(ctx, name);
}
fz_catch(ctx)
fz_rethrow(ctx);
}
/* Gives a field tree the source's look, fonts renamed. */
static void restyle(fz_context *ctx, pdf_obj *node, form_look *look, int root)
{
pdf_obj *kids = pdf_dict_get(ctx, node, PDF_NAME(Kids));
int i;
/* The form's defaults a field inherits: look and alignment. */
if (root && look->da && !pdf_dict_get(ctx, node, PDF_NAME(DA)))
pdf_dict_put(ctx, node, PDF_NAME(DA), look->da);
if (root && look->q && !pdf_dict_get(ctx, node, PDF_NAME(Q)))
pdf_dict_put(ctx, node, PDF_NAME(Q), look->q);
rename_fonts(ctx, node, look->renames);
for (i = 0; i < pdf_array_len(ctx, kids); i++)
restyle(ctx, pdf_array_get(ctx, kids, i), look, 0);
}
/* Joins `field` to the form; a taken name gets numbered. */
static void add_field(fz_context *ctx, pdf_document *pdf, pdf_obj *field, form_look *look)
{
pdf_obj *fields = pdf_dict_getp(ctx, pdf_trailer(ctx, pdf), "Root/AcroForm/Fields");
const char *name = pdf_dict_get_text_string(ctx, field, PDF_NAME(T));
char *renamed = NULL;
int i, n, taken;
fz_var(renamed);
if (!fields)
{
fields = pdf_new_array(ctx, pdf, 1);
pdf_dict_putp_drop(ctx, pdf_trailer(ctx, pdf), "Root/AcroForm/Fields", fields);
}
for (i = 0; i < pdf_array_len(ctx, fields); i++)
if (pdf_to_num(ctx, pdf_array_get(ctx, fields, i)) == pdf_to_num(ctx, field))
return;
fz_try(ctx)
{
for (n = 1, taken = 1; taken; n++)
{
fz_free(ctx, renamed);
renamed = candidate(ctx, name, " ", n);
for (taken = 0, i = 0; i < pdf_array_len(ctx, fields) && !taken; i++)
taken = !strcmp(pdf_dict_get_text_string(ctx, pdf_array_get(ctx, fields, i), PDF_NAME(T)), renamed);
}
if (strcmp(renamed, name))
pdf_dict_put_text_string(ctx, field, PDF_NAME(T), renamed);
restyle(ctx, field, look, 1);
pdf_array_push(ctx, fields, field);
}
fz_always(ctx)
fz_free(ctx, renamed);
fz_catch(ctx)
fz_rethrow(ctx);
}
/* Grafts each page's annotations and reconnects them. */
static void reconnect(fz_context *ctx, pdf_document *pdf, pdf_graft_map *map, pdf_document *src, int at, form_look *look)
{
int i, j, count = pdf_count_pages(ctx, src);
for (i = 0; i < count; i++)
{
pdf_obj *annots = pdf_dict_get(ctx, pdf_lookup_page_obj(ctx, src, i), PDF_NAME(Annots));
pdf_obj *page = pdf_lookup_page_obj(ctx, pdf, at + i);
pdf_obj *grafted;
if (!annots)
continue;
grafted = pdf_graft_mapped_object(ctx, map, annots);
pdf_dict_put_drop(ctx, page, PDF_NAME(Annots), grafted);
for (j = 0; j < pdf_array_len(ctx, grafted); j++)
{
pdf_obj *annot = pdf_array_get(ctx, grafted, j);
pdf_dict_put(ctx, annot, PDF_NAME(P), page);
if (pdf_name_eq(ctx, pdf_dict_get(ctx, annot, PDF_NAME(Subtype)), PDF_NAME(Widget)))
add_field(ctx, pdf, root_field(ctx, annot), look);
}
}
}
/* Annotations and form look come over; inner links point home. */
static void graft_annots(fz_context *ctx, pdf_document *pdf, pdf_graft_map *map, pdf_document *src, int at,
inner_link *links, int n)
{
form_look look = { NULL, NULL, NULL };
pdf_obj *da = pdf_dict_getp(ctx, pdf_trailer(ctx, src), "Root/AcroForm/DA");
pdf_obj *q = pdf_dict_getp(ctx, pdf_trailer(ctx, src), "Root/AcroForm/Q");
int i, j;
fz_var(look.da);
fz_var(look.renames);
fz_var(look.q);
fz_try(ctx)
{
look.renames = pdf_new_dict(ctx, pdf, 1);
look.da = da ? pdf_graft_mapped_object(ctx, map, da) : NULL;
look.q = q ? pdf_graft_mapped_object(ctx, map, q) : NULL;
merge_fonts(ctx, pdf, map, src, &look);
reconnect(ctx, pdf, map, src, at, &look);
}
fz_always(ctx)
{
pdf_drop_obj(ctx, look.renames);
pdf_drop_obj(ctx, look.da);
pdf_drop_obj(ctx, look.q);
}
fz_catch(ctx)
fz_rethrow(ctx);
for (i = 0; i < n; i++)
{
pdf_obj *annot = pdf_graft_mapped_object(ctx, map, links[i].annot);
pdf_obj *dest = pdf_new_array(ctx, pdf, 1 + pdf_array_len(ctx, links[i].rest));
pdf_array_push(ctx, dest, pdf_lookup_page_obj(ctx, pdf, at + links[i].page));
for (j = 0; j < pdf_array_len(ctx, links[i].rest); j++)
pdf_array_push(ctx, dest, pdf_array_get(ctx, links[i].rest, j));
pdf_dict_put_drop(ctx, annot, PDF_NAME(Dest), dest);
pdf_drop_obj(ctx, annot);
}
}
/* Every page of the PDF at `path`, inserted at `at`. */
int ely_insert_file(fz_context *ctx, fz_document *doc, int at, const char *path, int *count)
{
pdf_document *pdf = NULL, *src = NULL;
pdf_graft_map *map = NULL;
inner_link *links = NULL;
int begun = 0, i, n = 0, room = 0;
fz_var(pdf);
fz_var(src);
fz_var(map);
fz_var(links);
fz_var(n);
fz_var(begun);
fz_try(ctx)
{
pdf = ely_pdf_of(ctx, doc);
if (at < 0 || at > pdf_count_pages(ctx, pdf))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no place %d to insert at", at);
src = pdf_open_document(ctx, path);
if (pdf_needs_password(ctx, src))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the file to insert is locked");
*count = pdf_count_pages(ctx, src);
for (i = 0; i < *count; i++)
room += pdf_array_len(ctx, pdf_dict_get(ctx, pdf_lookup_page_obj(ctx, src, i), PDF_NAME(Annots)));
links = fz_calloc(ctx, room + 1, sizeof *links);
n = unhook(ctx, src, links, room);
map = pdf_new_graft_map(ctx, pdf);
pdf_begin_operation(ctx, pdf, "Insert pages");
begun = 1;
for (i = 0; i < *count; i++)
pdf_graft_mapped_page(ctx, map, at + i, src, i);
graft_annots(ctx, pdf, map, src, at, links, n);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
for (i = 0; links && i < n; i++)
{
pdf_drop_obj(ctx, links[i].annot);
pdf_drop_obj(ctx, links[i].rest);
}
fz_free(ctx, links);
pdf_drop_graft_map(ctx, map);
pdf_drop_document(ctx, src);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
+389
View File
@@ -0,0 +1,389 @@
/* Document properties and attachments (PDF-07). */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "ely_shim.h"
static const char *shown[] = { "Title", "Author", "Subject", "Keywords", "Creator", "Producer", "CreationDate",
"ModDate", NULL };
/* Only these change; the rest the producer wrote. */
static const char *editable[] = { "Title", "Author", "Subject", "Keywords", NULL };
/* Properties as JSON; null when the file has none. */
int ely_metadata(fz_context *ctx, fz_document *doc, unsigned char **data, size_t *len)
{
fz_buffer *buf = NULL;
fz_output *out = NULL;
char *value = NULL;
fz_var(buf);
fz_var(out);
fz_var(value);
fz_try(ctx)
{
char key[32];
int i, n;
buf = fz_new_buffer(ctx, 256);
out = fz_new_output_with_buffer(ctx, buf);
fz_write_byte(ctx, out, '{');
for (i = 0; shown[i]; i++)
{
fz_snprintf(key, sizeof key, "info:%s", shown[i]);
fz_write_printf(ctx, out, "%s\"%s\":", i ? "," : "", shown[i]);
n = fz_lookup_metadata(ctx, doc, key, NULL, 0);
if (n < 0)
{
fz_write_string(ctx, out, "null");
continue;
}
fz_free(ctx, value);
value = NULL;
value = fz_malloc(ctx, n);
fz_lookup_metadata(ctx, doc, key, value, n);
ely_json_string(ctx, out, value);
}
fz_write_byte(ctx, out, '}');
fz_close_output(ctx, out);
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
{
fz_free(ctx, value);
fz_drop_output(ctx, out);
fz_drop_buffer(ctx, buf);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* Sets the given properties, in one step. */
int ely_set_metadata(fz_context *ctx, fz_document *doc, const char *json)
{
pdf_document *pdf = NULL;
fz_pool *pool = NULL;
int begun = 0;
fz_var(pdf);
fz_var(pool);
fz_var(begun);
fz_try(ctx)
{
fz_json *spec;
fz_json_object *field;
char key[32];
int i;
pdf = ely_pdf_of(ctx, doc);
pool = fz_new_pool(ctx);
spec = fz_parse_json(ctx, pool, json);
if (spec->type != FZ_JSON_OBJECT || !spec->u.object)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no properties to set");
for (field = spec->u.object; field; field = field->next)
{
for (i = 0; editable[i] && strcmp(editable[i], field->key); i++)
continue;
if (!editable[i] || field->value->type != FZ_JSON_STRING)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a property that does not change: %s", field->key);
}
pdf_begin_operation(ctx, pdf, "Set properties");
begun = 1;
for (field = spec->u.object; field; field = field->next)
{
fz_snprintf(key, sizeof key, "info:%s", field->key);
fz_set_metadata(ctx, doc, key, field->value->u.string);
}
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
fz_drop_pool(ctx, pool);
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
static int by_name(const void *a, const void *b)
{
return strcmp(*(const char *const *)a, *(const char *const *)b);
}
/* The embedded files, by name; NULL names end it. */
static const char **names_of(fz_context *ctx, pdf_obj *tree, int *count)
{
const char **names;
int i, n = pdf_dict_len(ctx, tree);
names = fz_malloc_array(ctx, n + 1, const char *);
for (i = 0; i < n; i++)
names[i] = pdf_to_name(ctx, pdf_dict_get_key(ctx, tree, i));
names[n] = NULL;
qsort(names, n, sizeof *names, by_name);
*count = n;
return names;
}
/* An embedded file's length: its /Size, else its bytes. */
static int64_t file_size(fz_context *ctx, pdf_obj *fs)
{
pdf_obj *ef = pdf_dict_get(ctx, fs, PDF_NAME(EF));
pdf_obj *file = pdf_dict_get(ctx, ef, PDF_NAME(F)) ? pdf_dict_get(ctx, ef, PDF_NAME(F)) : pdf_dict_get(ctx, ef, PDF_NAME(UF));
pdf_obj *size = pdf_dict_getl(ctx, file, PDF_NAME(Params), PDF_NAME(Size), NULL);
fz_buffer *bytes;
int64_t n;
if (pdf_is_int(ctx, size))
return pdf_to_int64(ctx, size);
bytes = pdf_load_embedded_file_contents(ctx, fs);
n = (int64_t)fz_buffer_storage(ctx, bytes, NULL);
fz_drop_buffer(ctx, bytes);
return n;
}
static void write_file(fz_context *ctx, fz_output *out, const char *key, const char *name, int page, pdf_obj *fs, int *first)
{
pdf_filespec_params params;
pdf_get_filespec_params(ctx, fs, &params);
fz_write_string(ctx, out, *first ? "{\"key\":" : ",{\"key\":");
*first = 0;
ely_json_string(ctx, out, key);
fz_write_string(ctx, out, ",\"name\":");
ely_json_string(ctx, out, name);
fz_write_printf(ctx, out, ",\"page\":%d,\"size\":%ld,\"created\":%ld,\"modified\":%ld,\"type\":", page,
(long)file_size(ctx, fs), (long)params.created, (long)params.modified);
ely_json_string(ctx, out, params.mimetype ? params.mimetype : "");
fz_write_string(ctx, out, ",\"note\":");
ely_json_string(ctx, out, pdf_dict_get_text_string(ctx, fs, PDF_NAME(Desc)));
fz_write_byte(ctx, out, '}');
}
/* The page's file annotations, as attachments. */
static void write_page_files(fz_context *ctx, fz_output *out, pdf_document *pdf, int number, int *first)
{
pdf_page *page = pdf_load_page(ctx, pdf, number);
char key[48];
fz_try(ctx)
{
pdf_annot *annot;
for (annot = pdf_first_annot(ctx, page); annot; annot = pdf_next_annot(ctx, annot))
{
pdf_obj *fs;
pdf_filespec_params params;
if (pdf_annot_type(ctx, annot) != PDF_ANNOT_FILE_ATTACHMENT)
continue;
fs = pdf_annot_filespec(ctx, annot);
if (!pdf_is_embedded_file(ctx, fs))
continue;
pdf_get_filespec_params(ctx, fs, &params);
fz_snprintf(key, sizeof key, "a:%d:%d", number, pdf_to_num(ctx, pdf_annot_obj(ctx, annot)));
write_file(ctx, out, key, params.filename ? params.filename : "", number, fs, first);
}
}
fz_always(ctx)
fz_drop_page(ctx, (fz_page *)page);
fz_catch(ctx)
fz_rethrow(ctx);
}
/* Every embedded file: the tree's by name, then pages'. */
int ely_attachments(fz_context *ctx, fz_document *doc, unsigned char **data, size_t *len)
{
pdf_obj *tree = NULL;
const char **names = NULL;
fz_buffer *buf = NULL;
fz_output *out = NULL;
char *key = NULL;
fz_var(tree);
fz_var(names);
fz_var(buf);
fz_var(out);
fz_var(key);
fz_try(ctx)
{
pdf_document *pdf = ely_pdf_of(ctx, doc);
int i, n, first = 1;
tree = pdf_load_name_tree(ctx, pdf, PDF_NAME(EmbeddedFiles));
names = names_of(ctx, tree, &n);
buf = fz_new_buffer(ctx, 256);
out = fz_new_output_with_buffer(ctx, buf);
fz_write_byte(ctx, out, '[');
for (i = 0; i < n; i++)
{
pdf_obj *fs = pdf_dict_gets(ctx, tree, names[i]);
if (!pdf_is_embedded_file(ctx, fs))
continue;
fz_free(ctx, key);
key = NULL;
key = fz_asprintf(ctx, "n:%s", names[i]);
write_file(ctx, out, key, names[i], -1, fs, &first);
}
for (i = 0; i < pdf_count_pages(ctx, pdf); i++)
write_page_files(ctx, out, pdf, i, &first);
fz_write_byte(ctx, out, ']');
fz_close_output(ctx, out);
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
{
fz_free(ctx, key);
fz_drop_output(ctx, out);
fz_drop_buffer(ctx, buf);
fz_free(ctx, names);
pdf_drop_obj(ctx, tree);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* The file `key` names; a page annotation's loads its page. */
static pdf_obj *file_of(fz_context *ctx, pdf_document *pdf, const char *key, pdf_obj *tree, pdf_page **page, pdf_annot **annot)
{
pdf_obj *fs = NULL;
int number, id;
if (!strncmp(key, "n:", 2))
fs = pdf_dict_gets(ctx, tree, key + 2);
else if (sscanf(key, "a:%d:%d", &number, &id) == 2 && number >= 0 && number < pdf_count_pages(ctx, pdf))
{
*page = pdf_load_page(ctx, pdf, number);
*annot = ely_find_annot(ctx, *page, id, 0);
if (pdf_annot_type(ctx, *annot) == PDF_ANNOT_FILE_ATTACHMENT)
fs = pdf_annot_filespec(ctx, *annot);
}
if (!pdf_is_embedded_file(ctx, fs))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no attachment %s", key);
return fs;
}
/* An embedded file's bytes. */
int ely_attachment(fz_context *ctx, fz_document *doc, const char *key, unsigned char **data, size_t *len)
{
pdf_obj *tree = NULL;
pdf_page *page = NULL;
fz_buffer *buf = NULL;
fz_var(tree);
fz_var(page);
fz_var(buf);
fz_try(ctx)
{
pdf_document *pdf = ely_pdf_of(ctx, doc);
pdf_annot *annot = NULL;
tree = pdf_load_name_tree(ctx, pdf, PDF_NAME(EmbeddedFiles));
unsigned char *bytes;
buf = pdf_load_embedded_file_contents(ctx, file_of(ctx, pdf, key, tree, &page, &annot));
/* A copy: the document may hold this buffer. */
*len = fz_buffer_storage(ctx, buf, &bytes);
*data = fz_malloc(ctx, *len ? *len : 1);
memcpy(*data, bytes, *len);
}
fz_always(ctx)
{
fz_drop_buffer(ctx, buf);
fz_drop_page(ctx, (fz_page *)page);
pdf_drop_obj(ctx, tree);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* Writes `tree` back flat, its names in order. */
static void put_tree(fz_context *ctx, pdf_document *pdf, pdf_obj *tree)
{
pdf_obj *root = pdf_dict_get(ctx, pdf_trailer(ctx, pdf), PDF_NAME(Root));
pdf_obj *names = pdf_dict_get(ctx, root, PDF_NAME(Names)), *list;
const char **sorted = NULL;
int i, n;
fz_var(sorted);
if (!names)
names = pdf_dict_put_dict(ctx, root, PDF_NAME(Names), 1);
list = pdf_dict_put_array(ctx, pdf_dict_put_dict(ctx, names, PDF_NAME(EmbeddedFiles), 1), PDF_NAME(Names), 8);
fz_try(ctx)
{
sorted = names_of(ctx, tree, &n);
for (i = 0; i < n; i++)
{
pdf_array_push_text_string(ctx, list, sorted[i]);
pdf_array_push(ctx, list, pdf_dict_gets(ctx, tree, sorted[i]));
}
}
fz_always(ctx)
fz_free(ctx, sorted);
fz_catch(ctx)
fz_rethrow(ctx);
}
/* Embeds the file at `path` as `name`, in one step. */
int ely_attach(fz_context *ctx, fz_document *doc, const char *name, const char *path)
{
pdf_document *pdf = NULL;
pdf_obj *tree = NULL, *fs = NULL;
fz_buffer *contents = NULL;
int begun = 0;
fz_var(pdf);
fz_var(tree);
fz_var(fs);
fz_var(contents);
fz_var(begun);
fz_try(ctx)
{
pdf = ely_pdf_of(ctx, doc);
if (!name[0])
fz_throw(ctx, FZ_ERROR_ARGUMENT, "an attachment with no name");
tree = pdf_load_name_tree(ctx, pdf, PDF_NAME(EmbeddedFiles));
if (!tree)
tree = pdf_new_dict(ctx, pdf, 1);
if (pdf_dict_gets(ctx, tree, name))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "an attachment named %s exists", name);
contents = fz_read_file(ctx, path);
pdf_begin_operation(ctx, pdf, "Attach a file");
begun = 1;
fs = pdf_add_embedded_file(ctx, pdf, name, NULL, contents, -1, -1, 1);
pdf_dict_puts(ctx, tree, name, fs);
put_tree(ctx, pdf, tree);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
pdf_drop_obj(ctx, fs);
fz_drop_buffer(ctx, contents);
pdf_drop_obj(ctx, tree);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Removes the file `key`: a tree entry or its annotation. */
int ely_detach(fz_context *ctx, fz_document *doc, const char *key)
{
pdf_document *pdf = NULL;
pdf_obj *tree = NULL;
pdf_page *page = NULL;
int begun = 0;
fz_var(pdf);
fz_var(tree);
fz_var(page);
fz_var(begun);
fz_try(ctx)
{
pdf_annot *annot = NULL;
pdf = ely_pdf_of(ctx, doc);
tree = pdf_load_name_tree(ctx, pdf, PDF_NAME(EmbeddedFiles));
file_of(ctx, pdf, key, tree, &page, &annot);
pdf_begin_operation(ctx, pdf, "Remove an attachment");
begun = 1;
if (annot)
pdf_delete_annot(ctx, page, annot);
else
{
pdf_dict_dels(ctx, tree, key + 2);
put_tree(ctx, pdf, tree);
}
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
fz_drop_page(ctx, (fz_page *)page);
pdf_drop_obj(ctx, tree);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
+392
View File
@@ -0,0 +1,392 @@
/* MuPDF behind plain return codes: fz_try never crosses into Rust. */
#include <mupdf/fitz.h>
#include <mupdf/pdf.h>
#include <string.h>
static char last_error[512];
/* Takes the caught error to Rust; MuPDF counts it handled. */
int ely_fail(fz_context *ctx)
{
int code;
fz_strlcpy(last_error, fz_convert_error(ctx, &code), sizeof last_error);
return -1;
}
/* A second failure while handling the first. */
void ely_fail_also(fz_context *ctx, const char *what)
{
char also[256];
int code;
fz_snprintf(also, sizeof also, "; %s: %s", what, fz_convert_error(ctx, &code));
fz_strlcat(last_error, also, sizeof last_error);
}
const char *ely_error(void)
{
return last_error;
}
fz_context *ely_init(void)
{
fz_context *ctx = fz_new_context(NULL, NULL, FZ_STORE_DEFAULT);
if (!ctx)
{
fz_strlcpy(last_error, "no MuPDF context", sizeof last_error);
return NULL;
}
fz_try(ctx)
fz_register_document_handlers(ctx);
fz_catch(ctx)
{
ely_fail(ctx);
fz_drop_context(ctx);
return NULL;
}
return ctx;
}
/* What MuPDF warned while opening, for repair reports. */
static char warned[4096];
typedef struct
{
fz_warning_cb *next;
void *user;
} passing;
static void keep_warning(void *user, const char *message)
{
passing *p = user;
if (strlen(warned) + strlen(message) + 2 < sizeof warned)
{
fz_strlcat(warned, message, sizeof warned);
fz_strlcat(warned, "\n", sizeof warned);
}
if (p->next)
p->next(p->user, message);
}
static passing watching;
static int watched;
/* Collects MuPDF's warnings until `ely_watch_end`. */
void ely_watch_begin(fz_context *ctx)
{
watching.next = fz_warning_callback(ctx, &watching.user);
warned[0] = 0;
watched = 1;
fz_set_warning_callback(ctx, keep_warning, &watching);
}
/* 1 if MuPDF warned since `ely_watch_begin`. */
int ely_watch_end(fz_context *ctx)
{
if (!watched)
return 0;
fz_flush_warnings(ctx);
fz_set_warning_callback(ctx, watching.next, watching.user);
watched = 0;
return warned[0] != 0;
}
int ely_open(fz_context *ctx, const char *path, fz_document **out)
{
ely_watch_begin(ctx);
fz_try(ctx)
*out = fz_open_document(ctx, path);
fz_always(ctx)
ely_watch_end(ctx);
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* Reads every object and non-image stream; 1 if any warned. */
int ely_scan(fz_context *ctx, fz_document *doc)
{
pdf_document *pdf = pdf_document_from_fz_document(ctx, doc);
int i;
if (!pdf)
return 0;
ely_watch_begin(ctx);
for (i = 1; i < pdf_xref_len(ctx, pdf); i++)
{
pdf_obj *obj = NULL;
fz_buffer *bytes = NULL;
fz_var(obj);
fz_var(bytes);
fz_try(ctx)
{
obj = pdf_new_indirect(ctx, pdf, i, 0);
pdf_cache_object(ctx, pdf, i);
/* Pictures are checked as they draw, or before an overwrite. */
if (pdf_is_stream(ctx, obj) && !pdf_name_eq(ctx, pdf_dict_get(ctx, obj, PDF_NAME(Subtype)), PDF_NAME(Image)))
bytes = pdf_load_stream_number(ctx, pdf, i);
}
fz_always(ctx)
{
fz_drop_buffer(ctx, bytes);
pdf_drop_obj(ctx, obj);
}
fz_catch(ctx)
{
/* An unreadable object goes in the report. */
fz_warn(ctx, "object %d: %s", i, fz_caught_message(ctx));
fz_ignore_error(ctx);
}
}
return ely_watch_end(ctx);
}
/* The warnings of the last watch. */
const char *ely_open_warnings(void)
{
return warned;
}
void ely_close(fz_context *ctx, fz_document *doc)
{
fz_drop_document(ctx, doc);
}
/* 1 locked, 0 open, -1 an error. */
int ely_needs_password(fz_context *ctx, fz_document *doc)
{
int locked = 0;
fz_try(ctx)
locked = fz_needs_password(ctx, doc);
fz_catch(ctx)
return ely_fail(ctx);
return locked;
}
/* Access bits: 4 owner; 0 refused, -1 an error. */
int ely_authenticate(fz_context *ctx, fz_document *doc, const char *password)
{
int access = 0;
fz_try(ctx)
access = fz_authenticate_password(ctx, doc, password);
fz_catch(ctx)
return ely_fail(ctx);
return access;
}
int ely_count_pages(fz_context *ctx, fz_document *doc, int *out)
{
fz_try(ctx)
*out = fz_count_pages(ctx, doc);
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
int ely_page_bounds(fz_context *ctx, fz_document *doc, int number, float out[4])
{
fz_page *page = NULL;
fz_var(page);
fz_try(ctx)
{
page = fz_load_page(ctx, doc, number);
fz_rect box = fz_bound_page(ctx, page);
out[0] = box.x0; out[1] = box.y0; out[2] = box.x1; out[3] = box.y1;
}
fz_always(ctx)
fz_drop_page(ctx, page);
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* The page turned, scaled and moved to the origin. */
static fz_matrix placed(fz_rect box, float scale, int rotate)
{
fz_matrix ctm = fz_concat(fz_rotate(rotate), fz_scale(scale, scale));
fz_rect turned = fz_transform_rect(box, ctm);
return fz_concat(ctm, fz_translate(-turned.x0, -turned.y0));
}
/* Paints device pixels x, y, w, h as BGRA. */
int ely_render(fz_context *ctx, fz_document *doc, int number, float scale, int rotate,
const int tile[4], int dark, unsigned char *bgra)
{
fz_page *page = NULL;
fz_pixmap *pix = NULL;
fz_device *dev = NULL;
fz_var(page);
fz_var(pix);
fz_var(dev);
fz_try(ctx)
{
page = fz_load_page(ctx, doc, number);
fz_matrix ctm = placed(fz_bound_page(ctx, page), scale, rotate);
fz_irect area = fz_make_irect(tile[0], tile[1], tile[0] + tile[2], tile[1] + tile[3]);
pix = fz_new_pixmap_with_bbox(ctx, fz_device_bgr(ctx), area, NULL, 0);
fz_clear_pixmap_with_value(ctx, pix, 0xff);
dev = fz_new_draw_device(ctx, fz_identity, pix);
fz_run_page(ctx, page, dev, ctm, NULL);
fz_close_device(ctx, dev);
if (dark)
fz_invert_pixmap_luminance(ctx, pix);
int stride = (int)fz_pixmap_stride(ctx, pix);
unsigned char *rows = fz_pixmap_samples(ctx, pix);
for (int y = 0; y < tile[3]; y++)
for (int x = 0; x < tile[2]; x++)
{
const unsigned char *from = rows + y * stride + x * 3;
unsigned char *to = bgra + ((size_t)y * tile[2] + x) * 4;
to[0] = from[0]; to[1] = from[1]; to[2] = from[2]; to[3] = 0xff;
}
}
fz_always(ctx)
{
fz_drop_device(ctx, dev);
fz_drop_pixmap(ctx, pix);
fz_drop_page(ctx, page);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* One character: line, code point, box in page points. */
typedef struct
{
int line;
int c;
float x0, y0, x1, y1;
} ely_char;
static void add_chars(fz_context *ctx, fz_buffer *buf, fz_stext_block *block, int *line)
{
for (; block; block = block->next)
{
if (block->type == FZ_STEXT_BLOCK_STRUCT && block->u.s.down)
add_chars(ctx, buf, block->u.s.down->first_block, line);
if (block->type != FZ_STEXT_BLOCK_TEXT)
continue;
for (fz_stext_line *l = block->u.t.first_line; l; l = l->next, ++*line)
for (fz_stext_char *ch = l->first_char; ch; ch = ch->next)
{
fz_rect box = fz_rect_from_quad(ch->quad);
ely_char one = { *line, ch->c, box.x0, box.y0, box.x1, box.y1 };
fz_append_data(ctx, buf, &one, sizeof one);
}
}
}
/* The page's characters in reading order; free with ely_free. */
int ely_text(fz_context *ctx, fz_document *doc, int number, unsigned char **data, size_t *len)
{
fz_page *page = NULL;
fz_stext_page *text = NULL;
fz_buffer *buf = NULL;
fz_var(page);
fz_var(text);
fz_var(buf);
fz_try(ctx)
{
page = fz_load_page(ctx, doc, number);
text = fz_new_stext_page_from_page(ctx, page, NULL);
buf = fz_new_buffer(ctx, 4096);
int line = 0;
add_chars(ctx, buf, text->first_block, &line);
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
{
fz_drop_buffer(ctx, buf);
fz_drop_stext_page(ctx, text);
fz_drop_page(ctx, page);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* Each hit: its quad count, then those quads. */
static int add_hit(fz_context *ctx, void *opaque, int count, fz_quad *quads)
{
fz_buffer *buf = opaque;
fz_append_data(ctx, buf, &count, sizeof count);
fz_append_data(ctx, buf, quads, sizeof *quads * count);
return 0;
}
/* Every hit on a page; free with ely_free. */
int ely_search(fz_context *ctx, fz_document *doc, int number, const char *needle,
unsigned char **data, size_t *len)
{
fz_page *page = NULL;
fz_stext_page *text = NULL;
fz_buffer *buf = NULL;
fz_var(page);
fz_var(text);
fz_var(buf);
fz_try(ctx)
{
page = fz_load_page(ctx, doc, number);
text = fz_new_stext_page_from_page(ctx, page, NULL);
buf = fz_new_buffer(ctx, 256);
fz_search_stext_page_cb(ctx, text, needle, add_hit, buf);
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
{
fz_drop_buffer(ctx, buf);
fz_drop_stext_page(ctx, text);
fz_drop_page(ctx, page);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
static void outline_json(fz_context *ctx, fz_output *out, fz_document *doc, fz_outline *item)
{
fz_write_string(ctx, out, "[");
for (; item; item = item->next)
{
int page = item->page.page >= 0 ? fz_page_number_from_location(ctx, doc, item->page) : -1;
fz_write_printf(ctx, out, "{%q:%q,%q:%d,%q:%g,%q:", "title", item->title ? item->title : "",
"page", page, "y", item->y, "down");
outline_json(ctx, out, doc, item->down);
fz_write_string(ctx, out, item->next ? "}," : "}");
}
fz_write_string(ctx, out, "]");
}
/* Bookmarks as JSON: title, page or -1, y, down. */
int ely_outline(fz_context *ctx, fz_document *doc, unsigned char **data, size_t *len)
{
fz_outline *outline = NULL;
fz_buffer *buf = NULL;
fz_output *out = NULL;
fz_var(outline);
fz_var(buf);
fz_var(out);
fz_try(ctx)
{
outline = fz_load_outline(ctx, doc);
buf = fz_new_buffer(ctx, 1024);
out = fz_new_output_with_buffer(ctx, buf);
outline_json(ctx, out, doc, outline);
fz_close_output(ctx, out);
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
{
fz_drop_output(ctx, out);
fz_drop_buffer(ctx, buf);
fz_drop_outline(ctx, outline);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
void ely_free(fz_context *ctx, void *data)
{
fz_free(ctx, data);
}
+284
View File
@@ -0,0 +1,284 @@
/* OCR: recognise a page, add a hidden text layer (PDF-08). */
#include <string.h>
#include "ely_shim.h"
static const char to_unicode[] =
"/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n"
"/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n"
"/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n"
"1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n"
"1 beginbfrange\n<0000> <FFFF> <0000>\nendbfrange\n"
"endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n";
/* Lines Tesseract reads on a page, page space, as JSON. */
int ely_ocr_page(fz_context *ctx, fz_document *doc, int number, const char *lang, const char *datadir, float dpi,
unsigned char **data, size_t *len)
{
fz_page *page = NULL;
fz_pixmap *pix = NULL;
fz_image *image = NULL;
fz_stext_page *text = NULL;
fz_device *sdev = NULL, *ocr = NULL;
fz_buffer *buf = NULL, *words = NULL;
fz_output *out = NULL;
fz_var(page);
fz_var(pix);
fz_var(image);
fz_var(text);
fz_var(sdev);
fz_var(ocr);
fz_var(buf);
fz_var(words);
fz_var(out);
fz_try(ctx)
{
float zoom = dpi / 72;
fz_rect area;
fz_stext_block *block;
fz_stext_line *line;
fz_stext_char *ch;
int first = 1;
if (dpi < 72 || dpi > 600)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a resolution of %g dpi", dpi);
page = fz_load_page(ctx, doc, number);
pix = fz_new_pixmap_from_page(ctx, page, fz_scale(zoom, zoom), fz_device_gray(ctx), 0);
image = fz_new_image_from_pixmap(ctx, pix, NULL);
area = fz_make_rect(0, 0, (float)pix->w, (float)pix->h);
text = fz_new_stext_page(ctx, area);
sdev = fz_new_stext_device(ctx, text, NULL);
ocr = fz_new_ocr_device(ctx, sdev, fz_identity, area, 0, lang, datadir, NULL, NULL);
fz_fill_image(ctx, ocr, image, fz_make_matrix(area.x1, 0, 0, area.y1, 0, 0), 1, fz_default_color_params);
fz_close_device(ctx, ocr);
fz_close_device(ctx, sdev);
buf = fz_new_buffer(ctx, 1024);
out = fz_new_output_with_buffer(ctx, buf);
fz_write_byte(ctx, out, '[');
for (block = text->first_block; block; block = block->next)
{
if (block->type != FZ_STEXT_BLOCK_TEXT)
continue;
for (line = block->u.t.first_line; line; line = line->next)
{
fz_rect r = fz_transform_rect(line->bbox, fz_scale(1 / zoom, 1 / zoom));
if (!line->first_char)
continue;
fz_drop_buffer(ctx, words);
words = NULL;
words = fz_new_buffer(ctx, 128);
for (ch = line->first_char; ch; ch = ch->next)
fz_append_rune(ctx, words, ch->c);
fz_terminate_buffer(ctx, words);
fz_write_printf(ctx, out, "%s{\"rect\":[%g,%g,%g,%g],\"text\":", first ? "" : ",", r.x0, r.y0, r.x1, r.y1);
ely_json_string(ctx, out, fz_string_from_buffer(ctx, words));
fz_write_byte(ctx, out, '}');
first = 0;
}
}
fz_write_byte(ctx, out, ']');
fz_close_output(ctx, out);
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
{
fz_drop_output(ctx, out);
fz_drop_buffer(ctx, buf);
fz_drop_buffer(ctx, words);
fz_drop_device(ctx, ocr);
fz_drop_device(ctx, sdev);
fz_drop_stext_page(ctx, text);
fz_drop_image(ctx, image);
fz_drop_pixmap(ctx, pix);
fz_drop_page(ctx, page);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* A font with no glyphs; codes are UTF-16 units. */
static pdf_obj *glyphless(fz_context *ctx, pdf_document *pdf)
{
pdf_obj *font = NULL, *cid = NULL, *desc = NULL, *info, *bbox, *kids;
fz_buffer *cmap = NULL;
fz_var(font);
fz_var(cid);
fz_var(desc);
fz_var(cmap);
fz_try(ctx)
{
desc = pdf_add_new_dict(ctx, pdf, 8);
pdf_dict_put(ctx, desc, PDF_NAME(Type), PDF_NAME(FontDescriptor));
pdf_dict_put_name(ctx, desc, PDF_NAME(FontName), "GlyphLessFont");
pdf_dict_put_int(ctx, desc, PDF_NAME(Flags), 5);
bbox = pdf_dict_put_array(ctx, desc, PDF_NAME(FontBBox), 4);
pdf_array_push_int(ctx, bbox, 0), pdf_array_push_int(ctx, bbox, 0);
pdf_array_push_int(ctx, bbox, 500), pdf_array_push_int(ctx, bbox, 1000);
pdf_dict_put_int(ctx, desc, PDF_NAME(ItalicAngle), 0);
pdf_dict_put_int(ctx, desc, PDF_NAME(Ascent), 1000);
pdf_dict_put_int(ctx, desc, PDF_NAME(Descent), 0);
pdf_dict_put_int(ctx, desc, PDF_NAME(CapHeight), 1000);
pdf_dict_put_int(ctx, desc, PDF_NAME(StemV), 80);
cid = pdf_add_new_dict(ctx, pdf, 8);
pdf_dict_put(ctx, cid, PDF_NAME(Type), PDF_NAME(Font));
pdf_dict_put(ctx, cid, PDF_NAME(Subtype), PDF_NAME(CIDFontType2));
pdf_dict_put_name(ctx, cid, PDF_NAME(BaseFont), "GlyphLessFont");
info = pdf_dict_put_dict(ctx, cid, PDF_NAME(CIDSystemInfo), 3);
pdf_dict_put_text_string(ctx, info, PDF_NAME(Registry), "Adobe");
pdf_dict_put_text_string(ctx, info, PDF_NAME(Ordering), "Identity");
pdf_dict_put_int(ctx, info, PDF_NAME(Supplement), 0);
pdf_dict_put(ctx, cid, PDF_NAME(FontDescriptor), desc);
pdf_dict_put_int(ctx, cid, PDF_NAME(DW), 500);
pdf_dict_put(ctx, cid, PDF_NAME(CIDToGIDMap), PDF_NAME(Identity));
font = pdf_add_new_dict(ctx, pdf, 6);
pdf_dict_put(ctx, font, PDF_NAME(Type), PDF_NAME(Font));
pdf_dict_put(ctx, font, PDF_NAME(Subtype), PDF_NAME(Type0));
pdf_dict_put_name(ctx, font, PDF_NAME(BaseFont), "GlyphLessFont");
pdf_dict_put(ctx, font, PDF_NAME(Encoding), PDF_NAME(Identity_H));
kids = pdf_dict_put_array(ctx, font, PDF_NAME(DescendantFonts), 1);
pdf_array_push(ctx, kids, cid);
cmap = fz_new_buffer_from_copied_data(ctx, (const unsigned char *)to_unicode, strlen(to_unicode));
pdf_dict_put_drop(ctx, font, PDF_NAME(ToUnicode), pdf_add_stream(ctx, pdf, cmap, NULL, 0));
}
fz_always(ctx)
{
pdf_drop_obj(ctx, desc);
pdf_drop_obj(ctx, cid);
fz_drop_buffer(ctx, cmap);
}
fz_catch(ctx)
{
pdf_drop_obj(ctx, font);
fz_rethrow(ctx);
}
return font;
}
/* Drops the page's earlier Ely OCR layers. */
static void drop_layers(fz_context *ctx, pdf_obj *page)
{
pdf_obj *contents = pdf_dict_get(ctx, page, PDF_NAME(Contents));
int i;
for (i = pdf_array_len(ctx, contents) - 1; i >= 0; i--)
if (pdf_dict_gets(ctx, pdf_array_get(ctx, contents, i), "ElyOCR"))
pdf_array_delete(ctx, contents, i);
}
/* Throws unless every line is whole and in the BMP. */
static void check_layer(fz_context *ctx, fz_json *pages, int count)
{
int i, k;
for (i = 0; i < fz_json_array_length(ctx, pages); i++)
{
fz_json *spec = fz_json_array_get(ctx, pages, i);
fz_json *page = ely_member(ctx, spec, "page", FZ_JSON_NUMBER);
fz_json *lines = ely_member(ctx, spec, "lines", FZ_JSON_ARRAY);
if (!page || page->u.number < 0 || page->u.number >= count || !lines)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a layer with no page or lines");
for (k = 0; k < fz_json_array_length(ctx, lines); k++)
{
fz_json *line = fz_json_array_get(ctx, lines, k);
fz_json *words = ely_member(ctx, line, "text", FZ_JSON_STRING);
const char *s;
int c;
ely_rect(ctx, ely_member(ctx, line, "rect", FZ_JSON_ARRAY), 0.5f);
if (!words || !words->u.string[0])
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a line with no text");
for (s = words->u.string; *s; )
{
s += fz_chartorune(&c, s);
if (c > 0xFFFF || c == FZ_REPLACEMENT_CHARACTER)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a character the layer cannot hold: U+%X", c);
}
}
}
}
/* One line: hidden text stretched over its box. */
static void write_line(fz_context *ctx, fz_buffer *buf, fz_json *line, fz_matrix to_user, const char *font)
{
fz_rect r = ely_rect(ctx, ely_member(ctx, line, "rect", FZ_JSON_ARRAY), 0.5f);
const char *s = ely_member(ctx, line, "text", FZ_JSON_STRING)->u.string, *p;
float size = r.y1 - r.y0;
int n = 0, c;
fz_matrix m;
for (p = s; *p; n++)
p += fz_chartorune(&c, p);
/* Each code is half an em wide. */
m = fz_concat(fz_make_matrix((r.x1 - r.x0) / (0.5f * n), 0, 0, -size, r.x0, r.y1 - 0.2f * size), to_user);
fz_append_printf(ctx, buf, "BT 3 Tr /%s 1 Tf %g %g %g %g %g %g Tm <", font, m.a, m.b, m.c, m.d, m.e, m.f);
for (p = s; *p; )
{
p += fz_chartorune(&c, p);
fz_append_printf(ctx, buf, "%04x", c);
}
fz_append_string(ctx, buf, "> Tj ET\n");
}
/* Hidden text on pages, earlier layers first dropped if asked. */
int ely_text_layer(fz_context *ctx, fz_document *doc, const char *json, int replace)
{
pdf_document *pdf = NULL;
pdf_page *page = NULL;
pdf_obj *font = NULL;
fz_buffer *buf = NULL;
fz_pool *pool = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(font);
fz_var(buf);
fz_var(pool);
fz_var(begun);
fz_try(ctx)
{
fz_json *pages;
int i, k;
pdf = ely_pdf_of(ctx, doc);
pool = fz_new_pool(ctx);
pages = fz_parse_json(ctx, pool, json);
if (pages->type != FZ_JSON_ARRAY || fz_json_array_length(ctx, pages) == 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no pages to lay text on");
check_layer(ctx, pages, pdf_count_pages(ctx, pdf));
pdf_begin_operation(ctx, pdf, "Add recognised text");
begun = 1;
for (i = 0; i < fz_json_array_length(ctx, pages); i++)
{
fz_json *spec = fz_json_array_get(ctx, pages, i);
fz_json *lines = ely_member(ctx, spec, "lines", FZ_JSON_ARRAY);
char name[32];
page = pdf_load_page(ctx, pdf, (int)ely_member(ctx, spec, "page", FZ_JSON_NUMBER)->u.number);
if (replace)
drop_layers(ctx, page->obj);
if (fz_json_array_length(ctx, lines) > 0)
{
if (!font)
font = glyphless(ctx, pdf);
ely_add_resource(ctx, pdf, page->obj, PDF_NAME(Font), "ElyOCR", font, name, sizeof name);
buf = fz_new_buffer(ctx, 1024);
/* Its own state, so later text stays seen. */
fz_append_string(ctx, buf, "q\n");
for (k = 0; k < fz_json_array_length(ctx, lines); k++)
write_line(ctx, buf, fz_json_array_get(ctx, lines, k), ely_to_user(ctx, page), name);
fz_append_string(ctx, buf, "Q\n");
pdf_dict_puts(ctx, ely_append_content(ctx, pdf, page->obj, buf), "ElyOCR", PDF_TRUE);
fz_drop_buffer(ctx, buf);
buf = NULL;
}
fz_drop_page(ctx, (fz_page *)page);
page = NULL;
}
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
fz_drop_buffer(ctx, buf);
pdf_drop_obj(ctx, font);
fz_drop_page(ctx, (fz_page *)page);
fz_drop_pool(ctx, pool);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
+237
View File
@@ -0,0 +1,237 @@
/* Bookmarks and links: change, list (PDF-07). */
#include <string.h>
#include "ely_shim.h"
/* A page point as a link target. */
static char *target(fz_context *ctx, fz_document *doc, fz_json *spec)
{
fz_json *page = ely_member(ctx, spec, "page", FZ_JSON_NUMBER);
fz_json *y = ely_member(ctx, spec, "y", FZ_JSON_NUMBER);
if (!page || page->u.number < 0 || page->u.number >= fz_count_pages(ctx, doc))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a target with no page");
return fz_format_link_uri(ctx, doc, fz_make_link_dest_xyz(0, (int)page->u.number, 0, y ? (float)y->u.number : 0, 0));
}
/* Walks `path`; the last step may land past the end. */
static int walk(fz_context *ctx, fz_outline_iterator *iter, fz_json *path)
{
int i, k, n = fz_json_array_length(ctx, path), at = FZ_OUTLINE_ITERATOR_AT_ITEM;
if (n == 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "an empty bookmark path");
for (i = 0; i < n; i++)
{
int steps = (int)ely_number(ctx, path, i);
if (i > 0)
{
at = fz_outline_iterator_down(ctx, iter);
if (at < 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no bookmark at that path");
}
at = fz_outline_iterator_item(ctx, iter) ? FZ_OUTLINE_ITERATOR_AT_ITEM : FZ_OUTLINE_ITERATOR_AT_EMPTY;
for (k = 0; k < steps; k++)
if (at != FZ_OUTLINE_ITERATOR_AT_ITEM || (at = fz_outline_iterator_next(ctx, iter)) < 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no bookmark at that path");
if (i < n - 1 && at != FZ_OUTLINE_ITERATOR_AT_ITEM)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no bookmark at that path");
}
return at;
}
/* Adds, renames, retargets or deletes one bookmark, in one step. */
int ely_outline_edit(fz_context *ctx, fz_document *doc, const char *json)
{
pdf_document *pdf = NULL;
fz_outline_iterator *iter = NULL;
fz_pool *pool = NULL;
char *uri = NULL;
int begun = 0;
fz_var(pdf);
fz_var(iter);
fz_var(pool);
fz_var(uri);
fz_var(begun);
fz_try(ctx)
{
fz_json *spec, *op, *path, *title, *expect;
fz_outline_item *now, item = { 0 };
int at;
pdf = ely_pdf_of(ctx, doc);
pool = fz_new_pool(ctx);
spec = fz_parse_json(ctx, pool, json);
op = ely_member(ctx, spec, "op", FZ_JSON_STRING);
path = ely_member(ctx, spec, "path", FZ_JSON_ARRAY);
title = ely_member(ctx, spec, "title", FZ_JSON_STRING);
expect = ely_member(ctx, spec, "expect", FZ_JSON_STRING);
if (!op || !path || (strcmp(op->u.string, "add") && strcmp(op->u.string, "edit") && strcmp(op->u.string, "delete")))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad bookmark change");
if (title && !title->u.string[0])
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bookmark with no title");
if (!strcmp(op->u.string, "add") && !title)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a new bookmark with no title");
if (strcmp(op->u.string, "add") && !expect)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a change that does not name its bookmark");
if (ely_member(ctx, spec, "page", FZ_JSON_NUMBER))
uri = target(ctx, doc, spec);
else if (!strcmp(op->u.string, "add"))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a new bookmark with no target");
pdf_begin_operation(ctx, pdf, "Change bookmarks");
begun = 1;
iter = fz_new_outline_iterator(ctx, doc);
at = walk(ctx, iter, path);
if (!strcmp(op->u.string, "add"))
{
item.title = (char *)title->u.string;
item.uri = uri;
fz_outline_iterator_insert(ctx, iter, &item);
}
else
{
now = at == FZ_OUTLINE_ITERATOR_AT_ITEM ? fz_outline_iterator_item(ctx, iter) : NULL;
if (!now || strcmp(now->title ? now->title : "", expect->u.string))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the bookmark changed meanwhile");
if (!strcmp(op->u.string, "delete"))
fz_outline_iterator_delete(ctx, iter);
else
{
item = *now;
if (title)
item.title = (char *)title->u.string;
if (uri)
item.uri = uri;
fz_outline_iterator_update(ctx, iter, &item);
}
}
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
fz_free(ctx, uri);
fz_drop_outline_iterator(ctx, iter);
fz_drop_pool(ctx, pool);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* The page's links: box, address, and page if inside. */
int ely_links(fz_context *ctx, fz_document *doc, int number, unsigned char **data, size_t *len)
{
fz_page *page = NULL;
fz_link *links = NULL, *link;
fz_buffer *buf = NULL;
fz_output *out = NULL;
fz_var(page);
fz_var(links);
fz_var(buf);
fz_var(out);
fz_try(ctx)
{
page = fz_load_page(ctx, doc, number);
links = fz_load_links(ctx, page);
buf = fz_new_buffer(ctx, 256);
out = fz_new_output_with_buffer(ctx, buf);
fz_write_byte(ctx, out, '[');
for (link = links; link; link = link->next)
{
int to = -1;
float x = 0, y = 0;
if (!fz_is_external_link(ctx, link->uri))
to = fz_page_number_from_location(ctx, doc, fz_resolve_link(ctx, doc, link->uri, &x, &y));
fz_write_printf(ctx, out, "%s{\"rect\":[%g,%g,%g,%g],\"uri\":", link == links ? "" : ",",
link->rect.x0, link->rect.y0, link->rect.x1, link->rect.y1);
ely_json_string(ctx, out, link->uri ? link->uri : "");
fz_write_printf(ctx, out, ",\"page\":%d,\"y\":%g}", to, y);
}
fz_write_byte(ctx, out, ']');
fz_close_output(ctx, out);
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
{
fz_drop_output(ctx, out);
fz_drop_buffer(ctx, buf);
fz_drop_link(ctx, links);
fz_drop_page(ctx, page);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* Only web and mail addresses leave the document. */
static int outward(const char *uri)
{
return !strncmp(uri, "https://", 8) || !strncmp(uri, "http://", 7) || !strncmp(uri, "mailto:", 7);
}
/* Adds, retargets or deletes one link, in one step. */
int ely_link_edit(fz_context *ctx, fz_document *doc, int number, const char *json)
{
pdf_document *pdf = NULL;
pdf_page *page = NULL;
fz_pool *pool = NULL;
char *uri = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(pool);
fz_var(uri);
fz_var(begun);
fz_try(ctx)
{
fz_json *spec, *op, *index, *expect, *web;
fz_link *link = NULL;
fz_rect box = fz_empty_rect;
int i;
pdf = ely_pdf_of(ctx, doc);
page = pdf_load_page(ctx, pdf, number);
pool = fz_new_pool(ctx);
spec = fz_parse_json(ctx, pool, json);
op = ely_member(ctx, spec, "op", FZ_JSON_STRING);
index = ely_member(ctx, spec, "index", FZ_JSON_NUMBER);
expect = ely_member(ctx, spec, "expect", FZ_JSON_STRING);
web = ely_member(ctx, spec, "uri", FZ_JSON_STRING);
if (!op || (strcmp(op->u.string, "add") && strcmp(op->u.string, "edit") && strcmp(op->u.string, "delete")))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a bad link change");
if (web && !outward(web->u.string))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "only web and mail addresses: %s", web->u.string);
if (web)
uri = fz_strdup(ctx, web->u.string);
else if (ely_member(ctx, spec, "page", FZ_JSON_NUMBER))
uri = target(ctx, doc, spec);
else if (strcmp(op->u.string, "delete"))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a link with no target");
if (strcmp(op->u.string, "add"))
{
if (!index || !expect)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a change that does not name its link");
for (link = page->links, i = 0; link && i < (int)index->u.number; link = link->next)
i++;
if (!link || strcmp(link->uri ? link->uri : "", expect->u.string))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the link changed meanwhile");
}
if (!strcmp(op->u.string, "add"))
box = ely_rect(ctx, ely_member(ctx, spec, "rect", FZ_JSON_ARRAY), 2);
pdf_begin_operation(ctx, pdf, "Change links");
begun = 1;
if (!strcmp(op->u.string, "add"))
fz_drop_link(ctx, pdf_create_link(ctx, page, box, uri));
else if (!strcmp(op->u.string, "delete"))
pdf_delete_link(ctx, page, link);
else
fz_set_link_uri(ctx, link, uri);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
fz_free(ctx, uri);
fz_drop_pool(ctx, pool);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
+189
View File
@@ -0,0 +1,189 @@
/* Page images, text, optimized copies, repaired copies (PDF-13, PDF-14). */
#include <string.h>
#include "ely_shim.h"
/* Page `number` as a PNG at `dpi`, white, as printed. */
int ely_export_image(fz_context *ctx, fz_document *doc, int number, float dpi, const char *path)
{
fz_pixmap *pixmap = NULL;
fz_var(pixmap);
fz_try(ctx)
{
if (dpi < 18 || dpi > 1200)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a resolution outside 18 to 1200 dpi");
ely_watch_begin(ctx);
pixmap = fz_new_pixmap_from_page_number(ctx, doc, number, fz_scale(dpi / 72, dpi / 72), fz_device_rgb(ctx), 0);
if (ely_watch_end(ctx))
fz_throw(ctx, FZ_ERROR_FORMAT, "page %d did not read whole: %s", number + 1, ely_open_warnings());
fz_save_pixmap_as_png(ctx, pixmap, path);
}
fz_always(ctx)
{
ely_watch_end(ctx);
fz_drop_pixmap(ctx, pixmap);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* Every page's text, UTF-8, pages parted by form feeds. */
int ely_export_text(fz_context *ctx, fz_document *doc, const char *path)
{
fz_output *out = NULL;
fz_stext_page *text = NULL;
fz_buffer *buf = NULL;
fz_var(out);
fz_var(text);
fz_var(buf);
fz_try(ctx)
{
int i;
ely_watch_begin(ctx);
out = fz_new_output_with_path(ctx, path, 0);
for (i = 0; i < fz_count_pages(ctx, doc); i++)
{
if (i > 0)
fz_write_byte(ctx, out, '\f');
text = fz_new_stext_page_from_page_number(ctx, doc, i, NULL);
buf = fz_new_buffer_from_stext_page(ctx, text);
fz_write_buffer(ctx, out, buf);
fz_drop_buffer(ctx, buf);
buf = NULL;
fz_drop_stext_page(ctx, text);
text = NULL;
}
/* Partial text is no export. */
if (ely_watch_end(ctx))
fz_throw(ctx, FZ_ERROR_FORMAT, "the text did not read whole: %s", ely_open_warnings());
fz_close_output(ctx, out);
}
fz_always(ctx)
{
ely_watch_end(ctx);
fz_drop_buffer(ctx, buf);
fz_drop_stext_page(ctx, text);
fz_drop_output(ctx, out);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
typedef struct
{
int from, to;
} downsample;
static void shrink_images(fz_context *ctx, fz_document *copy, void *arg)
{
downsample *d = arg;
pdf_image_rewriter_options opts = { 0 };
if (!d->to)
return;
opts.color_lossless_image_subsample_method = opts.color_lossy_image_subsample_method = FZ_SUBSAMPLE_BICUBIC;
opts.gray_lossless_image_subsample_method = opts.gray_lossy_image_subsample_method = FZ_SUBSAMPLE_BICUBIC;
opts.bitonal_image_subsample_method = FZ_SUBSAMPLE_BICUBIC;
opts.color_lossless_image_subsample_threshold = opts.color_lossy_image_subsample_threshold = d->from;
opts.gray_lossless_image_subsample_threshold = opts.gray_lossy_image_subsample_threshold = d->from;
opts.bitonal_image_subsample_threshold = d->from;
opts.color_lossless_image_subsample_to = opts.color_lossy_image_subsample_to = d->to;
opts.gray_lossless_image_subsample_to = opts.gray_lossy_image_subsample_to = d->to;
opts.bitonal_image_subsample_to = d->to;
/* Never, the default, drops the smaller picture. */
opts.color_lossless_image_recompress_method = opts.gray_lossless_image_recompress_method = FZ_RECOMPRESS_LOSSLESS;
opts.color_lossy_image_recompress_method = opts.gray_lossy_image_recompress_method = FZ_RECOMPRESS_SAME;
opts.bitonal_image_recompress_method = FZ_RECOMPRESS_SAME;
pdf_rewrite_images(ctx, ely_pdf_of(ctx, copy), &opts);
}
/* Writes `out` compressed, images above `from` dpi taken to `to`. */
int ely_optimize(fz_context *ctx, fz_document *doc, const char *raw, const char *out, const char *password, int compress,
int from, int to)
{
fz_try(ctx)
{
pdf_write_options opts = pdf_default_write_options;
downsample d = { from, to };
if (to && (to < 36 || from <= to))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a downsampling from %d to %d dpi", from, to);
opts.do_compress = opts.do_compress_images = opts.do_compress_fonts = compress;
ely_save_copy(ctx, ely_pdf_of(ctx, doc), raw, out, password, &opts, shrink_images, &d);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* 1 when MuPDF rebuilt the file's structure to read it. */
int ely_repaired(fz_context *ctx, fz_document *doc)
{
int repaired = 0;
fz_try(ctx)
repaired = pdf_was_repaired(ctx, ely_pdf_of(ctx, doc));
fz_catch(ctx)
return ely_fail(ctx);
return repaired;
}
/* The repaired document written whole to `path`. */
int ely_write_repaired(fz_context *ctx, fz_document *doc, const char *path)
{
fz_try(ctx)
{
pdf_write_options opts = pdf_default_write_options;
opts.do_garbage = 1;
pdf_save_document(ctx, ely_pdf_of(ctx, doc), path, &opts);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* Leave to print was checked; the printer gets no lock. */
int ely_print_copy(fz_context *ctx, fz_document *doc, const char *raw, const char *out, const char *password)
{
fz_try(ctx)
{
pdf_write_options opts = pdf_default_write_options;
opts.do_encrypt = PDF_ENCRYPT_NONE;
ely_save_copy(ctx, ely_pdf_of(ctx, doc), raw, out, password, &opts, NULL, NULL);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* Decodes every picture; 1 if MuPDF warned on any. */
int ely_check_pictures(fz_context *ctx, fz_document *doc)
{
pdf_document *pdf = ely_pdf_of(ctx, doc);
int i;
ely_watch_begin(ctx);
for (i = 1; i < pdf_xref_len(ctx, pdf); i++)
{
pdf_obj *obj = NULL;
fz_buffer *bytes = NULL;
fz_var(obj);
fz_var(bytes);
fz_try(ctx)
{
obj = pdf_new_indirect(ctx, pdf, i, 0);
if (pdf_is_stream(ctx, obj) && pdf_name_eq(ctx, pdf_dict_get(ctx, obj, PDF_NAME(Subtype)), PDF_NAME(Image)))
bytes = pdf_load_stream_number(ctx, pdf, i);
}
fz_always(ctx)
{
fz_drop_buffer(ctx, bytes);
pdf_drop_obj(ctx, obj);
}
fz_catch(ctx)
{
fz_warn(ctx, "picture %d: %s", i, fz_caught_message(ctx));
fz_ignore_error(ctx);
}
}
return ely_watch_end(ctx);
}
+487
View File
@@ -0,0 +1,487 @@
/* Page edits, undo and saving; one undo step each. */
#include <stdlib.h>
#include "ely_shim.h"
pdf_document *ely_pdf_of(fz_context *ctx, fz_document *doc)
{
pdf_document *pdf = pdf_specifics(ctx, doc);
if (!pdf)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "not a PDF");
return pdf;
}
/* MuPDF's font and image caches outlive an undo; they go. */
void ely_forget_resources(fz_context *ctx, pdf_document *pdf)
{
pdf_drop_resource_tables(ctx, pdf);
pdf->resources.fonts = NULL;
pdf->resources.colorspaces = NULL;
pdf->resources.images = NULL;
}
/* Reports the failure; a begun step is rolled back. */
int ely_edit_failed(fz_context *ctx, pdf_document *pdf, int begun)
{
int code = ely_fail(ctx);
if (begun)
{
fz_try(ctx)
{
pdf_abandon_operation(ctx, pdf);
ely_forget_resources(ctx, pdf);
}
fz_catch(ctx)
ely_fail_also(ctx, "rolling back");
}
return code;
}
int ely_journal(fz_context *ctx, fz_document *doc)
{
fz_try(ctx)
pdf_enable_journal(ctx, ely_pdf_of(ctx, doc));
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
int ely_undo(fz_context *ctx, fz_document *doc, int redo)
{
fz_try(ctx)
{
pdf_document *pdf = ely_pdf_of(ctx, doc);
if (redo)
pdf_redo(ctx, pdf);
else
pdf_undo(ctx, pdf);
ely_forget_resources(ctx, pdf);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* MuPDF's undo position and step count; saves add steps. */
int ely_state(fz_context *ctx, fz_document *doc, int *at, int *count)
{
fz_try(ctx)
*at = pdf_undoredo_state(ctx, ely_pdf_of(ctx, doc), count);
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
static int by_number(const void *a, const void *b)
{
return *(const int *)a - *(const int *)b;
}
/* Object numbers, sorted for lookups. */
typedef struct
{
int *nums;
int n;
} gone_set;
static int is_gone(fz_context *ctx, gone_set *gone, pdf_obj *obj)
{
int num = pdf_to_num(ctx, obj);
return num && bsearch(&num, gone->nums, gone->n, sizeof(int), by_number) != NULL;
}
/* Deleted pages, and the annotations they held. */
typedef struct
{
gone_set pages;
gone_set annots;
} gone_pages;
/* The page a link, bookmark or action points to. */
pdf_obj *ely_target(fz_context *ctx, pdf_document *pdf, pdf_obj *obj)
{
pdf_obj *dest = pdf_dict_get(ctx, obj, PDF_NAME(Dest));
if (!dest)
{
pdf_obj *action = pdf_dict_get(ctx, obj, PDF_NAME(A));
if (!pdf_name_eq(ctx, pdf_dict_get(ctx, action, PDF_NAME(S)), PDF_NAME(GoTo)))
return NULL;
dest = pdf_dict_get(ctx, action, PDF_NAME(D));
}
if (pdf_is_name(ctx, dest) || pdf_is_string(ctx, dest))
dest = pdf_lookup_dest(ctx, pdf, dest);
if (pdf_is_dict(ctx, dest))
dest = pdf_dict_get(ctx, dest, PDF_NAME(D));
return pdf_is_array(ctx, dest) ? pdf_array_get(ctx, dest, 0) : NULL;
}
/* Bookmarks to gone pages keep their place, lose the jump. */
static void forget_in_outline(fz_context *ctx, pdf_document *pdf, pdf_obj *item, gone_set *gone)
{
for (; item; item = pdf_dict_get(ctx, item, PDF_NAME(Next)))
{
pdf_obj *page = ely_target(ctx, pdf, item);
if (page && is_gone(ctx, gone, page))
{
pdf_dict_del(ctx, item, PDF_NAME(Dest));
pdf_dict_del(ctx, item, PDF_NAME(A));
}
forget_in_outline(ctx, pdf, pdf_dict_get(ctx, item, PDF_NAME(First)), gone);
}
}
/* Whether a field keeps a widget on a kept page. */
static int prune_field(fz_context *ctx, pdf_obj *field, gone_pages *gone)
{
pdf_obj *kids = pdf_dict_get(ctx, field, PDF_NAME(Kids));
int i;
if (!kids)
{
/* A deleted page held it, or its /P names one. */
pdf_obj *page = pdf_dict_get(ctx, field, PDF_NAME(P));
if (!pdf_name_eq(ctx, pdf_dict_get(ctx, field, PDF_NAME(Subtype)), PDF_NAME(Widget)))
return 1;
return !is_gone(ctx, &gone->annots, field) && !(page && is_gone(ctx, &gone->pages, page));
}
for (i = pdf_array_len(ctx, kids) - 1; i >= 0; i--)
if (!prune_field(ctx, pdf_array_get(ctx, kids, i), gone))
pdf_array_delete(ctx, kids, i);
return pdf_array_len(ctx, kids) > 0;
}
/* Drops what pointed into deleted pages. */
static void forget_pages(fz_context *ctx, pdf_document *pdf, gone_pages *gone)
{
pdf_obj *root = pdf_dict_get(ctx, pdf_trailer(ctx, pdf), PDF_NAME(Root));
pdf_obj *outlines = pdf_dict_get(ctx, root, PDF_NAME(Outlines));
pdf_obj *fields = pdf_dict_getp(ctx, root, "AcroForm/Fields");
int i, j, count = pdf_count_pages(ctx, pdf);
forget_in_outline(ctx, pdf, pdf_dict_get(ctx, outlines, PDF_NAME(First)), &gone->pages);
for (i = 0; i < count; i++)
{
pdf_obj *annots = pdf_dict_get(ctx, pdf_lookup_page_obj(ctx, pdf, i), PDF_NAME(Annots));
for (j = pdf_array_len(ctx, annots) - 1; j >= 0; j--)
{
pdf_obj *annot = pdf_array_get(ctx, annots, j);
pdf_obj *page;
if (!pdf_name_eq(ctx, pdf_dict_get(ctx, annot, PDF_NAME(Subtype)), PDF_NAME(Link)))
continue;
page = ely_target(ctx, pdf, annot);
if (page && is_gone(ctx, &gone->pages, page))
pdf_array_delete(ctx, annots, j);
}
}
for (i = pdf_array_len(ctx, fields) - 1; i >= 0; i--)
if (!prune_field(ctx, pdf_array_get(ctx, fields, i), gone))
pdf_array_delete(ctx, fields, i);
}
/* Checks `pages` are sorted page numbers without repeats. */
static void check_pages(fz_context *ctx, pdf_document *pdf, const int *pages, int n)
{
int i, count = pdf_count_pages(ctx, pdf);
if (n <= 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no pages given");
for (i = 0; i < n; i++)
if (pages[i] < 0 || pages[i] >= count || (i > 0 && pages[i] <= pages[i - 1]))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "page %d is out of order or range", pages[i]);
}
/* Deletes checked `pages` and what pointed there. */
static void delete_pages(fz_context *ctx, pdf_document *pdf, const int *pages, int n)
{
gone_pages gone = { { NULL, n }, { NULL, 0 } };
int i, j, held = 0;
fz_var(gone.pages.nums);
fz_var(gone.annots.nums);
fz_try(ctx)
{
gone.pages.nums = fz_malloc_array(ctx, n, int);
for (i = 0; i < n; i++)
{
pdf_obj *page = pdf_lookup_page_obj(ctx, pdf, pages[i]);
gone.pages.nums[i] = pdf_to_num(ctx, page);
held += pdf_array_len(ctx, pdf_dict_get(ctx, page, PDF_NAME(Annots)));
}
gone.annots.nums = fz_malloc_array(ctx, held + 1, int);
for (i = 0; i < n; i++)
{
pdf_obj *annots = pdf_dict_get(ctx, pdf_lookup_page_obj(ctx, pdf, pages[i]), PDF_NAME(Annots));
for (j = 0; j < pdf_array_len(ctx, annots); j++)
gone.annots.nums[gone.annots.n++] = pdf_to_num(ctx, pdf_array_get(ctx, annots, j));
}
qsort(gone.pages.nums, n, sizeof(int), by_number);
qsort(gone.annots.nums, gone.annots.n, sizeof(int), by_number);
for (i = n - 1; i >= 0; i--)
pdf_delete_page(ctx, pdf, pages[i]);
forget_pages(ctx, pdf, &gone);
}
fz_always(ctx)
{
fz_free(ctx, gone.pages.nums);
fz_free(ctx, gone.annots.nums);
}
fz_catch(ctx)
fz_rethrow(ctx);
}
int ely_delete_pages(fz_context *ctx, fz_document *doc, const int *pages, int n)
{
pdf_document *pdf = NULL;
int begun = 0;
fz_var(pdf);
fz_var(begun);
fz_try(ctx)
{
pdf = ely_pdf_of(ctx, doc);
check_pages(ctx, pdf, pages, n);
if (n == pdf_count_pages(ctx, pdf))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a PDF keeps at least one page");
pdf_begin_operation(ctx, pdf, "Delete pages");
begun = 1;
delete_pages(ctx, pdf, pages, n);
pdf_end_operation(ctx, pdf);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Moves `pages` before page `to`, in their order. */
int ely_move_pages(fz_context *ctx, fz_document *doc, const int *pages, int n, int to)
{
pdf_document *pdf = NULL;
pdf_obj **moved = NULL;
int begun = 0, i, at;
fz_var(pdf);
fz_var(begun);
fz_var(moved);
fz_try(ctx)
{
pdf = ely_pdf_of(ctx, doc);
check_pages(ctx, pdf, pages, n);
if (to < 0 || to > pdf_count_pages(ctx, pdf))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no place %d to move to", to);
moved = fz_calloc(ctx, n, sizeof *moved);
for (i = 0; i < n; i++)
moved[i] = pdf_keep_obj(ctx, pdf_lookup_page_obj(ctx, pdf, pages[i]));
pdf_begin_operation(ctx, pdf, "Move pages");
begun = 1;
for (i = n - 1; i >= 0; i--)
pdf_delete_page(ctx, pdf, pages[i]);
for (at = to, i = 0; i < n; i++)
at -= pages[i] < to;
for (i = 0; i < n; i++)
pdf_insert_page(ctx, pdf, at + i, moved[i]);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
for (i = 0; moved && i < n; i++)
pdf_drop_obj(ctx, moved[i]);
fz_free(ctx, moved);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Turns `pages` by a multiple of 90 degrees. */
int ely_rotate_pages(fz_context *ctx, fz_document *doc, const int *pages, int n, int by)
{
pdf_document *pdf = NULL;
int begun = 0, i;
fz_var(pdf);
fz_var(begun);
fz_try(ctx)
{
pdf = ely_pdf_of(ctx, doc);
check_pages(ctx, pdf, pages, n);
if (by % 90)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a turn of %d degrees", by);
pdf_begin_operation(ctx, pdf, "Rotate pages");
begun = 1;
for (i = 0; i < n; i++)
{
pdf_obj *page = pdf_lookup_page_obj(ctx, pdf, pages[i]);
int now = pdf_to_int(ctx, pdf_dict_get_inheritable(ctx, page, PDF_NAME(Rotate)));
pdf_dict_put_int(ctx, page, PDF_NAME(Rotate), ((now + by) % 360 + 360) % 360);
}
pdf_end_operation(ctx, pdf);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Seen margins (top, right, bottom, left) to user sides. */
static fz_rect inset(fz_rect box, int rotate, const float seen[4])
{
/* User sides: 0 y1, 1 x1, 2 y0, 3 x0. */
static const int under[4][4] = { { 0, 1, 2, 3 }, { 3, 0, 1, 2 }, { 2, 3, 0, 1 }, { 1, 2, 3, 0 } };
float side[4] = { 0, 0, 0, 0 };
int i, turn = ((rotate % 360 + 360) % 360) / 90;
for (i = 0; i < 4; i++)
side[under[turn][i]] = seen[i];
box.y1 -= side[0];
box.x1 -= side[1];
box.y0 += side[2];
box.x0 += side[3];
return box;
}
/* Crops `pages` by seen margins in points; zeros uncrop. */
int ely_crop_pages(fz_context *ctx, fz_document *doc, const int *pages, int n, const float margins[4])
{
pdf_document *pdf = NULL;
int begun = 0, i;
fz_var(pdf);
fz_var(begun);
fz_try(ctx)
{
int none = margins[0] == 0 && margins[1] == 0 && margins[2] == 0 && margins[3] == 0;
pdf = ely_pdf_of(ctx, doc);
check_pages(ctx, pdf, pages, n);
pdf_begin_operation(ctx, pdf, "Crop pages");
begun = 1;
for (i = 0; i < n; i++)
{
pdf_obj *page = pdf_lookup_page_obj(ctx, pdf, pages[i]);
fz_rect media = pdf_to_rect(ctx, pdf_dict_get_inheritable(ctx, page, PDF_NAME(MediaBox)));
int rotate = pdf_to_int(ctx, pdf_dict_get_inheritable(ctx, page, PDF_NAME(Rotate)));
/* Points as shown; user units may be larger. */
float unit = pdf_to_real(ctx, pdf_dict_get(ctx, page, PDF_NAME(UserUnit)));
float user[4];
int side;
for (side = 0; side < 4; side++)
user[side] = margins[side] / (unit > 0 ? unit : 1);
fz_rect crop = inset(media, rotate, user);
if (none)
{
pdf_dict_del(ctx, page, PDF_NAME(CropBox));
continue;
}
if (crop.x1 - crop.x0 < 1 || crop.y1 - crop.y0 < 1)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the margins leave nothing of page %d", pages[i] + 1);
pdf_dict_put_rect(ctx, page, PDF_NAME(CropBox), crop);
}
pdf_end_operation(ctx, pdf);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* A blank page of w by h points at `at`. */
int ely_insert_blank(fz_context *ctx, fz_document *doc, int at, float w, float h)
{
pdf_document *pdf = NULL;
pdf_obj *page = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(begun);
fz_try(ctx)
{
pdf = ely_pdf_of(ctx, doc);
if (at < 0 || at > pdf_count_pages(ctx, pdf) || w < 1 || h < 1)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a blank page at %d of %g by %g", at, w, h);
pdf_begin_operation(ctx, pdf, "Insert a blank page");
begun = 1;
page = pdf_add_page(ctx, pdf, fz_make_rect(0, 0, w, h), 0, NULL, NULL);
pdf_insert_page(ctx, pdf, at, page);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
pdf_drop_obj(ctx, page);
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* Pages of `pdf` other than the sorted `keep`. */
static int *others(fz_context *ctx, pdf_document *pdf, const int *keep, int n, int *k)
{
int i, count = pdf_count_pages(ctx, pdf);
int *rest = fz_malloc_array(ctx, count + 1, int);
*k = 0;
for (i = 0; i < count; i++)
if (bsearch(&i, keep, n, sizeof(int), by_number) == NULL)
rest[(*k)++] = i;
return rest;
}
/* A snapshot, reopened, changed, unsigned, compacted to `out`. */
void ely_save_copy(fz_context *ctx, pdf_document *pdf, const char *raw, const char *out, const char *password,
const pdf_write_options *opts, ely_change *change, void *arg)
{
fz_document *copy = NULL;
fz_var(copy);
fz_try(ctx)
{
pdf_write_options compact = opts ? *opts : pdf_default_write_options;
pdf_save_snapshot(ctx, pdf, raw);
copy = fz_open_document(ctx, raw);
if (fz_needs_password(ctx, copy) && !fz_authenticate_password(ctx, copy, password))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the written copy refused its password");
if (change)
change(ctx, copy, arg);
ely_unsign(ctx, ely_pdf_of(ctx, copy));
/* Unused objects, such as deleted pages, go. */
compact.do_garbage = 2;
pdf_save_document(ctx, ely_pdf_of(ctx, copy), out, &compact);
}
fz_always(ctx)
fz_drop_document(ctx, copy);
fz_catch(ctx)
fz_rethrow(ctx);
}
typedef struct
{
const int *pages;
int n;
} kept;
/* Deletes every page not kept. */
static void keep_only(fz_context *ctx, fz_document *copy, void *arg)
{
kept *keep = arg;
pdf_document *written = ely_pdf_of(ctx, copy);
int k, *rest = others(ctx, written, keep->pages, keep->n, &k);
fz_try(ctx)
{
if (k > 0)
delete_pages(ctx, written, rest, k);
}
fz_always(ctx)
fz_free(ctx, rest);
fz_catch(ctx)
fz_rethrow(ctx);
}
int ely_save(fz_context *ctx, fz_document *doc, const char *raw, const char *out, const char *password)
{
fz_try(ctx)
ely_save_copy(ctx, ely_pdf_of(ctx, doc), raw, out, password, NULL, NULL, NULL);
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* Saves only `pages` to `out`; the document is untouched. */
int ely_extract(fz_context *ctx, fz_document *doc, const int *pages, int n,
const char *raw, const char *out, const char *password)
{
fz_try(ctx)
{
pdf_document *pdf = ely_pdf_of(ctx, doc);
kept keep = { pages, n };
check_pages(ctx, pdf, pages, n);
ely_save_copy(ctx, pdf, raw, out, password, NULL, keep_only, &keep);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
+336
View File
@@ -0,0 +1,336 @@
/* Images and form XObjects: list, move, scale, swap, add (PDF-04). */
#include <math.h>
#include "ely_shim.h"
/* One drawn object: its kind and where it lands. */
typedef struct
{
int form;
fz_matrix ctm;
fz_rect rect;
} drawn;
/* Counts top-level draws, following q, Q and cm. */
typedef struct
{
pdf_processor super;
fz_matrix ctm;
fz_matrix *stack;
int depth, room;
fz_matrix page;
drawn *found;
int n, cap;
} inventory;
static void inv_q(fz_context *ctx, pdf_processor *proc)
{
inventory *inv = (inventory *)proc;
if (inv->depth == inv->room)
{
inv->room = inv->room * 2 + 8;
inv->stack = fz_realloc_array(ctx, inv->stack, inv->room, fz_matrix);
}
inv->stack[inv->depth++] = inv->ctm;
}
static void inv_Q(fz_context *ctx, pdf_processor *proc)
{
inventory *inv = (inventory *)proc;
if (inv->depth > 0)
inv->ctm = inv->stack[--inv->depth];
}
static void inv_cm(fz_context *ctx, pdf_processor *proc, float a, float b, float c, float d, float e, float f)
{
inventory *inv = (inventory *)proc;
inv->ctm = fz_concat(fz_make_matrix(a, b, c, d, e, f), inv->ctm);
}
static void found(fz_context *ctx, inventory *inv, int form, fz_rect box)
{
if (inv->n == inv->cap)
{
inv->cap = inv->cap * 2 + 8;
inv->found = fz_realloc_array(ctx, inv->found, inv->cap, drawn);
}
inv->found[inv->n].form = form;
inv->found[inv->n].ctm = inv->ctm;
inv->found[inv->n++].rect = fz_transform_rect(box, fz_concat(inv->ctm, inv->page));
}
static void inv_image(fz_context *ctx, pdf_processor *proc, const char *name, fz_image *image)
{
found(ctx, (inventory *)proc, 0, fz_unit_rect);
}
static void inv_form(fz_context *ctx, pdf_processor *proc, const char *name, pdf_obj *form)
{
fz_rect box = fz_transform_rect(pdf_xobject_bbox(ctx, form), pdf_xobject_matrix(ctx, form));
found(ctx, (inventory *)proc, 1, box);
}
/* The page's top-level draws, in content order. */
static int take_inventory(fz_context *ctx, pdf_document *pdf, pdf_page *page, drawn **out)
{
inventory *inv = pdf_new_processor(ctx, sizeof *inv);
int n = 0;
fz_try(ctx)
{
inv->super.op_q = inv_q;
inv->super.op_Q = inv_Q;
inv->super.op_cm = inv_cm;
inv->super.op_Do_image = inv_image;
inv->super.op_Do_form = inv_form;
inv->ctm = fz_identity;
pdf_page_transform(ctx, page, NULL, &inv->page);
pdf_process_contents(ctx, &inv->super, pdf, pdf_page_resources(ctx, page), pdf_page_contents(ctx, page), NULL, NULL);
pdf_close_processor(ctx, &inv->super);
*out = inv->found;
n = inv->n;
inv->found = NULL;
}
fz_always(ctx)
{
fz_free(ctx, inv->stack);
fz_free(ctx, inv->found);
pdf_drop_processor(ctx, &inv->super);
}
fz_catch(ctx)
fz_rethrow(ctx);
return n;
}
/* JSON of the page's images and forms. */
int ely_objects(fz_context *ctx, fz_document *doc, int number, unsigned char **data, size_t *len)
{
pdf_page *page = NULL;
drawn *list = NULL;
fz_buffer *buf = NULL;
fz_var(page);
fz_var(list);
fz_var(buf);
fz_try(ctx)
{
pdf_document *pdf = ely_pdf_of(ctx, doc);
int i, n;
page = pdf_load_page(ctx, pdf, number);
n = take_inventory(ctx, pdf, page, &list);
buf = fz_new_buffer(ctx, 256);
fz_append_byte(ctx, buf, '[');
for (i = 0; i < n; i++)
fz_append_printf(ctx, buf, "%s{\"kind\":\"%s\",\"rect\":[%g,%g,%g,%g]}", i ? "," : "",
list[i].form ? "form" : "image", list[i].rect.x0, list[i].rect.y0, list[i].rect.x1, list[i].rect.y1);
fz_append_byte(ctx, buf, ']');
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
{
fz_drop_buffer(ctx, buf);
fz_free(ctx, list);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* What the rewrite does at draw `target`. */
typedef struct
{
int target, seen, drop;
fz_matrix x;
const char *rename;
pdf_processor *out;
void (*image)(fz_context *, pdf_processor *, const char *, fz_image *);
void (*form)(fz_context *, pdf_processor *, const char *, pdf_obj *);
} hook;
/* Wraps the chosen draw in `x`, renames or drops it. */
static int at_target(fz_context *ctx, hook *h, const char **name)
{
if (h->seen++ != h->target)
return 0;
if (!h->drop)
{
h->out->op_q(ctx, h->out);
h->out->op_cm(ctx, h->out, h->x.a, h->x.b, h->x.c, h->x.d, h->x.e, h->x.f);
if (h->rename)
*name = h->rename;
}
return 1;
}
static void hook_image(fz_context *ctx, pdf_processor *proc, const char *name, fz_image *image)
{
hook *h = fz_user_context(ctx);
int hit = at_target(ctx, h, &name);
if (!(hit && h->drop))
h->image(ctx, proc, name, image);
if (hit && !h->drop)
h->out->op_Q(ctx, h->out);
}
static void hook_form(fz_context *ctx, pdf_processor *proc, const char *name, pdf_obj *form)
{
hook *h = fz_user_context(ctx);
int hit = at_target(ctx, h, &name);
if (!(hit && h->drop))
h->form(ctx, proc, name, form);
if (hit && !h->drop)
h->out->op_Q(ctx, h->out);
}
/* Rewrites the page's content with `h` at its target draw. */
static void rewrite(fz_context *ctx, pdf_document *pdf, pdf_page *page, hook *h)
{
fz_buffer *buf = fz_new_buffer(ctx, 1024);
pdf_processor *out = NULL;
void *before = fz_user_context(ctx);
fz_var(out);
fz_try(ctx)
{
out = pdf_new_buffer_processor(ctx, buf, 0, 0);
h->out = out;
h->image = out->op_Do_image;
h->form = out->op_Do_form;
out->op_Do_image = hook_image;
out->op_Do_form = hook_form;
fz_set_user_context(ctx, h);
pdf_process_contents(ctx, out, pdf, pdf_page_resources(ctx, page), pdf_page_contents(ctx, page), NULL, NULL);
pdf_close_processor(ctx, out);
if (h->seen <= h->target)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no drawn object %d", h->target);
pdf_dict_put_drop(ctx, page->obj, PDF_NAME(Contents), pdf_add_stream(ctx, pdf, buf, NULL, 0));
}
fz_always(ctx)
{
fz_set_user_context(ctx, before);
pdf_drop_processor(ctx, out);
fz_drop_buffer(ctx, buf);
}
fz_catch(ctx)
fz_rethrow(ctx);
}
/* The draw's matrix in page space. */
static fz_matrix placed_at(fz_context *ctx, pdf_document *pdf, pdf_page *page, int index, fz_rect *rect)
{
drawn *list = NULL;
fz_matrix at = fz_identity, page_ctm;
fz_var(list);
fz_try(ctx)
{
int n = take_inventory(ctx, pdf, page, &list);
if (index < 0 || index >= n)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no drawn object %d", index);
pdf_page_transform(ctx, page, NULL, &page_ctm);
at = fz_concat(list[index].ctm, page_ctm);
*rect = list[index].rect;
}
fz_always(ctx)
fz_free(ctx, list);
fz_catch(ctx)
fz_rethrow(ctx);
return at;
}
/* Draw `index` moved by page-space `t`, swapped, or dropped. */
int ely_place(fz_context *ctx, fz_document *doc, int number, int index, const float t[6], int drop, const char *path)
{
pdf_document *pdf = NULL;
pdf_page *page = NULL;
fz_image *image = NULL;
pdf_obj *ref = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(image);
fz_var(ref);
fz_var(begun);
fz_try(ctx)
{
hook h = { index, 0, drop, fz_identity, NULL, NULL, NULL, NULL };
fz_matrix change = fz_make_matrix(t[0], t[1], t[2], t[3], t[4], t[5]), at;
fz_rect rect;
char name[32];
pdf = ely_pdf_of(ctx, doc);
page = pdf_load_page(ctx, pdf, number);
pdf_begin_operation(ctx, pdf, drop ? "Delete an object" : path ? "Replace an image" : "Move an object");
begun = 1;
at = placed_at(ctx, pdf, page, index, &rect);
if (path)
{
/* The new picture fits inside the old one's box. */
float box = (rect.x1 - rect.x0) / (rect.y1 - rect.y0), own;
float cx = (rect.x0 + rect.x1) / 2, cy = (rect.y0 + rect.y1) / 2;
image = fz_new_image_from_file(ctx, path);
own = (float)image->w / image->h;
change = fz_concat(fz_concat(fz_translate(-cx, -cy), fz_scale(fminf(1, own / box), fminf(1, box / own))), fz_translate(cx, cy));
ref = pdf_add_image(ctx, pdf, image);
h.rename = ely_add_resource(ctx, pdf, page->obj, PDF_NAME(XObject), "ElyIm", ref, name, sizeof name);
}
if (fz_matrix_expansion(change) < 0.001f)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a change that leaves nothing");
h.x = fz_concat(fz_concat(at, change), fz_invert_matrix(at));
rewrite(ctx, pdf, page, &h);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
pdf_drop_obj(ctx, ref);
fz_drop_image(ctx, image);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
/* An image file drawn into page-space `rect`, in one step. */
int ely_add_image(fz_context *ctx, fz_document *doc, int number, const char *path, const float rect[4])
{
pdf_document *pdf = NULL;
pdf_page *page = NULL;
fz_image *image = NULL;
pdf_obj *ref = NULL;
fz_buffer *buf = NULL;
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(image);
fz_var(ref);
fz_var(buf);
fz_var(begun);
fz_try(ctx)
{
fz_matrix m;
char name[32];
pdf = ely_pdf_of(ctx, doc);
page = pdf_load_page(ctx, pdf, number);
if (rect[2] - rect[0] < 1 || rect[3] - rect[1] < 1)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "an image box of nothing");
image = fz_new_image_from_file(ctx, path);
pdf_begin_operation(ctx, pdf, "Add an image");
begun = 1;
ref = pdf_add_image(ctx, pdf, image);
ely_add_resource(ctx, pdf, page->obj, PDF_NAME(XObject), "ElyIm", ref, name, sizeof name);
/* The unit square, bottom left first, onto the box. */
m = fz_concat(fz_make_matrix(rect[2] - rect[0], 0, 0, rect[1] - rect[3], rect[0], rect[3]), ely_to_user(ctx, page));
buf = fz_new_buffer(ctx, 128);
fz_append_printf(ctx, buf, "q %g %g %g %g %g %g cm /%s Do Q\n", m.a, m.b, m.c, m.d, m.e, m.f, name);
ely_append_content(ctx, pdf, page->obj, buf);
pdf_end_operation(ctx, pdf);
}
fz_always(ctx)
{
fz_drop_buffer(ctx, buf);
pdf_drop_obj(ctx, ref);
fz_drop_image(ctx, image);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
return ely_edit_failed(ctx, pdf, begun);
return 0;
}
+423
View File
@@ -0,0 +1,423 @@
/* True removal into a new file, then checked (PDF-09, QA-14). */
#include <math.h>
#include <string.h>
#include "ely_shim.h"
/* Regions by page, page space. */
typedef struct
{
int page;
fz_rect rect;
} region;
static int touches(fz_rect a, fz_rect b)
{
return !fz_is_empty_rect(fz_intersect_rect(a, b));
}
/* Throws unless every region is whole and on a page. */
static region *regions_of(fz_context *ctx, fz_json *list, int pages, int *n)
{
region *out;
int i;
*n = list && list->type == FZ_JSON_ARRAY ? fz_json_array_length(ctx, list) : 0;
if (*n == 0)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "nothing marked to remove");
out = fz_malloc_array(ctx, *n, region);
fz_try(ctx)
{
for (i = 0; i < *n; i++)
{
fz_json *spec = fz_json_array_get(ctx, list, i);
fz_json *page = ely_member(ctx, spec, "page", FZ_JSON_NUMBER);
if (!page || page->u.number < 0 || page->u.number >= pages)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a region off the pages");
out[i].page = (int)page->u.number;
out[i].rect = ely_rect(ctx, ely_member(ctx, spec, "rect", FZ_JSON_ARRAY), 1);
}
}
fz_catch(ctx)
{
fz_free(ctx, out);
fz_rethrow(ctx);
}
return out;
}
/* Drops annotations and fields that touch `area`; they carry text. */
static void drop_touching(fz_context *ctx, pdf_page *page, fz_rect area)
{
pdf_annot *annot;
int widgets;
for (widgets = 0; widgets < 2; widgets++)
{
restart:
for (annot = widgets ? pdf_first_widget(ctx, page) : pdf_first_annot(ctx, page); annot;
annot = widgets ? pdf_next_widget(ctx, annot) : pdf_next_annot(ctx, annot))
if (touches(pdf_bound_annot(ctx, annot), area))
{
pdf_obj *field = pdf_keep_obj(ctx, pdf_annot_obj(ctx, annot));
fz_try(ctx)
{
pdf_delete_annot(ctx, page, annot);
/* A field left with no widget still holds its value. */
if (widgets)
ely_unlink_field(ctx, page->doc, field);
}
fz_always(ctx)
pdf_drop_obj(ctx, field);
fz_catch(ctx)
fz_rethrow(ctx);
goto restart;
}
}
}
/* Applies `regions` to the copy, page by page. */
static void redact(fz_context *ctx, pdf_document *pdf, region *regions, int n)
{
pdf_redact_options opts = { 1, PDF_REDACT_IMAGE_PIXELS, PDF_REDACT_LINE_ART_REMOVE_IF_TOUCHED, PDF_REDACT_TEXT_REMOVE };
pdf_page *page = NULL;
int i, k;
fz_var(page);
fz_try(ctx)
{
for (i = 0; i < pdf_count_pages(ctx, pdf); i++)
{
int any = 0;
for (k = 0; k < n; k++)
any |= regions[k].page == i;
if (!any)
continue;
page = pdf_load_page(ctx, pdf, i);
for (k = 0; k < n; k++)
if (regions[k].page == i)
drop_touching(ctx, page, regions[k].rect);
/* Marks after drops, or a drop takes a mark. */
for (k = 0; k < n; k++)
if (regions[k].page == i)
{
pdf_annot *mark = pdf_create_annot(ctx, page, PDF_ANNOT_REDACT);
pdf_set_annot_rect(ctx, mark, regions[k].rect);
pdf_drop_annot(ctx, mark);
}
pdf_redact_page(ctx, pdf, page, &opts);
ely_drop_alt(ctx, pdf, page);
pdf_dict_del(ctx, page->obj, PDF_NAME(Thumb));
fz_drop_page(ctx, (fz_page *)page);
page = NULL;
}
}
fz_always(ctx)
fz_drop_page(ctx, (fz_page *)page);
fz_catch(ctx)
fz_rethrow(ctx);
}
/* A glyph box shrunk a tenth, as redaction judges. */
static fz_rect shrunk(fz_rect r)
{
float w = (r.x1 - r.x0) / 10, h = (r.y1 - r.y0) / 10;
return fz_make_rect(r.x0 + w, r.y0 + h, r.x1 - w, r.y1 - h);
}
/* Characters on `page` inside `area`, hidden ones too. */
static int chars_in(fz_context *ctx, fz_document *doc, int number, fz_rect area)
{
fz_stext_page *text = NULL;
int n = 0;
fz_var(text);
fz_try(ctx)
{
fz_stext_block *block;
fz_stext_line *line;
fz_stext_char *ch;
text = fz_new_stext_page_from_page_number(ctx, doc, number, NULL);
for (block = text->first_block; block; block = block->next)
if (block->type == FZ_STEXT_BLOCK_TEXT)
for (line = block->u.t.first_line; line; line = line->next)
for (ch = line->first_char; ch; ch = ch->next)
n += touches(shrunk(fz_rect_from_quad(ch->quad)), area);
}
fz_always(ctx)
fz_drop_stext_page(ctx, text);
fz_catch(ctx)
fz_rethrow(ctx);
return n;
}
/* Pixels in `area` that are not black once drawn. */
static int light_in(fz_context *ctx, fz_document *doc, int number, fz_rect area)
{
fz_pixmap *pix = NULL;
int n = 0;
fz_var(pix);
fz_try(ctx)
{
fz_page *page = fz_load_page(ctx, doc, number);
fz_irect box = fz_round_rect(fz_intersect_rect(area, fz_bound_page(ctx, page)));
unsigned char *s;
int x, y;
fz_drop_page(ctx, page);
/* One pixel in from each edge: boxes antialias. */
box.x0++, box.y0++, box.x1--, box.y1--;
if (box.x1 <= box.x0 || box.y1 <= box.y0)
break;
pix = fz_new_pixmap_with_bbox(ctx, fz_device_rgb(ctx), box, NULL, 0);
fz_clear_pixmap_with_value(ctx, pix, 255);
page = fz_load_page(ctx, doc, number);
fz_try(ctx)
{
fz_device *dev = fz_new_draw_device(ctx, fz_identity, pix);
fz_try(ctx)
{
fz_run_page(ctx, page, dev, fz_identity, NULL);
fz_close_device(ctx, dev);
}
fz_always(ctx)
fz_drop_device(ctx, dev);
fz_catch(ctx)
fz_rethrow(ctx);
}
fz_always(ctx)
fz_drop_page(ctx, page);
fz_catch(ctx)
fz_rethrow(ctx);
for (y = 0, s = pix->samples; y < pix->h; y++, s += pix->stride - pix->w * 3)
for (x = 0; x < pix->w; x++, s += 3)
n += s[0] > 40 || s[1] > 40 || s[2] > 40;
}
fz_always(ctx)
fz_drop_pixmap(ctx, pix);
fz_catch(ctx)
fz_rethrow(ctx);
return n;
}
/* Counts drawings in the regions, our boxes aside. */
typedef struct
{
fz_device super;
region *regions;
int n, page, leaks;
} leak_dev;
static int in_regions(leak_dev *d, fz_rect r)
{
int k;
for (k = 0; k < d->n; k++)
if (d->regions[k].page == d->page && touches(r, d->regions[k].rect))
return 1;
return 0;
}
static int black(fz_context *ctx, fz_colorspace *cs, const float *color)
{
float rgb[3];
if (!cs)
return 0;
fz_convert_color(ctx, cs, color, fz_device_rgb(ctx), rgb, NULL, fz_default_color_params);
return rgb[0] <= 0.02f && rgb[1] <= 0.02f && rgb[2] <= 0.02f;
}
/* Whether `r`, black, is one of the region boxes. */
static int is_box(fz_context *ctx, leak_dev *d, fz_rect r, fz_colorspace *cs, const float *color)
{
int k;
if (!black(ctx, cs, color))
return 0;
for (k = 0; k < d->n; k++)
{
fz_rect b = d->regions[k].rect;
if (d->regions[k].page == d->page && fabsf(r.x0 - b.x0) < 1 && fabsf(r.y0 - b.y0) < 1 &&
fabsf(r.x1 - b.x1) < 1 && fabsf(r.y1 - b.y1) < 1)
return 1;
}
return 0;
}
static void leak_fill_path(fz_context *ctx, fz_device *dev, const fz_path *path, int even_odd, fz_matrix ctm,
fz_colorspace *cs, const float *color, float alpha, fz_color_params cp)
{
leak_dev *d = (leak_dev *)dev;
fz_rect r = fz_bound_path(ctx, path, NULL, ctm);
(void)even_odd, (void)alpha, (void)cp;
if (in_regions(d, r) && !is_box(ctx, d, r, cs, color))
d->leaks++;
}
static void leak_stroke_path(fz_context *ctx, fz_device *dev, const fz_path *path, const fz_stroke_state *stroke,
fz_matrix ctm, fz_colorspace *cs, const float *color, float alpha, fz_color_params cp)
{
leak_dev *d = (leak_dev *)dev;
(void)cs, (void)color, (void)alpha, (void)cp;
d->leaks += in_regions(d, fz_bound_path(ctx, path, stroke, ctm));
}
static void leak_fill_shade(fz_context *ctx, fz_device *dev, fz_shade *shade, fz_matrix ctm, float alpha, fz_color_params cp)
{
leak_dev *d = (leak_dev *)dev;
(void)alpha, (void)cp;
d->leaks += in_regions(d, fz_bound_shade(ctx, shade, ctm));
}
static void leak_fill_image_mask(fz_context *ctx, fz_device *dev, fz_image *image, fz_matrix ctm, fz_colorspace *cs,
const float *color, float alpha, fz_color_params cp)
{
leak_dev *d = (leak_dev *)dev;
(void)image, (void)alpha, (void)cp;
d->leaks += in_regions(d, fz_transform_rect(fz_unit_rect, ctm)) && !black(ctx, cs, color);
}
/* Image pixels in `area` neither white nor black. */
static int image_light(fz_context *ctx, fz_image *image, fz_matrix ctm, fz_rect area)
{
fz_irect box = fz_round_rect(fz_intersect_rect(area, fz_transform_rect(fz_unit_rect, ctm)));
fz_pixmap *pix = NULL;
fz_device *draw = NULL;
int n = 0, x, y;
box.x0++, box.y0++, box.x1--, box.y1--;
if (box.x1 <= box.x0 || box.y1 <= box.y0)
return 0;
fz_var(pix);
fz_var(draw);
fz_try(ctx)
{
unsigned char *s;
pix = fz_new_pixmap_with_bbox(ctx, fz_device_rgb(ctx), box, NULL, 1);
fz_clear_pixmap(ctx, pix);
draw = fz_new_draw_device(ctx, fz_identity, pix);
fz_fill_image(ctx, draw, image, ctm, 1, fz_default_color_params);
fz_close_device(ctx, draw);
for (y = 0, s = pix->samples; y < pix->h; y++, s += pix->stride - pix->w * 4)
for (x = 0; x < pix->w; x++, s += 4)
{
int high = s[0] > 215 && s[1] > 215 && s[2] > 215, low = s[0] < 40 && s[1] < 40 && s[2] < 40;
n += s[3] > 200 && !high && !low;
}
}
fz_always(ctx)
{
fz_drop_device(ctx, draw);
fz_drop_pixmap(ctx, pix);
}
fz_catch(ctx)
fz_rethrow(ctx);
return n;
}
static void leak_fill_image(fz_context *ctx, fz_device *dev, fz_image *image, fz_matrix ctm, float alpha, fz_color_params cp)
{
leak_dev *d = (leak_dev *)dev;
int k;
(void)alpha, (void)cp;
for (k = 0; k < d->n; k++)
if (d->regions[k].page == d->page)
d->leaks += image_light(ctx, image, ctm, d->regions[k].rect);
}
/* Drawings left in the regions of page `number`. */
static int leaks_on(fz_context *ctx, fz_document *doc, int number, region *regions, int n)
{
leak_dev *d = fz_new_derived_device(ctx, leak_dev);
fz_page *page = NULL;
int leaks = 0;
fz_var(page);
d->super.fill_path = leak_fill_path;
d->super.stroke_path = leak_stroke_path;
d->super.fill_shade = leak_fill_shade;
d->super.fill_image = leak_fill_image;
d->super.fill_image_mask = leak_fill_image_mask;
d->regions = regions, d->n = n, d->page = number;
fz_try(ctx)
{
page = fz_load_page(ctx, doc, number);
fz_run_page(ctx, page, &d->super, fz_identity, NULL);
fz_close_device(ctx, &d->super);
leaks = d->leaks;
}
fz_always(ctx)
{
fz_drop_page(ctx, page);
fz_drop_device(ctx, &d->super);
}
fz_catch(ctx)
fz_rethrow(ctx);
return leaks;
}
typedef struct
{
region *regions;
int n;
} marked;
static void apply_marks(fz_context *ctx, fz_document *copy, void *arg)
{
marked *m = arg;
redact(ctx, ely_pdf_of(ctx, copy), m->regions, m->n);
}
/* Checks `out`: no text, no light in any region. */
static void check_redacted(fz_context *ctx, const char *out, const char *password, region *regions, int n, fz_buffer *report)
{
fz_document *doc = fz_open_document(ctx, out);
fz_try(ctx)
{
int i, text = 0, light = 0, objects = 0, versions;
if (fz_needs_password(ctx, doc) && !fz_authenticate_password(ctx, doc, password))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the written file refused its password");
for (i = 0; i < n; i++)
{
text += chars_in(ctx, doc, regions[i].page, regions[i].rect);
light += light_in(ctx, doc, regions[i].page, regions[i].rect);
objects += pdf_dict_get(ctx, pdf_lookup_page_obj(ctx, ely_pdf_of(ctx, doc), regions[i].page), PDF_NAME(Thumb)) != NULL;
}
for (i = 0; i < fz_count_pages(ctx, doc); i++)
objects += leaks_on(ctx, doc, i, regions, n);
versions = pdf_count_versions(ctx, ely_pdf_of(ctx, doc));
fz_append_printf(ctx, report, "{\"regions\":%d,\"text\":%d,\"light\":%d,\"objects\":%d,\"versions\":%d}", n, text,
light, objects, versions);
if (text || light || objects || versions != 1)
fz_throw(ctx, FZ_ERROR_FORMAT, "the written file still holds what was removed: %d characters, %d light pixels, %d drawings, %d versions",
text, light, objects, versions);
}
fz_always(ctx)
fz_drop_document(ctx, doc);
fz_catch(ctx)
fz_rethrow(ctx);
}
/* Removes the marked regions into `out`, then checks it. */
int ely_redact(fz_context *ctx, fz_document *doc, const char *json, const char *raw, const char *out,
const char *password, unsigned char **data, size_t *len)
{
fz_pool *pool = NULL;
marked m = { NULL, 0 };
fz_buffer *report = NULL;
fz_var(pool);
fz_var(m);
fz_var(report);
fz_try(ctx)
{
pdf_document *pdf = ely_pdf_of(ctx, doc);
pool = fz_new_pool(ctx);
m.regions = regions_of(ctx, fz_parse_json(ctx, pool, json), pdf_count_pages(ctx, pdf), &m.n);
ely_save_copy(ctx, pdf, raw, out, password, NULL, apply_marks, &m);
report = fz_new_buffer(ctx, 128);
check_redacted(ctx, out, password, m.regions, m.n, report);
*len = fz_buffer_extract(ctx, report, data);
}
fz_always(ctx)
{
fz_drop_buffer(ctx, report);
fz_free(ctx, m.regions);
fz_drop_pool(ctx, pool);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
+74
View File
@@ -0,0 +1,74 @@
/* Permissions and protected copies (PDF-11). */
#include <string.h>
#include "ely_shim.h"
enum { MAY_PRINT = 1, MAY_COPY = 2, MAY_EDIT = 4, MAY_ANNOTATE = 8, MAY_FILL = 16, MAY_ASSEMBLE = 32 };
/* What the document allows, as Ely's bits. */
int ely_permissions(fz_context *ctx, fz_document *doc)
{
int may = 0;
fz_try(ctx)
{
pdf_document *pdf = ely_pdf_of(ctx, doc);
int edit = pdf_has_permission(ctx, pdf, FZ_PERMISSION_EDIT) != 0;
int annotate = pdf_has_permission(ctx, pdf, FZ_PERMISSION_ANNOTATE) != 0;
may |= pdf_has_permission(ctx, pdf, FZ_PERMISSION_PRINT) ? MAY_PRINT : 0;
may |= pdf_has_permission(ctx, pdf, FZ_PERMISSION_COPY) ? MAY_COPY : 0;
may |= edit ? MAY_EDIT : 0;
may |= annotate ? MAY_ANNOTATE : 0;
may |= annotate || pdf_has_permission(ctx, pdf, FZ_PERMISSION_FORM) ? MAY_FILL : 0;
may |= edit || pdf_has_permission(ctx, pdf, FZ_PERMISSION_ASSEMBLE) ? MAY_ASSEMBLE : 0;
}
fz_catch(ctx)
return ely_fail(ctx);
return may;
}
/* PDF's permission bits for Ely's `may`. */
static int pdf_bits(int may)
{
int p = ~(PDF_PERM_PRINT | PDF_PERM_MODIFY | PDF_PERM_COPY | PDF_PERM_ANNOTATE | PDF_PERM_FORM |
PDF_PERM_ASSEMBLE | PDF_PERM_PRINT_HQ | 3);
p |= may & MAY_PRINT ? PDF_PERM_PRINT | PDF_PERM_PRINT_HQ : 0;
p |= may & MAY_COPY ? PDF_PERM_COPY : 0;
p |= may & MAY_EDIT ? PDF_PERM_MODIFY : 0;
p |= may & MAY_ANNOTATE ? PDF_PERM_ANNOTATE : 0;
p |= may & MAY_FILL ? PDF_PERM_FORM : 0;
p |= may & MAY_ASSEMBLE ? PDF_PERM_ASSEMBLE : 0;
return p;
}
static void put_password(fz_context *ctx, char *into, const char *password)
{
if (strlen(password) >= 128)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a password longer than 127 bytes");
strcpy(into, password);
}
/* Writes `out` with AES-256 and `may`, or unprotected. */
int ely_protect(fz_context *ctx, fz_document *doc, const char *raw, const char *out, const char *password,
int protect, const char *user, const char *owner, int may)
{
fz_try(ctx)
{
pdf_write_options opts = pdf_default_write_options;
opts.do_encrypt = protect ? PDF_ENCRYPT_AES_256 : PDF_ENCRYPT_NONE;
if (protect)
{
if (!*owner)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "an owner password is needed");
if (!strcmp(user, owner))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the owner password equals the open password");
put_password(ctx, opts.upwd_utf8, user);
put_password(ctx, opts.opwd_utf8, owner);
opts.permissions = pdf_bits(may);
}
ely_save_copy(ctx, ely_pdf_of(ctx, doc), raw, out, password, &opts, NULL, NULL);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
+56
View File
@@ -0,0 +1,56 @@
/* What the shim files share. */
#ifndef ELY_SHIM_H
#define ELY_SHIM_H
#include <mupdf/fitz.h>
#include <mupdf/pdf.h>
int ely_fail(fz_context *ctx);
const char *ely_error(void);
int ely_render(fz_context *ctx, fz_document *doc, int number, float scale, int rotate, const int tile[4], int dark,
unsigned char *bgra);
void ely_fail_also(fz_context *ctx, const char *what);
pdf_document *ely_pdf_of(fz_context *ctx, fz_document *doc);
int ely_edit_failed(fz_context *ctx, pdf_document *pdf, int begun);
void ely_forget_resources(fz_context *ctx, pdf_document *pdf);
pdf_obj *ely_target(fz_context *ctx, pdf_document *pdf, pdf_obj *obj);
typedef void (ely_change)(fz_context *ctx, fz_document *copy, void *arg);
void ely_save_copy(fz_context *ctx, pdf_document *pdf, const char *raw, const char *out, const char *password,
const pdf_write_options *opts,
ely_change *change, void *arg);
/* A text block as read, page space, and its glyphs. */
typedef struct
{
void *glyphs;
fz_font *font;
fz_point first;
float size;
int rgb;
fz_rect bbox;
int lines;
float gap;
} ely_shape;
fz_matrix ely_to_user(fz_context *ctx, pdf_page *page);
void ely_read_block(fz_context *ctx, fz_document *doc, pdf_page *page, int number, int index, const char *expected,
ely_shape *shape);
void ely_remove_block(fz_context *ctx, pdf_document *pdf, pdf_page *page, ely_shape *shape);
void ely_drop_shape(fz_context *ctx, ely_shape *shape);
pdf_obj *ely_append_content(fz_context *ctx, pdf_document *pdf, pdf_obj *page, fz_buffer *buf);
const char *ely_add_resource(fz_context *ctx, pdf_document *pdf, pdf_obj *page, pdf_obj *type, const char *stem,
pdf_obj *ref, char *name, size_t room);
void ely_json_string(fz_context *ctx, fz_output *out, const char *s);
fz_json *ely_member(fz_context *ctx, fz_json *obj, const char *key, int type);
float ely_number(fz_context *ctx, fz_json *array, int at);
fz_rect ely_rect(fz_context *ctx, fz_json *array, float least);
pdf_annot *ely_find_annot(fz_context *ctx, pdf_page *page, int id, int widget);
void ely_write_fields(fz_context *ctx, fz_output *out, pdf_page *page);
void ely_unlink_field(fz_context *ctx, pdf_document *pdf, pdf_obj *field);
void ely_unsign(fz_context *ctx, pdf_document *pdf);
void ely_watch_begin(fz_context *ctx);
int ely_watch_end(fz_context *ctx);
const char *ely_open_warnings(void);
void ely_drop_alt(fz_context *ctx, pdf_document *pdf, pdf_page *page);
#endif
+409
View File
@@ -0,0 +1,409 @@
/* Signatures: read and checked (PDF-12, QA-15). */
#include <limits.h>
#include <string.h>
#include <openssl/pkcs7.h>
#include <openssl/x509.h>
#include <mupdf/helpers/pkcs7-openssl.h>
#include "ely_shim.h"
static const char *digest_name(pdf_signature_error error)
{
switch (error)
{
case PDF_SIGNATURE_ERROR_OKAY: return "ok";
case PDF_SIGNATURE_ERROR_DIGEST_FAILURE: return "changed";
case PDF_SIGNATURE_ERROR_NO_CERTIFICATE: return "no_certificate";
default: return "unreadable";
}
}
static void put_hex(fz_context *ctx, fz_output *out, const unsigned char *data, int len)
{
int i;
fz_write_byte(ctx, out, '"');
for (i = 0; i < len; i++)
fz_write_printf(ctx, out, "%02x", data[i]);
fz_write_byte(ctx, out, '"');
}
static void put_cert(fz_context *ctx, fz_output *out, X509 *cert, int *first)
{
unsigned char *der = NULL;
int len = i2d_X509(cert, &der);
if (len <= 0)
fz_throw(ctx, FZ_ERROR_FORMAT, "a certificate that will not encode");
fz_try(ctx)
{
if (!*first)
fz_write_byte(ctx, out, ',');
*first = 0;
put_hex(ctx, out, der, len);
}
fz_always(ctx)
OPENSSL_free(der);
fz_catch(ctx)
fz_rethrow(ctx);
}
/* The signer's certificate first, then the rest it carries. */
static void put_certs(fz_context *ctx, fz_output *out, const char *contents, size_t len)
{
const unsigned char *at = (const unsigned char *)contents;
PKCS7 *p7 = len < INT_MAX ? d2i_PKCS7(NULL, &at, (long)len) : NULL;
STACK_OF(X509) *signers = NULL, *carried;
int i, first = 1;
if (!p7 || !PKCS7_type_is_signed(p7))
{
PKCS7_free(p7);
fz_throw(ctx, FZ_ERROR_FORMAT, "a signature that is not PKCS #7 signed data");
}
fz_try(ctx)
{
carried = p7->d.sign->cert;
signers = PKCS7_get0_signers(p7, NULL, 0);
fz_write_string(ctx, out, "[");
for (i = 0; i < sk_X509_num(signers); i++)
put_cert(ctx, out, sk_X509_value(signers, i), &first);
for (i = 0; i < sk_X509_num(carried); i++)
if (sk_X509_find(signers, sk_X509_value(carried, i)) < 0)
put_cert(ctx, out, sk_X509_value(carried, i), &first);
fz_write_string(ctx, out, "]");
}
fz_always(ctx)
{
sk_X509_free(signers);
PKCS7_free(p7);
}
fz_catch(ctx)
fz_rethrow(ctx);
}
static void put_text(fz_context *ctx, fz_output *out, const char *key, pdf_obj *value)
{
fz_write_printf(ctx, out, ",\"%s\":", key);
ely_json_string(ctx, out, pdf_is_string(ctx, value) ? pdf_to_text_string(ctx, value) : "");
}
static void put_signed(fz_context *ctx, fz_output *out, pdf_document *pdf, pdf_obj *field, pdf_pkcs7_verifier *verifier)
{
pdf_obj *v = pdf_dict_get_inheritable(ctx, field, PDF_NAME(V));
pdf_pkcs7_distinguished_name *dn = pdf_signature_get_signatory(ctx, verifier, pdf, field);
char *signer = NULL, *contents = NULL;
size_t len;
fz_var(signer);
fz_var(contents);
fz_try(ctx)
{
signer = dn ? pdf_signature_format_distinguished_name(ctx, dn) : NULL;
fz_write_printf(ctx, out, ",\"digest\":\"%s\"", digest_name(pdf_check_digest(ctx, verifier, pdf, field)));
fz_write_printf(ctx, out, ",\"later\":%d", pdf_signature_incremental_change_since_signing(ctx, pdf, field));
fz_write_printf(ctx, out, ",\"broken\":%d", pdf_validate_signature(ctx, pdf, field));
fz_write_string(ctx, out, ",\"signer\":");
ely_json_string(ctx, out, signer ? signer : "");
fz_write_string(ctx, out, ",\"common\":");
ely_json_string(ctx, out, dn && dn->cn ? dn->cn : "");
put_text(ctx, out, "date", pdf_dict_get(ctx, v, PDF_NAME(M)));
put_text(ctx, out, "reason", pdf_dict_get(ctx, v, PDF_NAME(Reason)));
put_text(ctx, out, "location", pdf_dict_get(ctx, v, PDF_NAME(Location)));
len = pdf_signature_contents(ctx, pdf, field, &contents);
fz_write_string(ctx, out, ",\"certs\":");
put_certs(ctx, out, contents, len);
}
fz_always(ctx)
{
fz_free(ctx, contents);
fz_free(ctx, signer);
pdf_signature_drop_distinguished_name(ctx, dn);
}
fz_catch(ctx)
fz_rethrow(ctx);
}
/* A broken signature reports why; the others still list. */
static void put_checked(fz_context *ctx, fz_output *out, pdf_document *pdf, pdf_obj *field, pdf_pkcs7_verifier *verifier)
{
fz_buffer *buf = fz_new_buffer(ctx, 256);
fz_output *part = NULL;
char why[256] = "";
fz_var(part);
fz_try(ctx)
{
part = fz_new_output_with_buffer(ctx, buf);
put_signed(ctx, part, pdf, field, verifier);
fz_close_output(ctx, part);
}
fz_always(ctx)
fz_drop_output(ctx, part);
fz_catch(ctx)
{
fz_strlcpy(why, fz_caught_message(ctx), sizeof why);
fz_ignore_error(ctx);
}
fz_try(ctx)
{
if (*why)
{
fz_write_string(ctx, out, ",\"digest\":\"unreadable\",\"error\":");
ely_json_string(ctx, out, why);
}
else
fz_write_buffer(ctx, out, buf);
}
fz_always(ctx)
fz_drop_buffer(ctx, buf);
fz_catch(ctx)
fz_rethrow(ctx);
}
/* A widget's page and page-space box; -1 for none. */
static int placed(fz_context *ctx, pdf_document *pdf, pdf_obj *field, fz_rect *box)
{
pdf_obj *widget = pdf_name_eq(ctx, pdf_dict_get(ctx, field, PDF_NAME(Subtype)), PDF_NAME(Widget)) ? field :
pdf_array_get(ctx, pdf_dict_get(ctx, field, PDF_NAME(Kids)), 0);
pdf_obj *page = pdf_dict_get(ctx, widget, PDF_NAME(P));
fz_matrix ctm;
int i, k, number = page ? pdf_lookup_page_number(ctx, pdf, page) : -1;
/* /P is optional; the page's /Annots tell. */
for (i = 0; number < 0 && i < pdf_count_pages(ctx, pdf); i++)
{
pdf_obj *annots = pdf_dict_get(ctx, pdf_lookup_page_obj(ctx, pdf, i), PDF_NAME(Annots));
for (k = 0; k < pdf_array_len(ctx, annots); k++)
if (pdf_to_num(ctx, pdf_array_get(ctx, annots, k)) == pdf_to_num(ctx, widget))
number = i, page = pdf_lookup_page_obj(ctx, pdf, i);
}
*box = fz_empty_rect;
if (number >= 0)
{
pdf_page_obj_transform(ctx, page, NULL, &ctm);
*box = fz_transform_rect(pdf_dict_get_rect(ctx, widget, PDF_NAME(Rect)), ctm);
}
return number;
}
static void put_field(fz_context *ctx, fz_output *out, pdf_document *pdf, pdf_obj *field, pdf_pkcs7_verifier *verifier, int *first)
{
fz_rect r;
int number = placed(ctx, pdf, field, &r), signd = pdf_signature_is_signed(ctx, pdf, field);
char *name = pdf_load_field_name(ctx, field);
fz_try(ctx)
{
fz_write_printf(ctx, out, "%s{\"page\":%d,\"object\":%d,\"rect\":[%g,%g,%g,%g],\"signed\":%s,\"name\":",
*first ? "" : ",", number, pdf_to_num(ctx, field), r.x0, r.y0, r.x1, r.y1, signd ? "true" : "false");
*first = 0;
ely_json_string(ctx, out, name);
if (signd)
put_checked(ctx, out, pdf, field, verifier);
fz_write_byte(ctx, out, '}');
}
fz_always(ctx)
fz_free(ctx, name);
fz_catch(ctx)
fz_rethrow(ctx);
}
/* Signature fields in the form tree, placed or not. */
static void put_tree(fz_context *ctx, fz_output *out, pdf_document *pdf, pdf_obj *node, pdf_pkcs7_verifier *verifier, int *first)
{
pdf_obj *kids = pdf_dict_get(ctx, node, PDF_NAME(Kids));
int i, leaf = 1;
if (pdf_mark_obj(ctx, node))
return;
fz_try(ctx)
{
for (i = 0; i < pdf_array_len(ctx, kids); i++)
leaf &= pdf_dict_get(ctx, pdf_array_get(ctx, kids, i), PDF_NAME(T)) == NULL;
if (!leaf)
for (i = 0; i < pdf_array_len(ctx, kids); i++)
put_tree(ctx, out, pdf, pdf_array_get(ctx, kids, i), verifier, first);
else if (pdf_name_eq(ctx, pdf_dict_get_inheritable(ctx, node, PDF_NAME(FT)), PDF_NAME(Sig)))
put_field(ctx, out, pdf, node, verifier, first);
}
fz_always(ctx)
pdf_unmark_obj(ctx, node);
fz_catch(ctx)
fz_rethrow(ctx);
}
/* Every signature field, as JSON, with its check. */
int ely_signatures(fz_context *ctx, fz_document *doc, unsigned char **data, size_t *len)
{
pdf_pkcs7_verifier *verifier = NULL;
fz_buffer *buf = NULL;
fz_output *out = NULL;
fz_var(verifier);
fz_var(buf);
fz_var(out);
fz_try(ctx)
{
pdf_document *pdf = ely_pdf_of(ctx, doc);
pdf_obj *fields = pdf_dict_getp(ctx, pdf_trailer(ctx, pdf), "Root/AcroForm/Fields");
int i, first = 1;
verifier = pkcs7_openssl_new_verifier(ctx);
buf = fz_new_buffer(ctx, 256);
out = fz_new_output_with_buffer(ctx, buf);
fz_write_byte(ctx, out, '[');
for (i = 0; i < pdf_array_len(ctx, fields); i++)
put_tree(ctx, out, pdf, pdf_array_get(ctx, fields, i), verifier, &first);
fz_write_byte(ctx, out, ']');
fz_close_output(ctx, out);
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
{
fz_drop_output(ctx, out);
fz_drop_buffer(ctx, buf);
pdf_drop_verifier(ctx, verifier);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* An empty field by number, or a new one. */
static pdf_annot *sign_field(fz_context *ctx, pdf_document *pdf, pdf_page *page, int object, const float *rect, const char *name)
{
pdf_obj *form = pdf_dict_getp(ctx, pdf_trailer(ctx, pdf), "Root/AcroForm/Fields");
pdf_annot *widget;
if (object >= 0)
{
widget = ely_find_annot(ctx, page, object, 1);
if (pdf_widget_type(ctx, widget) != PDF_WIDGET_TYPE_SIGNATURE)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "field %d is not a signature field", object);
if (pdf_signature_is_signed(ctx, pdf, pdf_annot_obj(ctx, widget)))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "field %d is signed already", object);
return widget;
}
if (!*name || strchr(name, '.') || (form && pdf_lookup_field(ctx, form, name)))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "a signature field needs a new name without dots");
widget = pdf_create_signature_widget(ctx, page, (char *)name);
pdf_set_annot_rect(ctx, widget, fz_make_rect(rect[0], rect[1], rect[2], rect[3]));
return widget;
}
/* Signs into `raw` by an update, then checks it. */
int ely_sign(fz_context *ctx, fz_document *doc, const char *raw, const char *password, const char *pfx, const char *pfx_password,
int number, int object, const float *rect, const char *name, const char *reason, const char *location)
{
pdf_pkcs7_signer *signer = NULL;
pdf_pkcs7_verifier *verifier = NULL;
fz_document *copy = NULL, *signd = NULL;
pdf_page *page = NULL;
pdf_obj *made = NULL;
fz_var(made);
fz_var(signer);
fz_var(verifier);
fz_var(copy);
fz_var(signd);
fz_var(page);
fz_try(ctx)
{
pdf_write_options update = pdf_default_write_options;
pdf_document *pdf;
pdf_annot *widget;
int field;
signer = pkcs7_openssl_read_pfx(ctx, pfx, pfx_password);
pdf_save_snapshot(ctx, ely_pdf_of(ctx, doc), raw);
copy = fz_open_document(ctx, raw);
if (fz_needs_password(ctx, copy) && !fz_authenticate_password(ctx, copy, password))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the copy refused its password");
pdf = ely_pdf_of(ctx, copy);
page = pdf_load_page(ctx, pdf, number);
widget = sign_field(ctx, pdf, page, object, rect, name);
field = pdf_to_num(ctx, pdf_annot_obj(ctx, widget));
pdf_sign_signature(ctx, widget, signer, PDF_SIGNATURE_SHOW_LABELS | PDF_SIGNATURE_SHOW_DATE | PDF_SIGNATURE_SHOW_TEXT_NAME, NULL, reason, location);
/* Earlier signatures cover earlier bytes; only append. */
update.do_incremental = 1;
pdf_save_document(ctx, pdf, raw, &update);
fz_drop_page(ctx, (fz_page *)page);
page = NULL;
signd = fz_open_document(ctx, raw);
if (fz_needs_password(ctx, signd) && !fz_authenticate_password(ctx, signd, password))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the signed copy refused its password");
verifier = pkcs7_openssl_new_verifier(ctx);
made = pdf_new_indirect(ctx, ely_pdf_of(ctx, signd), field, 0);
if (pdf_check_digest(ctx, verifier, ely_pdf_of(ctx, signd), made) != PDF_SIGNATURE_ERROR_OKAY)
fz_throw(ctx, FZ_ERROR_FORMAT, "the new signature does not check");
}
fz_always(ctx)
{
pdf_drop_obj(ctx, made);
fz_drop_page(ctx, (fz_page *)page);
fz_drop_document(ctx, signd);
fz_drop_document(ctx, copy);
pdf_drop_verifier(ctx, verifier);
pdf_drop_signer(ctx, signer);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* Signed fields under `field`; with `drop`, their values go. */
static int signed_under(fz_context *ctx, pdf_obj *field, int drop)
{
pdf_obj *kids = pdf_dict_get(ctx, field, PDF_NAME(Kids));
int i, n = 0;
if (pdf_mark_obj(ctx, field))
return 0;
fz_try(ctx)
{
if (pdf_name_eq(ctx, pdf_dict_get_inheritable(ctx, field, PDF_NAME(FT)), PDF_NAME(Sig)) &&
pdf_is_dict(ctx, pdf_dict_get(ctx, field, PDF_NAME(V))))
{
n++;
if (drop)
{
/* The drawn signature would still claim it. */
pdf_dict_del(ctx, field, PDF_NAME(V));
pdf_dict_del(ctx, field, PDF_NAME(AP));
/* Its own lock went with it; it can sign again. */
pdf_dict_put_int(ctx, field, PDF_NAME(Ff), pdf_dict_get_int(ctx, field, PDF_NAME(Ff)) & ~PDF_FIELD_IS_READ_ONLY);
for (i = 0; i < pdf_array_len(ctx, kids); i++)
pdf_dict_del(ctx, pdf_array_get(ctx, kids, i), PDF_NAME(AP));
}
}
for (i = 0; i < pdf_array_len(ctx, kids); i++)
n += signed_under(ctx, pdf_array_get(ctx, kids, i), drop);
}
fz_always(ctx)
pdf_unmark_obj(ctx, field);
fz_catch(ctx)
fz_rethrow(ctx);
return n;
}
/* SigFlags bit 2; MuPDF keeps its name private. */
enum { APPEND_ONLY = 2 };
static int signed_fields(fz_context *ctx, pdf_document *pdf, int drop)
{
pdf_obj *form = pdf_dict_getp(ctx, pdf_trailer(ctx, pdf), "Root/AcroForm");
pdf_obj *fields = pdf_dict_get(ctx, form, PDF_NAME(Fields));
int i, n = 0;
for (i = 0; i < pdf_array_len(ctx, fields); i++)
n += signed_under(ctx, pdf_array_get(ctx, fields, i), drop);
if (drop && n)
pdf_dict_put_int(ctx, form, PDF_NAME(SigFlags), pdf_dict_get_int(ctx, form, PDF_NAME(SigFlags)) & ~APPEND_ONLY);
return n;
}
/* Ely's whole-file writes break signatures, so carry none. */
void ely_unsign(fz_context *ctx, pdf_document *pdf)
{
signed_fields(ctx, pdf, 1);
}
/* How many signed fields the form holds. */
int ely_signed(fz_context *ctx, fz_document *doc)
{
int n = 0;
fz_try(ctx)
n = signed_fields(ctx, ely_pdf_of(ctx, doc), 0);
fz_catch(ctx)
return ely_fail(ctx);
return n;
}
+294
View File
@@ -0,0 +1,294 @@
/* Text blocks: list, take out glyphs, write new text (PDF-03). */
#include <math.h>
#include <string.h>
#include "ely_shim.h"
/* The page's text, read the same way every time. */
static fz_stext_page *read_text(fz_context *ctx, fz_document *doc, int number)
{
fz_stext_options opts = { 0 };
return fz_new_stext_page_from_page_number(ctx, doc, number, &opts);
}
/* The `index`th text block, or NULL. */
static fz_stext_block *nth_block(fz_stext_page *text, int index)
{
fz_stext_block *block;
for (block = text->first_block; block; block = block->next)
if (block->type == FZ_STEXT_BLOCK_TEXT && index-- == 0)
return block;
return NULL;
}
/* Glyph origins to take out, in user space. */
typedef struct
{
fz_point *at;
int *c;
int n;
int hit;
} targets;
/* The glyphs of `block` with their user-space origins. */
static void block_targets(fz_context *ctx, fz_stext_block *block, fz_matrix to_user, targets *t)
{
fz_stext_line *line;
fz_stext_char *ch;
int n = 0;
for (line = block->u.t.first_line; line; line = line->next)
for (ch = line->first_char; ch; ch = ch->next)
n++;
t->at = fz_malloc_array(ctx, n + 1, fz_point);
t->c = fz_malloc_array(ctx, n + 1, int);
t->n = t->hit = 0;
for (line = block->u.t.first_line; line; line = line->next)
for (ch = line->first_char; ch; ch = ch->next)
if (!(ch->flags & FZ_STEXT_SYNTHETIC))
{
t->at[t->n] = fz_transform_point(ch->origin, to_user);
t->c[t->n++] = ch->c;
}
}
/* Removes a glyph drawn at one of the targets. */
static int take_glyph(fz_context *ctx, void *opaque, int *ucs, int ucslen, fz_matrix trm, fz_matrix ctm,
fz_rect bbox, int tr, float ca, float CA)
{
targets *t = opaque;
fz_point at = fz_transform_point(fz_transform_point(fz_make_point(0, 0), trm), ctm);
int i, k;
for (i = 0; i < t->n; i++)
{
if (fabsf(at.x - t->at[i].x) > 0.5f || fabsf(at.y - t->at[i].y) > 0.5f)
continue;
for (k = 0; k < ucslen; k++)
if (ucs[k] == t->c[i] || t->c[i] == FZ_REPLACEMENT_CHARACTER)
{
t->hit++;
return 1;
}
}
return 0;
}
/* Runs the glyph filter over the page; `dry` changes nothing. */
static void filter_glyphs(fz_context *ctx, pdf_document *pdf, pdf_page *page, targets *t, int dry)
{
pdf_sanitize_filter_options sopts = { 0 };
pdf_filter_factory list[2] = { { pdf_new_sanitize_filter, NULL }, { NULL, NULL } };
pdf_filter_options opts = { 0 };
sopts.opaque = t;
sopts.text_filter = take_glyph;
list[0].options = &sopts;
opts.filters = list;
opts.no_update = dry;
t->hit = 0;
pdf_filter_page_contents(ctx, pdf, page, &opts);
}
/* User space from the page space MuPDF reports in. */
fz_matrix ely_to_user(fz_context *ctx, pdf_page *page)
{
fz_matrix ctm;
pdf_page_transform(ctx, page, NULL, &ctm);
return fz_invert_matrix(ctm);
}
/* Whether all of `block` sits in the page's own content. */
static int reachable(fz_context *ctx, pdf_document *pdf, pdf_page *page, fz_stext_block *block)
{
targets t = { NULL, NULL, 0, 0 };
int ok = 0;
fz_var(t.at);
fz_var(t.c);
pdf_begin_operation(ctx, pdf, "Look at text");
fz_try(ctx)
{
block_targets(ctx, block, ely_to_user(ctx, page), &t);
filter_glyphs(ctx, pdf, page, &t, 1);
ok = t.n > 0 && t.hit == t.n;
}
fz_always(ctx)
{
pdf_abandon_operation(ctx, pdf);
ely_forget_resources(ctx, pdf);
fz_free(ctx, t.at);
fz_free(ctx, t.c);
}
fz_catch(ctx)
fz_rethrow(ctx);
return ok;
}
/* The text of `block`, lines on lines. */
static void block_text(fz_context *ctx, fz_buffer *buf, fz_stext_block *block)
{
fz_stext_line *line;
fz_stext_char *ch;
for (line = block->u.t.first_line; line; line = line->next)
{
if (line != block->u.t.first_line)
fz_append_byte(ctx, buf, '\n');
for (ch = line->first_char; ch; ch = ch->next)
fz_append_rune(ctx, buf, ch->c);
}
}
/* Horizontal lines only; turned text is not edited. */
static int upright(fz_stext_block *block)
{
fz_stext_line *line;
for (line = block->u.t.first_line; line; line = line->next)
if (line->wmode || fabsf(line->dir.x - 1) > 0.01f)
return 0;
return 1;
}
/* JSON of the page's text blocks. */
int ely_text_blocks(fz_context *ctx, fz_document *doc, int number, unsigned char **data, size_t *len)
{
fz_stext_page *text = NULL;
pdf_page *page = NULL;
fz_buffer *buf = NULL, *words = NULL;
fz_output *out = NULL;
fz_var(text);
fz_var(page);
fz_var(buf);
fz_var(words);
fz_var(out);
fz_try(ctx)
{
pdf_document *pdf = ely_pdf_of(ctx, doc);
fz_stext_block *block;
int first = 1;
text = read_text(ctx, doc, number);
page = pdf_load_page(ctx, pdf, number);
buf = fz_new_buffer(ctx, 1024);
out = fz_new_output_with_buffer(ctx, buf);
fz_write_string(ctx, out, "[");
for (block = text->first_block; block; block = block->next)
{
fz_stext_char *ch;
int editable;
if (block->type != FZ_STEXT_BLOCK_TEXT)
continue;
ch = block->u.t.first_line->first_char;
editable = upright(block) && reachable(ctx, pdf, page, block);
fz_drop_buffer(ctx, words);
words = NULL;
words = fz_new_buffer(ctx, 256);
block_text(ctx, words, block);
fz_terminate_buffer(ctx, words);
fz_write_printf(ctx, out, "%s{\"rect\":[%g,%g,%g,%g],\"text\":%q,\"font\":%q,\"size\":%g,\"color\":%d,\"editable\":%s}",
first ? "" : ",", block->bbox.x0, block->bbox.y0, block->bbox.x1, block->bbox.y1,
fz_string_from_buffer(ctx, words), fz_font_name(ctx, ch->font), ch->size, (int)(ch->argb & 0xffffff),
editable ? "true" : "false");
first = 0;
}
fz_write_string(ctx, out, "]");
fz_close_output(ctx, out);
*len = fz_buffer_extract(ctx, buf, data);
}
fz_always(ctx)
{
fz_drop_output(ctx, out);
fz_drop_buffer(ctx, buf);
fz_drop_buffer(ctx, words);
fz_drop_page(ctx, (fz_page *)page);
fz_drop_stext_page(ctx, text);
}
fz_catch(ctx)
return ely_fail(ctx);
return 0;
}
/* Distinct baselines, and the gap between the first two. */
static void baselines(fz_stext_block *block, ely_shape *shape)
{
fz_stext_line *line;
float last = block->u.t.first_line->first_char->origin.y;
shape->lines = 1;
shape->gap = 0;
for (line = block->u.t.first_line->next; line; line = line->next)
{
float y = line->first_char->origin.y;
if (fabsf(y - last) <= 0.5f)
continue;
if (shape->lines == 1)
shape->gap = y - shape->first.y;
shape->lines++;
last = y;
}
/* Runs on one baseline: an ordinary line gap. */
if (shape->gap <= 0)
shape->gap = shape->size * 1.2f;
}
/* Reads block `index` and checks it; nothing changes. */
void ely_read_block(fz_context *ctx, fz_document *doc, pdf_page *page, int number, int index, const char *expected,
ely_shape *shape)
{
pdf_document *pdf = ely_pdf_of(ctx, doc);
fz_stext_page *text = NULL;
fz_buffer *words = NULL;
targets *t = fz_malloc_struct(ctx, targets);
shape->glyphs = t;
fz_var(text);
fz_var(words);
fz_try(ctx)
{
fz_stext_block *block;
fz_stext_char *ch;
text = read_text(ctx, doc, number);
block = nth_block(text, index);
if (!block)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no text block %d", index);
words = fz_new_buffer(ctx, 256);
block_text(ctx, words, block);
fz_terminate_buffer(ctx, words);
if (strcmp(fz_string_from_buffer(ctx, words), expected))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the text changed since it was read");
if (!upright(block) || !reachable(ctx, pdf, page, block))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "this text is not in the page's own content");
block_targets(ctx, block, ely_to_user(ctx, page), t);
ch = block->u.t.first_line->first_char;
shape->font = fz_keep_font(ctx, ch->font);
shape->first = ch->origin;
shape->size = ch->size;
shape->rgb = (int)(ch->argb & 0xffffff);
shape->bbox = block->bbox;
baselines(block, shape);
}
fz_always(ctx)
{
fz_drop_buffer(ctx, words);
fz_drop_stext_page(ctx, text);
}
fz_catch(ctx)
fz_rethrow(ctx);
}
/* Takes out the glyphs `ely_read_block` found. */
void ely_remove_block(fz_context *ctx, pdf_document *pdf, pdf_page *page, ely_shape *shape)
{
targets *t = shape->glyphs;
filter_glyphs(ctx, pdf, page, t, 0);
if (t->hit != t->n)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "%d of %d glyphs were found", t->hit, t->n);
}
void ely_drop_shape(fz_context *ctx, ely_shape *shape)
{
targets *t = shape->glyphs;
fz_drop_font(ctx, shape->font);
if (t)
{
fz_free(ctx, t->at);
fz_free(ctx, t->c);
fz_free(ctx, t);
}
shape->font = NULL;
shape->glyphs = NULL;
}
+449
View File
@@ -0,0 +1,449 @@
/* New text on a page: a pen, lines, content (PDF-03). */
#include <math.h>
#include <string.h>
#include "ely_shim.h"
typedef enum
{
PEN_CID,
PEN_SIMPLE,
PEN_CJK
} pen_kind;
/* A font to write with and its resource. */
typedef struct
{
pen_kind kind;
fz_font *font;
int ordering;
pdf_obj *ref;
} pen;
/* What to write, where, in what. */
typedef struct
{
const char *text;
const char *font;
float size;
int rgb;
/* Page space: left, top, width. */
float box[3];
} text_args;
/* The script face for typed signatures, from the worker. */
static const unsigned char *script_data;
static size_t script_len;
void ely_script_font(const unsigned char *data, size_t len)
{
script_data = data;
script_len = len;
}
/* The pen named `choice`; "original" writes in `own`. */
static void choose_pen(fz_context *ctx, const char *choice, fz_font *own, pen *p)
{
static const struct { const char *choice, *name; } latin[] = {
{ "helvetica", "Helvetica" }, { "times", "Times-Roman" }, { "courier", "Courier" } };
static const struct { const char *choice; int ordering; } cjk[] = {
{ "cjk-sc", FZ_ADOBE_GB }, { "cjk-tc", FZ_ADOBE_CNS }, { "cjk-jp", FZ_ADOBE_JAPAN }, { "cjk-kr", FZ_ADOBE_KOREA } };
size_t i;
if (!strcmp(choice, "script"))
{
if (!script_data)
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no script font was given");
p->kind = PEN_CID;
p->font = fz_new_font_from_memory(ctx, "GreatVibes", script_data, (int)script_len, 0, 0);
return;
}
if (!strcmp(choice, "original"))
{
if (!own || !pdf_font_writing_supported(ctx, own))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the document's font cannot write new text");
p->kind = PEN_CID;
p->font = fz_keep_font(ctx, own);
return;
}
for (i = 0; i < nelem(latin); i++)
if (!strcmp(choice, latin[i].choice))
{
p->kind = PEN_SIMPLE;
p->font = fz_new_base14_font(ctx, latin[i].name);
return;
}
for (i = 0; i < nelem(cjk); i++)
if (!strcmp(choice, cjk[i].choice))
{
p->kind = PEN_CJK;
p->ordering = cjk[i].ordering;
p->font = fz_new_cjk_font(ctx, cjk[i].ordering);
return;
}
fz_throw(ctx, FZ_ERROR_ARGUMENT, "no font %s", choice);
}
/* The pen's font as a resource of `pdf`. */
static void add_pen(fz_context *ctx, pdf_document *pdf, pen *p)
{
switch (p->kind)
{
case PEN_CID:
p->ref = pdf_add_cid_font(ctx, pdf, p->font);
break;
case PEN_SIMPLE:
p->ref = pdf_add_simple_font(ctx, pdf, p->font, PDF_SIMPLE_ENCODING_LATIN);
break;
case PEN_CJK:
p->ref = pdf_add_cjk_font(ctx, pdf, p->font, p->ordering, 0, 0);
break;
}
}
/* The pen's glyph for `u`; 0 when it has none. */
static int glyph(fz_context *ctx, pen *p, int u)
{
if (p->kind == PEN_SIMPLE && fz_windows_1252_from_unicode(u) < 0)
return 0;
return fz_encode_character(ctx, p->font, u);
}
/* `u` as the pen's code, in hex. */
static void code(fz_context *ctx, fz_buffer *buf, pen *p, int u, int gid)
{
switch (p->kind)
{
case PEN_CID:
fz_append_printf(ctx, buf, "%04x", gid);
break;
case PEN_SIMPLE:
fz_append_printf(ctx, buf, "%02x", fz_windows_1252_from_unicode(u));
break;
case PEN_CJK:
if (u > 0xffff)
fz_append_printf(ctx, buf, "%04x%04x", 0xd800 + ((u - 0x10000) >> 10), 0xdc00 + ((u - 0x10000) & 0x3ff));
else
fz_append_printf(ctx, buf, "%04x", u);
break;
}
}
/* `text` as code points; the count is returned. */
static int runes(fz_context *ctx, const char *text, int **out)
{
int n = 0, c;
const char *s;
for (s = text; *s; n++)
s += fz_chartorune(&c, s);
*out = fz_malloc_array(ctx, n + 1, int);
for (n = 0, s = text; *s; n++)
s += fz_chartorune(&(*out)[n], s);
return n;
}
static float advance(fz_context *ctx, pen *p, int u, float size)
{
return fz_advance_glyph(ctx, p->font, glyph(ctx, p, u), 0) * size;
}
/* Lines no wider than `width`: [from, to) each. */
static int lay_out(fz_context *ctx, pen *p, const int *u, int n, float size, float width, int *from, int *to,
float *widest)
{
int i = 0, lines = 0;
*widest = 0;
while (i <= n)
{
int j, space = -1;
float run = 0, before = 0;
for (j = i; j < n && u[j] != '\n'; j++)
{
float w = advance(ctx, p, u[j], size);
if (run + w > width && j > i)
break;
if (u[j] == ' ')
space = j, before = run;
run += w;
}
from[lines] = i;
if (j < n && u[j] != '\n' && space > i)
{
/* Too long for the line: wrap at a space. */
to[lines++] = space;
*widest = fz_max(*widest, before);
i = space + 1;
}
else
{
to[lines++] = j;
*widest = fz_max(*widest, run);
i = j < n && u[j] != '\n' ? j : j + 1;
}
}
return lines;
}
/* A resource name in `type` that is free, given `ref`. */
const char *ely_add_resource(fz_context *ctx, pdf_document *pdf, pdf_obj *page, pdf_obj *type, const char *stem,
pdf_obj *ref, char *name, size_t room)
{
pdf_obj *res = pdf_dict_get_inheritable(ctx, page, PDF_NAME(Resources));
pdf_obj *dict;
int n;
if (!res)
res = pdf_dict_put_dict(ctx, page, PDF_NAME(Resources), 2);
dict = pdf_dict_get(ctx, res, type);
if (!dict)
dict = pdf_dict_put_dict(ctx, res, type, 2);
for (n = 1;; n++)
{
fz_snprintf(name, room, "%s%d", stem, n);
if (!pdf_dict_gets(ctx, dict, name))
break;
}
pdf_dict_puts(ctx, dict, name, ref);
return name;
}
/* The marker of a page whose own content Ely wrapped. */
static const char wrap[] = "q\n%Ely\n";
/* Adds `buf` after the page's content; the new stream. */
pdf_obj *ely_append_content(fz_context *ctx, pdf_document *pdf, pdf_obj *page, fz_buffer *buf)
{
pdf_obj *contents = pdf_dict_get(ctx, page, PDF_NAME(Contents));
pdf_obj *list, *first = pdf_is_array(ctx, contents) ? pdf_array_get(ctx, contents, 0) : NULL;
fz_buffer *head = NULL, *mark = first ? pdf_load_stream(ctx, first) : NULL;
pdf_obj *added = NULL;
int wrapped;
fz_var(head);
fz_try(ctx)
{
wrapped = mark && mark->len == strlen(wrap) && !memcmp(mark->data, wrap, mark->len);
if (wrapped)
list = contents;
else
{
list = pdf_new_array(ctx, pdf, 4);
head = fz_new_buffer_from_copied_data(ctx, (const unsigned char *)wrap, strlen(wrap));
pdf_array_push_drop(ctx, list, pdf_add_stream(ctx, pdf, head, NULL, 0));
if (pdf_is_array(ctx, contents))
{
int i;
for (i = 0; i < pdf_array_len(ctx, contents); i++)
pdf_array_push(ctx, list, pdf_array_get(ctx, contents, i));
}
else if (contents)
pdf_array_push(ctx, list, contents);
fz_drop_buffer(ctx, head);
head = fz_new_buffer_from_copied_data(ctx, (const unsigned char *)"Q\n", 2);
pdf_array_push_drop(ctx, list, pdf_add_stream(ctx, pdf, head, NULL, 0));
pdf_dict_put_drop(ctx, page, PDF_NAME(Contents), list);
}
added = pdf_add_stream(ctx, pdf, buf, NULL, 0);
pdf_array_push_drop(ctx, list, added);
}
fz_always(ctx)
{
fz_drop_buffer(ctx, head);
fz_drop_buffer(ctx, mark);
}
fz_catch(ctx)
fz_rethrow(ctx);
return added;
}
/* Text laid out: the pen, code points, lines, placement. */
typedef struct
{
pen p;
int *u, n;
int *from, *to, lines;
float widest;
fz_point first;
float gap;
} laid;
static void drop_laid(fz_context *ctx, laid *l)
{
fz_drop_font(ctx, l->p.font);
pdf_drop_obj(ctx, l->p.ref);
fz_free(ctx, l->u);
fz_free(ctx, l->from);
fz_free(ctx, l->to);
}
/* Lays `a` out from `first`, or names a missing glyph. */
static int lay_text(fz_context *ctx, text_args *a, fz_font *own, fz_point first, float gap, laid *l)
{
int i;
choose_pen(ctx, a->font, own, &l->p);
l->n = runes(ctx, a->text, &l->u);
for (i = 0; i < l->n; i++)
if (l->u[i] != '\n' && !glyph(ctx, &l->p, l->u[i]))
return l->u[i];
l->from = fz_malloc_array(ctx, l->n + 2, int);
l->to = fz_malloc_array(ctx, l->n + 2, int);
l->lines = lay_out(ctx, &l->p, l->u, l->n, a->size, a->box[2], l->from, l->to, &l->widest);
l->first = first;
l->gap = gap;
return 0;
}
/* The area laid text covers, in page space. */
static fz_rect laid_area(laid *l, float size)
{
return fz_make_rect(l->first.x, l->first.y - size, l->first.x + l->widest, l->first.y + (l->lines - 1) * l->gap + size * 0.3f);
}
/* Writes laid text onto `page` of `pdf`. */
static void draw_laid(fz_context *ctx, pdf_document *pdf, pdf_page *page, laid *l, text_args *a)
{
fz_buffer *buf = NULL;
fz_var(buf);
fz_try(ctx)
{
fz_matrix to_user = ely_to_user(ctx, page);
char name[32];
int i, line;
pdf_drop_obj(ctx, l->p.ref);
l->p.ref = NULL;
add_pen(ctx, pdf, &l->p);
ely_add_resource(ctx, pdf, page->obj, PDF_NAME(Font), "ElyF", l->p.ref, name, sizeof name);
buf = fz_new_buffer(ctx, 256);
fz_append_string(ctx, buf, "q\n");
for (line = 0; line < l->lines; line++)
{
fz_matrix tm = fz_concat(fz_make_matrix(1, 0, 0, -1, l->first.x, l->first.y + line * l->gap), to_user);
fz_append_printf(ctx, buf, "BT /%s %g Tf %g %g %g rg %g %g %g %g %g %g Tm <", name, a->size,
((a->rgb >> 16) & 255) / 255.0f, ((a->rgb >> 8) & 255) / 255.0f, (a->rgb & 255) / 255.0f,
tm.a, tm.b, tm.c, tm.d, tm.e, tm.f);
for (i = l->from[line]; i < l->to[line]; i++)
code(ctx, buf, &l->p, l->u[i], glyph(ctx, &l->p, l->u[i]));
fz_append_string(ctx, buf, "> Tj ET\n");
}
fz_append_string(ctx, buf, "Q\n");
ely_append_content(ctx, pdf, page->obj, buf);
}
fz_always(ctx)
fz_drop_buffer(ctx, buf);
fz_catch(ctx)
fz_rethrow(ctx);
}
/* Edits a snapshot copy and draws `region`; the document stays. */
static void preview(fz_context *ctx, pdf_document *pdf, int number, int block, const char *expected, text_args *a,
laid *l, const char *password, fz_rect region, float scale, unsigned char **pixels, int *w, int *h)
{
fz_buffer *buf = fz_new_buffer(ctx, 4096);
fz_output *out = NULL;
fz_document *copy = NULL;
pdf_page *page = NULL;
ely_shape shape = { NULL };
fz_var(out);
fz_var(copy);
fz_var(page);
fz_try(ctx)
{
int tile[4];
out = fz_new_output_with_buffer(ctx, buf);
pdf_write_snapshot(ctx, pdf, out);
fz_close_output(ctx, out);
copy = fz_open_document_with_buffer(ctx, "application/pdf", buf);
if (fz_needs_password(ctx, copy) && !fz_authenticate_password(ctx, copy, password))
fz_throw(ctx, FZ_ERROR_ARGUMENT, "the copy refused its password");
page = pdf_load_page(ctx, ely_pdf_of(ctx, copy), number);
if (block >= 0)
{
ely_read_block(ctx, copy, page, number, block, expected, &shape);
ely_remove_block(ctx, ely_pdf_of(ctx, copy), page, &shape);
}
draw_laid(ctx, ely_pdf_of(ctx, copy), page, l, a);
region = fz_expand_rect(region, 8);
tile[0] = (int)floorf(region.x0 * scale), tile[1] = (int)floorf(region.y0 * scale);
tile[2] = (int)ceilf(region.x1 * scale) - tile[0], tile[3] = (int)ceilf(region.y1 * scale) - tile[1];
*pixels = fz_malloc(ctx, (size_t)tile[2] * tile[3] * 4);
if (ely_render(ctx, copy, number, scale, 0, tile, 0, *pixels) < 0)
fz_throw(ctx, FZ_ERROR_GENERIC, "%s", ely_error());
*w = tile[2], *h = tile[3];
}
fz_always(ctx)
{
ely_drop_shape(ctx, &shape);
fz_drop_page(ctx, (fz_page *)page);
fz_drop_document(ctx, copy);
fz_drop_output(ctx, out);
fz_drop_buffer(ctx, buf);
}
fz_catch(ctx)
fz_rethrow(ctx);
}
/* Edits, adds or previews text: 0, 2 missing, -1 failed. */
int ely_put_text(fz_context *ctx, fz_document *doc, int number, int block, const char *expected, const char *text,
const char *font, float size, int rgb, const float box[3], const char *password, float scale, int *missing,
int *grew, unsigned char **pixels, int *w, int *h)
{
pdf_document *pdf = NULL;
pdf_page *page = NULL;
ely_shape shape = { NULL };
laid l = { { PEN_SIMPLE, NULL, 0, NULL } };
int begun = 0;
fz_var(pdf);
fz_var(page);
fz_var(begun);
*pixels = NULL;
*grew = 0;
fz_try(ctx)
{
text_args a = { text, font, size, rgb, { box[0], box[1], box[2] } };
fz_point first = fz_make_point(box[0], box[1] + size * 0.8f);
float gap = size * 1.2f;
pdf = ely_pdf_of(ctx, doc);
page = pdf_load_page(ctx, pdf, number);
/* Checks precede the step, so redo survives a refusal. */
if (block >= 0)
{
ely_read_block(ctx, doc, page, number, block, expected, &shape);
first = shape.first;
gap = shape.gap;
}
*missing = lay_text(ctx, &a, shape.font, first, gap, &l);
if (!*missing)
{
fz_rect drawn = laid_area(&l, size), region = drawn;
if (block >= 0)
{
fz_rect room = fz_expand_rect(shape.bbox, 1);
region = fz_union_rect(shape.bbox, drawn);
*grew = fabsf(size - shape.size) > 0.01f || !fz_contains_rect(room, drawn);
}
if (scale > 0)
preview(ctx, pdf, number, block, expected, &a, &l, password, region, scale, pixels, w, h);
else
{
pdf_begin_operation(ctx, pdf, block >= 0 ? "Edit text" : "Add text");
begun = 1;
if (block >= 0)
ely_remove_block(ctx, pdf, page, &shape);
draw_laid(ctx, pdf, page, &l, &a);
pdf_end_operation(ctx, pdf);
begun = 0;
}
}
}
fz_always(ctx)
{
drop_laid(ctx, &l);
ely_drop_shape(ctx, &shape);
fz_drop_page(ctx, (fz_page *)page);
}
fz_catch(ctx)
{
fz_free(ctx, *pixels);
*pixels = NULL;
return ely_edit_failed(ctx, pdf, begun);
}
return *missing ? 2 : 0;
}
@@ -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(&copy)?)?;
// 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(&copy)?)?;
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(&copy);
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(|_| ())
}
}
+302
View File
@@ -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),
}
}
+151
View File
@@ -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()
}
}
+84
View File
@@ -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(|_| ())
}
}
+26
View File
@@ -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};
+267
View File
@@ -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()
}
}
+91
View File
@@ -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"));
}
}
+60
View File
@@ -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()?),
})
}
}
+209
View File
@@ -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: ";
+105
View File
@@ -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())?)
}
}
+263
View File
@@ -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)
}
+40
View File
@@ -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(|_| ())
}
}
+148
View File
@@ -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(|_| ())
}
}
+211
View File
@@ -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()
}
}
+277
View File
@@ -0,0 +1,277 @@
//! Text and object edits against the real worker (PDF-03, PDF-04).
use std::path::Path;
use ely_pdf_engine::{
Doc, Worker, Writing, Wrote,
sample::{Extras, pdf_with},
};
fn worker() -> Worker {
Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).expect("the worker")
}
/// A Letter page: two lines, a stamp, a picture.
fn opened(dir: &Path) -> (Worker, Doc) {
let path = dir.join("doc.pdf");
let extras = Extras { stamp: Some("STAMP"), picture: true, ..Extras::default() };
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["Hello world", "Second line"])], &[], &extras)).expect("writes");
let mut worker = worker();
let doc = worker.open(&path).expect("opens").doc;
(worker, doc)
}
fn page_text(worker: &mut Worker, doc: Doc) -> String {
worker.text(doc, 0).expect("reads").iter().map(|c| c.c).collect()
}
/// Rectangles equal to a hundredth of a point.
fn near(got: [f32; 4], want: [f32; 4]) {
assert!(got.iter().zip(want).all(|(a, b)| (a - b).abs() < 0.01), "{got:?} is not {want:?}");
}
/// A rewrite of block `block` as `text` in `font`.
fn rewrite(worker: &mut Worker, doc: Doc, block: usize, text: &str, font: &str) -> Writing {
let read = worker.text_blocks(doc, 0).expect("blocks")[block].clone();
Writing {
block: Some(block),
expected: read.text,
text: text.into(),
font: font.into(),
size: read.size,
rgb: read.rgb,
bx: [read.rect[0], read.rect[1], read.rect[2] - read.rect[0]],
}
}
#[test]
fn blocks_say_what_they_hold_and_whether_they_edit() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(dir.path());
let blocks = worker.text_blocks(doc, 0).expect("blocks");
let hello = blocks.iter().find(|b| b.text.starts_with("Hello")).expect("the page text");
assert!(hello.editable && hello.size == 18.0, "{hello:?}");
let stamp = blocks.iter().find(|b| b.text == "STAMP").expect("the stamp text");
assert!(!stamp.editable, "text inside a form is not the page's own");
let drawn = worker.objects(doc, 0).expect("objects");
assert_eq!(drawn.iter().map(|d| d.form).collect::<Vec<_>>(), [true, false]);
assert_eq!(drawn[1].rect, [72.0, 242.0, 172.0, 292.0], "the picture, in page space");
}
#[test]
fn edited_text_replaces_the_old_glyphs_in_one_step() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(dir.path());
let before = worker.history(doc).expect("a mark");
let index = worker.text_blocks(doc, 0).expect("blocks").iter().position(|b| b.text.starts_with("Hello")).expect("hello");
let writing = rewrite(&mut worker, doc, index, "Hi world\nSecond line", "helvetica");
assert!(matches!(worker.put_text(doc, 0, &writing, 0.0).expect("writes"), Wrote::Done(_)));
doc = worker.now(doc).expect("the state");
let now = page_text(&mut worker, doc);
assert!(now.contains("Hi world") && !now.contains("Hello"), "{now}");
worker.undo(doc, false).expect("undoes");
doc = worker.now(doc).expect("the state");
assert_eq!(worker.history(doc).expect("a mark"), before);
assert!(page_text(&mut worker, doc).contains("Hello world"));
}
#[test]
fn a_font_without_the_glyph_changes_nothing() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(dir.path());
let before = worker.history(doc).expect("a mark");
let index = worker.text_blocks(doc, 0).expect("blocks").iter().position(|b| b.text.starts_with("Hello")).expect("hello");
let writing = rewrite(&mut worker, doc, index, "你好", "helvetica");
assert_eq!(worker.put_text(doc, 0, &writing, 0.0).expect("answers"), Wrote::Missing('你'));
doc = worker.now(doc).expect("the state");
assert_eq!(worker.history(doc).expect("a mark"), before);
assert!(page_text(&mut worker, doc).contains("Hello world"), "the old text stays");
let chinese = Writing { font: "cjk-sc".into(), ..writing };
assert!(matches!(worker.put_text(doc, 0, &chinese, 0.0).expect("writes"), Wrote::Done(_)));
doc = worker.now(doc).expect("the state");
assert!(page_text(&mut worker, doc).contains("你好"));
}
#[test]
fn a_preview_draws_the_change_and_keeps_none_of_it() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(dir.path());
let before = worker.history(doc).expect("a mark");
let index = worker.text_blocks(doc, 0).expect("blocks").iter().position(|b| b.text.starts_with("Hello")).expect("hello");
let mut writing = rewrite(&mut worker, doc, index, "A much longer line that will not fit the old box at all", "times");
writing.bx[2] = 150.0;
let Wrote::Preview(pixels, (w, h), grew) = worker.put_text(doc, 0, &writing, 2.0).expect("previews") else {
panic!("a preview");
};
doc = worker.now(doc).expect("the state");
assert!(grew, "more lines than before");
assert_eq!(pixels.len(), (w * h * 4) as usize);
assert!(pixels.chunks_exact(4).any(|p| p[0] < 128), "it draws text");
assert_eq!(worker.history(doc).expect("a mark"), before);
assert!(page_text(&mut worker, doc).contains("Hello world"));
}
#[test]
fn new_text_goes_where_asked() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(dir.path());
let writing = Writing {
block: None,
expected: String::new(),
text: "Added".into(),
font: "courier".into(),
size: 12.0,
rgb: 0xff0000,
bx: [100.0, 600.0, 300.0],
};
assert!(matches!(worker.put_text(doc, 0, &writing, 0.0).expect("writes"), Wrote::Done(_)));
doc = worker.now(doc).expect("the state");
let added = worker.text_blocks(doc, 0).expect("blocks").into_iter().find(|b| b.text == "Added").expect("added");
assert!((added.rect[0] - 100.0).abs() < 1.0 && (added.rect[1] - 600.0).abs() < 3.0, "{added:?}");
assert_eq!(added.rgb, 0xff0000);
}
#[test]
fn objects_move_scale_swap_and_go() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(dir.path());
worker.place(doc, 0, 1, [1.0, 0.0, 0.0, 1.0, 50.0, 20.0]).expect("moves");
doc = worker.now(doc).expect("the state");
near(worker.objects(doc, 0).expect("objects")[1].rect, [122.0, 262.0, 222.0, 312.0]);
// Twice the size about its top left.
worker.place(doc, 0, 1, [2.0, 0.0, 0.0, 2.0, -122.0, -262.0]).expect("scales");
doc = worker.now(doc).expect("the state");
near(worker.objects(doc, 0).expect("objects")[1].rect, [122.0, 262.0, 322.0, 362.0]);
let square = dir.path().join("square.pnm");
std::fs::write(&square, b"P6\n2 2\n255\n\xff\xff\xff\x00\x00\x00\x00\x00\x00\xff\xff\xff").expect("writes");
worker.replace_image(doc, 0, 1, &square).expect("replaces");
doc = worker.now(doc).expect("the state");
let swapped = worker.objects(doc, 0).expect("objects")[1].rect;
near(swapped, [172.0, 262.0, 272.0, 362.0]);
worker.delete_object(doc, 0, 0).expect("deletes the stamp");
doc = worker.now(doc).expect("the state");
assert!(!page_text(&mut worker, doc).contains("STAMP"));
assert_eq!(worker.objects(doc, 0).expect("objects").len(), 1);
worker.add_image(doc, 0, &square, [400.0, 100.0, 450.0, 150.0]).expect("adds");
doc = worker.now(doc).expect("the state");
near(worker.objects(doc, 0).expect("objects").last().expect("added").rect, [400.0, 100.0, 450.0, 150.0]);
for _ in 0..5 {
worker.undo(doc, false).expect("undoes");
doc = worker.now(doc).expect("the state");
}
near(worker.objects(doc, 0).expect("objects")[1].rect, [72.0, 242.0, 172.0, 292.0]);
assert!(page_text(&mut worker, doc).contains("STAMP"));
}
#[test]
fn the_documents_own_font_writes_and_edits_edit_again() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(dir.path());
let hello = |worker: &mut Worker, doc: Doc, start: &str| {
worker.text_blocks(doc, 0).expect("blocks").iter().position(|b| b.text.starts_with(start)).expect("the block")
};
let index = hello(&mut worker, doc, "Hello");
let first = rewrite(&mut worker, doc, index, "Hi world\nSecond line", "original");
assert!(matches!(worker.put_text(doc, 0, &first, 0.0).expect("writes"), Wrote::Done(_)));
doc = worker.now(doc).expect("the state");
let again = hello(&mut worker, doc, "Hi");
assert!(worker.text_blocks(doc, 0).expect("blocks")[again].editable, "new text is the page's own");
let second = rewrite(&mut worker, doc, again, "Yo world\nSecond line", "original");
assert!(matches!(worker.put_text(doc, 0, &second, 0.0).expect("writes again"), Wrote::Done(_)));
doc = worker.now(doc).expect("the state");
let now = page_text(&mut worker, doc);
assert!(now.contains("Yo world") && !now.contains("Hi world"), "{now}");
}
#[test]
fn a_font_added_in_a_preview_or_undone_is_added_again() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(dir.path());
let index = worker.text_blocks(doc, 0).expect("blocks").iter().position(|b| b.text.starts_with("Hello")).expect("hello");
let writing = rewrite(&mut worker, doc, index, "你好", "cjk-sc");
assert!(matches!(worker.put_text(doc, 0, &writing, 2.0).expect("previews"), Wrote::Preview(..)));
doc = worker.now(doc).expect("the state");
assert!(matches!(worker.put_text(doc, 0, &writing, 0.0).expect("writes"), Wrote::Done(_)));
doc = worker.now(doc).expect("the state");
worker.undo(doc, false).expect("undoes");
doc = worker.now(doc).expect("the state");
assert!(matches!(worker.put_text(doc, 0, &writing, 0.0).expect("writes again"), Wrote::Done(_)));
doc = worker.now(doc).expect("the state");
let out = dir.path().join("out.pdf");
worker.save(doc, &out).expect("saves");
let saved = worker.open(&out).expect("reopens").doc;
assert!(page_text(&mut worker, saved).contains("你好"), "the font is in the file");
}
#[test]
fn previews_and_refusals_keep_the_redo() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(dir.path());
let index = worker.text_blocks(doc, 0).expect("blocks").iter().position(|b| b.text.starts_with("Hello")).expect("hello");
let first = rewrite(&mut worker, doc, index, "Hi world\nSecond line", "helvetica");
assert!(matches!(worker.put_text(doc, 0, &first, 0.0).expect("writes"), Wrote::Done(_)));
doc = worker.now(doc).expect("the state");
worker.undo(doc, false).expect("undoes");
doc = worker.now(doc).expect("the state");
let preview = rewrite(&mut worker, doc, index, "Yo world\nSecond line", "times");
assert!(matches!(worker.put_text(doc, 0, &preview, 2.0).expect("previews"), Wrote::Preview(..)));
doc = worker.now(doc).expect("the state");
let refused = Writing { text: "你好".into(), font: "helvetica".into(), ..preview };
assert!(matches!(worker.put_text(doc, 0, &refused, 0.0).expect("answers"), Wrote::Missing(_)));
doc = worker.now(doc).expect("the state");
worker.undo(doc, true).expect("the edit still redoes");
doc = worker.now(doc).expect("the state");
assert!(page_text(&mut worker, doc).contains("Hi world"));
}
#[test]
fn bigger_glyphs_count_as_a_new_layout() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(dir.path());
let index = worker.text_blocks(doc, 0).expect("blocks").iter().position(|b| b.text.starts_with("Hello")).expect("hello");
let same = rewrite(&mut worker, doc, index, "Hello world\nSecond line", "original");
let Wrote::Preview(_, _, grew) = worker.put_text(doc, 0, &same, 2.0).expect("previews") else { panic!("a preview") };
doc = worker.now(doc).expect("the state");
assert!(!grew, "the same text in the same place");
let big = Writing { text: "Hi".into(), size: 72.0, ..same };
let Wrote::Preview(_, _, grew) = worker.put_text(doc, 0, &big, 2.0).expect("previews") else { panic!("a preview") };
assert!(grew, "fewer lines, much bigger glyphs");
}
#[test]
fn runs_on_one_baseline_still_wrap_onto_new_lines() {
let dir = tempfile::tempdir().expect("a temp dir");
let content = "BT /F1 18 Tf 1 0 0 1 72 720 Tm (Hello) Tj 1 0 0 1 160 720 Tm (world) Tj ET";
let objs = [
"<< /Type /Catalog /Pages 2 0 R >>".to_string(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_string(),
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>".to_string(),
format!("<< /Length {} >>\nstream\n{content}\nendstream", content.len()),
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_string(),
];
let mut bytes = b"%PDF-1.7\n".to_vec();
let mut offsets = Vec::new();
for (n, obj) in objs.iter().enumerate() {
offsets.push(bytes.len());
bytes.extend_from_slice(format!("{} 0 obj\n{obj}\nendobj\n", n + 1).as_bytes());
}
let xref = bytes.len();
bytes.extend_from_slice(format!("xref\n0 {}\n0000000000 65535 f \n", objs.len() + 1).as_bytes());
for offset in offsets {
bytes.extend_from_slice(format!("{offset:010} 00000 n \n").as_bytes());
}
bytes.extend_from_slice(format!("trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n", objs.len() + 1).as_bytes());
let path = dir.path().join("runs.pdf");
std::fs::write(&path, bytes).expect("writes");
let mut worker = worker();
let mut doc = worker.open(&path).expect("opens").doc;
let block = worker.text_blocks(doc, 0).expect("blocks")[0].clone();
let mut writing = rewrite(&mut worker, doc, 0, "One two three four five", "helvetica");
writing.expected = block.text;
writing.bx[2] = 50.0;
assert!(matches!(worker.put_text(doc, 0, &writing, 0.0).expect("writes"), Wrote::Done(_)));
doc = worker.now(doc).expect("the state");
let mut tops: Vec<i32> = worker.text(doc, 0).expect("reads").iter().map(|c| c.rect[1].round() as i32).collect();
tops.dedup();
assert!(tops.len() >= 3, "each wrapped line has its own baseline: {tops:?}");
}
+304
View File
@@ -0,0 +1,304 @@
//! Page edits, undo and saving against the real worker (PDF-02).
use std::path::Path;
use ely_pdf_engine::{
Doc, Worker,
sample::{Extras, pdf, pdf_with},
};
fn worker() -> Worker {
Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).expect("the worker")
}
fn open(worker: &mut Worker, dir: &Path, name: &str, bytes: &[u8]) -> Doc {
let path = dir.join(name);
std::fs::write(&path, bytes).expect("writes");
worker.open(&path).expect("opens").doc
}
/// Each page's text.
fn texts(worker: &mut Worker, doc: Doc) -> Vec<String> {
let count = worker.pages(doc).expect("counts");
(0..count)
.map(|page| worker.text(doc, page).expect("reads").iter().map(|c| c.c).collect())
.collect()
}
/// Bookmark titles with their pages.
fn marks(worker: &mut Worker, doc: Doc) -> Vec<(String, Option<usize>)> {
worker.outline(doc).expect("the outline").into_iter().map(|m| (m.title, m.page)).collect()
}
const LETTER: (f32, f32) = (612.0, 792.0);
fn three() -> Vec<u8> {
let pages: &[(f32, f32, &[&str])] = &[(612.0, 792.0, &["one"]), (612.0, 792.0, &["two"]), (612.0, 792.0, &["three"])];
let extras = Extras { links: &[(0, 1), (2, 1), (2, 0)], fields: &[1, 2], labels: &[(0, "r"), (1, "D")], ..Extras::default() };
pdf_with(pages, &[("One", 0, &[]), ("Two", 1, &[]), ("Three", 2, &[])], &extras)
}
#[test]
fn a_delete_drops_what_pointed_there_and_undoes() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut worker = worker();
let mut doc = open(&mut worker, dir.path(), "three.pdf", &three());
let before = worker.history(doc).expect("a mark");
let after = worker.delete_pages(doc, &[1]).expect("deletes");
doc = worker.now(doc).expect("the state");
assert_ne!(before, after);
assert_eq!(texts(&mut worker, doc), ["one", "three"]);
let pointing = marks(&mut worker, doc);
assert_eq!(pointing, [("One".into(), Some(0)), ("Two".into(), None), ("Three".into(), Some(1))]);
let out = dir.path().join("saved.pdf");
worker.save(doc, &out).expect("saves");
let saved = String::from_utf8_lossy(&std::fs::read(&out).expect("reads")).into_owned();
assert!(saved.contains("(three)") && !saved.contains("(two)"), "the deleted page is gone from the file");
assert_eq!(saved.matches("/Link").count(), 1, "only the link to page one stays");
assert!(saved.contains("field1") && !saved.contains("field0"), "the field on the deleted page goes");
let labels = saved.split("/Nums").nth(1).expect("labels").replace(char::is_whitespace, "");
assert!(labels.starts_with("[0<</S/r>>1<</S/D>>]"), "{labels}");
assert!(!dir.path().join("saved.pdf.raw").exists(), "the scratch copy goes");
assert_eq!(worker.undo(doc, false).expect("undoes"), before);
doc = worker.now(doc).expect("the state");
assert_eq!(texts(&mut worker, doc), ["one", "two", "three"]);
assert_eq!(marks(&mut worker, doc)[1], ("Two".into(), Some(1)));
assert_eq!(worker.undo(doc, true).expect("redoes"), after);
doc = worker.now(doc).expect("the state");
assert_eq!(worker.pages(doc).expect("counts"), 2);
}
#[test]
fn moves_turns_and_crops_are_steps() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut worker = worker();
let mut doc = open(&mut worker, dir.path(), "three.pdf", &three());
worker.move_pages(doc, &[2], 0).expect("moves");
doc = worker.now(doc).expect("the state");
assert_eq!(texts(&mut worker, doc), ["three", "one", "two"]);
assert_eq!(marks(&mut worker, doc)[2], ("Three".into(), Some(0)), "bookmarks follow their page");
worker.move_pages(doc, &[0, 1], 3).expect("moves to the end");
doc = worker.now(doc).expect("the state");
assert_eq!(texts(&mut worker, doc), ["two", "three", "one"]);
worker.rotate_pages(doc, &[1], 90).expect("turns");
doc = worker.now(doc).expect("the state");
assert_eq!(worker.bounds(doc, 1, 1).expect("bounds"), [[0.0, 0.0, 792.0, 612.0]]);
// Turned a quarter, the text's left edge is on top.
let top = |worker: &mut Worker, doc: Doc| worker.text(doc, 1).expect("reads")[0].rect[1];
let was = top(&mut worker, doc);
worker.crop_pages(doc, &[1], [50.0, 0.0, 0.0, 0.0]).expect("crops");
doc = worker.now(doc).expect("the state");
assert_eq!(worker.bounds(doc, 1, 1).expect("bounds"), [[0.0, 0.0, 792.0, 562.0]]);
assert!((top(&mut worker, doc) - (was - 50.0)).abs() < 0.5, "the seen top was cut");
worker.crop_pages(doc, &[1], [0.0; 4]).expect("uncrops");
doc = worker.now(doc).expect("the state");
assert_eq!(worker.bounds(doc, 1, 1).expect("bounds"), [[0.0, 0.0, 792.0, 612.0]]);
for _ in 0..5 {
worker.undo(doc, false).expect("undoes");
doc = worker.now(doc).expect("the state");
}
assert_eq!(texts(&mut worker, doc), ["one", "two", "three"]);
assert_eq!(worker.bounds(doc, 0, 3).expect("bounds"), [[0.0, 0.0, LETTER.0, LETTER.1]; 3]);
assert!(worker.undo(doc, false).is_err(), "nothing left to undo");
}
#[test]
fn pages_come_in_blank_or_from_a_file() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut worker = worker();
let mut doc = open(&mut worker, dir.path(), "three.pdf", &three());
worker.insert_blank(doc, 1, (300.0, 400.0)).expect("inserts a blank page");
doc = worker.now(doc).expect("the state");
assert_eq!(worker.bounds(doc, 1, 1).expect("bounds"), [[0.0, 0.0, 300.0, 400.0]]);
let other = dir.path().join("other.pdf");
std::fs::write(&other, pdf(&[(612.0, 792.0, &["x"]), (612.0, 792.0, &["y"])], &[])).expect("writes");
let (count, _) = worker.insert_file(doc, 4, &other).expect("inserts a file");
doc = worker.now(doc).expect("the state");
assert_eq!(count, 2);
assert_eq!(texts(&mut worker, doc), ["one", "", "two", "three", "x", "y"]);
worker.undo(doc, false).expect("undoes");
doc = worker.now(doc).expect("the state");
assert_eq!(worker.pages(doc).expect("counts"), 4);
}
#[test]
fn an_extract_writes_some_pages_and_leaves_the_document() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut worker = worker();
let mut doc = open(&mut worker, dir.path(), "three.pdf", &three());
worker.rotate_pages(doc, &[0], 90).expect("turns");
doc = worker.now(doc).expect("the state");
let mark = worker.history(doc).expect("a mark");
let out = dir.path().join("part.pdf");
worker.extract(doc, &[0, 2], &out).expect("extracts");
assert_eq!(worker.history(doc).expect("a mark"), mark);
assert_eq!(texts(&mut worker, doc), ["one", "two", "three"]);
assert!(worker.undo(doc, true).is_err(), "the extract is not a redo");
let part = worker.open(&out).expect("opens the part").doc;
assert_eq!(texts(&mut worker, part), ["one", "three"]);
assert_eq!(worker.bounds(part, 0, 1).expect("bounds"), [[0.0, 0.0, 792.0, 612.0]], "with its edits");
worker.undo(doc, false).expect("the turn undoes");
}
#[test]
fn bad_edits_are_refused_and_change_nothing() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut worker = worker();
let pages: &[(f32, f32, &[&str])] = &[(612.0, 792.0, &["big"]), (100.0, 100.0, &["small"])];
let doc = open(&mut worker, dir.path(), "two.pdf", &pdf(pages, &[]));
let mark = worker.history(doc).expect("a mark");
assert!(worker.delete_pages(doc, &[0, 1]).is_err(), "a PDF keeps a page");
assert!(worker.delete_pages(doc, &[1, 0]).is_err(), "pages in order");
assert!(worker.rotate_pages(doc, &[0], 45).is_err(), "quarter turns only");
// Fine for the big page, too much for the small.
assert!(worker.crop_pages(doc, &[0, 1], [200.0, 0.0, 0.0, 0.0]).is_err());
assert_eq!(worker.bounds(doc, 0, 2).expect("bounds"), [[0.0, 0.0, 612.0, 792.0], [0.0, 0.0, 100.0, 100.0]], "rolled back");
assert_eq!(worker.history(doc).expect("a mark"), mark);
assert!(worker.undo(doc, false).is_err(), "no step was made");
}
#[test]
fn an_extract_after_an_undo_keeps_the_redo() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut worker = worker();
let mut doc = open(&mut worker, dir.path(), "three.pdf", &three());
let turned = worker.rotate_pages(doc, &[0], 90).expect("turns");
doc = worker.now(doc).expect("the state");
worker.save(doc, &dir.path().join("saved.pdf")).expect("saves");
worker.undo(doc, false).expect("undoes");
doc = worker.now(doc).expect("the state");
worker.extract(doc, &[0], &dir.path().join("part.pdf")).expect("extracts");
assert_eq!(worker.undo(doc, true).expect("redoes"), turned);
doc = worker.now(doc).expect("the state");
assert_eq!(texts(&mut worker, doc), ["one", "two", "three"]);
assert_eq!(worker.bounds(doc, 0, 1).expect("bounds"), [[0.0, 0.0, 792.0, 612.0]], "the turn is back");
}
#[test]
fn inserted_pages_keep_their_links_and_fields() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut worker = worker();
let mut doc = open(&mut worker, dir.path(), "three.pdf", &three());
let other = dir.path().join("other.pdf");
let pages: &[(f32, f32, &[&str])] = &[(612.0, 792.0, &["x"]), (612.0, 792.0, &["y"])];
let extras = Extras { links: &[(0, 1)], fields: &[0], ..Extras::default() };
std::fs::write(&other, pdf_with(pages, &[], &extras)).expect("writes");
worker.insert_file(doc, 3, &other).expect("inserts");
doc = worker.now(doc).expect("the state");
let saved = |worker: &mut Worker, doc: Doc, name: &str| {
let out = dir.path().join(name);
worker.save(doc, &out).expect("saves");
String::from_utf8_lossy(&std::fs::read(&out).expect("reads")).into_owned()
};
let merged = saved(&mut worker, doc, "merged.pdf");
assert_eq!(merged.matches("/Link").count(), 4, "three links of its own, one inserted");
assert!(merged.contains("(field0 2)") && merged.contains("(value0)"), "a clashing field is renamed, its value kept");
// The inserted link points at y: deleting y drops it.
worker.delete_pages(doc, &[4]).expect("deletes y");
doc = worker.now(doc).expect("the state");
assert_eq!(saved(&mut worker, doc, "less.pdf").matches("/Link").count(), 3);
}
#[test]
fn a_field_without_its_page_named_still_goes_with_the_page() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut worker = worker();
let pages: &[(f32, f32, &[&str])] = &[(612.0, 792.0, &["one"]), (612.0, 792.0, &["two"])];
let extras = Extras { fields: &[1], loose: true, ..Extras::default() };
let mut doc = open(&mut worker, dir.path(), "loose.pdf", &pdf_with(pages, &[], &extras));
worker.delete_pages(doc, &[1]).expect("deletes");
doc = worker.now(doc).expect("the state");
let out = dir.path().join("out.pdf");
worker.save(doc, &out).expect("saves");
let saved = String::from_utf8_lossy(&std::fs::read(&out).expect("reads")).into_owned();
assert!(!saved.contains("value0"), "the field and its value are gone");
}
#[test]
fn crop_margins_are_points_whatever_the_user_unit() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut worker = worker();
let extras = Extras { unit: Some(2.0), ..Extras::default() };
let mut doc = open(&mut worker, dir.path(), "big.pdf", &pdf_with(&[(612.0, 792.0, &["x"])], &[], &extras));
assert_eq!(worker.bounds(doc, 0, 1).expect("bounds"), [[0.0, 0.0, 1224.0, 1584.0]]);
worker.crop_pages(doc, &[0], [72.0, 0.0, 0.0, 0.0]).expect("crops");
doc = worker.now(doc).expect("the state");
assert_eq!(worker.bounds(doc, 0, 1).expect("bounds"), [[0.0, 0.0, 1224.0, 1512.0]]);
}
/// Runs `step` on its own thread, ten seconds at most.
fn bounded<T: Send + 'static>(step: impl FnOnce() -> T + Send + 'static) -> T {
let (done, answer) = std::sync::mpsc::channel();
std::thread::spawn(move || done.send(step()).expect("the test waits"));
answer.recv_timeout(std::time::Duration::from_secs(10)).expect("done in time")
}
#[test]
fn long_clashing_field_names_still_get_a_number() {
let dir = tempfile::tempdir().expect("a temp dir");
let long = "a".repeat(255);
let page: &[(f32, f32, &[&str])] = &[(612.0, 792.0, &["x"])];
let extras = Extras { fields: &[0], prefix: Some(&long), ..Extras::default() };
let (mine, other) = (dir.path().join("mine.pdf"), dir.path().join("other.pdf"));
std::fs::write(&mine, pdf_with(page, &[], &extras)).expect("writes");
std::fs::write(&other, pdf_with(page, &[], &extras)).expect("writes");
let out = dir.path().join("out.pdf");
let saved = out.clone();
bounded(move || {
let mut worker = worker();
let mut doc = worker.open(&mine).expect("opens").doc;
worker.insert_file(doc, 1, &other).expect("inserts");
doc = worker.now(doc).expect("the state");
worker.save(doc, &saved).expect("saves");
});
let saved = String::from_utf8_lossy(&std::fs::read(&out).expect("reads")).into_owned();
assert!(saved.contains(&format!("({long}0 2)")), "the second is numbered");
}
/// Red pixels of a page at one pixel a point.
fn red(worker: &mut Worker, doc: Doc, page: usize) -> usize {
let pixels = worker.render(doc, page, (1.0, 0), [0, 0, 612, 792], false).expect("renders").bgra;
pixels.chunks_exact(4).filter(|p| p[2] > 180 && p[1] < 80 && p[0] < 80).count()
}
#[test]
fn inserted_fields_keep_the_look_of_their_form() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut worker = worker();
let page: &[(f32, f32, &[&str])] = &[(612.0, 792.0, &["x"])];
let other = dir.path().join("other.pdf");
std::fs::write(&other, pdf_with(page, &[], &Extras { fields: &[0], look: Some(("Custom", "Custom")), ..Extras::default() })).expect("writes");
let source = worker.open(&other).expect("opens").doc;
let shown = red(&mut worker, source, 0);
assert!(shown > 100, "the source draws its value red: {shown}");
// Its own form gives the font name another font.
let mine = Extras { fields: &[0], prefix: Some("mine"), look: Some(("Custom", "Custom")), ..Extras::default() };
let mut doc = open(&mut worker, dir.path(), "mine.pdf", &pdf_with(page, &[], &mine));
worker.insert_file(doc, 1, &other).expect("inserts");
doc = worker.now(doc).expect("the state");
let out = dir.path().join("out.pdf");
worker.save(doc, &out).expect("saves");
let merged = worker.open(&out).expect("opens").doc;
assert_eq!(red(&mut worker, merged, 1), shown, "the same red, the same size");
let saved = String::from_utf8_lossy(&std::fs::read(&out).expect("reads")).into_owned();
assert!(saved.contains("(/Custom_2 24 Tf 1 0 0 rg)"), "the clashing font is renamed where it is used");
}
#[test]
fn an_escaped_font_name_and_the_form_alignment_come_along() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut worker = worker();
let page: &[(f32, f32, &[&str])] = &[(612.0, 792.0, &["x"])];
let other = dir.path().join("other.pdf");
let theirs = Extras { fields: &[0], look: Some(("Custom", "C#75stom")), align: Some(2), ..Extras::default() };
std::fs::write(&other, pdf_with(page, &[], &theirs)).expect("writes");
let mine = Extras { fields: &[0], prefix: Some("mine"), look: Some(("Custom", "Custom")), align: Some(0), ..Extras::default() };
let mut doc = open(&mut worker, dir.path(), "mine.pdf", &pdf_with(page, &[], &mine));
worker.insert_file(doc, 1, &other).expect("inserts");
doc = worker.now(doc).expect("the state");
let out = dir.path().join("out.pdf");
worker.save(doc, &out).expect("saves");
let saved = String::from_utf8_lossy(&std::fs::read(&out).expect("reads")).into_owned();
assert!(saved.contains("(/Custom_2 24 Tf 1 0 0 rg)"), "the escaped name is renamed too");
assert!(saved.contains("/Q 2"), "right-aligned as in its own form");
}
@@ -0,0 +1,63 @@
//! Ely's edits read back by another reader, PDFKit (QA-13).
#![cfg(target_os = "macos")]
use std::path::Path;
use ely_pdf_engine::{FieldKind, NewField, Worker, Writing, Wrote, sample::{Extras, pdf_with}};
use objc2::AllocAnyThread as _;
use objc2_foundation::{NSString, NSURL};
use objc2_pdf_kit::PDFDocument;
fn worker() -> Worker {
Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).expect("the worker")
}
/// Each page's text and widget values, as PDFKit reads them.
fn read_back(path: &Path) -> Vec<(String, Vec<String>)> {
let url = NSURL::fileURLWithPath(&NSString::from_str(path.to_str().expect("UTF-8")));
let document = unsafe { PDFDocument::initWithURL(PDFDocument::alloc(), &url) }.expect("PDFKit opens it");
(0..unsafe { document.pageCount() })
.map(|at| {
let page = unsafe { document.pageAtIndex(at) }.expect("a page");
let text = unsafe { page.string() }.map(|s| s.to_string()).unwrap_or_default();
let values = unsafe { page.annotations() }.to_vec().into_iter().filter_map(|a| unsafe { a.widgetStringValue() }.map(|s| s.to_string())).collect();
(text, values)
})
.collect()
}
#[test]
fn text_a_field_and_page_order_read_back_elsewhere() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = dir.path().join("doc.pdf");
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["First page"]), (612.0, 792.0, &["Second page"])], &[], &Extras::default())).expect("writes");
let mut worker = worker();
let mut doc = worker.open(&path).expect("opens").doc;
let block = worker.text_blocks(doc, 0).expect("blocks").into_iter().position(|b| b.text.contains("First page")).expect("the block");
let writing = Writing {
block: Some(block),
expected: "First page".into(),
text: "Changed words".into(),
font: "helvetica".into(),
size: 12.0,
rgb: 0,
bx: [72.0, 60.0, 400.0],
};
assert!(matches!(worker.put_text(doc, 0, &writing, 0.0).expect("writes"), Wrote::Done(_)));
doc = worker.now(doc).expect("the state");
let field = NewField { kind: FieldKind::Text, name: "name".into(), rect: [72.0, 200.0, 272.0, 220.0], options: Vec::new() };
worker.add_field(doc, 0, &field).expect("adds a field");
doc = worker.now(doc).expect("the state");
let id = worker.marks(doc, 0).expect("marks").fields[0].id;
worker.fill(doc, 0, id, "Ada").expect("fills");
doc = worker.now(doc).expect("the state");
worker.move_pages(doc, &[1], 0).expect("moves");
doc = worker.now(doc).expect("the state");
let out = dir.path().join("edited.pdf");
worker.save(doc, &out).expect("saves");
let pages = read_back(&out);
assert_eq!(pages.len(), 2);
assert!(pages[0].0.contains("Second page"), "{pages:?}");
assert!(pages[1].0.contains("Changed words") && !pages[1].0.contains("First page"), "{pages:?}");
assert_eq!(pages[1].1, vec!["Ada".to_string()]);
}
+274
View File
@@ -0,0 +1,274 @@
//! Annotations and form fields against the real worker (PDF-05, PDF-06).
use std::path::Path;
use ely_pdf_engine::{
Doc, FieldKind, NewAnnot, NewField, Shape, Worker,
sample::{Extras, pdf_with},
};
fn worker() -> Worker {
Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).expect("the worker")
}
/// A Letter page of two lines and a text field.
fn opened(path: &Path) -> (Worker, Doc) {
let extras = Extras { fields: &[0], ..Extras::default() };
std::fs::write(path, pdf_with(&[(612.0, 792.0, &["Hello world", "Second line"])], &[], &extras)).expect("writes");
let mut worker = worker();
let doc = worker.open(path).expect("opens").doc;
(worker, doc)
}
fn note(page: usize, shape: Shape) -> NewAnnot {
NewAnnot { page, shape, rgb: None, contents: String::new(), author: "Ann".into() }
}
/// Whether page one's pixels `at` hold colour `bgra`.
fn has_color(worker: &mut Worker, doc: Doc, at: [i32; 4], bgra: [u8; 3]) -> bool {
let pixels = worker.render(doc, 0, (1.0, 0), at, false).expect("draws").bgra;
pixels.chunks_exact(4).any(|p| p[..3].iter().zip(bgra).all(|(a, b)| a.abs_diff(b) < 40))
}
#[test]
fn every_kind_lands_in_one_step_and_reads_back() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
let hello: Vec<_> = worker.text(doc, 0).expect("text").into_iter().take(5).collect();
let line = [hello[0].rect[0], hello[0].rect[1], hello[4].rect[2], hello[4].rect[3]];
let before = worker.history(doc).expect("a mark");
let mut noted = note(0, Shape::Note((300.0, 300.0)));
noted.contents = "Check this".into();
let mut marked = note(0, Shape::Highlight(vec![line]));
marked.rgb = Some(0x00ff00);
let all = [
marked,
note(0, Shape::Underline(vec![line])),
note(0, Shape::StrikeOut(vec![line])),
noted,
note(0, Shape::Square([100.0, 400.0, 200.0, 450.0])),
note(0, Shape::Circle([250.0, 400.0, 350.0, 450.0])),
note(0, Shape::Line([100.0, 500.0, 300.0, 520.0])),
note(0, Shape::Ink(vec![vec![(100.0, 600.0), (150.0, 620.0), (200.0, 600.0)]])),
note(0, Shape::Stamp([350.0, 550.0, 540.0, 600.0], "Approved".into())),
];
let after = worker.annotate(doc, &all).expect("annotates");
doc = worker.now(doc).expect("the state");
let read = worker.marks(doc, 0).expect("marks").annots;
let kinds: Vec<&str> = read.iter().map(|a| a.kind.as_str()).collect();
assert_eq!(kinds, ["Highlight", "Underline", "StrikeOut", "Text", "Square", "Circle", "Line", "Ink", "Stamp"]);
assert_eq!((read[0].rgb, read[3].contents.as_str(), read[3].author.as_str()), (Some(0x00ff00), "Check this", "Ann"));
assert_eq!(read[8].icon, "Approved");
assert!(has_color(&mut worker, doc, [line[0] as i32, line[1] as i32, 20, 10], [0, 255, 0]), "the highlight is drawn");
assert_eq!(worker.undo(doc, false).expect("undoes"), before, "one step");
doc = worker.now(doc).expect("the state");
assert!(worker.marks(doc, 0).expect("marks").annots.is_empty());
assert_eq!(worker.undo(doc, true).expect("redoes"), after);
doc = worker.now(doc).expect("the state");
let saved = dir.path().join("saved.pdf");
worker.save(doc, &saved).expect("saves");
let mut other = self::worker();
let reopened = other.open(&saved).expect("opens").doc;
assert_eq!(other.marks(reopened, 0).expect("marks").annots.len(), 9, "kept in the file");
}
#[test]
fn a_bad_annotation_changes_nothing_and_keeps_redo() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
worker.annotate(doc, &[note(0, Shape::Square([100.0, 100.0, 200.0, 200.0]))]).expect("annotates");
doc = worker.now(doc).expect("the state");
worker.undo(doc, false).expect("undoes");
doc = worker.now(doc).expect("the state");
let good = note(0, Shape::Circle([100.0, 100.0, 200.0, 200.0]));
for bad in [
note(0, Shape::Stamp([100.0, 100.0, 200.0, 200.0], "Hacked".into())),
note(0, Shape::Highlight(Vec::new())),
note(0, Shape::Ink(vec![vec![(1.0, 1.0)]])),
note(3, Shape::Note((10.0, 10.0))),
] {
assert!(worker.annotate(doc, &[good.clone(), bad.clone()]).is_err(), "{bad:?}");
}
assert!(worker.marks(doc, 0).expect("marks").annots.is_empty(), "the good one did not land either");
worker.undo(doc, true).expect("redo survives");
doc = worker.now(doc).expect("the state");
assert_eq!(worker.marks(doc, 0).expect("marks").annots[0].kind, "Square");
}
#[test]
fn annotations_change_and_go_by_their_number() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
let two = [note(0, Shape::Note((50.0, 50.0))), note(0, Shape::Square([100.0, 100.0, 200.0, 200.0]))];
worker.annotate(doc, &two).expect("annotates");
doc = worker.now(doc).expect("the state");
let read = worker.marks(doc, 0).expect("marks").annots;
worker.change_annot(doc, 0, read[0].id, Some(0x0000ff), Some("Done")).expect("changes");
doc = worker.now(doc).expect("the state");
worker.delete_annot(doc, 0, read[1].id).expect("deletes");
doc = worker.now(doc).expect("the state");
let now = worker.marks(doc, 0).expect("marks").annots;
assert_eq!(now.len(), 1);
assert_eq!((now[0].id, now[0].rgb, now[0].contents.as_str()), (read[0].id, Some(0x0000ff), "Done"));
assert!(worker.delete_annot(doc, 0, read[1].id).is_err(), "gone already");
worker.undo(doc, false).expect("undoes the delete");
doc = worker.now(doc).expect("the state");
assert_eq!(worker.marks(doc, 0).expect("marks").annots[1].id, read[1].id, "the same object back");
}
fn field(kind: FieldKind, name: &str, rect: [f32; 4]) -> NewField {
let options = match kind {
FieldKind::Choice => vec!["Red".into(), "Blue".into()],
_ => Vec::new(),
};
NewField { kind, name: name.into(), rect, options }
}
#[test]
fn new_fields_fill_and_keep_their_values() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
for new in [
field(FieldKind::Text, "Name", [72.0, 300.0, 272.0, 320.0]),
field(FieldKind::Checkbox, "Agree", [72.0, 340.0, 86.0, 354.0]),
field(FieldKind::Radio, "Size", [72.0, 370.0, 86.0, 384.0]),
field(FieldKind::Radio, "Size", [100.0, 370.0, 114.0, 384.0]),
field(FieldKind::Choice, "Color", [72.0, 400.0, 200.0, 420.0]),
] {
worker.add_field(doc, 0, &new).expect("adds");
doc = worker.now(doc).expect("the state");
}
assert!(!has_color(&mut worker, doc, [76, 304, 190, 12], [0, 0, 0]), "an empty box");
let fields = worker.marks(doc, 0).expect("marks").fields;
let names: Vec<(&str, FieldKind)> = fields.iter().map(|f| (f.name.as_str(), f.kind)).collect();
assert_eq!(names, [
("field0", FieldKind::Text),
("Name", FieldKind::Text),
("Agree", FieldKind::Checkbox),
("Size", FieldKind::Radio),
("Size", FieldKind::Radio),
("Color", FieldKind::Choice),
]);
assert_ne!(fields[3].on, fields[4].on, "each radio has its own state");
assert_eq!(fields[5].options, ["Red", "Blue"]);
worker.fill(doc, 0, fields[0].id, "changed").expect("fills the old field");
doc = worker.now(doc).expect("the state");
worker.fill(doc, 0, fields[1].id, "Zoe").expect("fills");
doc = worker.now(doc).expect("the state");
worker.fill(doc, 0, fields[2].id, &fields[2].on).expect("checks");
doc = worker.now(doc).expect("the state");
worker.fill(doc, 0, fields[3].id, &fields[3].on).expect("picks");
doc = worker.now(doc).expect("the state");
worker.fill(doc, 0, fields[4].id, &fields[4].on).expect("picks the other");
doc = worker.now(doc).expect("the state");
worker.fill(doc, 0, fields[5].id, "Blue").expect("chooses");
doc = worker.now(doc).expect("the state");
assert!(worker.fill(doc, 0, fields[3].id, "Nonsense").is_err(), "a state the button lacks");
let saved = dir.path().join("saved.pdf");
worker.save(doc, &saved).expect("saves");
let mut other = self::worker();
let reopened = other.open(&saved).expect("opens").doc;
let read = other.marks(reopened, 0).expect("marks").fields;
let values: Vec<(&str, bool)> = read.iter().map(|f| (f.value.as_str(), f.checked)).collect();
assert_eq!(values[..2], [("changed", false), ("Zoe", false)]);
assert!(read[2].checked && !read[3].checked && read[4].checked, "one radio of the group");
assert_eq!(read[5].value, "Blue");
assert!(has_color(&mut other, reopened, [76, 304, 190, 12], [0, 0, 0]), "the typed name is drawn");
}
#[test]
fn field_names_stay_unique_and_groups_go_with_their_last_button() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
assert!(worker.add_field(doc, 0, &field(FieldKind::Text, "field0", [72.0, 300.0, 272.0, 320.0])).is_err(), "taken");
assert!(worker.add_field(doc, 0, &field(FieldKind::Radio, "field0", [72.0, 300.0, 86.0, 314.0])).is_err(), "not a group");
assert!(worker.add_field(doc, 0, &field(FieldKind::Text, "a.b", [72.0, 300.0, 272.0, 320.0])).is_err(), "a dotted name");
let mut empty = field(FieldKind::Choice, "Pick", [72.0, 300.0, 272.0, 320.0]);
empty.options.clear();
assert!(worker.add_field(doc, 0, &empty).is_err(), "a choice of nothing");
worker.add_field(doc, 0, &field(FieldKind::Radio, "Size", [72.0, 370.0, 86.0, 384.0])).expect("adds");
doc = worker.now(doc).expect("the state");
let fields = worker.marks(doc, 0).expect("marks").fields;
assert!(worker.change_field(doc, 0, fields[1].id, Some("field0"), None).is_err(), "renamed onto another");
worker.change_field(doc, 0, fields[0].id, Some("Title"), None).expect("renames");
doc = worker.now(doc).expect("the state");
assert!(worker.change_field(doc, 0, fields[0].id, None, Some(&["x".into()])).is_err(), "text has no options");
worker.delete_field(doc, 0, fields[1].id).expect("deletes the only button");
doc = worker.now(doc).expect("the state");
worker.add_field(doc, 0, &field(FieldKind::Text, "Size", [72.0, 300.0, 272.0, 320.0])).expect("the group is gone");
doc = worker.now(doc).expect("the state");
let names: Vec<String> = worker.marks(doc, 0).expect("marks").fields.into_iter().map(|f| f.name).collect();
assert_eq!(names, ["Title", "Size"]);
}
#[test]
fn a_choice_takes_new_options_and_drops_a_lost_value() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
worker.add_field(doc, 0, &field(FieldKind::Choice, "Color", [72.0, 400.0, 200.0, 420.0])).expect("adds");
doc = worker.now(doc).expect("the state");
let id = worker.marks(doc, 0).expect("marks").fields[1].id;
worker.fill(doc, 0, id, "Red").expect("chooses");
doc = worker.now(doc).expect("the state");
worker.change_field(doc, 0, id, None, Some(&["Red".into(), "Green".into()])).expect("keeps red");
doc = worker.now(doc).expect("the state");
assert_eq!(worker.marks(doc, 0).expect("marks").fields[1].value, "Red");
worker.change_field(doc, 0, id, None, Some(&["Green".into()])).expect("drops red");
doc = worker.now(doc).expect("the state");
let read = worker.marks(doc, 0).expect("marks").fields[1].clone();
assert_eq!((read.value.as_str(), read.options), ("", vec!["Green".to_string()]));
}
#[test]
fn a_filled_value_stays_drawn_through_undo_and_redo() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
worker.add_field(doc, 0, &field(FieldKind::Text, "Name", [72.0, 300.0, 272.0, 320.0])).expect("adds");
doc = worker.now(doc).expect("the state");
let id = worker.marks(doc, 0).expect("marks").fields[1].id;
worker.fill(doc, 0, id, "Zoe").expect("fills");
doc = worker.now(doc).expect("the state");
worker.undo(doc, false).expect("undoes");
doc = worker.now(doc).expect("the state");
worker.undo(doc, true).expect("redoes before a draw");
doc = worker.now(doc).expect("the state");
assert!(has_color(&mut worker, doc, [76, 304, 190, 12], [0, 0, 0]), "drawn");
worker.undo(doc, false).expect("undoes");
doc = worker.now(doc).expect("the state");
assert!(!has_color(&mut worker, doc, [76, 304, 190, 12], [0, 0, 0]), "gone");
worker.undo(doc, true).expect("redoes");
doc = worker.now(doc).expect("the state");
assert!(has_color(&mut worker, doc, [76, 304, 190, 12], [0, 0, 0]), "drawn again");
}
#[test]
fn a_nested_field_renames_within_its_parent() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = dir.path().join("doc.pdf");
let extras = Extras { fields: &[0, 0], parent: Some("Person"), ..Extras::default() };
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["Hello"])], &[], &extras)).expect("writes");
let mut worker = worker();
let mut doc = worker.open(&path).expect("opens").doc;
let fields = worker.marks(doc, 0).expect("marks").fields;
let names: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect();
assert_eq!(names, ["Person.field0", "Person.field1"]);
assert!(worker.change_field(doc, 0, fields[0].id, Some("field1"), None).is_err(), "a sibling's name");
worker.change_field(doc, 0, fields[0].id, Some("First"), None).expect("renames");
doc = worker.now(doc).expect("the state");
worker.add_field(doc, 0, &field(FieldKind::Text, "First", [72.0, 300.0, 272.0, 320.0])).expect("a top field of the same leaf");
doc = worker.now(doc).expect("the state");
let names: Vec<String> = worker.marks(doc, 0).expect("marks").fields.into_iter().map(|f| f.name).collect();
assert_eq!(names, ["Person.First", "Person.field1", "First"]);
}
#[test]
fn every_field_name_is_listed_widget_or_not() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = dir.path().join("doc.pdf");
let extras = Extras { fields: &[0], bare: Some(("Text1", "Child")), ..Extras::default() };
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["Hello"])], &[], &extras)).expect("writes");
let mut worker = worker();
let doc = worker.open(&path).expect("opens").doc;
assert_eq!(worker.field_names(doc).expect("names"), ["field0", "Text1", "Text1.Child"]);
assert!(worker.add_field(doc, 0, &field(FieldKind::Text, "Text1", [72.0, 300.0, 272.0, 320.0])).is_err(), "the bare parent holds it");
}
+152
View File
@@ -0,0 +1,152 @@
//! OCR and the hidden text layer, real worker (PDF-08).
use std::path::{Path, PathBuf};
use ely_pdf_engine::{
Doc, OcrLine, Worker, Writing, Wrote,
sample::{pdf, scan},
tessdata,
};
fn worker() -> Worker {
Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).expect("the worker")
}
/// Points a pixel when scanning.
const SCALE: f32 = 200.0 / 72.0;
/// Page one of `doc`, saved as a gray scan.
fn scan_of(worker: &mut Worker, doc: Doc, out: PathBuf) -> PathBuf {
let (w, h) = ((612.0 * SCALE).ceil() as i32, (792.0 * SCALE).ceil() as i32);
let bgra = worker.render(doc, 0, (SCALE, 0), [0, 0, w, h], false).expect("draws").bgra;
let gray: Vec<u8> = bgra.chunks_exact(4).map(|p| ((u16::from(p[0]) + u16::from(p[1]) + u16::from(p[2])) / 3) as u8).collect();
std::fs::write(&out, scan((612.0, 792.0), (w as u32, h as u32), &gray)).expect("writes");
out
}
/// A scan of two English lines, opened.
fn english(dir: &Path) -> (Worker, Doc) {
let source = dir.join("text.pdf");
std::fs::write(&source, pdf(&[(612.0, 792.0, &["Hello world", "Second line"])], &[])).expect("writes");
let mut worker = worker();
let doc = worker.open(&source).expect("opens").doc;
let path = scan_of(&mut worker, doc, dir.join("scan.pdf"));
let doc = worker.open(&path).expect("opens").doc;
(worker, doc)
}
fn words(worker: &mut Worker, doc: Doc) -> String {
worker.text(doc, 0).expect("text").iter().map(|c| c.c).collect()
}
#[test]
fn a_scan_reads_and_its_text_layer_finds_without_drawing() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = english(dir.path());
assert_eq!(words(&mut worker, doc), "", "a scan has no text");
let lines = worker.ocr_page(doc, 0, "eng", &tessdata().expect("data"), 300.0).expect("reads");
let read: Vec<&str> = lines.iter().map(|l| l.text.as_str()).collect();
assert!(read.iter().any(|l| l.contains("Hello world")) && read.iter().any(|l| l.contains("Second line")), "{read:?}");
let hello = lines.iter().find(|l| l.text.contains("Hello")).expect("hello");
assert!((hello.rect[0] - 72.0).abs() < 6.0 && (hello.rect[3] - 76.0).abs() < 8.0, "in page points: {:?}", hello.rect);
let before = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws").bgra;
let mark = worker.text_layer(doc, &[(0, lines)], false).expect("lays");
doc = worker.now(doc).expect("the state");
assert!(words(&mut worker, doc).contains("Hello world"), "searchable now");
assert_eq!(worker.search(doc, 0, "second line").expect("finds").len(), 1);
assert!(worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws").bgra == before, "nothing drawn");
worker.undo(doc, false).expect("undoes");
doc = worker.now(doc).expect("the state");
assert_eq!(words(&mut worker, doc), "", "one step");
assert_eq!(worker.undo(doc, true).expect("redoes"), mark);
doc = worker.now(doc).expect("the state");
let saved = dir.path().join("saved.pdf");
worker.save(doc, &saved).expect("saves");
let mut other = self::worker();
let reopened = other.open(&saved).expect("opens").doc;
assert!(words(&mut other, reopened).contains("Hello world"), "kept in the file");
}
#[test]
fn a_new_layer_replaces_the_last_or_adds_beside_it() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = english(dir.path());
let line = OcrLine { rect: [72.0, 60.0, 200.0, 78.0], text: "Hello world".into() };
worker.text_layer(doc, &[(0, vec![line.clone()])], false).expect("lays");
doc = worker.now(doc).expect("the state");
worker.text_layer(doc, &[(0, vec![line.clone()])], false).expect("lays again");
doc = worker.now(doc).expect("the state");
assert_eq!(worker.search(doc, 0, "hello").expect("finds").len(), 2, "two layers");
worker.text_layer(doc, &[(0, vec![line])], true).expect("replaces");
doc = worker.now(doc).expect("the state");
assert_eq!(worker.search(doc, 0, "hello").expect("finds").len(), 1, "old layers gone");
}
#[test]
fn a_bad_layer_changes_nothing() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = english(dir.path());
let before = worker.history(doc).expect("a mark");
let good = (0, vec![OcrLine { rect: [72.0, 60.0, 200.0, 78.0], text: "fine".into() }]);
for bad in [
(0, vec![OcrLine { rect: [72.0, 60.0, 200.0, 78.0], text: "𠀀".into() }]),
(0, vec![OcrLine { rect: [72.0, 60.0, 200.0, 78.0], text: String::new() }]),
(5, vec![OcrLine { rect: [72.0, 60.0, 200.0, 78.0], text: "x".into() }]),
] {
assert!(worker.text_layer(doc, &[good.clone(), bad.clone()], true).is_err(), "{bad:?}");
}
assert_eq!((worker.history(doc).expect("a mark"), words(&mut worker, doc)), (before, String::new()));
}
#[test]
fn chinese_reads_in_both_scripts() {
let dir = tempfile::tempdir().expect("a temp dir");
let source = dir.path().join("blank.pdf");
std::fs::write(&source, pdf(&[(612.0, 792.0, &[])], &[])).expect("writes");
let mut worker = worker();
let mut doc = worker.open(&source).expect("opens").doc;
for (font, text, at) in [("cjk-sc", "简体中文识别", 100.0), ("cjk-tc", "繁體中文識別", 300.0)] {
let writing = Writing { block: None, expected: String::new(), text: text.into(), font: font.into(), size: 36.0, rgb: 0, bx: [72.0, at, 400.0] };
assert!(matches!(worker.put_text(doc, 0, &writing, 0.0).expect("writes"), Wrote::Done(_)));
doc = worker.now(doc).expect("the state");
}
let path = scan_of(&mut worker, doc, dir.path().join("scan.pdf"));
let doc = worker.open(&path).expect("opens").doc;
let lines = worker.ocr_page(doc, 0, "chi_sim+chi_tra+eng", &tessdata().expect("data"), 300.0).expect("reads");
let read: String = lines.iter().map(|l| l.text.replace(' ', "")).collect();
assert!(read.contains("简体中文") && read.contains("繁體中文"), "{read}");
}
#[test]
fn a_bad_language_says_so() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = english(dir.path());
let error = worker.ocr_page(doc, 0, "xyz", &tessdata().expect("data"), 300.0).expect_err("no such data");
assert!(error.to_string().contains("Tesseract"), "{error}");
assert!(worker.ocr_page(doc, 0, "eng", &tessdata().expect("data"), 300.0).is_ok(), "the worker goes on");
}
#[test]
fn a_language_that_does_not_load_fails_the_read() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = english(dir.path());
let data = dir.path().join("tessdata");
std::fs::create_dir(&data).expect("a data dir");
std::fs::copy(tessdata().expect("data").join("eng.traineddata"), data.join("eng.traineddata")).expect("copies");
let error = worker.ocr_page(doc, 0, "eng+chi_sim", &data, 300.0).expect_err("chi_sim is missing");
assert!(error.to_string().contains("1 of 2 languages"), "{error}");
}
#[test]
fn text_added_after_a_layer_still_shows() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = english(dir.path());
let line = OcrLine { rect: [72.0, 60.0, 200.0, 78.0], text: "Hello world".into() };
worker.text_layer(doc, &[(0, vec![line])], false).expect("lays");
doc = worker.now(doc).expect("the state");
let before = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws").bgra;
let writing = Writing { block: None, expected: String::new(), text: "VISIBLE".into(), font: "helvetica".into(), size: 24.0, rgb: 0, bx: [72.0, 600.0, 300.0] };
assert!(matches!(worker.put_text(doc, 0, &writing, 0.0).expect("writes"), Wrote::Done(_)));
doc = worker.now(doc).expect("the state");
assert!(worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws").bgra != before, "the new text is drawn");
}
+187
View File
@@ -0,0 +1,187 @@
//! Images, text, optimized copies, printing leave, repaired files (C11.8).
use std::{io::ErrorKind, path::Path};
use ely_pdf_engine::{
Doc, Lock, Optimizing, Worker,
proto::may,
sample::{Extras, one_page, pdf_with},
};
fn worker() -> Worker {
Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).expect("the worker")
}
fn opened(path: &Path) -> (Worker, Doc) {
let mut worker = worker();
let doc = worker.open(path).expect("opens").doc;
(worker, doc)
}
fn two_pages(dir: &Path) -> std::path::PathBuf {
let path = dir.join("doc.pdf");
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["First page"]), (612.0, 792.0, &["Second page"])], &[], &Extras::default())).expect("writes");
path
}
#[test]
fn a_page_exports_as_a_png_of_its_size() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&two_pages(dir.path()));
let png = dir.path().join("page.png");
worker.export_image(doc, 1, 144.0, &png).expect("exports");
let bytes = std::fs::read(&png).expect("reads");
assert_eq!(&bytes[1..4], b"PNG");
let size = |at: usize| u32::from_be_bytes(bytes[at..at + 4].try_into().expect("four bytes"));
assert_eq!((size(16), size(20)), (1224, 1584), "twice the points");
assert!(worker.export_image(doc, 0, 5000.0, &png).is_err(), "an absurd resolution");
}
#[test]
fn text_exports_page_by_page() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&two_pages(dir.path()));
let txt = dir.path().join("doc.txt");
worker.export_text(doc, &txt).expect("exports");
let text = std::fs::read_to_string(&txt).expect("UTF-8");
let pages: Vec<&str> = text.split('\u{c}').collect();
assert_eq!(pages.len(), 2);
assert!(pages[0].contains("First page") && pages[1].contains("Second page"), "{text:?}");
}
/// A 600-pixel picture drawn 100 points wide: 432 dpi.
fn heavy(dir: &Path) -> std::path::PathBuf {
let pixels = vec![120u8; 600 * 600 * 3];
let mut picture = format!("<< /Type /XObject /Subtype /Image /Width 600 /Height 600 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Length {} >>\nstream\n", pixels.len()).into_bytes();
picture.extend_from_slice(&pixels);
picture.extend_from_slice(b"\nendstream");
let path = dir.join("heavy.pdf");
let mut bytes = one_page("", " /Resources << /XObject << /Im 5 0 R >> >>", "q 100 0 0 100 72 600 cm /Im Do Q", &["PICTURE".to_string()]);
let at = bytes.windows(7).position(|w| w == b"PICTURE").expect("the slot");
bytes.splice(at..at + 7, picture);
std::fs::write(&path, &bytes).expect("writes");
path
}
#[test]
fn an_optimized_copy_is_smaller_and_its_picture_downsampled() {
let dir = tempfile::tempdir().expect("a temp dir");
let source = heavy(dir.path());
let (mut worker, doc) = opened(&source);
let out = dir.path().join("small.pdf");
worker.optimize(doc, &out, Optimizing { compress: true, from: 300, to: 150 }).expect("optimizes");
let (before, after) = (std::fs::metadata(&source).expect("size").len(), std::fs::metadata(&out).expect("size").len());
assert!(after * 10 < before, "{before} -> {after}");
let mut other = self::worker();
let read = other.open(&out).expect("opens").doc;
assert_eq!(other.pages(read).expect("pages"), 1);
let text = String::from_utf8_lossy(&std::fs::read(&out).expect("reads")).into_owned();
let width: u32 = text.split("/Width ").nth(1).and_then(|rest| rest.split(|c: char| !c.is_ascii_digit()).next()).and_then(|n| n.parse().ok()).expect("a width");
assert!((205..=212).contains(&width), "150 dpi over 100 points, not {width}");
assert!(worker.optimize(doc, &out, Optimizing { compress: true, from: 150, to: 300 }).is_err(), "up is not down");
}
#[test]
fn printing_and_copying_need_leave() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&two_pages(dir.path()));
let limited = dir.path().join("limited.pdf");
worker.protect(doc, &limited, Some(&Lock { user: String::new(), owner: "owner".into(), allow: may::EDIT })).expect("protects");
let (mut other, read) = opened(&limited);
for refused in [other.print_copy(read, &dir.path().join("p.pdf")), other.export_text(read, &dir.path().join("t.txt"))] {
assert_eq!(refused.expect_err("refused").kind(), ErrorKind::PermissionDenied);
}
worker.print_copy(doc, &dir.path().join("print.pdf")).expect("an unprotected file prints");
}
#[test]
fn a_damaged_file_opens_as_a_repaired_copy_and_stays_as_it_was() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = two_pages(dir.path());
let mut bytes = std::fs::read(&path).expect("reads");
// Breaks every cross-reference offset.
let at = bytes.windows(9).rposition(|w| w == b"startxref").expect("the trailer");
bytes.truncate(at);
bytes.extend_from_slice(b"startxref\n9\n%%EOF\n");
std::fs::write(&path, &bytes).expect("writes");
let (mut worker, mut doc) = opened(&path);
let report = worker.repair_report(doc).expect("asks").expect("repaired");
assert!(!report.trim().is_empty(), "MuPDF says what it found");
assert_eq!(worker.pages(doc).expect("pages"), 2);
worker.rotate_pages(doc, &[0], 90).expect("edits the copy");
doc = worker.now(doc).expect("the state");
worker.save(doc, &dir.path().join("fixed.pdf")).expect("saves the copy");
assert_eq!(std::fs::read(&path).expect("reads"), bytes, "the damaged original stays");
let elsewhere = tempfile::tempdir().expect("a temp dir");
let (mut fresh, clean) = opened(&two_pages(elsewhere.path()));
assert_eq!(fresh.repair_report(clean).expect("asks"), None);
}
#[test]
fn a_broken_object_with_a_sound_trailer_is_repaired_on_open() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = two_pages(dir.path());
let mut bytes = std::fs::read(&path).expect("reads");
// The page tree's offset points nowhere; startxref is sound.
let table = bytes.windows(6).position(|w| w == b"\nxref\n").expect("the table") + 6;
let entries = table + bytes[table..].iter().position(|b| *b == b'\n').expect("a header") + 1;
let at = entries + 2 * 20;
bytes[at..at + 10].copy_from_slice(b"0000000009");
std::fs::write(&path, &bytes).expect("writes");
let (mut worker, mut doc) = opened(&path);
assert!(worker.repair_report(doc).expect("asks").is_some(), "found at open, not mid-edit");
worker.rotate_pages(doc, &[0], 90).expect("edits");
doc = worker.now(doc).expect("the state");
worker.save(doc, &dir.path().join("fixed.pdf")).expect("saves");
assert_eq!(std::fs::read(&path).expect("reads"), bytes);
}
#[test]
fn a_broken_stream_under_a_sound_table_is_damage_and_fails_exports() {
let dir = tempfile::tempdir().expect("a temp dir");
let broken = "<< /Length 12 /Filter /FlateDecode >>\nstream\nnot a zlib!!\nendstream".to_string();
let path = dir.path().join("doc.pdf");
let bytes = one_page("", " /Contents 5 0 R", "", &[broken]);
std::fs::write(&path, &bytes).expect("writes");
let (mut worker, mut doc) = opened(&path);
let report = worker.repair_report(doc).expect("asks").expect("damage is reported");
assert!(!report.trim().is_empty());
assert!(worker.export_text(doc, &dir.path().join("doc.txt")).is_err(), "partial text is no export");
worker.rotate_pages(doc, &[0], 90).expect("edits the copy");
doc = worker.now(doc).expect("the state");
worker.save(doc, &dir.path().join("fixed.pdf")).expect("saves the copy");
assert_eq!(std::fs::read(&path).expect("reads"), bytes, "the original stays");
}
#[test]
fn a_print_copy_carries_no_lock() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&two_pages(dir.path()));
let locked = dir.path().join("locked.pdf");
worker.protect(doc, &locked, Some(&Lock { user: "user".into(), owner: "owner".into(), allow: may::PRINT })).expect("protects");
let (mut other, read) = opened(&locked);
assert!(other.unlock(read, "user").expect("tries"));
let print = dir.path().join("print.pdf");
other.print_copy(read, &print).expect("writes");
let mut third = self::worker();
let opened = third.open(&print).expect("opens");
assert!(!opened.locked, "the printer needs no password");
assert_eq!(third.pages(opened.doc).expect("pages"), 2);
}
#[test]
fn a_broken_picture_shows_when_drawn_or_checked() {
let dir = tempfile::tempdir().expect("a temp dir");
let picture = "<< /Type /XObject /Subtype /Image /Width 4 /Height 4 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /FlateDecode /Length 12 >>\nstream\nnot a zlib!!\nendstream".to_string();
let path = dir.path().join("doc.pdf");
let bytes = one_page("", " /Resources << /XObject << /Im 5 0 R >> >>", "q 100 0 0 100 72 600 cm /Im Do Q", &[picture]);
std::fs::write(&path, &bytes).expect("writes");
let (mut worker, doc) = opened(&path);
assert_eq!(worker.repair_report(doc).expect("asks"), None, "pictures wait to be drawn");
let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws");
assert!(tile.damaged, "drawing finds it");
assert!(worker.repair_report(doc).expect("asks").is_some());
let (mut other, again) = opened(&path);
assert!(other.check_pictures(again).expect("checks"), "the check finds it undrawn");
assert_eq!(std::fs::read(&path).expect("reads"), bytes);
}
+57
View File
@@ -0,0 +1,57 @@
//! Worker records reach the app log; document text never.
use std::{
path::Path,
sync::{Mutex, OnceLock},
time::Duration,
};
use ely_pdf_engine::{LinkChange, Target, Worker, sample::pdf};
/// Every record the app would log, kept for the test.
struct Kept;
fn kept() -> &'static Mutex<Vec<String>> {
static KEPT: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
KEPT.get_or_init(Mutex::default)
}
impl log::Log for Kept {
fn enabled(&self, _: &log::Metadata) -> bool {
true
}
fn log(&self, record: &log::Record) {
kept().lock().unwrap().push(format!("{} {} {}", record.level(), record.target(), record.args()));
}
fn flush(&self) {}
}
#[test]
fn a_broken_stream_keeps_its_text_out_of_the_log() {
log::set_boxed_logger(Box::new(Kept)).unwrap();
log::set_max_level(log::LevelFilter::Trace);
// The worker logs at debug; the relay filters.
unsafe { std::env::set_var("ELY_LOG", "debug") };
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("broken.pdf");
// An unknown operator: MuPDF warns, quoting it.
let line = "ZqxPrivate42) Tj ZqxPrivate42 (tail";
std::fs::write(&path, pdf(&[(612.0, 792.0, &[line])], &[])).unwrap();
let mut worker = Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).unwrap();
let opened = worker.open(&path).unwrap();
let _ = worker.text(opened.doc, 0);
// A refused address holds a credential, unquoted.
let secret = Target::Web("ftp://ana:ZqxPrivate42@files.example/document".into());
let at = worker.now(opened.doc).unwrap();
assert!(worker.change_link(at, 0, &LinkChange::Add { rect: [72.0, 300.0, 200.0, 320.0], to: secret }).is_err());
worker.close(opened.doc).unwrap();
drop(worker);
std::thread::sleep(Duration::from_millis(500));
let records = kept().lock().unwrap().clone();
assert!(records.iter().any(|r| r.contains("native output withheld")), "MuPDF spoke: {records:#?}");
assert!(records.iter().any(|r| r.contains("ely_pdf_worker") || r.contains("pdf-worker")), "the worker's own records pass");
let leaked: Vec<_> = records.iter().filter(|r| r.contains("ZqxPrivate42")).collect();
assert!(leaked.is_empty(), "document text reached the log: {leaked:#?}");
}
+342
View File
@@ -0,0 +1,342 @@
//! Removal and cleaning, checked in the file (QA-14).
use std::path::Path;
use ely_pdf_engine::{
Cleaning, Doc, NewAnnot, OcrLine, Region, Shape, Worker,
sample::{Extras, pdf_with},
};
fn worker() -> Worker {
Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).expect("the worker")
}
/// Two lines, a picture at 72..172 across, 242..292 down.
fn opened(path: &Path) -> (Worker, Doc) {
let extras = Extras { picture: true, ..Extras::default() };
std::fs::write(path, pdf_with(&[(612.0, 792.0, &["Hello world", "Second line"])], &[], &extras)).expect("writes");
let mut worker = worker();
let doc = worker.open(path).expect("opens").doc;
(worker, doc)
}
fn words(worker: &mut Worker, doc: Doc) -> String {
worker.text(doc, 0).expect("text").iter().map(|c| c.c).collect()
}
/// The box of the first `n` characters.
fn first(worker: &mut Worker, doc: Doc, n: usize) -> [f32; 4] {
let chars = worker.text(doc, 0).expect("text");
let (a, b) = (chars[0].rect, chars[n - 1].rect);
[a[0] - 1.0, a[1] - 1.0, b[2] + 0.5, b[3] + 1.0]
}
fn reopen(path: &Path) -> (Worker, Doc) {
let mut worker = worker();
let doc = worker.open(path).expect("opens").doc;
(worker, doc)
}
#[test]
fn removed_text_is_gone_from_the_file_and_the_rest_stays() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
let hello = first(&mut worker, doc, 5);
let line = OcrLine { rect: hello, text: "Hidden".into() };
worker.text_layer(doc, &[(0, vec![line])], false).expect("an invisible layer over it");
doc = worker.now(doc).expect("the state");
let mark = worker.history(doc).expect("a mark");
let out = dir.path().join("out.pdf");
let report = worker.redact(doc, &[Region { page: 0, rect: hello }], &out).expect("removes");
assert_eq!((report.text, report.light, report.versions), (0, 0, 1));
assert_eq!(worker.history(doc).expect("a mark"), mark, "the open document is untouched");
assert!(words(&mut worker, doc).contains("Hello"));
let (mut other, written) = reopen(&out);
let left = words(&mut other, written);
assert!(!left.contains("Hello") && !left.contains("Hidden"), "{left}");
assert!(left.contains("world") && left.contains("Second line"), "{left}");
let bytes = std::fs::read(&out).expect("reads");
assert!(!bytes.windows(5).any(|w| w == b"Hello"), "no trace in the bytes");
assert_eq!(bytes.windows(9).filter(|w| w == b"startxref").count(), 1, "one revision");
}
#[test]
fn a_picture_under_a_region_loses_those_pixels() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&dir.path().join("doc.pdf"));
let out = dir.path().join("out.pdf");
worker.redact(doc, &[Region { page: 0, rect: [72.0, 242.0, 122.0, 292.0] }], &out).expect("removes");
let (mut other, written) = reopen(&out);
let left = other.render(written, 0, (1.0, 0), [124, 244, 46, 46], false).expect("draws").bgra;
assert!(left.chunks_exact(4).any(|p| p[0] > 100 || p[1] > 100 || p[2] > 100), "the right half keeps its colour");
let gone = other.render(written, 0, (1.0, 0), [74, 244, 46, 46], false).expect("draws").bgra;
assert!(gone.chunks_exact(4).all(|p| p[0] < 40 && p[1] < 40 && p[2] < 40), "the left half is black");
}
#[test]
fn notes_in_a_region_go_and_others_stay() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
let note = |at| NewAnnot { page: 0, shape: Shape::Note(at), rgb: None, contents: "secret".into(), author: String::new() };
worker.annotate(doc, &[note((80.0, 400.0)), note((400.0, 600.0))]).expect("notes");
doc = worker.now(doc).expect("the state");
let out = dir.path().join("out.pdf");
worker.redact(doc, &[Region { page: 0, rect: [70.0, 390.0, 120.0, 430.0] }], &out).expect("removes");
let (mut other, written) = reopen(&out);
let kept = other.marks(written, 0).expect("marks").annots;
assert_eq!(kept.len(), 1, "the far note stays");
assert!(kept[0].rect[0] > 300.0);
}
#[test]
fn bad_regions_write_nothing() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&dir.path().join("doc.pdf"));
let out = dir.path().join("out.pdf");
assert!(worker.redact(doc, &[], &out).is_err());
assert!(worker.redact(doc, &[Region { page: 4, rect: [0.0, 0.0, 10.0, 10.0] }], &out).is_err());
assert!(!out.exists());
}
#[test]
fn cleaning_reaches_what_it_counted_and_leaves_none() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
worker.set_properties(doc, &[("Title", "Secret plan"), ("Author", "Zoe")]).expect("sets");
doc = worker.now(doc).expect("the state");
let note = NewAnnot { page: 0, shape: Shape::Note((300.0, 300.0)), rgb: None, contents: "x".into(), author: String::new() };
worker.annotate(doc, &[note]).expect("notes");
doc = worker.now(doc).expect("the state");
let file = dir.path().join("a.txt");
std::fs::write(&file, "attached").expect("writes");
worker.attach(doc, "a.txt", &file).expect("attaches");
doc = worker.now(doc).expect("the state");
worker.text_layer(doc, &[(0, vec![OcrLine { rect: [72.0, 600.0, 200.0, 620.0], text: "invisible".into() }])], false).expect("lays");
doc = worker.now(doc).expect("the state");
let reach = worker.clean_scan(doc, &dir.path().join("scan.pdf")).expect("scans");
assert!(reach.meta >= 2 && reach.annots == 1 && reach.files == 1 && reach.hidden == 1, "{reach:?}");
let out = dir.path().join("out.pdf");
let all = Cleaning { meta: true, annots: true, files: true, hidden: true };
let left = worker.clean(doc, all, &out).expect("cleans");
assert_eq!((left.meta, left.annots, left.files, left.hidden, left.scripts), (0, 0, 0, 0, 0));
let (mut other, written) = reopen(&out);
assert_eq!(other.properties(written).expect("reads").title, None);
assert!(other.attachments(written).expect("lists").is_empty());
assert!(!words(&mut other, written).contains("invisible"));
assert!(words(&mut other, written).contains("Hello world"), "seen text stays");
assert_eq!(worker.clean_scan(doc, &dir.path().join("scan.pdf")).expect("scans"), reach, "the open document is untouched");
}
#[test]
fn cleaning_one_kind_leaves_the_others() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
worker.set_properties(doc, &[("Title", "Plan")]).expect("sets");
doc = worker.now(doc).expect("the state");
let note = NewAnnot { page: 0, shape: Shape::Note((300.0, 300.0)), rgb: None, contents: "x".into(), author: String::new() };
worker.annotate(doc, &[note]).expect("notes");
doc = worker.now(doc).expect("the state");
let out = dir.path().join("out.pdf");
let left = worker.clean(doc, Cleaning { annots: true, ..Cleaning::default() }, &out).expect("cleans");
assert_eq!(left.annots, 0);
assert!(left.meta >= 1, "properties were not asked for");
assert!(worker.clean(doc, Cleaning::default(), &dir.path().join("none.pdf")).is_err(), "nothing chosen");
}
/// Helvetica as object 5, for the hand-made pages.
fn helvetica() -> String {
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".into()
}
fn written(dir: &Path, bytes: Vec<u8>) -> (Worker, Doc) {
let path = dir.join("doc.pdf");
std::fs::write(&path, bytes).expect("writes");
reopen(&path)
}
fn holds(path: &Path, needle: &str) -> bool {
std::fs::read(path).expect("reads").windows(needle.len()).any(|w| w == needle.as_bytes())
}
#[test]
fn a_shape_touching_a_region_goes_whole() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = written(dir.path(), ely_pdf_engine::sample::one_page("", "", "1 0 0 rg 100 700 200 40 re f", &[]));
let out = dir.path().join("out.pdf");
let report = worker.redact(doc, &[Region { page: 0, rect: [90.0, 45.0, 180.0, 100.0] }], &out).expect("removes");
assert_eq!(report.objects, 0);
let (mut other, read) = reopen(&out);
let right = other.render(read, 0, (1.0, 0), [200, 55, 90, 30], false).expect("draws").bgra;
assert!(right.chunks_exact(4).all(|p| p[2] < 200 || p[1] > 200), "no red left past the box");
}
#[test]
fn a_field_in_a_region_takes_its_parent_and_value() {
let dir = tempfile::tempdir().expect("a temp dir");
let objects = [
"<< /FT /Tx /T (parent) /V (SECRET-FIELD-VALUE) /Kids [6 0 R] >>".to_string(),
"<< /Type /Annot /Subtype /Widget /Parent 5 0 R /Rect [70 700 220 740] /P 3 0 R >>".to_string(),
];
let (mut worker, doc) = written(dir.path(), ely_pdf_engine::sample::one_page(" /AcroForm << /Fields [5 0 R] >>", " /Annots [6 0 R]", "", &objects));
let out = dir.path().join("out.pdf");
worker.redact(doc, &[Region { page: 0, rect: [60.0, 40.0, 230.0, 100.0] }], &out).expect("removes");
assert!(!holds(&out, "SECRET-FIELD-VALUE"));
}
#[test]
fn hidden_layers_faint_text_and_hidden_notes_clean_out() {
let dir = tempfile::tempdir().expect("a temp dir");
let content = "BT /F1 12 Tf 72 700 Td (VISIBLE) Tj ET \
/OC /H BDC BT /F1 12 Tf 72 680 Td (SECRET-OFF-LAYER) Tj ET EMC \
q /G0 gs BT /F1 12 Tf 72 660 Td (SECRET-FAINT) Tj ET Q";
let objects = [
helvetica(),
"<< /Type /OCG /Name (Hidden) >>".to_string(),
"<< /Type /ExtGState /ca 0 >>".to_string(),
"<< /Type /Annot /Subtype /Text /F 2 /Rect [300 300 320 320] /Contents (SECRET-HIDDEN-NOTE) >>".to_string(),
];
let catalog = " /OCProperties << /OCGs [6 0 R] /D << /OFF [6 0 R] >> >>";
let page = " /Resources << /Font << /F1 5 0 R >> /Properties << /H 6 0 R >> /ExtGState << /G0 7 0 R >> >> /Annots [8 0 R]";
let (mut worker, doc) = written(dir.path(), ely_pdf_engine::sample::one_page(catalog, page, content, &objects));
let scratch = dir.path().join("scan.pdf");
let reach = worker.clean_scan(doc, &scratch).expect("scans");
assert_eq!((reach.hidden, reach.hidden_annots, reach.annots), (1, 1, 0), "{reach:?}");
let out = dir.path().join("out.pdf");
worker.clean(doc, Cleaning { hidden: true, ..Cleaning::default() }, &out).expect("cleans without /Names");
for secret in ["SECRET-OFF-LAYER", "SECRET-FAINT", "SECRET-HIDDEN-NOTE"] {
assert!(!holds(&out, secret), "{secret}");
}
assert!(holds(&out, "VISIBLE"));
}
#[test]
fn metadata_on_pages_cleans_out_too() {
let dir = tempfile::tempdir().expect("a temp dir");
let xmp = "<x:xmpmeta>SECRET-PAGE-METADATA</x:xmpmeta>";
let objects = [format!("<< /Type /Metadata /Subtype /XML /Length {} >>\nstream\n{xmp}\nendstream", xmp.len())];
let (mut worker, doc) = written(dir.path(), ely_pdf_engine::sample::one_page("", " /Metadata 5 0 R", "", &objects));
let reach = worker.clean_scan(doc, &dir.path().join("scan.pdf")).expect("scans");
assert_eq!(reach.meta, 1);
let out = dir.path().join("out.pdf");
worker.clean(doc, Cleaning { meta: true, ..Cleaning::default() }, &out).expect("cleans");
assert!(!holds(&out, "SECRET-PAGE-METADATA"));
}
#[test]
fn marked_content_says_nothing_after_removal() {
let dir = tempfile::tempdir().expect("a temp dir");
let content = "/Span << /MCID 0 /ActualText (SECRET-ACTUAL) >> BDC BT /F1 12 Tf 72 700 Td (Hello) Tj ET EMC";
let objects = [
helvetica(),
"<< /Type /StructTreeRoot /K [7 0 R] >>".to_string(),
"<< /Type /StructElem /S /P /P 6 0 R /Pg 3 0 R /K 0 /Alt (SECRET-STRUCT) /E (SECRET-EXPANDED) >>".to_string(),
];
let (mut worker, doc) = written(dir.path(), ely_pdf_engine::sample::one_page(" /StructTreeRoot 6 0 R", " /Resources << /Font << /F1 5 0 R >> >>", content, &objects));
let out = dir.path().join("out.pdf");
worker.redact(doc, &[Region { page: 0, rect: [60.0, 80.0, 200.0, 100.0] }], &out).expect("removes");
for secret in ["SECRET-ACTUAL", "SECRET-STRUCT", "SECRET-EXPANDED", "Hello"] {
assert!(!holds(&out, secret), "{secret}");
}
}
#[test]
fn overlapping_regions_both_remove() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&dir.path().join("doc.pdf"));
let hello = first(&mut worker, doc, 5);
let after = [hello[2] - 2.0, hello[1], hello[2] + 60.0, hello[3]];
let out = dir.path().join("out.pdf");
worker.redact(doc, &[Region { page: 0, rect: hello }, Region { page: 0, rect: after }], &out).expect("removes");
let (mut other, read) = reopen(&out);
assert!(!words(&mut other, read).contains("Hello"));
}
#[test]
fn nested_metadata_and_associated_files_clean_out() {
let dir = tempfile::tempdir().expect("a temp dir");
let secret = "SECRET-AF-BYTES";
let objects = [
"<< /Type /Filespec /F (a.txt) /EF << /F 6 0 R >> >>".to_string(),
format!("<< /Type /EmbeddedFile /Length {} >>\nstream\n{secret}\nendstream", secret.len()),
];
let page = " /Extra << /Deep << /PieceInfo << /App << /Private (SECRET-PIECE) >> >> >> >>";
let (mut worker, doc) = written(dir.path(), ely_pdf_engine::sample::one_page(" /AF [5 0 R]", page, "", &objects));
let reach = worker.clean_scan(doc, &dir.path().join("scan.pdf")).expect("scans");
assert_eq!((reach.meta, reach.files), (1, 1), "{reach:?}");
let out = dir.path().join("out.pdf");
worker.clean(doc, Cleaning { meta: true, files: true, ..Cleaning::default() }, &out).expect("cleans");
assert!(!holds(&out, secret) && !holds(&out, "SECRET-PIECE") && !holds(&out, "/AF"));
}
#[test]
fn scripts_on_links_fields_and_chains_clean_out() {
let dir = tempfile::tempdir().expect("a temp dir");
let objects = [
"<< /Type /Annot /Subtype /Link /Rect [72 600 200 620] /A << /S /JavaScript /JS (SECRET-LINK-JS) >> >>".to_string(),
"<< /FT /Tx /T (p) /AA << /K << /S /JavaScript /JS (SECRET-FIELD-JS) >> >> /Kids [7 0 R] >>".to_string(),
"<< /Type /Annot /Subtype /Widget /Parent 6 0 R /Rect [72 500 200 520] /P 3 0 R >>".to_string(),
];
let catalog = " /AcroForm << /Fields [6 0 R] >> /OpenAction << /S /GoTo /D [3 0 R /Fit] \
/Next << /S /JavaScript /JS (SECRET-NEXT-JS) /Next << /S /Named /N /LastPage >> >> >>";
let (mut worker, doc) = written(dir.path(), ely_pdf_engine::sample::one_page(catalog, " /Annots [5 0 R 7 0 R]", "", &objects));
let reach = worker.clean_scan(doc, &dir.path().join("scan.pdf")).expect("scans");
assert_eq!(reach.scripts, 3, "{reach:?}");
let out = dir.path().join("out.pdf");
worker.clean(doc, Cleaning { hidden: true, ..Cleaning::default() }, &out).expect("cleans");
for secret in ["SECRET-LINK-JS", "SECRET-FIELD-JS", "SECRET-NEXT-JS"] {
assert!(!holds(&out, secret), "{secret}");
}
assert!(holds(&out, "/LastPage") && holds(&out, "/GoTo"), "what follows a script stays");
}
#[test]
fn elements_placed_by_their_kids_say_nothing_after_removal() {
let dir = tempfile::tempdir().expect("a temp dir");
let content = "/Span << /MCID 0 >> BDC BT /F1 12 Tf 72 700 Td (Hello) Tj ET EMC";
let objects = [
helvetica(),
"<< /Type /StructTreeRoot /K [7 0 R] >>".to_string(),
"<< /Type /StructElem /S /P /P 6 0 R /K << /Type /MCR /Pg 3 0 R /MCID 0 >> /ActualText (SECRET-MCR) >>".to_string(),
];
let (mut worker, doc) = written(dir.path(), ely_pdf_engine::sample::one_page(" /StructTreeRoot 6 0 R", " /Resources << /Font << /F1 5 0 R >> >>", content, &objects));
let out = dir.path().join("out.pdf");
worker.redact(doc, &[Region { page: 0, rect: [60.0, 80.0, 200.0, 100.0] }], &out).expect("removes");
assert!(!holds(&out, "SECRET-MCR"));
}
#[test]
fn a_redacted_page_loses_its_thumbnail() {
let dir = tempfile::tempdir().expect("a temp dir");
let pixels = "SECRET-THUMB";
let objects = [format!("<< /Type /XObject /Subtype /Image /Width 4 /Height 1 /ColorSpace /DeviceRGB /BitsPerComponent 8 /Length {} >>\nstream\n{pixels}\nendstream", pixels.len())];
let page = " /Resources << /XObject << /Im 5 0 R >> >> /Thumb 5 0 R";
let (mut worker, doc) = written(dir.path(), ely_pdf_engine::sample::one_page("", page, "q 100 0 0 50 0 742 cm /Im Do Q", &objects));
let out = dir.path().join("out.pdf");
worker.redact(doc, &[Region { page: 0, rect: [0.0, 0.0, 100.0, 50.0] }], &out).expect("removes");
assert!(!holds(&out, pixels) && !holds(&out, "/Thumb"));
}
#[test]
fn hidden_text_takes_what_its_marks_say() {
let dir = tempfile::tempdir().expect("a temp dir");
let content = "/Span << /ActualText (SECRET-HIDDEN-ACTUAL) >> BDC BT /F1 12 Tf 3 Tr 72 700 Td (Unseen) Tj ET EMC";
let (mut worker, doc) = written(dir.path(), ely_pdf_engine::sample::one_page("", " /Resources << /Font << /F1 5 0 R >> >>", content, &[helvetica()]));
let out = dir.path().join("out.pdf");
worker.clean(doc, Cleaning { hidden: true, ..Cleaning::default() }, &out).expect("cleans");
assert!(!holds(&out, "SECRET-HIDDEN-ACTUAL") && !holds(&out, "Unseen"));
}
#[test]
fn hidden_links_and_fields_clean_out() {
let dir = tempfile::tempdir().expect("a temp dir");
let objects = [
"<< /Type /Annot /Subtype /Link /F 2 /Rect [72 600 200 620] /Contents (SECRET-HIDDEN-LINK) /A << /S /URI /URI (https://x.test/SECRET-HIDDEN-LINK) >> >>".to_string(),
"<< /Type /Annot /Subtype /Widget /F 2 /FT /Tx /T (f) /V (SECRET-HIDDEN-WIDGET) /Rect [72 500 200 520] /P 3 0 R >>".to_string(),
];
let (mut worker, doc) = written(dir.path(), ely_pdf_engine::sample::one_page(" /AcroForm << /Fields [6 0 R] >>", " /Annots [5 0 R 6 0 R]", "", &objects));
let reach = worker.clean_scan(doc, &dir.path().join("scan.pdf")).expect("scans");
assert_eq!(reach.hidden_annots, 2, "{reach:?}");
let out = dir.path().join("out.pdf");
worker.clean(doc, Cleaning { hidden: true, ..Cleaning::default() }, &out).expect("cleans");
assert!(!holds(&out, "SECRET-HIDDEN-LINK") && !holds(&out, "SECRET-HIDDEN-WIDGET"));
}
+118
View File
@@ -0,0 +1,118 @@
//! Protected copies and what they allow (PDF-11).
use std::{io::ErrorKind, path::Path};
use ely_pdf_engine::{
Doc, Lock, Worker,
proto::may,
sample::{Extras, pdf_with},
};
fn worker() -> Worker {
Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).expect("the worker")
}
fn plain(dir: &Path) -> (Worker, Doc) {
let path = dir.join("doc.pdf");
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["Hello world"])], &[], &Extras::default())).expect("writes");
let mut worker = worker();
let doc = worker.open(&path).expect("opens").doc;
(worker, doc)
}
/// Writes a copy that opens with "user", allowing only `allow`.
fn locked(dir: &Path, allow: u32) -> std::path::PathBuf {
let (mut worker, doc) = plain(dir);
let out = dir.join("locked.pdf");
let lock = Lock { user: "user".into(), owner: "owner".into(), allow };
worker.protect(doc, &out, Some(&lock)).expect("protects");
out
}
#[test]
fn a_protected_copy_asks_and_holds_its_limits() {
let dir = tempfile::tempdir().expect("a temp dir");
let out = locked(dir.path(), may::PRINT);
let bytes = std::fs::read(&out).expect("reads");
assert!(bytes.windows(5).any(|w| w == b"AESV3"), "AES-256");
let mut worker = worker();
let opened = worker.open(&out).expect("opens");
assert!(opened.locked);
assert!(!worker.unlock(opened.doc, "wrong").expect("tries"));
assert!(worker.unlock(opened.doc, "user").expect("tries"));
let allowed = worker.permissions(opened.doc).expect("reads");
assert_eq!(allowed & (may::PRINT | may::COPY | may::EDIT | may::OWNER), may::PRINT);
let refused = worker.rotate_pages(opened.doc, &[0], 90).expect_err("refused");
assert_eq!((refused.kind(), refused.to_string()), (ErrorKind::PermissionDenied, "assemble".to_string()));
let text: String = worker.text(opened.doc, 0).expect("still reads").iter().map(|c| c.c).collect();
assert!(text.contains("Hello"));
}
#[test]
fn a_wrong_owner_password_keeps_access_and_the_right_one_lifts_limits() {
let dir = tempfile::tempdir().expect("a temp dir");
let out = locked(dir.path(), 0);
let mut worker = worker();
let doc = worker.open(&out).expect("opens").doc;
worker.unlock(doc, "user").expect("unlocks");
assert!(!worker.owner(doc, "nope").expect("tries"));
assert_eq!(worker.pages(doc).expect("still open"), 1);
assert!(worker.text(doc, 0).is_ok());
assert!(worker.owner(doc, "owner").expect("tries"));
assert_ne!(worker.permissions(doc).expect("reads") & may::OWNER, 0);
worker.rotate_pages(doc, &[0], 90).expect("allowed now");
}
#[test]
fn changing_protection_needs_the_owner() {
let dir = tempfile::tempdir().expect("a temp dir");
let out = locked(dir.path(), may::PRINT | may::COPY | may::EDIT);
let mut worker = worker();
let doc = worker.open(&out).expect("opens").doc;
worker.unlock(doc, "user").expect("unlocks");
let bare = dir.path().join("bare.pdf");
let refused = worker.protect(doc, &bare, None).expect_err("refused");
assert_eq!(refused.kind(), ErrorKind::PermissionDenied);
assert!(!bare.exists());
worker.owner(doc, "owner").expect("tries");
worker.protect(doc, &bare, None).expect("unprotects");
let mut other = self::worker();
assert!(!other.open(&bare).expect("opens").locked);
}
#[test]
fn an_owner_password_is_needed_and_must_differ() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = plain(dir.path());
let out = dir.path().join("out.pdf");
for (user, owner) in [("a", ""), ("same", "same")] {
let lock = Lock { user: user.into(), owner: owner.into(), allow: 0 };
assert!(worker.protect(doc, &out, Some(&lock)).is_err(), "{user}/{owner}");
assert!(!out.exists());
}
}
#[test]
fn a_file_with_no_open_password_still_holds_limits() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = plain(dir.path());
let out = dir.path().join("out.pdf");
let lock = Lock { user: String::new(), owner: "owner".into(), allow: may::PRINT };
worker.protect(doc, &out, Some(&lock)).expect("protects");
let mut other = self::worker();
let opened = other.open(&out).expect("opens");
assert!(!opened.locked);
let refused = other.annotate(opened.doc, &[]).expect_err("refused");
assert_eq!((refused.kind(), refused.to_string()), (ErrorKind::PermissionDenied, "annotate".to_string()));
}
#[test]
fn a_wrong_owner_try_on_aes_128_keeps_the_text_readable() {
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/pdf/aes128.pdf");
let mut worker = worker();
let doc = worker.open(&fixture).expect("opens").doc;
assert!(worker.unlock(doc, "user").expect("tries"));
assert!(!worker.owner(doc, "nope").expect("tries"));
let text: String = worker.text(doc, 0).expect("reads").iter().map(|c| c.c).collect();
assert_eq!(text.trim(), "Hello");
}
+244
View File
@@ -0,0 +1,244 @@
//! Signing by update and checking what was signed (PDF-12, QA-15).
use std::{io::ErrorKind, path::{Path, PathBuf}};
use ely_pdf_engine::{
Digest, Doc, Lock, Signing, Worker,
proto::may,
sample::{Extras, one_page, pdf_with},
};
fn worker() -> Worker {
Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).expect("the worker")
}
fn opened(path: &Path) -> (Worker, Doc) {
let mut worker = worker();
let doc = worker.open(path).expect("opens").doc;
(worker, doc)
}
fn plain(dir: &Path) -> PathBuf {
let path = dir.join("doc.pdf");
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["Hello world"])], &[], &Extras::default())).expect("writes");
path
}
fn signing(object: Option<u32>, name: &str, password: &str) -> Signing {
let pfx = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/pdf/signer.p12");
Signing {
page: 0,
object,
rect: [72.0, 600.0, 272.0, 660.0],
name: name.into(),
pfx: pfx.to_str().expect("UTF-8").into(),
pfx_password: password.into(),
reason: "Approved".into(),
location: "Here".into(),
}
}
#[test]
fn a_new_signature_checks_and_the_document_stays() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = plain(dir.path());
let (mut worker, doc) = opened(&path);
let before = worker.history(doc).expect("history");
let out = dir.path().join("signed.pdf");
worker.sign(doc, &out, &signing(None, "Signature1", "ely")).expect("signs");
assert!(worker.signatures(doc, &path).expect("reads").is_empty(), "the open document has none");
assert_eq!(worker.history(doc).expect("history"), before);
let (mut other, read) = opened(&out);
let found = other.signatures(read, &out).expect("reads");
assert_eq!(found.len(), 1);
let signed = found[0].signed.as_ref().expect("signed");
assert_eq!((signed.digest, signed.later, signed.broken), (Digest::Intact, false, false));
assert_eq!((signed.common.as_str(), signed.reason.as_str(), signed.location.as_str()), ("Ely Test Signer", "Approved", "Here"));
assert!(signed.date.starts_with("D:"), "{}", signed.date);
assert_eq!(found[0].page, Some(0), "found through the page's annotations");
assert!(signed.certs.first().is_some_and(|der| der.first() == Some(&0x30)), "DER");
}
#[test]
fn a_wrong_certificate_password_writes_nothing() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&plain(dir.path()));
let out = dir.path().join("signed.pdf");
assert!(worker.sign(doc, &out, &signing(None, "Signature1", "nope")).is_err());
assert!(!out.exists());
}
#[test]
fn a_changed_byte_shows_as_changed() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&plain(dir.path()));
let out = dir.path().join("signed.pdf");
worker.sign(doc, &out, &signing(None, "Signature1", "ely")).expect("signs");
let mut bytes = std::fs::read(&out).expect("reads");
let at = bytes.windows(5).position(|w| w == b"Hello").expect("the text");
bytes[at] = b'J';
std::fs::write(&out, bytes).expect("writes");
let (mut other, read) = opened(&out);
let signed = other.signatures(read, &out).expect("reads")[0].signed.clone().expect("signed");
assert_eq!(signed.digest, Digest::Changed);
}
#[test]
fn a_second_signature_leaves_the_first_intact_and_marked_later() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&plain(dir.path()));
let once = dir.path().join("once.pdf");
worker.sign(doc, &once, &signing(None, "First", "ely")).expect("signs");
let (mut other, read) = opened(&once);
let twice = dir.path().join("twice.pdf");
let mut second = signing(None, "Second", "ely");
second.rect = [300.0, 600.0, 500.0, 660.0];
other.sign(read, &twice, &second).expect("signs again");
let (mut third, last) = opened(&twice);
let found = third.signatures(last, &twice).expect("reads");
let first = found.iter().find(|s| s.name == "First").and_then(|s| s.signed.clone()).expect("first");
assert_eq!((first.digest, first.later), (Digest::Intact, true));
let next = found.iter().find(|s| s.name == "Second").and_then(|s| s.signed.clone()).expect("second");
assert_eq!((next.digest, next.later), (Digest::Intact, false));
}
#[test]
fn an_empty_field_is_signed_in_place() {
let dir = tempfile::tempdir().expect("a temp dir");
let field = "<< /Type /Annot /Subtype /Widget /FT /Sig /T (Sign here) /Rect [72 600 272 660] /P 3 0 R /F 4 >>".to_string();
let path = dir.path().join("form.pdf");
std::fs::write(&path, one_page(" /AcroForm << /Fields [5 0 R] /SigFlags 3 >>", " /Annots [5 0 R]", "", &[field])).expect("writes");
let (mut worker, doc) = opened(&path);
let empty = worker.signatures(doc, &path).expect("reads");
assert_eq!((empty.len(), empty[0].signed.is_none(), empty[0].name.as_str()), (1, true, "Sign here"));
let out = dir.path().join("signed.pdf");
worker.sign(doc, &out, &signing(Some(empty[0].object), "", "ely")).expect("signs");
let (mut other, read) = opened(&out);
let found = other.signatures(read, &out).expect("reads");
assert_eq!(found.len(), 1);
assert_eq!(found[0].signed.as_ref().map(|s| s.digest), Some(Digest::Intact));
}
#[test]
fn a_new_field_needs_leave_to_change_the_page() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&plain(dir.path()));
let limited = dir.path().join("limited.pdf");
let lock = Lock { user: String::new(), owner: "owner".into(), allow: may::PRINT | may::FILL };
worker.protect(doc, &limited, Some(&lock)).expect("protects");
let (mut other, read) = opened(&limited);
let refused = other.sign(read, &dir.path().join("signed.pdf"), &signing(None, "S", "ely")).expect_err("refused");
assert_eq!(refused.kind(), ErrorKind::PermissionDenied);
}
#[test]
fn an_ordinary_save_of_a_signed_file_carries_no_signature() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&plain(dir.path()));
let signed = dir.path().join("signed.pdf");
worker.sign(doc, &signed, &signing(None, "Signature1", "ely")).expect("signs");
let (mut other, read) = opened(&signed);
assert_eq!(other.signed(read).expect("counts"), 1);
other.rotate_pages(read, &[0], 90).expect("edits");
let copy = dir.path().join("copy.pdf");
other.save(read, &copy).expect("saves");
let (mut third, again) = opened(&copy);
assert_eq!(third.signed(again).expect("counts"), 0);
let fields = third.signatures(again, &copy).expect("reads");
assert_eq!((fields.len(), fields[0].signed.is_none()), (1, true), "the field stays, empty");
}
#[test]
fn the_signer_is_the_certificate_given() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&plain(dir.path()));
let out = dir.path().join("signed.pdf");
worker.sign(doc, &out, &signing(None, "Signature1", "ely")).expect("signs");
let (mut other, read) = opened(&out);
let signed = other.signatures(read, &out).expect("reads")[0].signed.clone().expect("signed");
let der = std::fs::read(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/pdf/signer.der")).expect("reads");
assert_eq!(signed.certs, vec![der]);
}
#[test]
fn a_typed_signature_writes_in_the_script_face_and_says_what_it_lacks() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&plain(dir.path()));
let writing = |text: &str| ely_pdf_engine::Writing {
block: None,
expected: String::new(),
text: text.into(),
font: "script".into(),
size: 28.0,
rgb: 0,
bx: [72.0, 600.0, 400.0],
};
assert!(matches!(worker.put_text(doc, 0, &writing("Ada Lovelace"), 0.0).expect("writes"), ely_pdf_engine::Wrote::Done(_)));
doc = worker.now(doc).expect("the state");
let text: String = worker.text(doc, 0).expect("reads").iter().map(|c| c.c).collect();
assert!(text.contains("Ada Lovelace"), "{text}");
let saved = dir.path().join("typed.pdf");
worker.save(doc, &saved).expect("saves");
let bytes = std::fs::read(&saved).expect("reads");
assert!(bytes.windows(10).any(|w| w == b"GreatVibes"), "the script face is embedded");
assert!(matches!(worker.put_text(doc, 0, &writing("王"), 0.0).expect("asks"), ely_pdf_engine::Wrote::Missing('王')));
}
#[test]
fn signatures_are_read_from_the_file_named() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&plain(dir.path()));
let signed = dir.path().join("signed.pdf");
worker.sign(doc, &signed, &signing(None, "Signature1", "ely")).expect("signs");
let (mut other, read) = opened(&signed);
let (mark, copy) = (other.history(read).expect("history"), dir.path().join("copy.pdf"));
other.save(read, &copy).expect("saves");
assert_eq!(other.history(read).expect("history"), mark, "saving edits nothing");
assert!(other.signatures(read, &copy).expect("reads")[0].signed.is_none(), "the copy holds none");
assert!(other.signatures(read, &signed).expect("reads")[0].signed.is_some(), "the signed file still does");
}
#[test]
fn a_signature_on_no_page_is_still_listed() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&plain(dir.path()));
let out = dir.path().join("signed.pdf");
worker.sign(doc, &out, &signing(None, "Signature1", "ely")).expect("signs");
let mut bytes = std::fs::read(&out).expect("reads");
// No page lists the widget; the form still does.
let at = bytes.windows(7).rposition(|w| w == b"/Annots").expect("the page's annotations");
bytes[at + 1] = b'X';
std::fs::write(&out, bytes).expect("writes");
let (mut other, read) = opened(&out);
let found = other.signatures(read, &out).expect("reads");
assert_eq!(found.len(), 1);
assert_eq!((found[0].page, found[0].signed.is_some()), (None, true));
}
#[test]
fn a_failed_save_leaves_the_history_whole() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&plain(dir.path()));
assert!(worker.save(doc, &dir.path().join("missing/out.pdf")).is_err());
let mark = worker.rotate_pages(doc, &[0], 90).expect("rotates");
doc = worker.now(doc).expect("the state");
assert_ne!(mark, "0", "the edit is a step of its own");
worker.undo(doc, false).expect("undoes");
}
#[test]
fn a_field_emptied_by_a_save_can_be_signed_again() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&plain(dir.path()));
let signed = dir.path().join("signed.pdf");
worker.sign(doc, &signed, &signing(None, "Signature1", "ely")).expect("signs");
let (mut other, read) = opened(&signed);
let copy = dir.path().join("copy.pdf");
other.save(read, &copy).expect("saves");
let (mut third, again) = opened(&copy);
let field = third.signatures(again, &copy).expect("reads")[0].clone();
let out = dir.path().join("resigned.pdf");
third.sign(again, &out, &signing(Some(field.object), "", "ely")).expect("signs the emptied field");
let found = third.signatures(again, &out).expect("reads");
assert_eq!(found[0].signed.as_ref().map(|s| s.digest), Some(Digest::Intact));
}
+191
View File
@@ -0,0 +1,191 @@
//! Bookmarks, links, properties, attachments against the real worker (PDF-07).
use std::path::Path;
use ely_pdf_engine::{
BookmarkChange, Doc, LinkChange, Target, Worker,
sample::{Extras, pdf_with},
};
fn worker() -> Worker {
Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).expect("the worker")
}
/// Two pages, bookmarks One (with Sub) and Two, a link.
fn opened(path: &Path) -> (Worker, Doc) {
let pages: [(f32, f32, &[&str]); 2] = [(612.0, 792.0, &["Page one"]), (612.0, 792.0, &["Page two"])];
let marks = [("One", 0, &[("Sub", 1)][..]), ("Two", 1, &[][..])];
std::fs::write(path, pdf_with(&pages, &marks, &Extras { links: &[(0, 1)], ..Extras::default() })).expect("writes");
let mut worker = worker();
let doc = worker.open(path).expect("opens").doc;
(worker, doc)
}
/// The outline as indented titles with their pages.
fn tree(worker: &mut Worker, doc: Doc) -> Vec<String> {
fn walk(marks: &[ely_pdf_engine::Bookmark], depth: usize, out: &mut Vec<String>) {
for mark in marks {
out.push(format!("{}{} {}", "-".repeat(depth), mark.title, mark.page.map_or(-1, |p| p as i64)));
walk(&mark.down, depth + 1, out);
}
}
let mut out = Vec::new();
walk(&worker.outline(doc).expect("outline"), 0, &mut out);
out
}
#[test]
fn bookmarks_add_rename_retarget_and_go() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
assert_eq!(tree(&mut worker, doc), ["One 0", "-Sub 1", "Two 1"]);
worker.change_bookmark(doc, &BookmarkChange::Add { path: vec![2], title: "Three".into(), page: 1, y: 100.0 }).expect("adds");
doc = worker.now(doc).expect("the state");
worker.change_bookmark(doc, &BookmarkChange::Add { path: vec![0, 1], title: "Under".into(), page: 0, y: 0.0 }).expect("adds below");
doc = worker.now(doc).expect("the state");
let rename = BookmarkChange::Edit { path: vec![1], expect: "Two".into(), title: Some("Deux".into()), to: Some((0, 50.0)) };
worker.change_bookmark(doc, &rename).expect("edits");
doc = worker.now(doc).expect("the state");
assert_eq!(tree(&mut worker, doc), ["One 0", "-Sub 1", "-Under 0", "Deux 0", "Three 1"]);
assert!(worker.change_bookmark(doc, &BookmarkChange::Delete { path: vec![1], expect: "Two".into() }).is_err(), "renamed since");
assert!(worker.change_bookmark(doc, &BookmarkChange::Delete { path: vec![9], expect: "One".into() }).is_err(), "no such place");
worker.change_bookmark(doc, &BookmarkChange::Delete { path: vec![0], expect: "One".into() }).expect("deletes with its children");
doc = worker.now(doc).expect("the state");
assert_eq!(tree(&mut worker, doc), ["Deux 0", "Three 1"]);
worker.undo(doc, false).expect("undoes");
doc = worker.now(doc).expect("the state");
assert_eq!(tree(&mut worker, doc).len(), 5, "one step");
let saved = dir.path().join("saved.pdf");
worker.save(doc, &saved).expect("saves");
let mut other = self::worker();
let reopened = other.open(&saved).expect("opens").doc;
assert_eq!(tree(&mut other, reopened), ["One 0", "-Sub 1", "-Under 0", "Deux 0", "Three 1"]);
}
#[test]
fn links_list_add_retarget_and_go() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
let links = worker.links(doc, 0).expect("links");
assert_eq!((links.len(), links[0].page), (1, Some(1)), "the sample links to page two");
let web = Target::Web("https://example.com/".into());
worker.change_link(doc, 0, &LinkChange::Add { rect: [72.0, 300.0, 200.0, 320.0], to: web.clone() }).expect("adds");
doc = worker.now(doc).expect("the state");
let links = worker.links(doc, 0).expect("links");
assert_eq!((links[1].uri.as_str(), links[1].page), ("https://example.com/", None));
let first = links[0].uri.clone();
worker.change_link(doc, 0, &LinkChange::Edit { index: 0, expect: first.clone(), to: Target::Page(0, 200.0) }).expect("retargets");
doc = worker.now(doc).expect("the state");
assert_eq!(worker.links(doc, 0).expect("links")[0].page, Some(0));
assert!(worker.change_link(doc, 0, &LinkChange::Delete { index: 0, expect: first }).is_err(), "changed since");
for unsafe_uri in ["javascript:alert(1)", "file:///etc/passwd"] {
let add = LinkChange::Add { rect: [72.0, 400.0, 200.0, 420.0], to: Target::Web(unsafe_uri.into()) };
assert!(worker.change_link(doc, 0, &add).is_err(), "{unsafe_uri}");
}
let now = worker.links(doc, 0).expect("links")[1].uri.clone();
worker.change_link(doc, 0, &LinkChange::Delete { index: 1, expect: now }).expect("deletes");
doc = worker.now(doc).expect("the state");
assert_eq!(worker.links(doc, 0).expect("links").len(), 1);
}
#[test]
fn properties_read_set_and_keep() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
assert_eq!(worker.properties(doc).expect("reads").title, None, "the sample has none");
worker.set_properties(doc, &[("Title", "Report"), ("Author", "Zoe")]).expect("sets");
doc = worker.now(doc).expect("the state");
let read = worker.properties(doc).expect("reads");
assert_eq!((read.title.as_deref(), read.author.as_deref()), (Some("Report"), Some("Zoe")));
assert!(worker.set_properties(doc, &[("Title", "x"), ("Producer", "Ely")]).is_err(), "the producer is the producer's");
assert_eq!(worker.properties(doc).expect("reads").title.as_deref(), Some("Report"), "nothing changed");
let saved = dir.path().join("saved.pdf");
worker.save(doc, &saved).expect("saves");
let mut other = self::worker();
let reopened = other.open(&saved).expect("opens").doc;
assert_eq!(other.properties(reopened).expect("reads").author.as_deref(), Some("Zoe"));
}
#[test]
fn attachments_embed_read_and_go() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
assert!(worker.attachments(doc).expect("lists").is_empty());
let file = dir.path().join("notes.txt");
std::fs::write(&file, "seven words of notes for the file").expect("writes");
worker.attach(doc, "notes.txt", &file).expect("attaches");
doc = worker.now(doc).expect("the state");
worker.attach(doc, "b.txt", &file).expect("attaches another");
doc = worker.now(doc).expect("the state");
let list = worker.attachments(doc).expect("lists");
let names: Vec<(&str, u64)> = list.iter().map(|a| (a.name.as_str(), a.size)).collect();
assert_eq!(names, [("b.txt", 33), ("notes.txt", 33)], "by name");
assert_eq!(worker.attachment(doc, "n:notes.txt").expect("reads"), b"seven words of notes for the file");
assert!(worker.attach(doc, "notes.txt", &file).is_err(), "the name is taken");
worker.detach(doc, "n:b.txt").expect("removes");
doc = worker.now(doc).expect("the state");
assert!(worker.attachment(doc, "n:b.txt").is_err());
let saved = dir.path().join("saved.pdf");
worker.save(doc, &saved).expect("saves");
let mut other = self::worker();
let reopened = other.open(&saved).expect("opens").doc;
let kept: Vec<String> = other.attachments(reopened).expect("lists").into_iter().map(|a| a.name).collect();
assert_eq!(kept, ["notes.txt"]);
}
#[test]
fn a_file_on_a_page_lists_reads_and_goes_by_its_key() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = dir.path().join("doc.pdf");
let extras = Extras { attached: Some(("note.txt", b"notes")), ..Extras::default() };
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["Hello"])], &[], &extras)).expect("writes");
let mut worker = worker();
let mut doc = worker.open(&path).expect("opens").doc;
let list = worker.attachments(doc).expect("lists");
assert_eq!(list.len(), 1, "the page's file is an attachment");
assert_eq!((list[0].name.as_str(), list[0].page, list[0].size), ("note.txt", Some(0), 5), "its size from its bytes");
assert!(list[0].key.starts_with("a:0:"));
assert_eq!(worker.attachment(doc, &list[0].key).expect("reads"), b"notes");
worker.detach(doc, &list[0].key).expect("removes");
doc = worker.now(doc).expect("the state");
assert!(worker.attachments(doc).expect("lists").is_empty());
assert!(worker.marks(doc, 0).expect("marks").annots.is_empty(), "with its annotation");
}
#[test]
fn reading_an_attachment_leaves_its_bytes_in_the_document() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, mut doc) = opened(&dir.path().join("doc.pdf"));
let file = dir.path().join("note.txt");
std::fs::write(&file, "notes").expect("writes");
worker.attach(doc, "note.txt", &file).expect("attaches");
doc = worker.now(doc).expect("the state");
let mark = worker.history(doc).expect("a mark");
for _ in 0..2 {
assert_eq!(worker.attachment(doc, "n:note.txt").expect("reads"), b"notes");
}
assert_eq!(worker.history(doc).expect("a mark"), mark, "reading changes nothing");
let saved = dir.path().join("saved.pdf");
worker.save(doc, &saved).expect("saves");
let mut other = self::worker();
let reopened = other.open(&saved).expect("opens").doc;
assert_eq!(other.attachment(reopened, "n:note.txt").expect("reads"), b"notes", "whole in the file");
}
#[test]
fn an_attachment_read_before_a_change_is_refused() {
let dir = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(&dir.path().join("doc.pdf"));
let (old, new) = (dir.path().join("old.txt"), dir.path().join("new.txt"));
std::fs::write(&old, "old report").expect("writes");
std::fs::write(&new, "new report").expect("writes");
let first = doc.at(&worker.attach(doc, "report.txt", &old).expect("attaches")).expect("the state");
let gone = first.at(&worker.detach(first, "n:report.txt").expect("removes")).expect("the state");
let now = gone.at(&worker.attach(gone, "report.txt", &new).expect("attaches again")).expect("the state");
// The same key names another file now: refused.
let read = worker.attachment(first, "n:report.txt").expect_err("an older state");
assert!(ely_pdf_engine::is_stale(&read), "{read}");
let removed = worker.detach(first, "n:report.txt").expect_err("an older state");
assert!(ely_pdf_engine::is_stale(&removed), "{removed}");
assert_eq!(worker.attachment(now, "n:report.txt").expect("reads"), b"new report", "the new file stays");
}
+287
View File
@@ -0,0 +1,287 @@
//! The worker against generated PDFs.
use std::path::Path;
use ely_pdf_engine::{Doc, Worker, sample::pdf};
fn worker() -> Worker {
Worker::spawn_from(Path::new(env!("CARGO_BIN_EXE_ely-pdf-worker"))).expect("the worker")
}
fn opened(dir: &Path, bytes: &[u8]) -> (Worker, Doc) {
let path = dir.join("doc.pdf");
std::fs::write(&path, bytes).expect("writes");
let mut worker = worker();
let opened = worker.open(&path).expect("opens");
assert!(!opened.locked);
(worker, opened.doc)
}
#[test]
fn pages_measure_render_and_read_their_text() {
let dir = tempfile::tempdir().expect("a temp dir");
let bytes = pdf(
&[
(612.0, 792.0, &["Ely Office contract", "Second line"]),
(842.0, 595.0, &["Wide page"]),
],
&[],
);
let (mut worker, doc) = opened(dir.path(), &bytes);
assert_eq!(worker.pages(doc).expect("counts"), 2);
assert_eq!(
worker.bounds(doc, 0, 2).expect("bounds"),
[[0.0, 0.0, 612.0, 792.0], [0.0, 0.0, 842.0, 595.0]]
);
// Page one's top left, twice its size: text is dark.
let tile = worker
.render(doc, 0, (2.0, 0), [0, 0, 600, 300], false)
.expect("renders").bgra;
assert_eq!(tile.len(), 600 * 300 * 4);
let dark = tile.chunks_exact(4).filter(|p| p[0] < 128).count();
assert!(dark > 100, "text shows: {dark}");
let night = worker
.render(doc, 0, (2.0, 0), [0, 0, 600, 300], true)
.expect("renders dark").bgra;
let light = night.chunks_exact(4).filter(|p| p[0] > 128).count();
assert!(
light > 100 && night[0] < 64,
"dark mode turns paper dark and text light"
);
// A quarter turn makes the page tall side wide.
assert!(
worker
.render(doc, 1, (1.0, 90), [0, 0, 595, 842], false)
.is_ok()
);
let text: String = worker
.text(doc, 0)
.expect("reads")
.iter()
.map(|c| c.c)
.collect();
assert!(
text.contains("Ely Office contract") && text.contains("Second line"),
"{text}"
);
let chars = worker.text(doc, 0).expect("reads");
assert!(
chars.windows(2).all(|w| w[0].line <= w[1].line),
"reading order"
);
assert!(
chars[0].rect[1] < chars.last().expect("chars").rect[1],
"first line above"
);
}
#[test]
fn search_finds_every_hit_and_bookmarks_nest() {
let dir = tempfile::tempdir().expect("a temp dir");
let pages: &[(f32, f32, &[&str])] = &[
(612.0, 792.0, &["alpha beta alpha"]),
(612.0, 792.0, &["no match here"]),
(612.0, 792.0, &["Alpha at last"]),
];
let bytes = pdf(pages, &[("Intro", 0, &[("Detail", 1)]), ("End", 2, &[])]);
let (mut worker, doc) = opened(dir.path(), &bytes);
let hits: Vec<usize> = (0..3)
.map(|page| worker.search(doc, page, "alpha").expect("searches").len())
.collect();
assert_eq!(hits, [2, 0, 1], "case is ignored");
let quad = worker.search(doc, 0, "beta").expect("searches")[0][0];
assert!(quad[0] > 72.0 && quad[1] > 0.0, "{quad:?}");
let marks = worker.outline(doc).expect("outline");
let flat: Vec<(&str, Option<usize>, usize)> = marks
.iter()
.map(|m| (m.title.as_str(), m.page, m.down.len()))
.collect();
assert_eq!(flat, [("Intro", Some(0), 1), ("End", Some(2), 0)]);
assert_eq!(
(marks[0].down[0].title.as_str(), marks[0].down[0].page),
("Detail", Some(1))
);
worker.close(doc).expect("closes");
assert!(worker.pages(doc).is_err(), "a closed document is gone");
}
#[test]
fn a_broken_file_is_an_error_and_the_worker_lives_on() {
let dir = tempfile::tempdir().expect("a temp dir");
let path = dir.path().join("junk.pdf");
std::fs::write(&path, b"this is not a pdf at all").expect("writes");
let mut worker = worker();
let failed = worker.open(&path).expect_err("not a PDF");
assert!(!failed.to_string().is_empty());
assert!(worker.open(&dir.path().join("missing.pdf")).is_err());
let good = dir.path().join("good.pdf");
std::fs::write(&good, pdf(&[(612.0, 792.0, &["Still here"])], &[])).expect("writes");
assert_eq!(
worker.open(&good).map(|o| o.locked).ok(),
Some(false),
"the worker still works"
);
}
#[test]
fn a_thousand_pages_show_the_first_in_time() {
let dir = tempfile::tempdir().expect("a temp dir");
let lines: &[&str] = &["A long report, page after page."];
let pages: Vec<(f32, f32, &[&str])> = (0..1000).map(|_| (612.0, 792.0, lines)).collect();
let bytes = pdf(&pages, &[]);
let started = std::time::Instant::now();
let (mut worker, doc) = opened(dir.path(), &bytes);
assert_eq!(worker.pages(doc).expect("counts"), 1000);
// What the viewer asks before its first frame.
worker.bounds(doc, 0, 100).expect("the first sizes");
worker.render(doc, 0, (2.0, 0), [0, 0, 512, 512], false).expect("the first tile");
let took = started.elapsed();
assert!(took < std::time::Duration::from_secs(3), "PERF-05: {took:?}");
}
#[test]
fn every_hit_counts_once_however_many_or_however_split() {
let dir = tempfile::tempdir().expect("a temp dir");
let busy = ["alpha "; 12].concat();
let many: Vec<&str> = (0..50).map(|_| busy.as_str()).collect();
let pages: &[(f32, f32, &[&str])] = &[(1000.0, 1400.0, &many), (612.0, 792.0, &["hello", "world"])];
let (mut worker, doc) = opened(dir.path(), &pdf(pages, &[]));
assert_eq!(worker.search(doc, 0, "alpha").expect("searches").len(), 600, "no cap on hits");
let split = worker.search(doc, 1, "hello world").expect("searches");
assert_eq!(split.len(), 1, "one phrase over two lines");
assert_eq!(split[0].len(), 2, "with a quad on each line");
}
#[cfg(unix)]
#[test]
fn a_worker_from_other_sources_is_refused() {
let dir = tempfile::tempdir().expect("a temp dir");
// Answers HELLO with someone else's stamp.
let stale = dir.path().join("stale-worker");
std::fs::write(
&stale,
"#!/usr/bin/env python3\nimport struct, sys\nhead = sys.stdin.buffer.read(13)\nstamp = b'old'\nbody = struct.pack('<I', len(stamp)) + stamp\nsys.stdout.buffer.write(struct.pack('<I', len(body)) + bytes([100]) + head[5:13] + body)\nsys.stdout.flush()\n",
)
.expect("writes");
std::fs::set_permissions(&stale, std::os::unix::fs::PermissionsExt::from_mode(0o755)).expect("executable");
let refused = Worker::spawn_from(&stale).err().expect("refused");
assert!(refused.to_string().contains("other sources"), "{refused}");
}
#[cfg(unix)]
#[test]
fn a_reply_to_another_request_is_refused() {
let dir = tempfile::tempdir().expect("a temp dir");
// Answers HELLO under the next request's id.
let crossed = dir.path().join("crossed-worker");
std::fs::write(
&crossed,
"#!/usr/bin/env python3\nimport struct, sys\nhead = sys.stdin.buffer.read(13)\nasked = struct.unpack('<Q', head[5:13])[0]\nbody = struct.pack('<I', 3) + b'any'\nsys.stdout.buffer.write(struct.pack('<I', len(body)) + bytes([100]) + struct.pack('<Q', asked + 1) + body)\nsys.stdout.flush()\n",
)
.expect("writes");
std::fs::set_permissions(&crossed, std::os::unix::fs::PermissionsExt::from_mode(0o755)).expect("executable");
let refused = Worker::spawn_from(&crossed).err().expect("refused");
assert!(refused.to_string().contains("not 1"), "{refused}");
}
/// A noisy RGB PNG, stored uncompressed: large on disk.
fn big_png(path: &Path, side: u32) {
let crc = |bytes: &[u8]| {
let mut crc = 0xffff_ffffu32;
for byte in bytes {
crc ^= u32::from(*byte);
for _ in 0..8 {
crc = if crc & 1 == 1 { 0xedb8_8320 ^ (crc >> 1) } else { crc >> 1 };
}
}
!crc
};
let chunk = |kind: &[u8], data: &[u8]| {
let body = [kind, data].concat();
[&(data.len() as u32).to_be_bytes()[..], &body, &crc(&body).to_be_bytes()].concat()
};
let mut raw = Vec::new();
let mut seed = 7u32;
for _ in 0..side {
raw.push(0);
for _ in 0..side * 3 {
seed = seed.wrapping_mul(1_103_515_245).wrapping_add(12_345);
raw.push((seed >> 16) as u8);
}
}
// zlib with stored blocks; Adler-32 at the end.
let mut zlib = vec![0x78, 0x01];
let blocks: Vec<&[u8]> = raw.chunks(65_535).collect();
for (n, block) in blocks.iter().enumerate() {
zlib.push(u8::from(n + 1 == blocks.len()));
let len = block.len() as u16;
zlib.extend_from_slice(&len.to_le_bytes());
zlib.extend_from_slice(&(!len).to_le_bytes());
zlib.extend_from_slice(block);
}
let (mut a, mut b) = (1u32, 0u32);
for byte in &raw {
a = (a + u32::from(*byte)) % 65_521;
b = (b + a) % 65_521;
}
zlib.extend_from_slice(&((b << 16) | a).to_be_bytes());
let header = [&side.to_be_bytes()[..], &side.to_be_bytes(), &[8, 2, 0, 0, 0]].concat();
let png = [&b"\x89PNG\r\n\x1a\n"[..], &chunk(b"IHDR", &header), &chunk(b"IDAT", &zlib), &chunk(b"IEND", &[])].concat();
std::fs::write(path, png).expect("writes");
}
#[cfg(unix)]
#[test]
fn a_failed_open_keeps_no_memory() {
// An image MuPDF opens, then refuses to journal.
let dir = tempfile::tempdir().expect("a temp dir");
let png = dir.path().join("big.png");
big_png(&png, 1200);
let mut worker = Worker::spawn().expect("a worker");
let rss = |pid: u32| -> u64 {
let out = std::process::Command::new("ps").args(["-o", "rss=", "-p", &pid.to_string()]).output().expect("ps");
String::from_utf8_lossy(&out.stdout).trim().parse().expect("kilobytes")
};
// MuPDF's 256 MB store fills first; then nothing grows.
for _ in 0..70 {
let refused = worker.open(&png).err().expect("not a PDF");
assert!(refused.to_string().contains("not a PDF"), "{refused}");
}
let before = rss(worker.pid());
for _ in 0..50 {
assert!(worker.open(&png).is_err(), "not a PDF");
}
let grown = rss(worker.pid()).saturating_sub(before);
assert!(grown < 40_000, "50 more failed opens of a 4 MB image kept {grown} KiB");
}
#[test]
fn a_document_belongs_to_its_worker() {
let first = tempfile::tempdir().expect("a temp dir");
let (old_worker, old) = opened(first.path(), &pdf(&[(300.0, 500.0, &["A"])], &[]));
drop(old_worker);
let second = tempfile::tempdir().expect("a temp dir");
let (mut worker, doc) = opened(second.path(), &pdf(&[(300.0, 500.0, &["B"])], &[]));
// An old worker's handle never reaches this one's documents.
assert!(worker.close(old).is_err(), "another worker's document closed");
assert!(worker.pages(old).is_err(), "another worker's document answered");
assert_eq!(worker.pages(doc).expect("its own document"), 1);
}
#[test]
fn an_edit_made_for_an_older_state_is_refused() {
let dir = tempfile::tempdir().expect("a temp dir");
let pages: &[(f32, f32, &[&str])] = &[(300.0, 500.0, &["A"]), (300.0, 500.0, &["B"]), (300.0, 500.0, &["C"])];
let (mut worker, before) = opened(dir.path(), &pdf(pages, &[]));
let mark = worker.delete_pages(before, &[0]).expect("deletes A");
let after = before.at(&mark).expect("the new state");
// Page 1 was B, now C; the old aim fails.
let stale = worker.rotate_pages(before, &[1], 90).expect_err("an older state");
assert!(ely_pdf_engine::is_stale(&stale), "{stale}");
assert!(worker.bounds(before, 0, 1).is_err(), "a read for an older state");
assert_eq!(worker.bounds(after, 1, 1).expect("C")[0], [0.0, 0.0, 300.0, 500.0], "C stayed upright");
worker.rotate_pages(after, &[1], 90).expect("rotates C");
// Whole-document requests take any state.
assert_eq!(worker.pages(before).expect("pages"), 2);
}