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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/Cargo.lock +124 -3
  4. data/Cargo.toml +1 -1
  5. data/README.md +123 -23
  6. data/ext/imprimatur_native/cli/Cargo.toml +2 -2
  7. data/ext/imprimatur_native/cli/src/cli/grep.rs +1 -1
  8. data/ext/imprimatur_native/cli/src/cli/markdown.rs +2 -2
  9. data/ext/imprimatur_native/cli/src/cli.rs +5 -5
  10. data/ext/imprimatur_native/cli/src/main.rs +1 -1
  11. data/ext/imprimatur_native/core/Cargo.toml +12 -2
  12. data/ext/imprimatur_native/core/README.md +15 -1
  13. data/ext/imprimatur_native/core/src/archive.rs +279 -0
  14. data/ext/imprimatur_native/core/src/doc/paragraph.rs +10 -3
  15. data/ext/imprimatur_native/core/src/doc/picture.rs +192 -0
  16. data/ext/imprimatur_native/core/src/doc/table.rs +21 -0
  17. data/ext/imprimatur_native/core/src/doc.rs +86 -2
  18. data/ext/imprimatur_native/core/src/docx/picture.rs +72 -0
  19. data/ext/imprimatur_native/core/src/docx.rs +91 -28
  20. data/ext/imprimatur_native/core/src/error.rs +13 -1
  21. data/ext/imprimatur_native/core/src/font.rs +229 -0
  22. data/ext/imprimatur_native/core/src/lib.rs +5 -0
  23. data/ext/imprimatur_native/core/src/pdf/layout.rs +38 -15
  24. data/ext/imprimatur_native/core/src/pdf/objects.rs +29 -5
  25. data/ext/imprimatur_native/core/src/pdf/picture.rs +197 -0
  26. data/ext/imprimatur_native/core/src/pdf.rs +31 -3
  27. data/ext/imprimatur_native/core/src/read/content.rs +91 -3
  28. data/ext/imprimatur_native/core/src/read/json.rs +96 -7
  29. data/ext/imprimatur_native/core/src/read/markdown.rs +1 -1
  30. data/ext/imprimatur_native/core/src/read/{xlsx/numbers.rs → numbers.rs} +69 -45
  31. data/ext/imprimatur_native/core/src/read/ods.rs +356 -0
  32. data/ext/imprimatur_native/core/src/read/ole.rs +244 -0
  33. data/ext/imprimatur_native/core/src/read/package.rs +0 -5
  34. data/ext/imprimatur_native/core/src/read/pdf/layout/bidi.rs +212 -0
  35. data/ext/imprimatur_native/core/src/read/pdf/layout.rs +25 -119
  36. data/ext/imprimatur_native/core/src/read/xls/strings.rs +156 -0
  37. data/ext/imprimatur_native/core/src/read/xls.rs +342 -0
  38. data/ext/imprimatur_native/core/src/read/xlsx/formats.rs +155 -0
  39. data/ext/imprimatur_native/core/src/read/xlsx.rs +104 -20
  40. data/ext/imprimatur_native/core/src/read.rs +68 -21
  41. data/ext/imprimatur_native/core/src/script.rs +116 -0
  42. data/ext/imprimatur_native/core/src/style.rs +21 -0
  43. data/ext/imprimatur_native/core/src/test_support.rs +90 -2
  44. data/ext/imprimatur_native/core/src/xlsx/autofit.rs +60 -0
  45. data/ext/imprimatur_native/core/src/xlsx/date.rs +7 -1
  46. data/ext/imprimatur_native/core/src/xlsx/format.rs +22 -6
  47. data/ext/imprimatur_native/core/src/xlsx/package.rs +22 -8
  48. data/ext/imprimatur_native/core/src/xlsx/styles.rs +9 -8
  49. data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +35 -7
  50. data/ext/imprimatur_native/core/src/xlsx.rs +40 -3
  51. data/ext/imprimatur_native/core/src/xml/reader.rs +10 -0
  52. data/ext/imprimatur_native/ffi/Cargo.toml +3 -2
  53. data/ext/imprimatur_native/ffi/src/archive.rs +119 -0
  54. data/ext/imprimatur_native/ffi/src/blocking.rs +63 -0
  55. data/ext/imprimatur_native/ffi/src/document.rs +48 -1
  56. data/ext/imprimatur_native/ffi/src/lib.rs +52 -1
  57. data/ext/imprimatur_native/ffi/src/read.rs +9 -14
  58. data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
  59. data/lib/imprimatur/document.rb +30 -6
  60. data/lib/imprimatur/error.rb +4 -3
  61. data/lib/imprimatur/reader.rb +3 -3
  62. data/lib/imprimatur/spreadsheet.rb +151 -0
  63. data/lib/imprimatur/version.rb +1 -1
  64. data/lib/imprimatur/xlsx.rb +25 -4
  65. data/lib/imprimatur/zip.rb +127 -0
  66. data/lib/imprimatur.rb +18 -5
  67. metadata +23 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 86be49aef4927f618e1ae2a92d7211f4c07239631f2b9e280d8a6d44730fbf26
