imprimatur 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -5
- data/Cargo.lock +4 -3
- data/Cargo.toml +2 -1
- data/README.md +18 -7
- data/ext/imprimatur_native/cli/Cargo.toml +1 -1
- data/ext/imprimatur_native/cli/src/cli/markdown.rs +17 -5
- data/ext/imprimatur_native/core/Cargo.toml +3 -2
- data/ext/imprimatur_native/core/README.md +6 -1
- data/ext/imprimatur_native/core/src/read/budget.rs +15 -0
- data/ext/imprimatur_native/core/src/read/content.rs +1 -0
- data/ext/imprimatur_native/core/src/read/json.rs +1 -1
- data/ext/imprimatur_native/core/src/read/markdown.rs +8 -1
- data/ext/imprimatur_native/core/src/read/pdf/bounded.rs +94 -35
- data/ext/imprimatur_native/core/src/read/pdf/document/labels.rs +152 -0
- data/ext/imprimatur_native/core/src/read/pdf/document.rs +1 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation/protocol.rs +145 -76
- data/ext/imprimatur_native/core/src/read/pdf/isolation/stream.rs +113 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation/system.rs +27 -0
- data/ext/imprimatur_native/core/src/read/pdf/isolation/worker.rs +49 -18
- data/ext/imprimatur_native/core/src/read/pdf/isolation.rs +117 -28
- data/ext/imprimatur_native/core/src/read/pdf/layout.rs +190 -54
- data/ext/imprimatur_native/core/src/read/pdf.rs +2 -1
- data/ext/imprimatur_native/ffi/Cargo.toml +1 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +2 -2
- data/ext/imprimatur_native/ffi/src/pdf.rs +20 -5
- data/lib/imprimatur/pdf_reader.rb +6 -4
- data/lib/imprimatur/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9812ca055242755ab6117d2c5eba9bb1991e02a21572682f9f36c2a0481e2139
|
|
4
|
+
data.tar.gz: 8e1d32d440ec79ea732c62dd0a830c0b13f76655df7f9b85d60d539e0d429936
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3572b1a17819ae0fa85346788343f19ef9410d19f48b7257edb95dc00544ae1234991a5d0b50284ed3d10e21f34ca991f965aa7956fecb6a03244805870cea13
|
|
7
|
+
data.tar.gz: 9399ac644f99a699a12a2a8bdcefe73977bddcec75571e8053d984d829ddc33817fd40bb6c9c79fd15ce89f56fbe472035c276e3177430b9a35ab20023b66f16
|
data/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
- Isolate PDF metadata and text reads in the bundled resource-limited Rust
|
|
6
|
+
worker, as rendering already is; all three accept `memory_bytes:`.
|
|
7
|
+
- On Linux/FreeBSD, re-exec the worker under its address-space limit before
|
|
8
|
+
reading the PDF, including tiny metadata-only reads.
|
|
9
|
+
- Preserve completed pages on worker termination and recover healthy neighbors
|
|
10
|
+
after page-local stream limits without renewing the overall deadline.
|
|
11
|
+
- Allow a bounded exit grace when macOS cannot inspect an exiting worker;
|
|
12
|
+
persistent monitor failures still terminate and reap a live worker.
|
|
13
|
+
|
|
14
|
+
## [0.7.0](https://github.com/seuros/imprimatur/compare/v0.6.1...v0.7.0) (2026-10-26)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **pdf:** add --drop-repeated to remove watermarks anywhere on the page ([8ea82c9](https://github.com/seuros/imprimatur/commit/8ea82c98f6b507ff7ffd296f8e0ec8f30aed529f))
|
|
20
|
+
* **pdf:** emit printed page labels from /PageLabels or footer text ([f761555](https://github.com/seuros/imprimatur/commit/f76155578acfc8514465b93b5b019df100d54de7))
|
|
21
|
+
* **pdf:** recognize numbered sections as headings in markdown output ([341f651](https://github.com/seuros/imprimatur/commit/341f651631b74ff93879f9eba71d0874ae261fd1))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Bug Fixes
|
|
25
|
+
|
|
26
|
+
* **pdf:** keep ordinal superscripts attached to their base text ([21fbb51](https://github.com/seuros/imprimatur/commit/21fbb510f90ab7c029d22e4e396b887fa66e16c6))
|
|
27
|
+
|
|
28
|
+
## [0.6.1](https://github.com/seuros/imprimatur/compare/v0.6.0...v0.6.1) (2026-10-25)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Bug Fixes
|
|
32
|
+
|
|
33
|
+
* **pdf:** isolate bounded reads and preserve page recovery ([fbfbbc5](https://github.com/seuros/imprimatur/commit/fbfbbc5e7e44768efae293b37be255b8b797f831))
|
|
10
34
|
|
|
11
35
|
## [0.6.0](https://github.com/seuros/imprimatur/compare/v0.5.0...v0.6.0) (2026-10-25)
|
|
12
36
|
|
data/Cargo.lock
CHANGED
|
@@ -513,7 +513,7 @@ dependencies = [
|
|
|
513
513
|
|
|
514
514
|
[[package]]
|
|
515
515
|
name = "imprimatur"
|
|
516
|
-
version = "0.
|
|
516
|
+
version = "0.7.0"
|
|
517
517
|
dependencies = [
|
|
518
518
|
"aes",
|
|
519
519
|
"crc32fast",
|
|
@@ -526,6 +526,7 @@ dependencies = [
|
|
|
526
526
|
"nix",
|
|
527
527
|
"qrcode",
|
|
528
528
|
"read-fonts 0.45.1",
|
|
529
|
+
"serde",
|
|
529
530
|
"serde_json",
|
|
530
531
|
"sha2",
|
|
531
532
|
"tempfile",
|
|
@@ -535,7 +536,7 @@ dependencies = [
|
|
|
535
536
|
|
|
536
537
|
[[package]]
|
|
537
538
|
name = "imprimatur-cli"
|
|
538
|
-
version = "0.
|
|
539
|
+
version = "0.7.0"
|
|
539
540
|
dependencies = [
|
|
540
541
|
"imprimatur",
|
|
541
542
|
"regex",
|
|
@@ -547,7 +548,7 @@ dependencies = [
|
|
|
547
548
|
|
|
548
549
|
[[package]]
|
|
549
550
|
name = "imprimatur_native"
|
|
550
|
-
version = "0.
|
|
551
|
+
version = "0.7.0"
|
|
551
552
|
dependencies = [
|
|
552
553
|
"imprimatur",
|
|
553
554
|
"magnus",
|
data/Cargo.toml
CHANGED
|
@@ -15,7 +15,7 @@ crc32fast = "1.5.2"
|
|
|
15
15
|
datamatrix = "0.3.3"
|
|
16
16
|
font-test-data = "0.10.0"
|
|
17
17
|
hayro = { version = "0.8.0", default-features = false, features = ["embed-fonts", "embed-cmaps"] }
|
|
18
|
-
imprimatur = { path = "ext/imprimatur_native/core", version = "0.
|
|
18
|
+
imprimatur = { path = "ext/imprimatur_native/core", version = "0.7.0" }
|
|
19
19
|
libproc = "0.14.11"
|
|
20
20
|
magnus = { version = "0.9.2", default-features = false }
|
|
21
21
|
md-5 = "0.11.0"
|
|
@@ -27,6 +27,7 @@ read-fonts = "0.45.1"
|
|
|
27
27
|
# rb-sys, features and all.
|
|
28
28
|
rb-sys = { version = "0.9.130", default-features = false, features = ["stable-api"] }
|
|
29
29
|
regex = "1.13.1"
|
|
30
|
+
serde = { version = "1.0.229", features = ["derive"] }
|
|
30
31
|
serde_json = "1.0.154"
|
|
31
32
|
sha2 = "0.11.1"
|
|
32
33
|
tempfile = "3"
|
data/README.md
CHANGED
|
@@ -362,20 +362,31 @@ before allocation (`max_pixels:`, 4 million) and encoded output
|
|
|
362
362
|
(`max_output_bytes:`, 8 MiB). Preflight rejects pages drawing more than 40 million
|
|
363
363
|
source-image pixels. Renderer warnings fail instead of silently dropping content.
|
|
364
364
|
The crate exposes `read::pdf::{info, pages, render_page}` (`render` feature) and
|
|
365
|
-
`read::pdf::isolation::render_page_isolated`
|
|
366
|
-
|
|
367
|
-
The gem bundles that worker and always
|
|
368
|
-
back to
|
|
365
|
+
`read::pdf::isolation::{info_isolated, pages_isolated, render_page_isolated}`
|
|
366
|
+
(`isolation` feature). These take an absolute path to the trusted
|
|
367
|
+
`imprimatur-render-worker` executable. The gem bundles that worker and always
|
|
368
|
+
isolates all three PDF operations; it never falls back to in-process parsing
|
|
369
|
+
or searches PATH.
|
|
369
370
|
|
|
370
|
-
All three accept the normal read limits
|
|
371
|
+
All three accept the normal read limits and `memory_bytes:`
|
|
371
372
|
(1 GiB by default): Linux/FreeBSD enforce an address-space cap; macOS uses a
|
|
372
373
|
sampled RSS watchdog, which can briefly overshoot. A worker is killed and reaped
|
|
373
|
-
on timeout, cancellation or supervisor failure.
|
|
374
|
+
on timeout, cancellation or supervisor failure. Each operation has a mandatory
|
|
374
375
|
60-second process ceiling even with `timeout: nil`; a shorter `timeout:` still
|
|
375
376
|
applies. CPU time, output files and core dumps are also limited.
|
|
376
377
|
|
|
378
|
+
The page worker flushes completed records as it goes. An exhausted stream
|
|
379
|
+
budget triggers individual-page recovery inside that worker: each attempt has
|
|
380
|
+
its own `max_inflated` allowance, while the original overall deadline, process
|
|
381
|
+
memory limit and aggregate text cap remain in force. An oversized page is
|
|
382
|
+
`failed`, not blank, without discarding healthy neighbors. A worker terminated
|
|
383
|
+
after metadata was read preserves completed pages and explicitly marks the
|
|
384
|
+
unreached pages; an expired overall deadline marks them `skipped`. Cancellation
|
|
385
|
+
still raises rather than returning a partial successful call.
|
|
386
|
+
|
|
377
387
|
This is resource containment, **not a filesystem/network security sandbox**.
|
|
378
|
-
The raw crate `render_page`
|
|
388
|
+
The raw crate `info`, `pages` and `render_page` APIs remain in-process;
|
|
389
|
+
`render_page` cannot interrupt Hayro.
|
|
379
390
|
`max_inflated` bounds our preflight, not Hayro's internal decoders.
|
|
380
391
|
|
|
381
392
|
```ruby
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "imprimatur-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "Read PDF, XLSX, XLS, ODS and DOCX documents as Markdown or JSON, search them, extract their images."
|
|
5
5
|
keywords = ["pdf", "xlsx", "docx", "markdown", "cli"]
|
|
6
6
|
categories = ["command-line-utilities", "text-processing"]
|
|
@@ -15,9 +15,11 @@ use crate::app::{self, App, Out, Status};
|
|
|
15
15
|
///
|
|
16
16
|
/// PDF: the text layer, placed glyph by glyph, with headings by font size,
|
|
17
17
|
/// running headers and footers left out, and pages after the first marked
|
|
18
|
-
/// `<!-- page N
|
|
19
|
-
///
|
|
20
|
-
///
|
|
18
|
+
/// `<!-- page N -->` (or `<!-- page N (label L) -->` with its printed page
|
|
19
|
+
/// label, from `/PageLabels` or a footer that looks like one). Encrypted
|
|
20
|
+
/// PDFs open when their user password is empty. XLSX, XLS and ODS: each
|
|
21
|
+
/// sheet as a table, values as shown, then its comments. DOCX: headings,
|
|
22
|
+
/// lists, emphasis, links and tables.
|
|
21
23
|
///
|
|
22
24
|
/// With several documents, each is introduced by `<!-- file: PATH -->`. A
|
|
23
25
|
/// document that fails is reported on standard error and the others are
|
|
@@ -29,6 +31,10 @@ use crate::app::{self, App, Out, Status};
|
|
|
29
31
|
/// (number, date, bool, text, error, formula) and comments; a Word
|
|
30
32
|
/// document's blocks (heading, paragraph, list item, table) with the byte
|
|
31
33
|
/// ranges of their bold, italic and linked text.
|
|
34
|
+
///
|
|
35
|
+
/// With `--drop-repeated` on a PDF, text that sits at the same spot on most
|
|
36
|
+
/// pages is dropped wherever it falls, not only in the top and bottom
|
|
37
|
+
/// margins: diagonal or rotated watermark stamps included. Off by default.
|
|
32
38
|
#[derive(Args, Debug)]
|
|
33
39
|
#[usage(
|
|
34
40
|
effect = "read",
|
|
@@ -38,7 +44,8 @@ use crate::app::{self, App, Out, Status};
|
|
|
38
44
|
example("imprimatur markdown audit.pdf", help = "Print a PDF's text"),
|
|
39
45
|
example("imprimatur md -o notes/ *.docx", help = "Write notes/NAME.md for each"),
|
|
40
46
|
example("curl -s $URL | imprimatur md -", help = "Read standard input"),
|
|
41
|
-
example("imprimatur --json md sales.xlsx", help = "Typed cells, as JSON")
|
|
47
|
+
example("imprimatur --json md sales.xlsx", help = "Typed cells, as JSON"),
|
|
48
|
+
example("imprimatur md --drop-repeated stamped.pdf", help = "Drop a repeated watermark")
|
|
42
49
|
)]
|
|
43
50
|
pub struct Markdown {
|
|
44
51
|
/// Documents to read; `-` reads standard input
|
|
@@ -49,6 +56,11 @@ pub struct Markdown {
|
|
|
49
56
|
#[usage(short = 'o', long, value_name = "DIR", value_hint = usage_rs::ValueHint::DirPath)]
|
|
50
57
|
pub output: Option<PathBuf>,
|
|
51
58
|
|
|
59
|
+
/// PDF: drop text repeated at the same spot on most pages, anywhere on
|
|
60
|
+
/// the page (watermarks); off by default
|
|
61
|
+
#[usage(long)]
|
|
62
|
+
pub drop_repeated: bool,
|
|
63
|
+
|
|
52
64
|
#[usage(flatten)]
|
|
53
65
|
pub limits: LimitFlags,
|
|
54
66
|
}
|
|
@@ -58,7 +70,7 @@ impl RunWith<&App> for Markdown {
|
|
|
58
70
|
|
|
59
71
|
fn run_with(self, app: &App) -> ExitCode {
|
|
60
72
|
let limits = match self.limits.checked() {
|
|
61
|
-
Ok(limits) => limits,
|
|
73
|
+
Ok(limits) => limits.drop_repeated(self.drop_repeated),
|
|
62
74
|
Err(status) => return status,
|
|
63
75
|
};
|
|
64
76
|
if let Some(directory) = &self.output
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "imprimatur"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.0"
|
|
4
4
|
description = "XLSX, PDF and DOCX authoring, and PDF, XLSX, XLS, ODS and DOCX reading, with its own XML, zip and PDF code."
|
|
5
5
|
keywords = ["xlsx", "excel", "pdf", "docx", "spreadsheet"]
|
|
6
6
|
categories = ["encoding", "text-processing"]
|
|
@@ -33,6 +33,7 @@ md-5.workspace = true
|
|
|
33
33
|
miniz_oxide.workspace = true
|
|
34
34
|
qrcode.workspace = true
|
|
35
35
|
read-fonts = { workspace = true, optional = true }
|
|
36
|
+
serde = { workspace = true, optional = true }
|
|
36
37
|
serde_json = { workspace = true, optional = true }
|
|
37
38
|
sha2.workspace = true
|
|
38
39
|
thiserror.workspace = true
|
|
@@ -51,7 +52,7 @@ archive = ["dep:zip"]
|
|
|
51
52
|
fonts = ["dep:read-fonts"]
|
|
52
53
|
# Pure-Rust full-page PDF rasterization; readers and writers remain independent.
|
|
53
54
|
render = ["dep:hayro"]
|
|
54
|
-
isolation = ["render", "dep:tempfile", "dep:nix", "dep:libproc"]
|
|
55
|
+
isolation = ["render", "json", "dep:serde", "dep:tempfile", "dep:nix", "dep:libproc"]
|
|
55
56
|
|
|
56
57
|
[target.'cfg(unix)'.dependencies]
|
|
57
58
|
nix = { workspace = true, optional = true }
|
|
@@ -11,10 +11,15 @@ states and UTF-8-safe text limits. `read::pdf::render_page`, under the optional
|
|
|
11
11
|
`render` feature, rasterizes a full page through Hayro (pure Rust).
|
|
12
12
|
That raw API is not preemptible. For untrusted documents, enable `isolation`,
|
|
13
13
|
build the `imprimatur-render-worker` binary and pass its absolute path to
|
|
14
|
-
`read::pdf::isolation::render_page_isolated`.
|
|
14
|
+
`read::pdf::isolation::{info_isolated, pages_isolated, render_page_isolated}`.
|
|
15
|
+
Rust supervises finite wall/CPU,
|
|
15
16
|
memory and file-output limits, killing and reaping failed/cancelled workers.
|
|
16
17
|
Linux/FreeBSD enforce address-space limits; macOS samples RSS and can briefly
|
|
17
18
|
overshoot. This is resource containment, not a filesystem/network sandbox.
|
|
19
|
+
Isolated page reads flush completed records, retain them on worker termination,
|
|
20
|
+
and recover healthy neighbors after page-local decompression failures. Recovery
|
|
21
|
+
attempts renew only the individual decompression allowance, never the overall
|
|
22
|
+
deadline, process-memory limit or aggregate text cap.
|
|
18
23
|
|
|
19
24
|
```rust
|
|
20
25
|
use imprimatur::xlsx::{Cell, Color, DateTime, Format, Workbook};
|
|
@@ -28,6 +28,7 @@ pub struct Limits {
|
|
|
28
28
|
pub(crate) max_depth: usize,
|
|
29
29
|
pub(crate) timeout: Option<Duration>,
|
|
30
30
|
pub(crate) cancel: Option<Arc<AtomicBool>>,
|
|
31
|
+
pub(crate) drop_repeated: bool,
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
impl Default for Limits {
|
|
@@ -38,6 +39,7 @@ impl Default for Limits {
|
|
|
38
39
|
max_depth: 256,
|
|
39
40
|
timeout: Some(Duration::from_mins(1)),
|
|
40
41
|
cancel: None,
|
|
42
|
+
drop_repeated: false,
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
}
|
|
@@ -79,6 +81,15 @@ impl Limits {
|
|
|
79
81
|
self.cancel = Some(flag);
|
|
80
82
|
self
|
|
81
83
|
}
|
|
84
|
+
|
|
85
|
+
/// For a PDF: drop text that repeats at the same spot on most pages
|
|
86
|
+
/// anywhere on the page, not only in the top and bottom margins. Off by
|
|
87
|
+
/// default.
|
|
88
|
+
#[must_use]
|
|
89
|
+
pub const fn drop_repeated(mut self, drop: bool) -> Self {
|
|
90
|
+
self.drop_repeated = drop;
|
|
91
|
+
self
|
|
92
|
+
}
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
/// One read's consumption. Single-threaded: a read runs on one thread.
|
|
@@ -100,6 +111,10 @@ impl<'a> Budget<'a> {
|
|
|
100
111
|
self.limits.max_depth
|
|
101
112
|
}
|
|
102
113
|
|
|
114
|
+
pub const fn drop_repeated(&self) -> bool {
|
|
115
|
+
self.limits.drop_repeated
|
|
116
|
+
}
|
|
117
|
+
|
|
103
118
|
/// Decompressed bytes still allowed.
|
|
104
119
|
pub fn inflatable(&self) -> usize {
|
|
105
120
|
let left = self.limits.max_inflated.saturating_sub(self.inflated.get());
|
|
@@ -55,7 +55,7 @@ fn page(page: &Page) -> Json {
|
|
|
55
55
|
object
|
|
56
56
|
})
|
|
57
57
|
.collect();
|
|
58
|
-
json!({ "page": page.number, "images": page.images, "blocks": blocks })
|
|
58
|
+
json!({ "page": page.number, "label": page.label, "images": page.images, "blocks": blocks })
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
/// An image's description, its data left out: `format`, `width`, `height`,
|
|
@@ -48,7 +48,14 @@ fn pdf(out: &mut String, pages: &[Page]) {
|
|
|
48
48
|
if page.number > 1 {
|
|
49
49
|
end_block(out);
|
|
50
50
|
// Writing to a String cannot fail.
|
|
51
|
-
|
|
51
|
+
match &page.label {
|
|
52
|
+
Some(label) => {
|
|
53
|
+
let _ = writeln!(out, "<!-- page {} (label {label}) -->", page.number);
|
|
54
|
+
}
|
|
55
|
+
None => {
|
|
56
|
+
let _ = writeln!(out, "<!-- page {} -->", page.number);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
52
59
|
}
|
|
53
60
|
write_blocks(out, page.blocks.iter().map(|placed| &placed.block), Mode::Body);
|
|
54
61
|
}
|
|
@@ -11,6 +11,7 @@ use crate::{Error, Result};
|
|
|
11
11
|
|
|
12
12
|
/// Metadata read without interpreting page content.
|
|
13
13
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
14
|
+
#[cfg_attr(feature = "isolation", derive(serde::Serialize, serde::Deserialize))]
|
|
14
15
|
pub struct Info {
|
|
15
16
|
pub page_count: u32,
|
|
16
17
|
/// Opened with the empty password: permissions, not a locked document.
|
|
@@ -40,7 +41,7 @@ impl Default for ReadOptions {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
impl ReadOptions {
|
|
43
|
-
const fn validate(&self) -> Result<()> {
|
|
44
|
+
pub(super) const fn validate(&self) -> Result<()> {
|
|
44
45
|
if self.first_page == 0 || self.last_page < self.first_page {
|
|
45
46
|
return Err(Error::InvalidPdfOption("pages must be an ascending one-based range"));
|
|
46
47
|
}
|
|
@@ -53,6 +54,7 @@ impl ReadOptions {
|
|
|
53
54
|
|
|
54
55
|
/// What is known about a page; failure is never reported as empty.
|
|
55
56
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
57
|
+
#[cfg_attr(feature = "isolation", derive(serde::Serialize, serde::Deserialize))]
|
|
56
58
|
pub enum Status {
|
|
57
59
|
Text,
|
|
58
60
|
Empty,
|
|
@@ -73,6 +75,7 @@ impl Status {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
#[derive(Debug)]
|
|
78
|
+
#[cfg_attr(feature = "isolation", derive(serde::Serialize, serde::Deserialize))]
|
|
76
79
|
pub struct PageResult {
|
|
77
80
|
pub number: u32,
|
|
78
81
|
pub status: Status,
|
|
@@ -106,6 +109,24 @@ pub fn info(data: &[u8], limits: &Limits) -> Result<Info> {
|
|
|
106
109
|
/// # Errors
|
|
107
110
|
/// As [`info`], and invalid options.
|
|
108
111
|
pub fn pages(data: &[u8], options: &ReadOptions, limits: &Limits) -> Result<Pages> {
|
|
112
|
+
let mut pages = Vec::new();
|
|
113
|
+
let info = visit_pages(data, options, limits, false, |page| {
|
|
114
|
+
pages.push(page);
|
|
115
|
+
Ok(())
|
|
116
|
+
})?;
|
|
117
|
+
Ok(Pages { info, pages })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// The worker flushes each completed page. After an exhausted stream budget,
|
|
121
|
+
/// single-page attempts get fresh decompression budgets, as individual reads
|
|
122
|
+
/// would; the supervisor still enforces one wall-clock and process limit.
|
|
123
|
+
pub(super) fn visit_pages(
|
|
124
|
+
data: &[u8],
|
|
125
|
+
options: &ReadOptions,
|
|
126
|
+
limits: &Limits,
|
|
127
|
+
recover: bool,
|
|
128
|
+
mut emit: impl FnMut(PageResult) -> Result<()>,
|
|
129
|
+
) -> Result<Info> {
|
|
109
130
|
options.validate()?;
|
|
110
131
|
let budget = Budget::new(limits, data.len())?;
|
|
111
132
|
let document = open(data, &budget)?;
|
|
@@ -114,52 +135,89 @@ pub fn pages(data: &[u8], options: &ReadOptions, limits: &Limits) -> Result<Page
|
|
|
114
135
|
let mut fonts = Fonts::new();
|
|
115
136
|
let mut text = PageText { strict: true, ..PageText::default() };
|
|
116
137
|
let mut remaining = options.max_output_bytes;
|
|
117
|
-
let mut
|
|
138
|
+
let mut individual = false;
|
|
118
139
|
for (number, page) in selected.pages {
|
|
119
140
|
budget.check()?;
|
|
120
141
|
if remaining == 0 {
|
|
121
|
-
|
|
122
|
-
number,
|
|
123
|
-
Status::Skipped,
|
|
124
|
-
"the text output limit was reached".into(),
|
|
125
|
-
));
|
|
142
|
+
emit(unread(number, Status::Skipped, "the text output limit was reached".into()))?;
|
|
126
143
|
continue;
|
|
127
144
|
}
|
|
128
145
|
text.max_bytes = Some(options.max_page_bytes.min(remaining));
|
|
129
|
-
let read =
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
146
|
+
let read = if individual {
|
|
147
|
+
single_page(data, number, options, remaining, limits)
|
|
148
|
+
} else {
|
|
149
|
+
page.and_then(|page| page_text(&document, &page, &mut fonts, &mut text))
|
|
150
|
+
.map(|()| text_result(number, &text, text.max_bytes.unwrap_or(remaining)))
|
|
151
|
+
};
|
|
152
|
+
let read = match read {
|
|
153
|
+
Err(error @ Error::LimitExceeded(_)) if recover => {
|
|
154
|
+
// Never renew an expired time budget or swallow cancellation.
|
|
155
|
+
budget.check()?;
|
|
156
|
+
if individual {
|
|
157
|
+
Err(error)
|
|
158
|
+
} else {
|
|
159
|
+
individual = true;
|
|
160
|
+
single_page(data, number, options, remaining, limits)
|
|
139
161
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
images: Some(u32::try_from(text.images).unwrap_or(u32::MAX)),
|
|
149
|
-
truncated,
|
|
150
|
-
reason: truncated.then(|| "the page text limit was reached".into()),
|
|
151
|
-
});
|
|
162
|
+
}
|
|
163
|
+
other => other,
|
|
164
|
+
};
|
|
165
|
+
let result = match read {
|
|
166
|
+
Ok(page) => page,
|
|
167
|
+
Err(error @ Error::LimitExceeded(_)) if recover => {
|
|
168
|
+
budget.check()?;
|
|
169
|
+
unread(number, Status::Failed, error.to_string())
|
|
152
170
|
}
|
|
153
171
|
Err(error) if xref::is_limit(&error) || matches!(error, Error::PasswordRequired) => {
|
|
154
172
|
return Err(error);
|
|
155
173
|
}
|
|
156
|
-
Err(error) =>
|
|
174
|
+
Err(error) => unread(number, Status::Failed, error.to_string()),
|
|
175
|
+
};
|
|
176
|
+
remaining -= result.text.len();
|
|
177
|
+
emit(result)?;
|
|
178
|
+
}
|
|
179
|
+
Ok(Info { page_count: selected.count, encrypted: document.encrypted() })
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fn single_page(
|
|
183
|
+
data: &[u8],
|
|
184
|
+
number: u32,
|
|
185
|
+
options: &ReadOptions,
|
|
186
|
+
remaining: usize,
|
|
187
|
+
limits: &Limits,
|
|
188
|
+
) -> Result<PageResult> {
|
|
189
|
+
let options = ReadOptions {
|
|
190
|
+
first_page: number,
|
|
191
|
+
last_page: number,
|
|
192
|
+
max_pages: 1,
|
|
193
|
+
max_output_bytes: remaining,
|
|
194
|
+
..options.clone()
|
|
195
|
+
};
|
|
196
|
+
pages(data, &options, limits)?
|
|
197
|
+
.pages
|
|
198
|
+
.pop()
|
|
199
|
+
.ok_or_else(|| Error::malformed("PDF", "the selected page disappeared"))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fn text_result(number: u32, text: &PageText, limit: usize) -> PageResult {
|
|
203
|
+
let lines = layout::lines(text);
|
|
204
|
+
let mut output = String::new();
|
|
205
|
+
for line in &lines.lines {
|
|
206
|
+
if !output.is_empty() {
|
|
207
|
+
output.push('\n');
|
|
157
208
|
}
|
|
209
|
+
output.push_str(lines.text(line));
|
|
210
|
+
}
|
|
211
|
+
let truncated = text.truncated || output.len() > limit;
|
|
212
|
+
truncate(&mut output, limit);
|
|
213
|
+
PageResult {
|
|
214
|
+
number,
|
|
215
|
+
status: if output.trim().is_empty() { Status::Empty } else { Status::Text },
|
|
216
|
+
text: output,
|
|
217
|
+
images: Some(u32::try_from(text.images).unwrap_or(u32::MAX)),
|
|
218
|
+
truncated,
|
|
219
|
+
reason: truncated.then(|| "the page text limit was reached".into()),
|
|
158
220
|
}
|
|
159
|
-
Ok(Pages {
|
|
160
|
-
info: Info { page_count: selected.count, encrypted: document.encrypted() },
|
|
161
|
-
pages: results,
|
|
162
|
-
})
|
|
163
221
|
}
|
|
164
222
|
|
|
165
223
|
pub(super) fn open<'a>(data: &'a [u8], budget: &'a Budget<'a>) -> Result<Document<'a>> {
|
|
@@ -170,7 +228,8 @@ pub(super) fn open<'a>(data: &'a [u8], budget: &'a Budget<'a>) -> Result<Documen
|
|
|
170
228
|
Document::open(data, budget).map(Document::strict)
|
|
171
229
|
}
|
|
172
230
|
|
|
173
|
-
|
|
231
|
+
pub(super) fn unread(number: u32, status: Status, mut reason: String) -> PageResult {
|
|
232
|
+
truncate(&mut reason, 4096);
|
|
174
233
|
PageResult {
|
|
175
234
|
number,
|
|
176
235
|
status,
|