imprimatur 0.7.0 → 0.8.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 +12 -0
- data/Cargo.lock +3 -3
- data/Cargo.toml +1 -1
- data/README.md +1 -1
- data/ext/imprimatur_native/cli/Cargo.toml +1 -1
- data/ext/imprimatur_native/cli/src/cli/markdown.rs +2 -1
- data/ext/imprimatur_native/core/Cargo.toml +1 -1
- data/ext/imprimatur_native/core/src/read/markdown.rs +4 -1
- data/ext/imprimatur_native/core/src/read/pdf/layout.rs +28 -14
- data/ext/imprimatur_native/ffi/Cargo.toml +1 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +4 -4
- data/ext/imprimatur_native/ffi/src/pdf.rs +1 -1
- data/ext/imprimatur_native/ffi/src/read.rs +25 -9
- data/ext/imprimatur_native/ffi/src/sheets.rs +3 -1
- data/lib/imprimatur/reader.rb +2 -2
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur.rb +2 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c343d074a7ed5e2ba382910345401adaaa92269eb915c901db443d97ed6f68a
|
|
4
|
+
data.tar.gz: 00cfe2c403d09f50708469c6228813f8ab9361775b848ac23457389008e643ab
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e003c94d4f867c4d957a7921a8ef50eb36f0393196535064bfab45cfead076f881a39dbbc7d2e73aa15487e57046a18ea3df1483f8b493f926a8d6e362e6ee7c
|
|
7
|
+
data.tar.gz: ac5657121fd5b657880fee907b3a4508087aff20a15ad057b1d43254796d767e908eec39952ac44a0aa65f814a55d36db8c9b56119effe647d8ea018bcb2e6b4
|
data/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,18 @@
|
|
|
11
11
|
- Allow a bounded exit grace when macOS cannot inspect an exiting worker;
|
|
12
12
|
persistent monitor failures still terminate and reap a live worker.
|
|
13
13
|
|
|
14
|
+
## [0.8.0](https://github.com/seuros/imprimatur/compare/v0.7.0...v0.8.0) (2026-10-27)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **reader:** accept drop_repeated in the Ruby API ([4e537d6](https://github.com/seuros/imprimatur/commit/4e537d641bafd8d3cdb8b1c20d16f4cb766ad073))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* **pdf:** read printed page numbers from headers too, not just footers ([c036434](https://github.com/seuros/imprimatur/commit/c0364341ee8fc36be2c97d25fd06da0aa76a4ad6))
|
|
25
|
+
|
|
14
26
|
## [0.7.0](https://github.com/seuros/imprimatur/compare/v0.6.1...v0.7.0) (2026-10-26)
|
|
15
27
|
|
|
16
28
|
|
data/Cargo.lock
CHANGED
|
@@ -513,7 +513,7 @@ dependencies = [
|
|
|
513
513
|
|
|
514
514
|
[[package]]
|
|
515
515
|
name = "imprimatur"
|
|
516
|
-
version = "0.
|
|
516
|
+
version = "0.8.0"
|
|
517
517
|
dependencies = [
|
|
518
518
|
"aes",
|
|
519
519
|
"crc32fast",
|
|
@@ -536,7 +536,7 @@ dependencies = [
|
|
|
536
536
|
|
|
537
537
|
[[package]]
|
|
538
538
|
name = "imprimatur-cli"
|
|
539
|
-
version = "0.
|
|
539
|
+
version = "0.8.0"
|
|
540
540
|
dependencies = [
|
|
541
541
|
"imprimatur",
|
|
542
542
|
"regex",
|
|
@@ -548,7 +548,7 @@ dependencies = [
|
|
|
548
548
|
|
|
549
549
|
[[package]]
|
|
550
550
|
name = "imprimatur_native"
|
|
551
|
-
version = "0.
|
|
551
|
+
version = "0.8.0"
|
|
552
552
|
dependencies = [
|
|
553
553
|
"imprimatur",
|
|
554
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.8.0" }
|
|
19
19
|
libproc = "0.14.11"
|
|
20
20
|
magnus = { version = "0.9.2", default-features = false }
|
|
21
21
|
md-5 = "0.11.0"
|
data/README.md
CHANGED
|
@@ -32,7 +32,7 @@ end
|
|
|
32
32
|
gem "imprimatur"
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Precompiled gems ship for Linux (x86_64 and aarch64, glibc 2.34+
|
|
35
|
+
Precompiled gems ship for Linux (x86_64 and aarch64, glibc 2.34+),
|
|
36
36
|
macOS 13+ (arm64, x86_64) and FreeBSD 15.1 (amd64): nothing to compile.
|
|
37
37
|
Anywhere else, the source gem compiles the extension on install, with the
|
|
38
38
|
toolchain from [rustup.rs](https://rustup.rs) (1.99+).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "imprimatur-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.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"]
|
|
@@ -16,7 +16,8 @@ use crate::app::{self, App, Out, Status};
|
|
|
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
18
|
/// `<!-- page N -->` (or `<!-- page N (label L) -->` with its printed page
|
|
19
|
-
/// label, from `/PageLabels` or a footer that looks like
|
|
19
|
+
/// label, from `/PageLabels` or a header or footer line that looks like
|
|
20
|
+
/// one). Encrypted
|
|
20
21
|
/// PDFs open when their user password is empty. XLSX, XLS and ODS: each
|
|
21
22
|
/// sheet as a table, values as shown, then its comments. DOCX: headings,
|
|
22
23
|
/// lists, emphasis, links and tables.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "imprimatur"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.8.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"]
|
|
@@ -44,7 +44,10 @@ pub enum Mode {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
fn pdf(out: &mut String, pages: &[Page]) {
|
|
47
|
-
|
|
47
|
+
// Every page past the first gets its marker, blocks or none: an empty
|
|
48
|
+
// page (left bare by `--drop-repeated`, or genuinely blank) still needs
|
|
49
|
+
// one, so the markers stay a reliable page count and index.
|
|
50
|
+
for page in pages {
|
|
48
51
|
if page.number > 1 {
|
|
49
52
|
end_block(out);
|
|
50
53
|
// Writing to a String cannot fail.
|
|
@@ -293,27 +293,41 @@ pub fn pages(
|
|
|
293
293
|
reason = "`blocks` keeps its buffer for the next page"
|
|
294
294
|
)]
|
|
295
295
|
let blocks = blocks.drain(..).collect();
|
|
296
|
-
let label = labels
|
|
297
|
-
||
|
|
298
|
-
|labels| labels.get(index).cloned().flatten(),
|
|
299
|
-
);
|
|
296
|
+
let label = labels
|
|
297
|
+
.map_or_else(|| printed_label(page), |labels| labels.get(index).cloned().flatten());
|
|
300
298
|
out.push(Page { number, blocks, images: page.images, label });
|
|
301
299
|
}
|
|
302
300
|
out
|
|
303
301
|
}
|
|
304
302
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
303
|
+
/// The page's own printed label, from a single header or footer line that
|
|
304
|
+
/// looks like one: a bare page number or roman numeral ("`-i-`", "`7`"), or
|
|
305
|
+
/// the same after a short label word ("`Page 70`" -> "`70`"). Unlike the
|
|
306
|
+
/// boilerplate this document's other margin lines are checked against, a
|
|
307
|
+
/// running "`Page 70`", "`Page 71`", ... header is not noise here: each
|
|
308
|
+
/// instance *is* that page's label, not a repeated line to drop.
|
|
309
|
+
fn printed_label(page: &PageLines) -> Option<String> {
|
|
310
|
+
let mut candidates =
|
|
311
|
+
page.lines.iter().filter(|line: &&Line| line.margin != 0).filter_map(|line| {
|
|
312
|
+
let text = page.text(line).trim();
|
|
313
|
+
page_number_label(text)
|
|
314
|
+
});
|
|
315
|
+
let label = candidates.next()?;
|
|
316
|
+
candidates.next().is_none().then_some(label)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/// `text` as a page label, if it looks like one, stripped of a leading
|
|
320
|
+
/// label word such as "Page" when there is one.
|
|
321
|
+
fn page_number_label(text: &str) -> Option<String> {
|
|
322
|
+
if looks_like_page_number(text) {
|
|
323
|
+
return Some(text.to_owned());
|
|
324
|
+
}
|
|
325
|
+
let word_end = text.find(|ch: char| !ch.is_ascii_alphabetic())?;
|
|
326
|
+
if word_end == 0 {
|
|
313
327
|
return None;
|
|
314
328
|
}
|
|
315
|
-
let
|
|
316
|
-
looks_like_page_number(
|
|
329
|
+
let rest = text[word_end..].trim_start_matches(|ch: char| !ch.is_alphanumeric());
|
|
330
|
+
looks_like_page_number(rest).then(|| rest.to_owned())
|
|
317
331
|
}
|
|
318
332
|
|
|
319
333
|
fn looks_like_page_number(text: &str) -> bool {
|
|
@@ -323,13 +323,13 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
323
323
|
barcode::define(ruby, imprimatur)?;
|
|
324
324
|
|
|
325
325
|
let native = imprimatur.define_module("Native")?;
|
|
326
|
-
native.define_singleton_method("markdown", function!(read::markdown_native,
|
|
327
|
-
native.define_singleton_method("content", function!(read::content_native,
|
|
328
|
-
native.define_singleton_method("images", function!(read::images_native,
|
|
326
|
+
native.define_singleton_method("markdown", function!(read::markdown_native, 5))?;
|
|
327
|
+
native.define_singleton_method("content", function!(read::content_native, 6))?;
|
|
328
|
+
native.define_singleton_method("images", function!(read::images_native, 5))?;
|
|
329
329
|
native.define_singleton_method("pdf_info", function!(pdf::info, 4))?;
|
|
330
330
|
native.define_singleton_method("pdf_pages", function!(pdf::pages, 5))?;
|
|
331
331
|
native.define_singleton_method("render_pdf_page", function!(pdf::render, 5))?;
|
|
332
|
-
native.define_singleton_method("sheets", function!(sheets::sheets_native,
|
|
332
|
+
native.define_singleton_method("sheets", function!(sheets::sheets_native, 6))?;
|
|
333
333
|
let zip_writer = native.define_class("ZipWriter", ruby.class_object())?;
|
|
334
334
|
zip_writer.define_singleton_method("new", function!(archive::ZipWriter::new, 1))?;
|
|
335
335
|
zip_writer.define_method("add_file", method!(archive::ZipWriter::add_file, 3))?;
|
|
@@ -25,15 +25,19 @@ struct Read<T, F> {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/// Called by `Imprimatur.markdown`, which owns the keyword interface.
|
|
28
|
-
/// `timeout` is in seconds; `nil` limits keep their defaults.
|
|
28
|
+
/// `timeout` is in seconds; `nil` limits keep their defaults. `drop_repeated`
|
|
29
|
+
/// drops PDF text repeated at the same spot on most pages, anywhere on the
|
|
30
|
+
/// page (watermarks); off by default.
|
|
29
31
|
pub fn markdown_native(
|
|
30
32
|
ruby: &Ruby,
|
|
31
33
|
document: RString,
|
|
32
34
|
max_input: Option<usize>,
|
|
33
35
|
max_inflated: Option<u64>,
|
|
34
36
|
timeout: Option<f64>,
|
|
37
|
+
drop_repeated: bool,
|
|
35
38
|
) -> Result<RString, Error> {
|
|
36
|
-
let text =
|
|
39
|
+
let text =
|
|
40
|
+
without_gvl(ruby, document, (max_input, max_inflated, timeout, drop_repeated), markdown)?;
|
|
37
41
|
Ok(ruby.str_new(&text))
|
|
38
42
|
}
|
|
39
43
|
|
|
@@ -45,9 +49,11 @@ pub fn content_native(
|
|
|
45
49
|
max_input: Option<usize>,
|
|
46
50
|
max_inflated: Option<u64>,
|
|
47
51
|
timeout: Option<f64>,
|
|
52
|
+
drop_repeated: bool,
|
|
48
53
|
styles: bool,
|
|
49
54
|
) -> Result<RString, Error> {
|
|
50
|
-
let read =
|
|
55
|
+
let read =
|
|
56
|
+
without_gvl(ruby, document, (max_input, max_inflated, timeout, drop_repeated), content)?;
|
|
51
57
|
let options = json::Options { styles };
|
|
52
58
|
Ok(ruby.str_new(&Json::Object(json::content_with(&read, options)).to_string()))
|
|
53
59
|
}
|
|
@@ -60,9 +66,13 @@ pub fn images_native(
|
|
|
60
66
|
max_input: Option<usize>,
|
|
61
67
|
max_inflated: Option<u64>,
|
|
62
68
|
timeout: Option<f64>,
|
|
69
|
+
drop_repeated: bool,
|
|
63
70
|
) -> Result<RArray, Error> {
|
|
64
|
-
let found =
|
|
65
|
-
|
|
71
|
+
let found = without_gvl(
|
|
72
|
+
ruby,
|
|
73
|
+
document,
|
|
74
|
+
(max_input, max_inflated, timeout, drop_repeated),
|
|
75
|
+
|bytes, limits| {
|
|
66
76
|
let mut found = Vec::new();
|
|
67
77
|
images(bytes, limits, |image| {
|
|
68
78
|
let description = Json::Object(json::image(&image)).to_string();
|
|
@@ -70,7 +80,8 @@ pub fn images_native(
|
|
|
70
80
|
Ok(())
|
|
71
81
|
})?;
|
|
72
82
|
Ok(found)
|
|
73
|
-
}
|
|
83
|
+
},
|
|
84
|
+
)?;
|
|
74
85
|
let pairs = ruby.ary_new_capa(found.len());
|
|
75
86
|
for (data, description) in found {
|
|
76
87
|
let pair = [
|
|
@@ -86,9 +97,14 @@ pub fn images_native(
|
|
|
86
97
|
fn limits(
|
|
87
98
|
ruby: &Ruby,
|
|
88
99
|
cancel: &Arc<AtomicBool>,
|
|
89
|
-
(max_input, max_inflated, timeout): (
|
|
100
|
+
(max_input, max_inflated, timeout, drop_repeated): (
|
|
101
|
+
Option<usize>,
|
|
102
|
+
Option<u64>,
|
|
103
|
+
Option<f64>,
|
|
104
|
+
bool,
|
|
105
|
+
),
|
|
90
106
|
) -> Result<Limits, Error> {
|
|
91
|
-
let mut limits = Limits::default().cancel_on(Arc::clone(cancel));
|
|
107
|
+
let mut limits = Limits::default().cancel_on(Arc::clone(cancel)).drop_repeated(drop_repeated);
|
|
92
108
|
if let Some(bytes) = max_input {
|
|
93
109
|
limits = limits.max_input(bytes);
|
|
94
110
|
}
|
|
@@ -107,7 +123,7 @@ fn limits(
|
|
|
107
123
|
pub fn without_gvl<T, F>(
|
|
108
124
|
ruby: &Ruby,
|
|
109
125
|
document: RString,
|
|
110
|
-
keywords: (Option<usize>, Option<u64>, Option<f64
|
|
126
|
+
keywords: (Option<usize>, Option<u64>, Option<f64>, bool),
|
|
111
127
|
job: F,
|
|
112
128
|
) -> Result<T, Error>
|
|
113
129
|
where
|
|
@@ -23,9 +23,11 @@ pub fn sheets_native(
|
|
|
23
23
|
max_input: Option<usize>,
|
|
24
24
|
max_inflated: Option<u64>,
|
|
25
25
|
timeout: Option<f64>,
|
|
26
|
+
drop_repeated: bool,
|
|
26
27
|
styles: bool,
|
|
27
28
|
) -> Result<RArray, Error> {
|
|
28
|
-
let read =
|
|
29
|
+
let read =
|
|
30
|
+
without_gvl(ruby, document, (max_input, max_inflated, timeout, drop_repeated), content)?;
|
|
29
31
|
let Some(sheets) = read.sheets() else {
|
|
30
32
|
let kind = if matches!(read, Content::Pdf(_)) { "a PDF" } else { "a Word document" };
|
|
31
33
|
let error = imprimatur::Error::UnsupportedDocument(format!("{kind} is no spreadsheet"));
|
data/lib/imprimatur/reader.rb
CHANGED
|
@@ -41,8 +41,8 @@ module Imprimatur
|
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# @api private
|
|
44
|
-
def self.limits(timeout: 60, max_bytes: nil, max_inflated: nil)
|
|
45
|
-
[max_bytes, max_inflated, timeout && Float(timeout)]
|
|
44
|
+
def self.limits(timeout: 60, max_bytes: nil, max_inflated: nil, drop_repeated: false)
|
|
45
|
+
[max_bytes, max_inflated, timeout && Float(timeout), drop_repeated ? true : false]
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
# A path or an IO is read no further than +max_bytes+ (one byte past it
|
data/lib/imprimatur/version.rb
CHANGED
data/lib/imprimatur.rb
CHANGED
|
@@ -72,6 +72,8 @@ module Imprimatur
|
|
|
72
72
|
# @param timeout [Numeric, nil] seconds the read may take (60), +nil+ for none
|
|
73
73
|
# @param max_bytes [Integer, nil] the largest document accepted (512 MiB)
|
|
74
74
|
# @param max_inflated [Integer, nil] decompressed bytes allowed in all (1 GiB)
|
|
75
|
+
# @param drop_repeated [Boolean] PDF only: drop text repeated at the same
|
|
76
|
+
# spot on most pages, anywhere on the page (watermarks); off by default
|
|
75
77
|
# @return [String] Markdown, UTF-8
|
|
76
78
|
# @raise [NoTextLayer] a PDF of scanned pages: run OCR instead
|
|
77
79
|
# @raise [PasswordRequired] a PDF that needs a password to open
|