imprimatur 0.1.0 → 0.3.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 +31 -0
- data/Cargo.lock +124 -3
- data/Cargo.toml +1 -1
- data/README.md +123 -23
- data/ext/imprimatur_native/cli/Cargo.toml +2 -2
- data/ext/imprimatur_native/cli/src/cli/grep.rs +1 -1
- data/ext/imprimatur_native/cli/src/cli/markdown.rs +2 -2
- data/ext/imprimatur_native/cli/src/cli.rs +5 -5
- data/ext/imprimatur_native/cli/src/main.rs +1 -1
- data/ext/imprimatur_native/core/Cargo.toml +12 -2
- data/ext/imprimatur_native/core/README.md +15 -1
- data/ext/imprimatur_native/core/src/archive.rs +279 -0
- data/ext/imprimatur_native/core/src/doc/paragraph.rs +10 -3
- data/ext/imprimatur_native/core/src/doc/picture.rs +192 -0
- data/ext/imprimatur_native/core/src/doc/table.rs +21 -0
- data/ext/imprimatur_native/core/src/doc.rs +86 -2
- data/ext/imprimatur_native/core/src/docx/picture.rs +72 -0
- data/ext/imprimatur_native/core/src/docx.rs +91 -28
- data/ext/imprimatur_native/core/src/error.rs +13 -1
- data/ext/imprimatur_native/core/src/font.rs +229 -0
- data/ext/imprimatur_native/core/src/lib.rs +5 -0
- data/ext/imprimatur_native/core/src/pdf/layout.rs +38 -15
- data/ext/imprimatur_native/core/src/pdf/objects.rs +29 -5
- data/ext/imprimatur_native/core/src/pdf/picture.rs +197 -0
- data/ext/imprimatur_native/core/src/pdf.rs +31 -3
- data/ext/imprimatur_native/core/src/read/content.rs +91 -3
- data/ext/imprimatur_native/core/src/read/json.rs +96 -7
- data/ext/imprimatur_native/core/src/read/markdown.rs +1 -1
- data/ext/imprimatur_native/core/src/read/{xlsx/numbers.rs → numbers.rs} +69 -45
- data/ext/imprimatur_native/core/src/read/ods.rs +356 -0
- data/ext/imprimatur_native/core/src/read/ole.rs +244 -0
- data/ext/imprimatur_native/core/src/read/package.rs +0 -5
- data/ext/imprimatur_native/core/src/read/pdf/layout/bidi.rs +212 -0
- data/ext/imprimatur_native/core/src/read/pdf/layout.rs +25 -119
- data/ext/imprimatur_native/core/src/read/xls/strings.rs +156 -0
- data/ext/imprimatur_native/core/src/read/xls.rs +342 -0
- data/ext/imprimatur_native/core/src/read/xlsx/formats.rs +155 -0
- data/ext/imprimatur_native/core/src/read/xlsx.rs +104 -20
- data/ext/imprimatur_native/core/src/read.rs +68 -21
- data/ext/imprimatur_native/core/src/script.rs +116 -0
- data/ext/imprimatur_native/core/src/style.rs +21 -0
- data/ext/imprimatur_native/core/src/test_support.rs +90 -2
- data/ext/imprimatur_native/core/src/xlsx/autofit.rs +60 -0
- data/ext/imprimatur_native/core/src/xlsx/date.rs +7 -1
- data/ext/imprimatur_native/core/src/xlsx/format.rs +22 -6
- data/ext/imprimatur_native/core/src/xlsx/package.rs +22 -8
- data/ext/imprimatur_native/core/src/xlsx/styles.rs +9 -8
- data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +35 -7
- data/ext/imprimatur_native/core/src/xlsx.rs +40 -3
- data/ext/imprimatur_native/core/src/xml/reader.rs +10 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +3 -2
- data/ext/imprimatur_native/ffi/src/archive.rs +119 -0
- data/ext/imprimatur_native/ffi/src/blocking.rs +63 -0
- data/ext/imprimatur_native/ffi/src/document.rs +48 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +52 -1
- data/ext/imprimatur_native/ffi/src/read.rs +9 -14
- data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
- data/lib/imprimatur/document.rb +30 -6
- data/lib/imprimatur/error.rb +4 -3
- data/lib/imprimatur/reader.rb +3 -3
- data/lib/imprimatur/spreadsheet.rb +151 -0
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur/xlsx.rb +25 -4
- data/lib/imprimatur/zip.rb +127 -0
- data/lib/imprimatur.rb +18 -5
- metadata +23 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72d405b951859304a851be99269f01972fd7817960cb53f01dbfca257f700112
|
|
4
|
+
data.tar.gz: 5dbc90d5aec863460a9d61134961d0c384aafd073890d52023b9291aa7ba24b4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1e1ea6bb4b63996aea9b36d1241e34c7133cbf3f2305778542c1267fdea7b62db170a8434d4af174fc8d49930a348a2682f07f0b8bec1d2be80c8696ebd75664
|
|
7
|
+
data.tar.gz: bc7c2139ba52cf9f33256c7d23972d0b2eaa55a7252e810d0cca1897ed5405bacd64b22b8eacd9636c938c39cf241051af1ae68b4d5a0e81595fffd8d1a61964
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/seuros/imprimatur/compare/v0.2.0...v0.3.0) (2026-10-17)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* a spreadsheet's sheets give their comments, columns, validations and styles ([1797632](https://github.com/seuros/imprimatur/commit/1797632aad2948c1c0a493501de3684187062246))
|
|
9
|
+
* draw PNG and JPEG pictures, on pages of any size ([419524a](https://github.com/seuros/imprimatur/commit/419524afe36bbb06e3ef0ecb48a4fab4f80fb989))
|
|
10
|
+
* **font:** load static TrueType fonts and metrics ([fd85e3d](https://github.com/seuros/imprimatur/commit/fd85e3d78ece99506b2dde2c61bab6997cb20d21))
|
|
11
|
+
* read column widths, data validations and cell styles ([2930964](https://github.com/seuros/imprimatur/commit/2930964cde82cc998113aa83336a547cda8bd9f8))
|
|
12
|
+
* read whether a list validation shows its drop-down ([b75ef53](https://github.com/seuros/imprimatur/commit/b75ef530ef790876c58e40bb02dfc7257a48aab5))
|
|
13
|
+
* read workbooks as typed rows with Imprimatur::Spreadsheet ([927033b](https://github.com/seuros/imprimatur/commit/927033bb5ef9725c38726bc0785bbd90be8f1dc8))
|
|
14
|
+
* size columns to their cells, and give workbooks a font ([c88a058](https://github.com/seuros/imprimatur/commit/c88a0580a7ba052c4994faf4f53b42e75f48f587))
|
|
15
|
+
* write right-to-left text in its own direction ([137206e](https://github.com/seuros/imprimatur/commit/137206e08622f33ae21b6e52013e3ec7b071bb6b))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* build the gem with every feature of the crate ([7b48599](https://github.com/seuros/imprimatur/commit/7b485998abdb8b14b59e22a6ff97ca7dec143986))
|
|
21
|
+
* **font:** put the TrueType loader behind a `fonts` feature ([f9d1915](https://github.com/seuros/imprimatur/commit/f9d1915f3c98c0c7351477c6e8f21799d01e02f9))
|
|
22
|
+
* read right-to-left PDF text in its page's direction ([ca173ef](https://github.com/seuros/imprimatur/commit/ca173efd019513f25df004feafeda0e8551e64c0))
|
|
23
|
+
* show numbers with their format's decimals and grouping ([d0089ee](https://github.com/seuros/imprimatur/commit/d0089ee2dcf364ac32c7f92dc6c3af9aa6a9e9cb))
|
|
24
|
+
|
|
25
|
+
## [0.2.0](https://github.com/seuros/imprimatur/compare/v0.1.0...v0.2.0) (2026-10-16)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### Features
|
|
29
|
+
|
|
30
|
+
* read legacy XLS and ODS workbooks ([96b86fa](https://github.com/seuros/imprimatur/commit/96b86faab8eb04b80d33885c61b17ebad4f1f63a))
|
|
31
|
+
* zip archive comments and entry listings ([a87f223](https://github.com/seuros/imprimatur/commit/a87f22330859ee4a583943ddbf107571cca9e841))
|
|
32
|
+
* zip archives of any files ([d5ea579](https://github.com/seuros/imprimatur/commit/d5ea579ae1fc80a8ec6a1b213f442d490ededfba))
|
|
33
|
+
|
|
3
34
|
## 0.1.0 (2026-10-16)
|
|
4
35
|
|
|
5
36
|
|
data/Cargo.lock
CHANGED
|
@@ -67,6 +67,26 @@ dependencies = [
|
|
|
67
67
|
"hybrid-array",
|
|
68
68
|
]
|
|
69
69
|
|
|
70
|
+
[[package]]
|
|
71
|
+
name = "bytemuck"
|
|
72
|
+
version = "1.25.2"
|
|
73
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
74
|
+
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
|
75
|
+
dependencies = [
|
|
76
|
+
"bytemuck_derive",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
[[package]]
|
|
80
|
+
name = "bytemuck_derive"
|
|
81
|
+
version = "1.12.1"
|
|
82
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
83
|
+
checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c"
|
|
84
|
+
dependencies = [
|
|
85
|
+
"proc-macro2",
|
|
86
|
+
"quote",
|
|
87
|
+
"syn 3.0.9",
|
|
88
|
+
]
|
|
89
|
+
|
|
70
90
|
[[package]]
|
|
71
91
|
name = "cexpr"
|
|
72
92
|
version = "0.6.0"
|
|
@@ -196,6 +216,30 @@ dependencies = [
|
|
|
196
216
|
"zlib-rs",
|
|
197
217
|
]
|
|
198
218
|
|
|
219
|
+
[[package]]
|
|
220
|
+
name = "foldhash"
|
|
221
|
+
version = "0.2.0"
|
|
222
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
223
|
+
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
|
224
|
+
|
|
225
|
+
[[package]]
|
|
226
|
+
name = "font-test-data"
|
|
227
|
+
version = "0.10.0"
|
|
228
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
229
|
+
checksum = "9b7088ff167a44404649ff2221f3ba67c9eeff076cfe026c1308f54e25702a81"
|
|
230
|
+
dependencies = [
|
|
231
|
+
"font-types",
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
[[package]]
|
|
235
|
+
name = "font-types"
|
|
236
|
+
version = "0.12.7"
|
|
237
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
238
|
+
checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d"
|
|
239
|
+
dependencies = [
|
|
240
|
+
"bytemuck",
|
|
241
|
+
]
|
|
242
|
+
|
|
199
243
|
[[package]]
|
|
200
244
|
name = "getrandom"
|
|
201
245
|
version = "0.4.3"
|
|
@@ -230,21 +274,24 @@ dependencies = [
|
|
|
230
274
|
|
|
231
275
|
[[package]]
|
|
232
276
|
name = "imprimatur"
|
|
233
|
-
version = "0.
|
|
277
|
+
version = "0.3.0"
|
|
234
278
|
dependencies = [
|
|
235
279
|
"aes",
|
|
236
280
|
"crc32fast",
|
|
281
|
+
"font-test-data",
|
|
237
282
|
"md-5",
|
|
238
283
|
"miniz_oxide",
|
|
284
|
+
"read-fonts",
|
|
239
285
|
"serde_json",
|
|
240
286
|
"sha2",
|
|
287
|
+
"tempfile",
|
|
241
288
|
"thiserror",
|
|
242
289
|
"zip",
|
|
243
290
|
]
|
|
244
291
|
|
|
245
292
|
[[package]]
|
|
246
293
|
name = "imprimatur-cli"
|
|
247
|
-
version = "0.
|
|
294
|
+
version = "0.3.0"
|
|
248
295
|
dependencies = [
|
|
249
296
|
"imprimatur",
|
|
250
297
|
"regex",
|
|
@@ -256,7 +303,7 @@ dependencies = [
|
|
|
256
303
|
|
|
257
304
|
[[package]]
|
|
258
305
|
name = "imprimatur_native"
|
|
259
|
-
version = "0.
|
|
306
|
+
version = "0.3.0"
|
|
260
307
|
dependencies = [
|
|
261
308
|
"imprimatur",
|
|
262
309
|
"magnus",
|
|
@@ -499,6 +546,19 @@ version = "0.2.3"
|
|
|
499
546
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
500
547
|
checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
|
|
501
548
|
|
|
549
|
+
[[package]]
|
|
550
|
+
name = "read-fonts"
|
|
551
|
+
version = "0.45.1"
|
|
552
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
553
|
+
checksum = "4edc8db0d972de88e53a085b1f01fb3fbdae7bb0cb7a0ce2b2f8285a412fe3ef"
|
|
554
|
+
dependencies = [
|
|
555
|
+
"bytemuck",
|
|
556
|
+
"foldhash",
|
|
557
|
+
"font-types",
|
|
558
|
+
"once_cell",
|
|
559
|
+
"yoke",
|
|
560
|
+
]
|
|
561
|
+
|
|
502
562
|
[[package]]
|
|
503
563
|
name = "regex"
|
|
504
564
|
version = "1.13.1"
|
|
@@ -631,6 +691,12 @@ version = "1.3.0"
|
|
|
631
691
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
632
692
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|
633
693
|
|
|
694
|
+
[[package]]
|
|
695
|
+
name = "stable_deref_trait"
|
|
696
|
+
version = "1.2.1"
|
|
697
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
698
|
+
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
|
699
|
+
|
|
634
700
|
[[package]]
|
|
635
701
|
name = "syn"
|
|
636
702
|
version = "2.0.119"
|
|
@@ -653,6 +719,17 @@ dependencies = [
|
|
|
653
719
|
"unicode-ident",
|
|
654
720
|
]
|
|
655
721
|
|
|
722
|
+
[[package]]
|
|
723
|
+
name = "synstructure"
|
|
724
|
+
version = "0.14.0"
|
|
725
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
726
|
+
checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
|
|
727
|
+
dependencies = [
|
|
728
|
+
"proc-macro2",
|
|
729
|
+
"quote",
|
|
730
|
+
"syn 3.0.9",
|
|
731
|
+
]
|
|
732
|
+
|
|
656
733
|
[[package]]
|
|
657
734
|
name = "tempfile"
|
|
658
735
|
version = "3.27.0"
|
|
@@ -808,6 +885,50 @@ dependencies = [
|
|
|
808
885
|
"memchr",
|
|
809
886
|
]
|
|
810
887
|
|
|
888
|
+
[[package]]
|
|
889
|
+
name = "yoke"
|
|
890
|
+
version = "0.8.3"
|
|
891
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
892
|
+
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
|
|
893
|
+
dependencies = [
|
|
894
|
+
"stable_deref_trait",
|
|
895
|
+
"yoke-derive",
|
|
896
|
+
"zerofrom",
|
|
897
|
+
]
|
|
898
|
+
|
|
899
|
+
[[package]]
|
|
900
|
+
name = "yoke-derive"
|
|
901
|
+
version = "0.8.4"
|
|
902
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
903
|
+
checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
|
|
904
|
+
dependencies = [
|
|
905
|
+
"proc-macro2",
|
|
906
|
+
"quote",
|
|
907
|
+
"syn 3.0.9",
|
|
908
|
+
"synstructure",
|
|
909
|
+
]
|
|
910
|
+
|
|
911
|
+
[[package]]
|
|
912
|
+
name = "zerofrom"
|
|
913
|
+
version = "0.1.8"
|
|
914
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
915
|
+
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
|
|
916
|
+
dependencies = [
|
|
917
|
+
"zerofrom-derive",
|
|
918
|
+
]
|
|
919
|
+
|
|
920
|
+
[[package]]
|
|
921
|
+
name = "zerofrom-derive"
|
|
922
|
+
version = "0.1.8"
|
|
923
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
924
|
+
checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
|
|
925
|
+
dependencies = [
|
|
926
|
+
"proc-macro2",
|
|
927
|
+
"quote",
|
|
928
|
+
"syn 3.0.9",
|
|
929
|
+
"synstructure",
|
|
930
|
+
]
|
|
931
|
+
|
|
811
932
|
[[package]]
|
|
812
933
|
name = "zip"
|
|
813
934
|
version = "9.0.3"
|
data/Cargo.toml
CHANGED
|
@@ -10,7 +10,7 @@ license = "BSD-3-Clause"
|
|
|
10
10
|
repository = "https://github.com/seuros/imprimatur"
|
|
11
11
|
|
|
12
12
|
[workspace.dependencies]
|
|
13
|
-
imprimatur = { path = "ext/imprimatur_native/core", version = "0.
|
|
13
|
+
imprimatur = { path = "ext/imprimatur_native/core", version = "0.3.0" }
|
|
14
14
|
magnus = { version = "0.9.2", default-features = false }
|
|
15
15
|
# For rb_thread_call_without_gvl2, which magnus does not wrap; magnus's own
|
|
16
16
|
# rb-sys, features and all.
|
data/README.md
CHANGED
|
@@ -102,6 +102,21 @@ columns past its end are left unstyled:
|
|
|
102
102
|
sheet.row "", "", "", "Notes", style: [yellow] * 3
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
The workbook can have a font of its own, for unstyled cells and for styles
|
|
106
|
+
that name no font or size, and size its columns to their cells:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
Imprimatur.xlsx("export.xlsx", font: "Arial", size: 10, autofit: true) do |book|
|
|
110
|
+
book.sheet("Orders") { it.row "Supplier", "Amount", style: header }
|
|
111
|
+
book.sheet("Notes").autofit!(false) # this one keeps Excel's widths
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`autofit` (or `sheet.autofit!`) sizes each column without a width of its own
|
|
116
|
+
to its widest cell, estimated as `(characters + 3) × points / 10`,
|
|
117
|
+
bold cells one and a half times wider, at most 255. A text counts its longest
|
|
118
|
+
line, a date `yyyy-mm-dd`; formulas are left out, their results unknown.
|
|
119
|
+
|
|
105
120
|
## Validation and comments
|
|
106
121
|
|
|
107
122
|
```ruby
|
|
@@ -137,22 +152,23 @@ shown all the time with `visible: true`. The cell need not hold a value.
|
|
|
137
152
|
```ruby
|
|
138
153
|
rtl = Imprimatur.style(direction: :rtl)
|
|
139
154
|
|
|
140
|
-
book.sheet("مبيعات") do |sheet| # Arabic
|
|
141
|
-
sheet.
|
|
142
|
-
sheet.row "المنطقة", "الشمال", style: rtl
|
|
155
|
+
book.sheet("مبيعات") do |sheet| # Arabic: right to left
|
|
156
|
+
sheet.row "المنطقة", "الشمال", style: rtl # column A on the right
|
|
143
157
|
end
|
|
144
158
|
|
|
145
|
-
book.sheet("
|
|
146
|
-
sheet.right_to_left!
|
|
147
|
-
sheet.row "אזור", "
|
|
159
|
+
book.sheet("Suppliers") do |sheet|
|
|
160
|
+
sheet.right_to_left! # told so: a Hebrew layout
|
|
161
|
+
sheet.row "אזור", "Region"
|
|
148
162
|
end
|
|
149
163
|
```
|
|
150
164
|
|
|
151
165
|
Text in any right-to-left language needs nothing special: Hebrew, Yiddish,
|
|
152
166
|
Arabic, Persian, Urdu, Pashto, Sindhi, Kurdish (Sorani), Uyghur, Syriac,
|
|
153
|
-
Dhivehi (Thaana), N'Ko, Adlam and the rest.
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
Dhivehi (Thaana), N'Ko, Adlam and the rest. A sheet whose letters (its name
|
|
168
|
+
and text cells) are mostly right to left is laid out the way Excel does for
|
|
169
|
+
them, column A on the right; `right_to_left!` and `right_to_left!(false)`
|
|
170
|
+
decide instead. `direction: :rtl` fixes a cell's reading order instead of
|
|
171
|
+
letting Excel infer it from its first letter.
|
|
156
172
|
|
|
157
173
|
Not yet: merged cells, frozen panes, streaming for very large sheets.
|
|
158
174
|
|
|
@@ -171,6 +187,7 @@ doc.paragraph do |p|
|
|
|
171
187
|
end
|
|
172
188
|
doc.paragraph "Figures are unaudited.", italic: true, align: :center
|
|
173
189
|
doc.table [["Region", "Q1"], ["North", 12_500.5]], header: true
|
|
190
|
+
doc.image "chart.png", width: 300, align: :center
|
|
174
191
|
doc.page_break
|
|
175
192
|
doc.paragraph "Appendix."
|
|
176
193
|
|
|
@@ -178,15 +195,26 @@ doc.save_pdf("report.pdf")
|
|
|
178
195
|
doc.save_docx("report.docx")
|
|
179
196
|
|
|
180
197
|
Imprimatur.pdf("memo.pdf") { it.paragraph "Or in one go." } # Imprimatur.docx alike
|
|
198
|
+
Imprimatur.pdf(page_size: [200, 200], margin: 10) { it.image qr_png, width: 180 }
|
|
181
199
|
```
|
|
182
200
|
|
|
183
201
|
- Headings 1 to 3 (real Word heading styles in DOCX), paragraphs of runs
|
|
184
202
|
(bold, italic, underline, color), alignment, `\n` line breaks and tabs.
|
|
185
203
|
- Tables: equal columns, borders, a shaded header row that repeats on every
|
|
186
204
|
page; cells are converted with `to_s`.
|
|
187
|
-
-
|
|
188
|
-
|
|
189
|
-
`
|
|
205
|
+
- Pictures: PNG or JPEG (a path, the bytes or an IO), at `width:` or
|
|
206
|
+
`height:` points (the other follows), or else at 96 pixels an inch, never
|
|
207
|
+
larger than the page; `align:` like a paragraph. In PDF a JPEG and an
|
|
208
|
+
opaque PNG are stored as they are, a PNG with alpha gets a soft mask.
|
|
209
|
+
Interlaced PNGs are refused.
|
|
210
|
+
- Page breaks; A4, Letter or `page_size: [width, height]` in points (36 to
|
|
211
|
+
1584); `margin:` in points (one inch unless set); `1 / N` page numbers.
|
|
212
|
+
- Right to left without being told: a paragraph, heading or table cell
|
|
213
|
+
reads in the direction of its first letter (right aligned, and `w:bidi` in
|
|
214
|
+
DOCX), figures alone in their table's or document's, each run in its own;
|
|
215
|
+
a mostly right-to-left table has its first column on the right, and a
|
|
216
|
+
mostly right-to-left document its pages laid out right to left.
|
|
217
|
+
`direction: :rtl` or `:ltr` decides for a paragraph.
|
|
190
218
|
|
|
191
219
|
**PDF text is Latin only for now.** PDFs use the standard Helvetica fonts,
|
|
192
220
|
which carry Windows-1252: English and Western European text, curly quotes,
|
|
@@ -197,8 +225,8 @@ PDF, are the next step.
|
|
|
197
225
|
|
|
198
226
|
## Reading: documents as Markdown
|
|
199
227
|
|
|
200
|
-
The other direction: a PDF, XLSX
|
|
201
|
-
indexes and language models.
|
|
228
|
+
The other direction: a PDF, a workbook (XLSX, legacy XLS, ODS) or a DOCX
|
|
229
|
+
printed as Markdown, for search indexes and language models.
|
|
202
230
|
|
|
203
231
|
```ruby
|
|
204
232
|
Imprimatur.markdown("audit.pdf") # a path
|
|
@@ -213,17 +241,28 @@ end
|
|
|
213
241
|
```
|
|
214
242
|
|
|
215
243
|
- **PDF**: the text layer, placed glyph by glyph (fonts, encodings and
|
|
216
|
-
ToUnicode maps, forms, rotated pages), in reading order
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
244
|
+
ToUnicode maps, forms, rotated pages), in reading order, with headings by
|
|
245
|
+
font size, hyphenated line breaks rejoined, presentation forms (`fi`,
|
|
246
|
+
Arabic contextual forms) as plain letters, and running headers and
|
|
247
|
+
footers left out. Pages after the first start with `<!-- page N -->`.
|
|
248
|
+
Encrypted PDFs open when the user password is empty, as it is for
|
|
249
|
+
documents that only restrict printing or copying (RC4, AES-128, AES-256).
|
|
250
|
+
- **Right to left in PDFs** (Hebrew, Arabic, Persian, Urdu, Syriac,
|
|
251
|
+
Dhivehi, N'Ko, Adlam): a PDF records where glyphs go, not the order they
|
|
252
|
+
read in, so each line is put back in order the way the Unicode
|
|
253
|
+
bidirectional algorithm laid it out, with its page's direction for
|
|
254
|
+
context. An Arabic page's table rows read from the right, figures
|
|
255
|
+
included; an Arabic or Hebrew name in an English line reads forwards
|
|
256
|
+
inside that line; numbers stay whole (`2024-03-15`, `1,250.50`, `50%`,
|
|
257
|
+
`+98 21 5555 1234`, Arabic-Indic and Persian digits); vowel marks stay on
|
|
258
|
+
their letters. Not yet: pages set in several columns, whose lines run
|
|
259
|
+
together whatever their direction.
|
|
224
260
|
- **XLSX**: each sheet as a table under its name, values as Excel shows them
|
|
225
261
|
(dates as dates, percentages, 15 significant digits), formulas without a
|
|
226
262
|
cached result as `=FORMULA`, then the sheet's comments.
|
|
263
|
+
- **XLS** (Excel 97–2003, and Excel 5.0/95 in Western European code pages)
|
|
264
|
+
and **ODS** (LibreOffice, OpenOffice): the same, sheet by sheet; an ODS
|
|
265
|
+
number keeps the text it shows. Legacy `.xls` comments are left out.
|
|
227
266
|
- **DOCX**: headings (from styles and outline levels), bulleted and numbered
|
|
228
267
|
lists, bold, italic, links, tables, text boxes; deleted text and field
|
|
229
268
|
codes left out.
|
|
@@ -237,7 +276,7 @@ Documents are untrusted input, so reads are bounded: `max_bytes:` (512 MiB),
|
|
|
237
276
|
`Imprimatur::LimitExceeded`. Reading runs without the GVL: other threads and
|
|
238
277
|
fibers keep running, and `Timeout` or `Thread#raise` cancels it.
|
|
239
278
|
`Imprimatur::PasswordRequired` and `Imprimatur::UnsupportedDocument` (legacy
|
|
240
|
-
`.
|
|
279
|
+
`.doc`, `.xlsb`, presentations) are the other errors to expect.
|
|
241
280
|
|
|
242
281
|
### Structure and images
|
|
243
282
|
|
|
@@ -258,6 +297,15 @@ Imprimatur.content("audit.pdf")[:pages].first[:blocks].first
|
|
|
258
297
|
# bounds: {left: 72.0, top: 61.4, right: 290.1, bottom: 85.0}}
|
|
259
298
|
```
|
|
260
299
|
|
|
300
|
+
An `.xlsx` sheet also gives its `columns` widths and data `validations`, and
|
|
301
|
+
with `styles: true` the look of each formatted cell, empty ones included:
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
sheet = Imprimatur.content("template.xlsx", styles: true)[:sheets].first
|
|
305
|
+
sheet[:validations].first # => {range: "G2:G1000", type: "list", formula1: "\"Yes,No\"", ...}
|
|
306
|
+
sheet[:styles].first # => {cell: "A1", font: "Arial", size: 12.0, bold: true, fill: "FF808080", ...}
|
|
307
|
+
```
|
|
308
|
+
|
|
261
309
|
`Imprimatur.images` hands over every image as a file: a PDF's JPEG and JPEG
|
|
262
310
|
2000 as stored, fax scans as TIFF, raw pixels as PNG (soft masks as alpha),
|
|
263
311
|
with the page and place each is drawn at; a Word or Excel document's media,
|
|
@@ -270,6 +318,58 @@ Imprimatur.images("scan.pdf") do |image|
|
|
|
270
318
|
end
|
|
271
319
|
```
|
|
272
320
|
|
|
321
|
+
### Workbooks as rows
|
|
322
|
+
|
|
323
|
+
For importing uploads, `Imprimatur.spreadsheet` reads an `.xlsx`, `.xls` or
|
|
324
|
+
`.ods` into rows of Ruby values: Strings, Integers for whole numbers shown
|
|
325
|
+
without a decimal point or a percent sign, Floats, `true`/`false`, Dates,
|
|
326
|
+
DateTimes for a time of day, and `nil` for an empty cell or a formula never
|
|
327
|
+
computed.
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
book = Imprimatur.spreadsheet(upload.download) # a path, the bytes or an IO
|
|
331
|
+
book.names # => ["Suppliers", "Countries"]
|
|
332
|
+
book.sheet.rows.first # => ["Instructions: one a row"]
|
|
333
|
+
|
|
334
|
+
book.sheet("Suppliers").parse(clean: true, name: "Company Name", since: "Since")
|
|
335
|
+
# => [{ name: "Acme", since: #<Date: 2024-03-15> }, ...]
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
`parse` finds the header row among the first 100 (`header_rows:`), the first
|
|
339
|
+
holding every header, and maps the rows under it. `clean: true` strips control
|
|
340
|
+
characters and the whitespace at both ends of every String. Without such a row
|
|
341
|
+
it raises `Imprimatur::HeaderRowNotFound`, whose `missing` lists the headers the
|
|
342
|
+
closest row lacks.
|
|
343
|
+
|
|
344
|
+
Each sheet also has its `comments`, its `columns` (runs of widths, and whether
|
|
345
|
+
hidden) and its `validations`, as `Imprimatur.content` gives them. Opened with
|
|
346
|
+
`styles: true`, it has every formatted cell's look too:
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
sheet = Imprimatur.spreadsheet("template.xlsx", styles: true).sheet("BOM")
|
|
350
|
+
sheet.validation("G2:G1000") # => { range: "G2:G1000", type: "list", formula1: "Lists!$A$1:$A$9", ... }
|
|
351
|
+
sheet.style("A1") # => { cell: "A1", font: "Arial", bold: true, fill: "FFDDEEFF", ... }
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Zip archives
|
|
355
|
+
|
|
356
|
+
Archives of any files, for what is not a document (an export of an order's
|
|
357
|
+
files, a download to unpack). Entries keep their modification times and
|
|
358
|
+
comments, files are streamed from disk, and archives past 4 GiB use ZIP64;
|
|
359
|
+
reading is bounded per entry. Both run without the GVL.
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
Imprimatur::Zip.write("export.zip") do |zip|
|
|
363
|
+
zip.add_file("contracts/0contract.pdf", "/tmp/contract.pdf", comment: "Contract")
|
|
364
|
+
zip.add("manifest.json", JSON.generate(manifest))
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
Imprimatur::Zip.open("geolite.zip") do |zip|
|
|
368
|
+
csv = zip.read(zip.glob("**/*-Locations-en.csv").first) # max_bytes: 1 GiB by default
|
|
369
|
+
zip.extract("README.txt", "/tmp/readme.txt")
|
|
370
|
+
end
|
|
371
|
+
```
|
|
372
|
+
|
|
273
373
|
## Command line
|
|
274
374
|
|
|
275
375
|
The same reader as a static binary, for scripts and agents that would
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "imprimatur-cli"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "Read PDF, XLSX and DOCX documents as Markdown,
|
|
3
|
+
version = "0.3.0"
|
|
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"]
|
|
7
7
|
readme = "README.md"
|
|
@@ -127,7 +127,7 @@ fn lines<'c>(content: &'c Content, mut each: impl FnMut(Location<'c>, &str) -> b
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
-
Content::Xlsx(sheets) => {
|
|
130
|
+
Content::Xlsx(sheets) | Content::Ods(sheets) | Content::Xls(sheets) => {
|
|
131
131
|
for sheet in sheets {
|
|
132
132
|
for cell in sheet.cells.cells() {
|
|
133
133
|
let reference = cell.reference();
|
|
@@ -16,8 +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 -->`. Encrypted PDFs open when their user password is
|
|
19
|
-
/// empty. XLSX: each sheet as a table, values as
|
|
20
|
-
/// comments. DOCX: headings, lists, emphasis, links and tables.
|
|
19
|
+
/// empty. XLSX, XLS and ODS: each sheet as a table, values as shown, then
|
|
20
|
+
/// its comments. DOCX: headings, lists, emphasis, links and tables.
|
|
21
21
|
///
|
|
22
22
|
/// With several documents, each is introduced by `<!-- file: PATH -->`. A
|
|
23
23
|
/// document that fails is reported on standard error and the others are
|
|
@@ -22,11 +22,11 @@ pub const EXIT_STATUS: &str = "Exit status:
|
|
|
22
22
|
1: a document is damaged, or a file could not be read or written
|
|
23
23
|
2: the command line is wrong
|
|
24
24
|
3: a PDF has no text layer: its pages are scans, which need OCR
|
|
25
|
-
4: a PDF is encrypted with a password
|
|
26
|
-
5: not a PDF, XLSX or DOCX (legacy .
|
|
25
|
+
4: a PDF or .xls is encrypted with a password
|
|
26
|
+
5: not a PDF, XLSX, XLS, ODS or DOCX (legacy .doc, .xlsb, presentations)
|
|
27
27
|
6: a limit was hit: size, decompressed size, nesting or time";
|
|
28
28
|
|
|
29
|
-
/// Read PDF, XLSX and DOCX documents: as Markdown or JSON, searched, or their images
|
|
29
|
+
/// Read PDF, XLSX, XLS, ODS and DOCX documents: as Markdown or JSON, searched, or their images
|
|
30
30
|
///
|
|
31
31
|
/// The format is told by a document's content, not its name. Documents are
|
|
32
32
|
/// untrusted input: reading is bounded in size, decompressed size, nesting
|
|
@@ -45,8 +45,8 @@ pub const EXIT_STATUS: &str = "Exit status:
|
|
|
45
45
|
exit_code(1, "a document is damaged, or a file could not be read or written"),
|
|
46
46
|
exit_code(2, "the command line is wrong"),
|
|
47
47
|
exit_code(3, "a PDF has no text layer: its pages are scans, which need OCR"),
|
|
48
|
-
exit_code(4, "a PDF is encrypted with a password"),
|
|
49
|
-
exit_code(5, "not a PDF, XLSX or DOCX: legacy .
|
|
48
|
+
exit_code(4, "a PDF or .xls is encrypted with a password"),
|
|
49
|
+
exit_code(5, "not a PDF, XLSX, XLS, ODS or DOCX: legacy .doc, .xlsb, presentations"),
|
|
50
50
|
exit_code(6, "a limit was hit: size, decompressed size, nesting or time"),
|
|
51
51
|
after_long_help = EXIT_STATUS
|
|
52
52
|
)]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "imprimatur"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "XLSX, PDF and DOCX authoring with its own XML, zip and PDF
|
|
3
|
+
version = "0.3.0"
|
|
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"]
|
|
7
7
|
readme = "README.md"
|
|
@@ -20,14 +20,24 @@ aes = "0.9.3"
|
|
|
20
20
|
crc32fast = "1.5.2"
|
|
21
21
|
md-5 = "0.11.0"
|
|
22
22
|
miniz_oxide = "0.9.1"
|
|
23
|
+
read-fonts = { version = "0.45.1", optional = true }
|
|
23
24
|
serde_json = { version = "1.0.154", optional = true }
|
|
24
25
|
sha2 = "0.11.1"
|
|
25
26
|
thiserror = "2.0.21"
|
|
27
|
+
zip = { version = "9.0.3", default-features = false, features = ["deflate-flate2-zlib-rs"], optional = true }
|
|
26
28
|
|
|
27
29
|
[features]
|
|
28
30
|
# `read::json`: content and images as JSON, the schema the command line and
|
|
29
31
|
# the Ruby binding share.
|
|
30
32
|
json = ["dep:serde_json"]
|
|
33
|
+
# `archive`: zip archives of any files (written with their times and
|
|
34
|
+
# comments, ZIP64 when large; read within limits), through the `zip` crate.
|
|
35
|
+
archive = ["dep:zip"]
|
|
36
|
+
# `font`: static TrueType font data (glyphs, advances, line metrics),
|
|
37
|
+
# through `read-fonts`.
|
|
38
|
+
fonts = ["dep:read-fonts"]
|
|
31
39
|
|
|
32
40
|
[dev-dependencies]
|
|
41
|
+
font-test-data = "0.10.0"
|
|
42
|
+
tempfile = "3"
|
|
33
43
|
zip = { version = "9.0.3", default-features = false, features = ["deflate-flate2-zlib-rs"] }
|
|
@@ -56,7 +56,7 @@ text over 32,767 characters, formulas over 8,192, NaN and infinity, font
|
|
|
56
56
|
sizes Excel cannot set, inline validation lists over 255 characters, and
|
|
57
57
|
validations whose ranges overlap. A rejected row writes nothing.
|
|
58
58
|
|
|
59
|
-
Reading goes the other way, any PDF, XLSX or DOCX to Markdown, within
|
|
59
|
+
Reading goes the other way, any PDF, XLSX, XLS, ODS or DOCX to Markdown, within
|
|
60
60
|
limits (input size, decompressed size, nesting, time, a cancel flag):
|
|
61
61
|
|
|
62
62
|
```rust
|
|
@@ -97,9 +97,23 @@ assert!(found.is_empty(), "no pictures in this one");
|
|
|
97
97
|
# Ok::<(), imprimatur::Error>(())
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
With the `fonts` feature, `font::TrueTypeFont::from_bytes` loads borrowed
|
|
101
|
+
static TrueType data and exposes Unicode glyph IDs, horizontal advances and
|
|
102
|
+
raw `hhea` line metrics in font units. It checks the required tables and
|
|
103
|
+
outline offsets, but is not a full font sanitizer. Collections, WOFF, CFF and
|
|
104
|
+
variable fonts are not supported. This is a Rust font-data API only: document
|
|
105
|
+
font selection, PDF embedding, shaping and bidirectional layout are not
|
|
106
|
+
implemented by it; PDF writing still uses Helvetica.
|
|
107
|
+
|
|
100
108
|
Dependencies: `miniz_oxide`, `crc32fast` and `thiserror`; reading encrypted
|
|
101
109
|
PDFs adds `md-5`, `sha2` and `aes`.
|
|
102
110
|
|
|
111
|
+
Three features add what the core does not need: `json` (`read::json`, the
|
|
112
|
+
schema the command line and the Ruby gem print, through `serde_json`),
|
|
113
|
+
`archive` (`archive::{Writer, Archive}`: zip archives of any files, with
|
|
114
|
+
times, comments and ZIP64, through the `zip` crate) and `fonts`
|
|
115
|
+
(`font::TrueTypeFont`: static TrueType font data, through `read-fonts`).
|
|
116
|
+
|
|
103
117
|
## License
|
|
104
118
|
|
|
105
119
|
BSD 3-Clause
|