4
- data.tar.gz: 5ead3ff11b3ce1a0de6121138d45e049c316afd82e0e10d55c05cc7d183d2f3a
3
+ metadata.gz: 72d405b951859304a851be99269f01972fd7817960cb53f01dbfca257f700112
4
+ data.tar.gz: 5dbc90d5aec863460a9d61134961d0c384aafd073890d52023b9291aa7ba24b4
5
5
  SHA512:
6
- metadata.gz: 1d56f28369bd9373122a014fa597e11a948ef324a9462a8d7e231d39dd9e4472d5951cd36185af54c73686b3adc64d0fa5feae20b8b38125d239ac97da65e5e8
7
- data.tar.gz: a5f77f9af0cbf86147f38af60c27a23b5674869eb480bd831538b440af88dffbaaf125071d568f590176b504104ea624458e8c2f6a571a23f6fb0cb809293fc7
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.1.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.1.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.1.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.1.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.right_to_left! # column A on the right
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("מכירות") do |sheet| # Hebrew
146
- sheet.right_to_left!
147
- sheet.row "אזור", "צפון", style: rtl
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. `right_to_left!` lays the sheet
154
- out the way Excel does for them, and `direction: :rtl` fixes a cell's reading
155
- order instead of letting Excel infer it from its first letter.
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
- - Page breaks, A4 or Letter, `1 / N` page numbers.
188
- - `direction: :rtl` makes a right-to-left paragraph (right aligned, and
189
- `w:bidi` in DOCX).
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 or DOCX printed as Markdown, for search
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 (right-to-left
217
- lines too: Hebrew, Arabic, Persian, Urdu, Syriac, Dhivehi, N'Ko, Adlam,
218
- their vowel marks kept on their letters), with headings by font size, hyphenated line breaks rejoined,
219
- presentation forms (`fi`, Arabic contextual forms) as plain letters, and
220
- running headers and footers left out. Pages after the first start with
221
- `<!-- page N -->`. Encrypted PDFs open when the user password is empty, as
222
- it is for documents that only restrict printing or copying (RC4, AES-128,
223
- AES-256).
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
- `.xls` and `.doc`, `.xlsb`, presentations) are the other errors to expect.
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.1.0"
4
- description = "Read PDF, XLSX and DOCX documents as Markdown, from the command line."
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 Excel shows them, then its
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 .xls and .doc, .xlsb, presentations)
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 .xls and .doc, .xlsb, presentations"),
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,4 +1,4 @@
1
- //! `imprimatur`: documents in, Markdown out. Reads PDF, XLSX and DOCX with
1
+ //! `imprimatur`: documents in, Markdown out. Reads PDF, XLSX, XLS, ODS and DOCX with
2
2
  //! the imprimatur crate; see `imprimatur --help`.
3
3
 
4
4
  mod app;
@@ -1,7 +1,7 @@
1
1
  [package]
2
2
  name = "imprimatur"
3
- version = "0.1.0"
4
- description = "XLSX, PDF and DOCX authoring with its own XML, zip and PDF writers."
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