imprimatur 0.2.0 → 0.4.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 +153 -3
- data/Cargo.toml +1 -1
- data/README.md +139 -21
- data/ext/imprimatur_native/cli/Cargo.toml +3 -1
- data/ext/imprimatur_native/core/Cargo.toml +20 -4
- data/ext/imprimatur_native/core/README.md +36 -4
- data/ext/imprimatur_native/core/src/archive.rs +3 -16
- data/ext/imprimatur_native/core/src/barcode/code128.rs +162 -0
- data/ext/imprimatur_native/core/src/barcode/draw.rs +124 -0
- data/ext/imprimatur_native/core/src/barcode/ean.rs +91 -0
- data/ext/imprimatur_native/core/src/barcode/gs1.rs +188 -0
- data/ext/imprimatur_native/core/src/barcode/matrix.rs +130 -0
- data/ext/imprimatur_native/core/src/barcode.rs +314 -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/symbol.rs +136 -0
- data/ext/imprimatur_native/core/src/doc/table.rs +21 -0
- data/ext/imprimatur_native/core/src/doc.rs +96 -2
- data/ext/imprimatur_native/core/src/docx/picture.rs +72 -0
- data/ext/imprimatur_native/core/src/docx.rs +103 -28
- data/ext/imprimatur_native/core/src/error.rs +18 -0
- data/ext/imprimatur_native/core/src/font.rs +231 -0
- data/ext/imprimatur_native/core/src/lib.rs +6 -1
- data/ext/imprimatur_native/core/src/pdf/layout.rs +72 -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/truetype/descriptor.rs +129 -0
- data/ext/imprimatur_native/core/src/pdf/truetype/program.rs +70 -0
- data/ext/imprimatur_native/core/src/pdf/truetype.rs +149 -0
- data/ext/imprimatur_native/core/src/pdf.rs +40 -4
- data/ext/imprimatur_native/core/src/read/content.rs +75 -2
- data/ext/imprimatur_native/core/src/read/image.rs +17 -0
- data/ext/imprimatur_native/core/src/read/json.rs +94 -7
- data/ext/imprimatur_native/core/src/read/numbers.rs +65 -42
- data/ext/imprimatur_native/core/src/read/ods.rs +1 -2
- 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.rs +1 -1
- data/ext/imprimatur_native/core/src/read/xlsx/formats.rs +155 -0
- data/ext/imprimatur_native/core/src/read/xlsx.rs +104 -19
- data/ext/imprimatur_native/core/src/read.rs +5 -1
- data/ext/imprimatur_native/core/src/script.rs +116 -0
- data/ext/imprimatur_native/core/src/style.rs +32 -0
- 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 +1 -3
- data/ext/imprimatur_native/ffi/src/barcode.rs +147 -0
- data/ext/imprimatur_native/ffi/src/document.rs +76 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +40 -1
- data/ext/imprimatur_native/ffi/src/read.rs +8 -11
- data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
- data/lib/imprimatur/barcode.rb +78 -0
- data/lib/imprimatur/document.rb +46 -6
- data/lib/imprimatur/reader.rb +2 -2
- data/lib/imprimatur/spreadsheet.rb +151 -0
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur/xlsx.rb +25 -4
- data/lib/imprimatur.rb +19 -3
- metadata +23 -2
- data/ext/imprimatur_native/core/src/test_support.rs +0 -250
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4b0c00d27618298889f1419aec02a69a9de179fb9f3caaa5f33e57db05a68a9b
|
|
4
|
+
data.tar.gz: f8c77e28a3f3ada14611260a7f1ffffc830ba3f6e0ec6d0675b4c8177b391f72
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4780e0a1754d6b1df1562910bf63f1cf70b2d7c5750c1d04e6518898408eded1d13ced430564a966f3980fa1b5db0af131aba34db66de50ca0ec19cd4db1c827
|
|
7
|
+
data.tar.gz: a4a2dde579ad49d425a2e22e39907cf35041061d245b41c8cde6f1abd3c2abdcda1c0e3c28bafcbdfe6a2bf55eab5a5adc7e30750af5fcbb1739216d5c2708d6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0](https://github.com/seuros/imprimatur/compare/v0.3.0...v0.4.0) (2026-10-18)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* barcodes: QR Code, Data Matrix, EAN-13, EAN-8, UPC-A and Code 128, GS1 forms included, as SVG, PNG and in documents ([ade7b68](https://github.com/seuros/imprimatur/commit/ade7b68f4c5adcab9d2ee0b935288eea7ec2eea6))
|
|
9
|
+
* **pdf:** add embedded TrueType font resources ([c0e3be4](https://github.com/seuros/imprimatur/commit/c0e3be4ef46ede94fcc840840e82d8ccb6679f3e))
|
|
10
|
+
* **pdf:** add embedded TrueType font resources ([313075f](https://github.com/seuros/imprimatur/commit/313075fe8ba1bedc9f4156060aeedcbb0e0a8bef))
|
|
11
|
+
|
|
12
|
+
## [0.3.0](https://github.com/seuros/imprimatur/compare/v0.2.0...v0.3.0) (2026-10-17)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* a spreadsheet's sheets give their comments, columns, validations and styles ([1797632](https://github.com/seuros/imprimatur/commit/1797632aad2948c1c0a493501de3684187062246))
|
|
18
|
+
* draw PNG and JPEG pictures, on pages of any size ([419524a](https://github.com/seuros/imprimatur/commit/419524afe36bbb06e3ef0ecb48a4fab4f80fb989))
|
|
19
|
+
* **font:** load static TrueType fonts and metrics ([fd85e3d](https://github.com/seuros/imprimatur/commit/fd85e3d78ece99506b2dde2c61bab6997cb20d21))
|
|
20
|
+
* read column widths, data validations and cell styles ([2930964](https://github.com/seuros/imprimatur/commit/2930964cde82cc998113aa83336a547cda8bd9f8))
|
|
21
|
+
* read whether a list validation shows its drop-down ([b75ef53](https://github.com/seuros/imprimatur/commit/b75ef530ef790876c58e40bb02dfc7257a48aab5))
|
|
22
|
+
* read workbooks as typed rows with Imprimatur::Spreadsheet ([927033b](https://github.com/seuros/imprimatur/commit/927033bb5ef9725c38726bc0785bbd90be8f1dc8))
|
|
23
|
+
* size columns to their cells, and give workbooks a font ([c88a058](https://github.com/seuros/imprimatur/commit/c88a0580a7ba052c4994faf4f53b42e75f48f587))
|
|
24
|
+
* write right-to-left text in its own direction ([137206e](https://github.com/seuros/imprimatur/commit/137206e08622f33ae21b6e52013e3ec7b071bb6b))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* build the gem with every feature of the crate ([7b48599](https://github.com/seuros/imprimatur/commit/7b485998abdb8b14b59e22a6ff97ca7dec143986))
|
|
30
|
+
* **font:** put the TrueType loader behind a `fonts` feature ([f9d1915](https://github.com/seuros/imprimatur/commit/f9d1915f3c98c0c7351477c6e8f21799d01e02f9))
|
|
31
|
+
* read right-to-left PDF text in its page's direction ([ca173ef](https://github.com/seuros/imprimatur/commit/ca173efd019513f25df004feafeda0e8551e64c0))
|
|
32
|
+
* show numbers with their format's decimals and grouping ([d0089ee](https://github.com/seuros/imprimatur/commit/d0089ee2dcf364ac32c7f92dc6c3af9aa6a9e9cb))
|
|
33
|
+
|
|
3
34
|
## [0.2.0](https://github.com/seuros/imprimatur/compare/v0.1.0...v0.2.0) (2026-10-16)
|
|
4
35
|
|
|
5
36
|
|
data/Cargo.lock
CHANGED
|
@@ -28,6 +28,12 @@ dependencies = [
|
|
|
28
28
|
"memchr",
|
|
29
29
|
]
|
|
30
30
|
|
|
31
|
+
[[package]]
|
|
32
|
+
name = "arrayvec"
|
|
33
|
+
version = "0.7.8"
|
|
34
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
35
|
+
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
|
36
|
+
|
|
31
37
|
[[package]]
|
|
32
38
|
name = "autocfg"
|
|
33
39
|
version = "1.5.1"
|
|
@@ -67,6 +73,26 @@ dependencies = [
|
|
|
67
73
|
"hybrid-array",
|
|
68
74
|
]
|
|
69
75
|
|
|
76
|
+
[[package]]
|
|
77
|
+
name = "bytemuck"
|
|
78
|
+
version = "1.25.2"
|
|
79
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
80
|
+
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
|
81
|
+
dependencies = [
|
|
82
|
+
"bytemuck_derive",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
[[package]]
|
|
86
|
+
name = "bytemuck_derive"
|
|
87
|
+
version = "1.12.1"
|
|
88
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
89
|
+
checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c"
|
|
90
|
+
dependencies = [
|
|
91
|
+
"proc-macro2",
|
|
92
|
+
"quote",
|
|
93
|
+
"syn 3.0.9",
|
|
94
|
+
]
|
|
95
|
+
|
|
70
96
|
[[package]]
|
|
71
97
|
name = "cexpr"
|
|
72
98
|
version = "0.6.0"
|
|
@@ -148,6 +174,16 @@ dependencies = [
|
|
|
148
174
|
"hybrid-array",
|
|
149
175
|
]
|
|
150
176
|
|
|
177
|
+
[[package]]
|
|
178
|
+
name = "datamatrix"
|
|
179
|
+
version = "0.3.3"
|
|
180
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
181
|
+
checksum = "b2d3882a013aca40e2e24f0e454ef7a581a50c603d67eded130013ab641a289d"
|
|
182
|
+
dependencies = [
|
|
183
|
+
"arrayvec",
|
|
184
|
+
"flagset",
|
|
185
|
+
]
|
|
186
|
+
|
|
151
187
|
[[package]]
|
|
152
188
|
name = "digest"
|
|
153
189
|
version = "0.11.3"
|
|
@@ -187,6 +223,12 @@ version = "2.5.0"
|
|
|
187
223
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
188
224
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
|
189
225
|
|
|
226
|
+
[[package]]
|
|
227
|
+
name = "flagset"
|
|
228
|
+
version = "0.4.7"
|
|
229
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
230
|
+
checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe"
|
|
231
|
+
|
|
190
232
|
[[package]]
|
|
191
233
|
name = "flate2"
|
|
192
234
|
version = "1.1.10"
|
|
@@ -196,6 +238,30 @@ dependencies = [
|
|
|
196
238
|
"zlib-rs",
|
|
197
239
|
]
|
|
198
240
|
|
|
241
|
+
[[package]]
|
|
242
|
+
name = "foldhash"
|
|
243
|
+
version = "0.2.0"
|
|
244
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
245
|
+
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
|
246
|
+
|
|
247
|
+
[[package]]
|
|
248
|
+
name = "font-test-data"
|
|
249
|
+
version = "0.10.0"
|
|
250
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
251
|
+
checksum = "9b7088ff167a44404649ff2221f3ba67c9eeff076cfe026c1308f54e25702a81"
|
|
252
|
+
dependencies = [
|
|
253
|
+
"font-types",
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
[[package]]
|
|
257
|
+
name = "font-types"
|
|
258
|
+
version = "0.12.7"
|
|
259
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
260
|
+
checksum = "835ae9d781464a79026cde387f9cbc650c9a4909d7cf7e658b127e3c35d2f35d"
|
|
261
|
+
dependencies = [
|
|
262
|
+
"bytemuck",
|
|
263
|
+
]
|
|
264
|
+
|
|
199
265
|
[[package]]
|
|
200
266
|
name = "getrandom"
|
|
201
267
|
version = "0.4.3"
|
|
@@ -230,12 +296,16 @@ dependencies = [
|
|
|
230
296
|
|
|
231
297
|
[[package]]
|
|
232
298
|
name = "imprimatur"
|
|
233
|
-
version = "0.
|
|
299
|
+
version = "0.4.0"
|
|
234
300
|
dependencies = [
|
|
235
301
|
"aes",
|
|
236
302
|
"crc32fast",
|
|
303
|
+
"datamatrix",
|
|
304
|
+
"font-test-data",
|
|
237
305
|
"md-5",
|
|
238
306
|
"miniz_oxide",
|
|
307
|
+
"qrcode",
|
|
308
|
+
"read-fonts",
|
|
239
309
|
"serde_json",
|
|
240
310
|
"sha2",
|
|
241
311
|
"tempfile",
|
|
@@ -245,7 +315,7 @@ dependencies = [
|
|
|
245
315
|
|
|
246
316
|
[[package]]
|
|
247
317
|
name = "imprimatur-cli"
|
|
248
|
-
version = "0.
|
|
318
|
+
version = "0.4.0"
|
|
249
319
|
dependencies = [
|
|
250
320
|
"imprimatur",
|
|
251
321
|
"regex",
|
|
@@ -257,7 +327,7 @@ dependencies = [
|
|
|
257
327
|
|
|
258
328
|
[[package]]
|
|
259
329
|
name = "imprimatur_native"
|
|
260
|
-
version = "0.
|
|
330
|
+
version = "0.4.0"
|
|
261
331
|
dependencies = [
|
|
262
332
|
"imprimatur",
|
|
263
333
|
"magnus",
|
|
@@ -455,6 +525,12 @@ dependencies = [
|
|
|
455
525
|
"unicase",
|
|
456
526
|
]
|
|
457
527
|
|
|
528
|
+
[[package]]
|
|
529
|
+
name = "qrcode"
|
|
530
|
+
version = "0.14.1"
|
|
531
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
532
|
+
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
|
|
533
|
+
|
|
458
534
|
[[package]]
|
|
459
535
|
name = "quote"
|
|
460
536
|
version = "1.0.47"
|
|
@@ -500,6 +576,19 @@ version = "0.2.3"
|
|
|
500
576
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
501
577
|
checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
|
|
502
578
|
|
|
579
|
+
[[package]]
|
|
580
|
+
name = "read-fonts"
|
|
581
|
+
version = "0.45.1"
|
|
582
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
583
|
+
checksum = "4edc8db0d972de88e53a085b1f01fb3fbdae7bb0cb7a0ce2b2f8285a412fe3ef"
|
|
584
|
+
dependencies = [
|
|
585
|
+
"bytemuck",
|
|
586
|
+
"foldhash",
|
|
587
|
+
"font-types",
|
|
588
|
+
"once_cell",
|
|
589
|
+
"yoke",
|
|
590
|
+
]
|
|
591
|
+
|
|
503
592
|
[[package]]
|
|
504
593
|
name = "regex"
|
|
505
594
|
version = "1.13.1"
|
|
@@ -632,6 +721,12 @@ version = "1.3.0"
|
|
|
632
721
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
633
722
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|
634
723
|
|
|
724
|
+
[[package]]
|
|
725
|
+
name = "stable_deref_trait"
|
|
726
|
+
version = "1.2.1"
|
|
727
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
728
|
+
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
|
729
|
+
|
|
635
730
|
[[package]]
|
|
636
731
|
name = "syn"
|
|
637
732
|
version = "2.0.119"
|
|
@@ -654,6 +749,17 @@ dependencies = [
|
|
|
654
749
|
"unicode-ident",
|
|
655
750
|
]
|
|
656
751
|
|
|
752
|
+
[[package]]
|
|
753
|
+
name = "synstructure"
|
|
754
|
+
version = "0.14.0"
|
|
755
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
756
|
+
checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02"
|
|
757
|
+
dependencies = [
|
|
758
|
+
"proc-macro2",
|
|
759
|
+
"quote",
|
|
760
|
+
"syn 3.0.9",
|
|
761
|
+
]
|
|
762
|
+
|
|
657
763
|
[[package]]
|
|
658
764
|
name = "tempfile"
|
|
659
765
|
version = "3.27.0"
|
|
@@ -809,6 +915,50 @@ dependencies = [
|
|
|
809
915
|
"memchr",
|
|
810
916
|
]
|
|
811
917
|
|
|
918
|
+
[[package]]
|
|
919
|
+
name = "yoke"
|
|
920
|
+
version = "0.8.3"
|
|
921
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
922
|
+
checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
|
|
923
|
+
dependencies = [
|
|
924
|
+
"stable_deref_trait",
|
|
925
|
+
"yoke-derive",
|
|
926
|
+
"zerofrom",
|
|
927
|
+
]
|
|
928
|
+
|
|
929
|
+
[[package]]
|
|
930
|
+
name = "yoke-derive"
|
|
931
|
+
version = "0.8.4"
|
|
932
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
933
|
+
checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71"
|
|
934
|
+
dependencies = [
|
|
935
|
+
"proc-macro2",
|
|
936
|
+
"quote",
|
|
937
|
+
"syn 3.0.9",
|
|
938
|
+
"synstructure",
|
|
939
|
+
]
|
|
940
|
+
|
|
941
|
+
[[package]]
|
|
942
|
+
name = "zerofrom"
|
|
943
|
+
version = "0.1.8"
|
|
944
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
945
|
+
checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
|
|
946
|
+
dependencies = [
|
|
947
|
+
"zerofrom-derive",
|
|
948
|
+
]
|
|
949
|
+
|
|
950
|
+
[[package]]
|
|
951
|
+
name = "zerofrom-derive"
|
|
952
|
+
version = "0.1.8"
|
|
953
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
954
|
+
checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a"
|
|
955
|
+
dependencies = [
|
|
956
|
+
"proc-macro2",
|
|
957
|
+
"quote",
|
|
958
|
+
"syn 3.0.9",
|
|
959
|
+
"synstructure",
|
|
960
|
+
]
|
|
961
|
+
|
|
812
962
|
[[package]]
|
|
813
963
|
name = "zip"
|
|
814
964
|
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.4.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
|
@@ -9,7 +9,8 @@ a 40,000-character cell is refused at the door, not discovered by Excel's
|
|
|
9
9
|
as `imprimatur` on crates.io; the gem wraps it.
|
|
10
10
|
- **No third-party format stack.** The XML writer and the zip container are
|
|
11
11
|
written here, from ECMA-376 and the zip specification; only deflate
|
|
12
|
-
(`miniz_oxide`) and CRC-32 (`crc32fast`) come from crates
|
|
12
|
+
(`miniz_oxide`) and CRC-32 (`crc32fast`) come from crates, and QR Code and
|
|
13
|
+
Data Matrix encoding (`qrcode`, `datamatrix`) for barcodes.
|
|
13
14
|
- **Deterministic.** The same document always gives the same bytes.
|
|
14
15
|
- **Native only.** CRuby 4.0+. No pure Ruby fallback, no JRuby, no TruffleRuby.
|
|
15
16
|
|
|
@@ -102,6 +103,21 @@ columns past its end are left unstyled:
|
|
|
102
103
|
sheet.row "", "", "", "Notes", style: [yellow] * 3
|
|
103
104
|
```
|
|
104
105
|
|
|
106
|
+
The workbook can have a font of its own, for unstyled cells and for styles
|
|
107
|
+
that name no font or size, and size its columns to their cells:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
Imprimatur.xlsx("export.xlsx", font: "Arial", size: 10, autofit: true) do |book|
|
|
111
|
+
book.sheet("Orders") { it.row "Supplier", "Amount", style: header }
|
|
112
|
+
book.sheet("Notes").autofit!(false) # this one keeps Excel's widths
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`autofit` (or `sheet.autofit!`) sizes each column without a width of its own
|
|
117
|
+
to its widest cell, estimated as `(characters + 3) × points / 10`,
|
|
118
|
+
bold cells one and a half times wider, at most 255. A text counts its longest
|
|
119
|
+
line, a date `yyyy-mm-dd`; formulas are left out, their results unknown.
|
|
120
|
+
|
|
105
121
|
## Validation and comments
|
|
106
122
|
|
|
107
123
|
```ruby
|
|
@@ -137,22 +153,23 @@ shown all the time with `visible: true`. The cell need not hold a value.
|
|
|
137
153
|
```ruby
|
|
138
154
|
rtl = Imprimatur.style(direction: :rtl)
|
|
139
155
|
|
|
140
|
-
book.sheet("مبيعات") do |sheet| # Arabic
|
|
141
|
-
sheet.
|
|
142
|
-
sheet.row "المنطقة", "الشمال", style: rtl
|
|
156
|
+
book.sheet("مبيعات") do |sheet| # Arabic: right to left
|
|
157
|
+
sheet.row "المنطقة", "الشمال", style: rtl # column A on the right
|
|
143
158
|
end
|
|
144
159
|
|
|
145
|
-
book.sheet("
|
|
146
|
-
sheet.right_to_left!
|
|
147
|
-
sheet.row "אזור", "
|
|
160
|
+
book.sheet("Suppliers") do |sheet|
|
|
161
|
+
sheet.right_to_left! # told so: a Hebrew layout
|
|
162
|
+
sheet.row "אזור", "Region"
|
|
148
163
|
end
|
|
149
164
|
```
|
|
150
165
|
|
|
151
166
|
Text in any right-to-left language needs nothing special: Hebrew, Yiddish,
|
|
152
167
|
Arabic, Persian, Urdu, Pashto, Sindhi, Kurdish (Sorani), Uyghur, Syriac,
|
|
153
|
-
Dhivehi (Thaana), N'Ko, Adlam and the rest.
|
|
154
|
-
|
|
155
|
-
|
|
168
|
+
Dhivehi (Thaana), N'Ko, Adlam and the rest. A sheet whose letters (its name
|
|
169
|
+
and text cells) are mostly right to left is laid out the way Excel does for
|
|
170
|
+
them, column A on the right; `right_to_left!` and `right_to_left!(false)`
|
|
171
|
+
decide instead. `direction: :rtl` fixes a cell's reading order instead of
|
|
172
|
+
letting Excel infer it from its first letter.
|
|
156
173
|
|
|
157
174
|
Not yet: merged cells, frozen panes, streaming for very large sheets.
|
|
158
175
|
|
|
@@ -171,6 +188,7 @@ doc.paragraph do |p|
|
|
|
171
188
|
end
|
|
172
189
|
doc.paragraph "Figures are unaudited.", italic: true, align: :center
|
|
173
190
|
doc.table [["Region", "Q1"], ["North", 12_500.5]], header: true
|
|
191
|
+
doc.image "chart.png", width: 300, align: :center
|
|
174
192
|
doc.page_break
|
|
175
193
|
doc.paragraph "Appendix."
|
|
176
194
|
|
|
@@ -178,15 +196,26 @@ doc.save_pdf("report.pdf")
|
|
|
178
196
|
doc.save_docx("report.docx")
|
|
179
197
|
|
|
180
198
|
Imprimatur.pdf("memo.pdf") { it.paragraph "Or in one go." } # Imprimatur.docx alike
|
|
199
|
+
Imprimatur.pdf(page_size: [200, 200], margin: 10) { it.image qr_png, width: 180 }
|
|
181
200
|
```
|
|
182
201
|
|
|
183
202
|
- Headings 1 to 3 (real Word heading styles in DOCX), paragraphs of runs
|
|
184
203
|
(bold, italic, underline, color), alignment, `\n` line breaks and tabs.
|
|
185
204
|
- Tables: equal columns, borders, a shaded header row that repeats on every
|
|
186
205
|
page; cells are converted with `to_s`.
|
|
187
|
-
-
|
|
188
|
-
|
|
189
|
-
`
|
|
206
|
+
- Pictures: PNG or JPEG (a path, the bytes or an IO), at `width:` or
|
|
207
|
+
`height:` points (the other follows), or else at 96 pixels an inch, never
|
|
208
|
+
larger than the page; `align:` like a paragraph. In PDF a JPEG and an
|
|
209
|
+
opaque PNG are stored as they are, a PNG with alpha gets a soft mask.
|
|
210
|
+
Interlaced PNGs are refused.
|
|
211
|
+
- Page breaks; A4, Letter or `page_size: [width, height]` in points (36 to
|
|
212
|
+
1584); `margin:` in points (one inch unless set); `1 / N` page numbers.
|
|
213
|
+
- Right to left without being told: a paragraph, heading or table cell
|
|
214
|
+
reads in the direction of its first letter (right aligned, and `w:bidi` in
|
|
215
|
+
DOCX), figures alone in their table's or document's, each run in its own;
|
|
216
|
+
a mostly right-to-left table has its first column on the right, and a
|
|
217
|
+
mostly right-to-left document its pages laid out right to left.
|
|
218
|
+
`direction: :rtl` or `:ltr` decides for a paragraph.
|
|
190
219
|
|
|
191
220
|
**PDF text is Latin only for now.** PDFs use the standard Helvetica fonts,
|
|
192
221
|
which carry Windows-1252: English and Western European text, curly quotes,
|
|
@@ -195,6 +224,45 @@ dashes, the euro sign. Anything else (Arabic, Hebrew, Greek, CJK, emoji) raises
|
|
|
195
224
|
script. Embedded TrueType fonts, and with them every script and proper RTL in
|
|
196
225
|
PDF, are the next step.
|
|
197
226
|
|
|
227
|
+
## Barcodes
|
|
228
|
+
|
|
229
|
+
QR Code, Data Matrix, EAN-13, EAN-8, UPC-A and Code 128, and the GS1 forms of
|
|
230
|
+
QR Code, Data Matrix and Code 128 (GS1-128):
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
qr = Imprimatur.barcode(:qr, "https://id.gs1.org/01/09506000134352", level: :m)
|
|
234
|
+
File.write("qr.svg", qr.svg(module_size: 4))
|
|
235
|
+
File.binwrite("qr.png", qr.png(module_size: 8, background: nil)) # transparent
|
|
236
|
+
|
|
237
|
+
ean = Imprimatur.barcode(:ean13, "950600013435")
|
|
238
|
+
ean.text # => "9506000134352", the check digit added (or checked when given)
|
|
239
|
+
|
|
240
|
+
label = Imprimatur.barcode(:code128, "(00)106141412345678908", gs1: true) # GS1-128
|
|
241
|
+
dpp = Imprimatur.barcode(:datamatrix, "(01)09506000134352(21)ABC123", gs1: true)
|
|
242
|
+
|
|
243
|
+
Imprimatur.pdf("label.pdf", page_size: [300, 200], margin: 10) do |doc|
|
|
244
|
+
doc.barcode label, width: 280
|
|
245
|
+
doc.barcode dpp, width: 72, align: :right
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
- `:qr` (`level:` `:l`, `:m`, `:q` or `:h`), `:datamatrix` (square),
|
|
250
|
+
`:ean13`, `:ean8`, `:upca` and `:code128`; `gs1: true` for the GS1 forms of
|
|
251
|
+
`:qr`, `:datamatrix` and `:code128`.
|
|
252
|
+
- GS1 element strings are written as people write them, AIs in parentheses,
|
|
253
|
+
and checked: AI syntax, GS1's character set, the AIs whose lengths are
|
|
254
|
+
predefined, and the check digits of SSCCs, GTINs and GLNs. Separators go in
|
|
255
|
+
only where a variable-length field needs one.
|
|
256
|
+
- `svg` and `png` take `module_size:`, `color:`, `background:` (nil for
|
|
257
|
+
none), `quiet_zone:` and, for linear codes, `bar_height:` in modules. PNGs
|
|
258
|
+
are one bit a pixel.
|
|
259
|
+
- In documents, barcodes are vector shapes in a PDF and a 300 dpi picture in
|
|
260
|
+
DOCX, at `width:` or `height:` points or else at their usual module size
|
|
261
|
+
(0.33 mm for EAN and UPC, 0.495 mm for GS1-128, 0.7 mm for QR Code and Data
|
|
262
|
+
Matrix), quiet zone included.
|
|
263
|
+
- `text`, `symbology`, `gs1?`, `width`, `height` (in modules), and `modules`
|
|
264
|
+
(rows of booleans) for drawing them some other way.
|
|
265
|
+
|
|
198
266
|
## Reading: documents as Markdown
|
|
199
267
|
|
|
200
268
|
The other direction: a PDF, a workbook (XLSX, legacy XLS, ODS) or a DOCX
|
|
@@ -213,14 +281,22 @@ end
|
|
|
213
281
|
```
|
|
214
282
|
|
|
215
283
|
- **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
|
-
|
|
284
|
+
ToUnicode maps, forms, rotated pages), in reading order, with headings by
|
|
285
|
+
font size, hyphenated line breaks rejoined, presentation forms (`fi`,
|
|
286
|
+
Arabic contextual forms) as plain letters, and running headers and
|
|
287
|
+
footers left out. Pages after the first start with `<!-- page N -->`.
|
|
288
|
+
Encrypted PDFs open when the user password is empty, as it is for
|
|
289
|
+
documents that only restrict printing or copying (RC4, AES-128, AES-256).
|
|
290
|
+
- **Right to left in PDFs** (Hebrew, Arabic, Persian, Urdu, Syriac,
|
|
291
|
+
Dhivehi, N'Ko, Adlam): a PDF records where glyphs go, not the order they
|
|
292
|
+
read in, so each line is put back in order the way the Unicode
|
|
293
|
+
bidirectional algorithm laid it out, with its page's direction for
|
|
294
|
+
context. An Arabic page's table rows read from the right, figures
|
|
295
|
+
included; an Arabic or Hebrew name in an English line reads forwards
|
|
296
|
+
inside that line; numbers stay whole (`2024-03-15`, `1,250.50`, `50%`,
|
|
297
|
+
`+98 21 5555 1234`, Arabic-Indic and Persian digits); vowel marks stay on
|
|
298
|
+
their letters. Not yet: pages set in several columns, whose lines run
|
|
299
|
+
together whatever their direction.
|
|
224
300
|
- **XLSX**: each sheet as a table under its name, values as Excel shows them
|
|
225
301
|
(dates as dates, percentages, 15 significant digits), formulas without a
|
|
226
302
|
cached result as `=FORMULA`, then the sheet's comments.
|
|
@@ -261,6 +337,15 @@ Imprimatur.content("audit.pdf")[:pages].first[:blocks].first
|
|
|
261
337
|
# bounds: {left: 72.0, top: 61.4, right: 290.1, bottom: 85.0}}
|
|
262
338
|
```
|
|
263
339
|
|
|
340
|
+
An `.xlsx` sheet also gives its `columns` widths and data `validations`, and
|
|
341
|
+
with `styles: true` the look of each formatted cell, empty ones included:
|
|
342
|
+
|
|
343
|
+
```ruby
|
|
344
|
+
sheet = Imprimatur.content("template.xlsx", styles: true)[:sheets].first
|
|
345
|
+
sheet[:validations].first # => {range: "G2:G1000", type: "list", formula1: "\"Yes,No\"", ...}
|
|
346
|
+
sheet[:styles].first # => {cell: "A1", font: "Arial", size: 12.0, bold: true, fill: "FF808080", ...}
|
|
347
|
+
```
|
|
348
|
+
|
|
264
349
|
`Imprimatur.images` hands over every image as a file: a PDF's JPEG and JPEG
|
|
265
350
|
2000 as stored, fax scans as TIFF, raw pixels as PNG (soft masks as alpha),
|
|
266
351
|
with the page and place each is drawn at; a Word or Excel document's media,
|
|
@@ -273,6 +358,39 @@ Imprimatur.images("scan.pdf") do |image|
|
|
|
273
358
|
end
|
|
274
359
|
```
|
|
275
360
|
|
|
361
|
+
### Workbooks as rows
|
|
362
|
+
|
|
363
|
+
For importing uploads, `Imprimatur.spreadsheet` reads an `.xlsx`, `.xls` or
|
|
364
|
+
`.ods` into rows of Ruby values: Strings, Integers for whole numbers shown
|
|
365
|
+
without a decimal point or a percent sign, Floats, `true`/`false`, Dates,
|
|
366
|
+
DateTimes for a time of day, and `nil` for an empty cell or a formula never
|
|
367
|
+
computed.
|
|
368
|
+
|
|
369
|
+
```ruby
|
|
370
|
+
book = Imprimatur.spreadsheet(upload.download) # a path, the bytes or an IO
|
|
371
|
+
book.names # => ["Suppliers", "Countries"]
|
|
372
|
+
book.sheet.rows.first # => ["Instructions: one a row"]
|
|
373
|
+
|
|
374
|
+
book.sheet("Suppliers").parse(clean: true, name: "Company Name", since: "Since")
|
|
375
|
+
# => [{ name: "Acme", since: #<Date: 2024-03-15> }, ...]
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
`parse` finds the header row among the first 100 (`header_rows:`), the first
|
|
379
|
+
holding every header, and maps the rows under it. `clean: true` strips control
|
|
380
|
+
characters and the whitespace at both ends of every String. Without such a row
|
|
381
|
+
it raises `Imprimatur::HeaderRowNotFound`, whose `missing` lists the headers the
|
|
382
|
+
closest row lacks.
|
|
383
|
+
|
|
384
|
+
Each sheet also has its `comments`, its `columns` (runs of widths, and whether
|
|
385
|
+
hidden) and its `validations`, as `Imprimatur.content` gives them. Opened with
|
|
386
|
+
`styles: true`, it has every formatted cell's look too:
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
sheet = Imprimatur.spreadsheet("template.xlsx", styles: true).sheet("BOM")
|
|
390
|
+
sheet.validation("G2:G1000") # => { range: "G2:G1000", type: "list", formula1: "Lists!$A$1:$A$9", ... }
|
|
391
|
+
sheet.style("A1") # => { cell: "A1", font: "Arial", bold: true, fill: "FFDDEEFF", ... }
|
|
392
|
+
```
|
|
393
|
+
|
|
276
394
|
## Zip archives
|
|
277
395
|
|
|
278
396
|
Archives of any files, for what is not a document (an export of an order's
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "imprimatur-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.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"]
|
|
@@ -10,6 +10,8 @@ rust-version.workspace = true
|
|
|
10
10
|
authors.workspace = true
|
|
11
11
|
license.workspace = true
|
|
12
12
|
repository.workspace = true
|
|
13
|
+
# Tests and their fixtures build nothing a published crate needs.
|
|
14
|
+
exclude = ["tests/**", "src/**/tests.rs"]
|
|
13
15
|
|
|
14
16
|
[[bin]]
|
|
15
17
|
name = "imprimatur"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "imprimatur"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.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"]
|
|
@@ -10,7 +10,16 @@ rust-version.workspace = true
|
|
|
10
10
|
authors.workspace = true
|
|
11
11
|
license.workspace = true
|
|
12
12
|
repository.workspace = true
|
|
13
|
-
include = [
|
|
13
|
+
include = [
|
|
14
|
+
"src/**/*.rs",
|
|
15
|
+
"src/**/*.txt",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE*",
|
|
18
|
+
# Tests and their fixtures build nothing a published crate needs.
|
|
19
|
+
"!src/**/tests.rs",
|
|
20
|
+
"!src/**/tests/**",
|
|
21
|
+
"!src/**/test_support.rs",
|
|
22
|
+
]
|
|
14
23
|
|
|
15
24
|
[lints]
|
|
16
25
|
workspace = true
|
|
@@ -18,9 +27,12 @@ workspace = true
|
|
|
18
27
|
[dependencies]
|
|
19
28
|
aes = "0.9.3"
|
|
20
29
|
crc32fast = "1.5.2"
|
|
30
|
+
datamatrix = "0.3.3"
|
|
21
31
|
md-5 = "0.11.0"
|
|
22
32
|
miniz_oxide = "0.9.1"
|
|
23
|
-
|
|
33
|
+
qrcode = { version = "0.14.1", default-features = false }
|
|
34
|
+
read-fonts = { version = "0.45.1", optional = true }
|
|
35
|
+
serde_json = { workspace = true, optional = true }
|
|
24
36
|
sha2 = "0.11.1"
|
|
25
37
|
thiserror = "2.0.21"
|
|
26
38
|
zip = { version = "9.0.3", default-features = false, features = ["deflate-flate2-zlib-rs"], optional = true }
|
|
@@ -32,7 +44,11 @@ json = ["dep:serde_json"]
|
|
|
32
44
|
# `archive`: zip archives of any files (written with their times and
|
|
33
45
|
# comments, ZIP64 when large; read within limits), through the `zip` crate.
|
|
34
46
|
archive = ["dep:zip"]
|
|
47
|
+
# `font`: static TrueType font data (glyphs, advances, line metrics),
|
|
48
|
+
# through `read-fonts`.
|
|
49
|
+
fonts = ["dep:read-fonts"]
|
|
35
50
|
|
|
36
51
|
[dev-dependencies]
|
|
37
|
-
|
|
52
|
+
font-test-data = "0.10.0"
|
|
53
|
+
tempfile.workspace = true
|
|
38
54
|
zip = { version = "9.0.3", default-features = false, features = ["deflate-flate2-zlib-rs"] }
|
|
@@ -50,6 +50,20 @@ sheet.add_validation("A2:A1000".parse()?, short)?;
|
|
|
50
50
|
# Ok::<(), imprimatur::Error>(())
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
```rust
|
|
54
|
+
use imprimatur::barcode::{Barcode, Level, Look};
|
|
55
|
+
use imprimatur::doc::{Document, Symbol};
|
|
56
|
+
|
|
57
|
+
let qr = Barcode::qr("https://id.gs1.org/01/09506000134352", Level::Medium)?;
|
|
58
|
+
let svg = qr.svg(4.0, &Look::default())?; // or png(scale, &look)
|
|
59
|
+
let label = Barcode::gs1_128("(00)106141412345678908")?; // GS1 element strings, checked
|
|
60
|
+
let mut doc = Document::new();
|
|
61
|
+
doc.barcode(Symbol::new(label).width(250.0)?); // vector shapes in PDF
|
|
62
|
+
doc.barcode(Barcode::ean13("950600013435")?); // the check digit, added
|
|
63
|
+
let pdf = doc.to_pdf()?;
|
|
64
|
+
# Ok::<(), imprimatur::Error>(())
|
|
65
|
+
```
|
|
66
|
+
|
|
53
67
|
Everything Excel would refuse is an `Error` before a byte is written: invalid
|
|
54
68
|
or duplicate sheet names, positions beyond 1,048,576 rows or 16,384 columns,
|
|
55
69
|
text over 32,767 characters, formulas over 8,192, NaN and infinity, font
|
|
@@ -97,13 +111,31 @@ assert!(found.is_empty(), "no pictures in this one");
|
|
|
97
111
|
# Ok::<(), imprimatur::Error>(())
|
|
98
112
|
```
|
|
99
113
|
|
|
114
|
+
With the `fonts` feature, `font::TrueTypeFont::from_bytes` loads borrowed
|
|
115
|
+
static TrueType data and exposes Unicode glyph IDs, horizontal advances and
|
|
116
|
+
raw `hhea` line metrics in font units. It checks the required tables and
|
|
117
|
+
outline offsets, but is not a full font sanitizer. Collections, WOFF, CFF and
|
|
118
|
+
variable fonts are not supported. This is a Rust font-data API only. The
|
|
119
|
+
internal PDF writer can serialize TrueType font resources with every glyph
|
|
120
|
+
and Unicode text mappings; document font selection, shaping and bidirectional
|
|
121
|
+
layout still need to connect to them. Document PDF output uses Helvetica.
|
|
122
|
+
|
|
123
|
+
Barcodes: QR Code and Data Matrix (their GS1 forms too), EAN-13, EAN-8, UPC-A,
|
|
124
|
+
Code 128 and GS1-128, drawn as SVG, as PNG, or into a document (shapes in a
|
|
125
|
+
PDF, a 300 dpi picture in DOCX). GS1 element strings are checked: AI syntax,
|
|
126
|
+
GS1's character set, predefined lengths, and the check digits of SSCCs, GTINs
|
|
127
|
+
and GLNs.
|
|
128
|
+
|
|
100
129
|
Dependencies: `miniz_oxide`, `crc32fast` and `thiserror`; reading encrypted
|
|
101
|
-
PDFs adds `md-5`, `sha2` and `aes
|
|
130
|
+
PDFs adds `md-5`, `sha2` and `aes`; barcodes add `qrcode` and `datamatrix`
|
|
131
|
+
(for their error correction and placement; the linear codes, GS1 and the
|
|
132
|
+
drawing are ours).
|
|
102
133
|
|
|
103
|
-
|
|
104
|
-
schema the command line and the Ruby gem print, through `serde_json`)
|
|
134
|
+
Three features add what the core does not need: `json` (`read::json`, the
|
|
135
|
+
schema the command line and the Ruby gem print, through `serde_json`),
|
|
105
136
|
`archive` (`archive::{Writer, Archive}`: zip archives of any files, with
|
|
106
|
-
times, comments and ZIP64, through the `zip` crate)
|
|
137
|
+
times, comments and ZIP64, through the `zip` crate) and `fonts`
|
|
138
|
+
(`font::TrueTypeFont`: static TrueType font data, through `read-fonts`).
|
|
107
139
|
|
|
108
140
|
## License
|
|
109
141
|
|