imprimatur 0.1.0-x86_64-linux-gnu → 0.3.0-x86_64-linux-gnu
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/README.md +123 -23
- data/lib/imprimatur/4.0/imprimatur_native.so +0 -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 +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f23670009061b9f9bf1faf3d9c9400ef4c6a136561aefe2ab3d67c197d91fbe5
|
|
4
|
+
data.tar.gz: 2cd106d5795727dcc12188cef98a6d8ce9fe19374aa0611dedcaa1184b1a6028
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 15c21dc23dacbbea052ab4115fca4a6a35385474af9136b1bada72b3c374b6fcedbd521cd6c6741dd392ccf92b5125f861159ec822d4c26a342e201b4b5e827f
|
|
7
|
+
data.tar.gz: 9a189350ce6ac19418097246554a94dcd0f0d994c26ab47f3632caca45ca2fc8045bd81bfea53da0f95c9d62cbe1656ec5feccccad91a414d731621c13d6c467
|
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/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
|
|
Binary file
|
data/lib/imprimatur/document.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Imprimatur
|
|
4
|
-
# A flowing document (headings, paragraphs, tables, page breaks)
|
|
5
|
-
# renders to PDF and DOCX from the same content.
|
|
4
|
+
# A flowing document (headings, paragraphs, tables, pictures, page breaks)
|
|
5
|
+
# that renders to PDF and DOCX from the same content.
|
|
6
6
|
#
|
|
7
7
|
# doc = Imprimatur::Document.new(page_numbers: true)
|
|
8
8
|
# doc.heading 'Quarterly report'
|
|
@@ -18,12 +18,35 @@ module Imprimatur
|
|
|
18
18
|
# PDF uses the standard Helvetica fonts for now: Windows-1252 (Latin) text
|
|
19
19
|
# only, anything else raises {Imprimatur::Error}. DOCX carries any script.
|
|
20
20
|
class Document
|
|
21
|
-
# @param page_size [:a4, :letter]
|
|
21
|
+
# @param page_size [:a4, :letter, Array(Numeric, Numeric)] or its width
|
|
22
|
+
# and height in points, 36 to 1584 a side
|
|
23
|
+
# @param margin [Numeric, nil] points on every side, one inch (72) unless set
|
|
22
24
|
# @param page_numbers [Boolean] number pages "1 / N" in the footer
|
|
23
25
|
# @raise [ArgumentError] an unknown page size
|
|
24
|
-
|
|
26
|
+
# @raise [Imprimatur::Error] a page size or margin out of range
|
|
27
|
+
def initialize(page_size: :a4, margin: nil, page_numbers: false)
|
|
25
28
|
@native = Native::Document.new
|
|
26
|
-
|
|
29
|
+
numbers = page_numbers ? true : false
|
|
30
|
+
if page_size.is_a?(Array)
|
|
31
|
+
width, height = page_size
|
|
32
|
+
@native.set_page_points(Float(width), Float(height), numbers)
|
|
33
|
+
else
|
|
34
|
+
@native.set_page(page_size.to_s, numbers)
|
|
35
|
+
end
|
|
36
|
+
@native.set_margin(Float(margin)) if margin
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# A PNG or JPEG in the flow, at +width+ or +height+ points (the other
|
|
40
|
+
# follows its proportions), or else at 96 pixels an inch; never larger
|
|
41
|
+
# than the page leaves room for.
|
|
42
|
+
#
|
|
43
|
+
# @param source [String, Pathname, #read] a path, the bytes or an IO
|
|
44
|
+
# @param align [:left, :center, :right, nil]
|
|
45
|
+
# @return [self]
|
|
46
|
+
# @raise [Imprimatur::Error] no PNG or JPEG, or one it cannot draw
|
|
47
|
+
def image(source, width: nil, height: nil, align: nil)
|
|
48
|
+
@native.picture(Reader.read(source), width && Float(width), height && Float(height), align&.to_s)
|
|
49
|
+
self
|
|
27
50
|
end
|
|
28
51
|
|
|
29
52
|
# @param text [String]
|
|
@@ -40,7 +63,8 @@ module Imprimatur
|
|
|
40
63
|
#
|
|
41
64
|
# @param text [String, nil] plain text, styled by the keywords below
|
|
42
65
|
# @param align [:left, :center, :right, nil]
|
|
43
|
-
# @param direction [:ltr, :rtl, nil]
|
|
66
|
+
# @param direction [:ltr, :rtl, nil] nil takes the direction of the first letter;
|
|
67
|
+
# right to left aligns right unless told otherwise
|
|
44
68
|
# @yieldparam paragraph [Paragraph] add runs with {Paragraph#text}
|
|
45
69
|
# @return [self]
|
|
46
70
|
def paragraph(text = nil, align: nil, direction: nil, **look)
|
data/lib/imprimatur/error.rb
CHANGED
|
@@ -9,11 +9,12 @@ module Imprimatur
|
|
|
9
9
|
# A PDF without a text layer: its pages are images (scans), which need OCR.
|
|
10
10
|
class NoTextLayer < Error; end
|
|
11
11
|
|
|
12
|
-
# A PDF encrypted with a user password
|
|
12
|
+
# A PDF encrypted with a user password (only an empty one is tried), or
|
|
13
|
+
# an encrypted legacy +.xls+.
|
|
13
14
|
class PasswordRequired < Error; end
|
|
14
15
|
|
|
15
|
-
# A format {Imprimatur.markdown} does not read: legacy +.
|
|
16
|
-
#
|
|
16
|
+
# A format {Imprimatur.markdown} does not read: legacy +.doc+, binary
|
|
17
|
+
# +.xlsb+, presentations, anything not a PDF, XLSX, XLS, ODS or DOCX.
|
|
17
18
|
class UnsupportedDocument < Error; end
|
|
18
19
|
|
|
19
20
|
# The read hit one of its limits (size, decompressed size, nesting, time),
|
data/lib/imprimatur/reader.rb
CHANGED
|
@@ -14,15 +14,15 @@ module Imprimatur
|
|
|
14
14
|
module Reader
|
|
15
15
|
# File signatures a document's bytes start with; a String that starts
|
|
16
16
|
# with neither is taken for a path.
|
|
17
|
-
SIGNATURES = ["%PDF-".b, "PK\x03\x04".b, "PK\x05\x06".b].freeze
|
|
17
|
+
SIGNATURES = ["%PDF-".b, "PK\x03\x04".b, "PK\x05\x06".b, "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1".b].freeze
|
|
18
18
|
private_constant :SIGNATURES
|
|
19
19
|
|
|
20
20
|
# @see Imprimatur.markdown
|
|
21
21
|
def self.markdown(document, **) = Native.markdown(read(document), *limits(**))
|
|
22
22
|
|
|
23
23
|
# @see Imprimatur.content
|
|
24
|
-
def self.content(document, **)
|
|
25
|
-
JSON.parse(Native.content(read(document), *limits(**)), symbolize_names: true)
|
|
24
|
+
def self.content(document, styles: false, **)
|
|
25
|
+
JSON.parse(Native.content(read(document), *limits(**), styles ? true : false), symbolize_names: true)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# @see Imprimatur.images
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Imprimatur
|
|
7
|
+
# No row among the first ones of a sheet holds every header
|
|
8
|
+
# {Spreadsheet::Sheet#parse} looks for.
|
|
9
|
+
class HeaderRowNotFound < Error
|
|
10
|
+
# @return [Array<String>] the headers missing from the row that holds the most of them
|
|
11
|
+
attr_reader :missing
|
|
12
|
+
|
|
13
|
+
def initialize(missing)
|
|
14
|
+
@missing = missing.freeze
|
|
15
|
+
super("no header row has #{missing.join(', ')}")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A workbook read for its values: +.xlsx+, +.xls+ (Excel 97-2003, and
|
|
20
|
+
# Excel 5.0/95) or +.ods+. Values come as Ruby has them: Strings,
|
|
21
|
+
# Integers for whole numbers shown without a decimal point or a percent
|
|
22
|
+
# sign, Floats, true and false, Dates, and DateTimes for a time of day;
|
|
23
|
+
# a formula without a computed result is nil.
|
|
24
|
+
#
|
|
25
|
+
# book = Imprimatur::Spreadsheet.open('suppliers.xlsx')
|
|
26
|
+
# book.names # => ["Suppliers", "Countries"]
|
|
27
|
+
# book.sheet.rows.first # => ["Name", "Country", "Since"]
|
|
28
|
+
# book.sheet.parse(name: 'Name', since: 'Since')
|
|
29
|
+
# # => [{ name: "Acme", since: #<Date: 2024-03-15> }, ...]
|
|
30
|
+
class Spreadsheet
|
|
31
|
+
# One sheet's values.
|
|
32
|
+
class Sheet
|
|
33
|
+
# How many rows from the top {#parse} searches for the header row.
|
|
34
|
+
HEADER_ROWS = 100
|
|
35
|
+
|
|
36
|
+
# @return [String]
|
|
37
|
+
attr_reader :name
|
|
38
|
+
|
|
39
|
+
# @return [Array<Array>] every row from the first to the last with a
|
|
40
|
+
# value, each an Array of values by column, nil where a cell is
|
|
41
|
+
# empty; a row without a value is an empty Array
|
|
42
|
+
attr_reader :rows
|
|
43
|
+
|
|
44
|
+
# @api private
|
|
45
|
+
def initialize(name, hidden, rows, details)
|
|
46
|
+
@name = name
|
|
47
|
+
@hidden = hidden
|
|
48
|
+
@rows = rows
|
|
49
|
+
@details = details
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def hidden? = @hidden
|
|
53
|
+
|
|
54
|
+
# @return [Array<Hash>] +{cell:, author:, text:}+
|
|
55
|
+
def comments = details[:comments]
|
|
56
|
+
|
|
57
|
+
# @return [Array<Hash>] runs of +{first:, last:, width:, hidden:}+ columns, by letter
|
|
58
|
+
def columns = details[:columns]
|
|
59
|
+
|
|
60
|
+
# @return [Array<Hash>] +{range:, type:, formula1:, ...}+, as {Imprimatur.content} gives them
|
|
61
|
+
def validations = details[:validations]
|
|
62
|
+
|
|
63
|
+
# @return [Array<Hash>] each formatted cell's +{cell:, font:, size:, bold:, fill:, ...}+;
|
|
64
|
+
# empty unless the workbook was opened with +styles: true+
|
|
65
|
+
def styles = details.fetch(:styles, [])
|
|
66
|
+
|
|
67
|
+
# @return [Hash, nil] the validation over exactly +range+ (+"G2:G1000"+)
|
|
68
|
+
def validation(range) = validations.find { it[:range] == range }
|
|
69
|
+
|
|
70
|
+
# @return [Hash, nil] the look of the cell at +reference+ (+"A1"+)
|
|
71
|
+
def style(reference) = styles.find { it[:cell] == reference }
|
|
72
|
+
|
|
73
|
+
# The rows under the header row, as Hashes of +headers+' keys to the
|
|
74
|
+
# values in the columns their headers name. The header row is the
|
|
75
|
+
# first, among the first +header_rows+, to hold every header.
|
|
76
|
+
#
|
|
77
|
+
# sheet.parse(clean: true, name: 'Company Name', country: 'Country')
|
|
78
|
+
# # => [{ name: "Acme", country: "PL" }, ...]
|
|
79
|
+
#
|
|
80
|
+
# @param clean [Boolean] strips control characters, and whitespace at
|
|
81
|
+
# both ends, from every String, headers included
|
|
82
|
+
# @param header_rows [Integer]
|
|
83
|
+
# @param headers [Hash{Symbol => String}]
|
|
84
|
+
# @return [Array<Hash>]
|
|
85
|
+
# @raise [HeaderRowNotFound] no row holds every header
|
|
86
|
+
def parse(clean: false, header_rows: HEADER_ROWS, **headers)
|
|
87
|
+
rows = clean ? @rows.map { |row| row.map { clean(it) } } : @rows
|
|
88
|
+
header, columns = header_row(rows.first(header_rows), headers.values)
|
|
89
|
+
rows.drop(header + 1).map do |row|
|
|
90
|
+
headers.keys.zip(columns).to_h { |key, column| [key, row[column]] }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
# The index of the first row holding every header, and their columns.
|
|
97
|
+
def header_row(rows, headers)
|
|
98
|
+
closest = []
|
|
99
|
+
rows.each_with_index do |row, index|
|
|
100
|
+
found = headers.filter_map { row.index(it) }
|
|
101
|
+
return [index, found] if found.size == headers.size
|
|
102
|
+
|
|
103
|
+
closest = headers.select { row.include?(it) } if found.size > closest.size
|
|
104
|
+
end
|
|
105
|
+
raise HeaderRowNotFound, headers - closest
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def clean(value)
|
|
109
|
+
value.is_a?(String) ? value.gsub(/[[:cntrl:]]|\A[[:space:]]+|[[:space:]]+\z/, '') : value
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def details
|
|
113
|
+
@details = JSON.parse(@details, symbolize_names: true) if @details.is_a?(String)
|
|
114
|
+
@details
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# @param document [String, Pathname, #read] a path, the bytes or an IO
|
|
119
|
+
# @param styles [Boolean] each sheet's {Sheet#styles} too
|
|
120
|
+
# @param limits [Hash] +max_bytes:+, +max_inflated:+ and +timeout:+, as
|
|
121
|
+
# {Imprimatur.content} takes them
|
|
122
|
+
# @return [Spreadsheet]
|
|
123
|
+
# @raise [UnsupportedDocument] a PDF, a Word document or another format
|
|
124
|
+
# @raise [Imprimatur::Error] a damaged workbook, or past a limit
|
|
125
|
+
def self.open(document, styles: false, **limits)
|
|
126
|
+
new(Native.sheets(Reader.read(document), *Reader.limits(**limits), styles ? true : false))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @return [Array<Sheet>] in the workbook's order, hidden ones included
|
|
130
|
+
attr_reader :sheets
|
|
131
|
+
|
|
132
|
+
# @api private
|
|
133
|
+
def initialize(sheets)
|
|
134
|
+
@sheets = sheets.map { |name, hidden, rows, details| Sheet.new(name, hidden, rows, details) }.freeze
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @return [Array<String>]
|
|
138
|
+
def names = sheets.map(&:name)
|
|
139
|
+
|
|
140
|
+
# The sheet named +name+, or the first one.
|
|
141
|
+
#
|
|
142
|
+
# @return [Sheet]
|
|
143
|
+
# @raise [KeyError] the workbook has no sheet of that name
|
|
144
|
+
def sheet(name = nil)
|
|
145
|
+
return sheets.first unless name
|
|
146
|
+
|
|
147
|
+
sheets.find { it.name == name } or
|
|
148
|
+
raise KeyError.new("no sheet named #{name.inspect}, only #{names.inspect}", receiver: self, key: name)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
data/lib/imprimatur/version.rb
CHANGED
data/lib/imprimatur/xlsx.rb
CHANGED
|
@@ -22,10 +22,11 @@ module Imprimatur
|
|
|
22
22
|
# Builds a workbook. With a path, saves it there and returns the path;
|
|
23
23
|
# without, returns the +.xlsx+ bytes as a binary String.
|
|
24
24
|
#
|
|
25
|
+
# @param options [Hash] +font:+, +size:+ and +autofit:+, as {Workbook#initialize} takes them
|
|
25
26
|
# @yieldparam book [Workbook]
|
|
26
27
|
# @raise [Imprimatur::Error] the workbook has no sheets, or the file cannot be written
|
|
27
|
-
def self.build(path = nil)
|
|
28
|
-
book = Workbook.new
|
|
28
|
+
def self.build(path = nil, **options)
|
|
29
|
+
book = Workbook.new(**options)
|
|
29
30
|
yield book
|
|
30
31
|
return book.render unless path
|
|
31
32
|
|
|
@@ -35,8 +36,15 @@ module Imprimatur
|
|
|
35
36
|
|
|
36
37
|
# An ordered set of uniquely named sheets.
|
|
37
38
|
class Workbook
|
|
38
|
-
|
|
39
|
+
# @param font [String, nil] the font of unstyled cells and of styles that
|
|
40
|
+
# name none; Calibri without one
|
|
41
|
+
# @param size [Numeric, nil] its size in points; 11 without one
|
|
42
|
+
# @param autofit [Boolean] every sheet sizes its columns to its cells, as
|
|
43
|
+
# {Sheet#autofit!} does
|
|
44
|
+
# @raise [Imprimatur::Error] a font name or size Excel rejects
|
|
45
|
+
def initialize(font: nil, size: nil, autofit: false)
|
|
39
46
|
@native = Native::Workbook.new
|
|
47
|
+
@native.configure(font&.to_s, size && Float(size), autofit ? true : false)
|
|
40
48
|
@sheets = []
|
|
41
49
|
end
|
|
42
50
|
|
|
@@ -166,9 +174,22 @@ module Imprimatur
|
|
|
166
174
|
self
|
|
167
175
|
end
|
|
168
176
|
|
|
177
|
+
# Sizes the columns without a width of their own to their widest cell
|
|
178
|
+
# when the workbook is written, estimated as +(characters + 3) x points
|
|
179
|
+
# / 10+, bold cells one and a half times wider, at most 255.
|
|
180
|
+
# Formulas are left out.
|
|
181
|
+
#
|
|
182
|
+
# @return [self]
|
|
183
|
+
def autofit!(enabled = true)
|
|
184
|
+
@native.set_autofit(@index, enabled ? true : false)
|
|
185
|
+
self
|
|
186
|
+
end
|
|
187
|
+
|
|
169
188
|
# Lays the sheet out right to left, column A on the right, as Excel
|
|
170
189
|
# shows sheets in right-to-left languages: Hebrew, Arabic, Persian,
|
|
171
|
-
# Urdu and the rest.
|
|
190
|
+
# Urdu and the rest; +false+ keeps it left to right. Without a call, a
|
|
191
|
+
# sheet is right to left when most of the letters of its name and text
|
|
192
|
+
# cells are.
|
|
172
193
|
#
|
|
173
194
|
# @return [self]
|
|
174
195
|
def right_to_left!(enabled = true)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Imprimatur
|
|
6
|
+
# Zip archives of any files: entries written with their modification times
|
|
7
|
+
# and comments (ZIP64 when large), read one at a time within a size limit.
|
|
8
|
+
# Compressing and reading run without the GVL.
|
|
9
|
+
#
|
|
10
|
+
# Imprimatur::Zip.write('export.zip') do |zip|
|
|
11
|
+
# zip.add_file('contracts/0contract.pdf', '/tmp/contract.pdf', comment: 'Contract')
|
|
12
|
+
# zip.add('manifest.json', JSON.generate(manifest))
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Imprimatur::Zip.open('geolite.zip') do |zip|
|
|
16
|
+
# zip.read(zip.glob('**/*-Locations-en.csv').first)
|
|
17
|
+
# end
|
|
18
|
+
module Zip
|
|
19
|
+
# Bytes an entry may hold once decompressed by default: 1 GiB.
|
|
20
|
+
MAX_ENTRY_BYTES = 1024**3
|
|
21
|
+
|
|
22
|
+
# An entry added to an archive being written.
|
|
23
|
+
Added = Data.define(:name, :comment)
|
|
24
|
+
|
|
25
|
+
# An entry of an archive being read, as its directory states it.
|
|
26
|
+
Entry = Data.define(:name, :size, :compressed_size, :crc32, :comment)
|
|
27
|
+
|
|
28
|
+
# Writes an archive at +path+, the block adding its entries. On an
|
|
29
|
+
# error, the partial file is removed.
|
|
30
|
+
#
|
|
31
|
+
# @yieldparam zip [Writer]
|
|
32
|
+
# @return the path
|
|
33
|
+
def self.write(path)
|
|
34
|
+
writer = Writer.new(path)
|
|
35
|
+
yield writer
|
|
36
|
+
writer.finish
|
|
37
|
+
path
|
|
38
|
+
rescue Exception # any failure, an interrupt included: the partial file goes, then the error
|
|
39
|
+
FileUtils.rm_f(path.to_s) if writer
|
|
40
|
+
raise
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The archive at +path+.
|
|
44
|
+
#
|
|
45
|
+
# @yieldparam zip [Reader] when a block is given
|
|
46
|
+
# @return [Reader, Object] the reader, or what the block returns
|
|
47
|
+
# @raise [Errno::ENOENT] when there is no such file
|
|
48
|
+
# @raise [Error] when it is no zip
|
|
49
|
+
def self.open(path)
|
|
50
|
+
reader = Reader.new(path)
|
|
51
|
+
block_given? ? yield(reader) : reader
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Adds entries to an archive being written; see {Zip.write}.
|
|
55
|
+
class Writer
|
|
56
|
+
# @return [Array<Added>] the entries added so far, in order
|
|
57
|
+
attr_reader :entries
|
|
58
|
+
|
|
59
|
+
def initialize(path)
|
|
60
|
+
@native = Native::ZipWriter.new(path.to_s)
|
|
61
|
+
@entries = []
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Adds the file at +source+ as +name+, streamed from disk, with its
|
|
65
|
+
# modification time.
|
|
66
|
+
def add_file(name, source, comment: nil)
|
|
67
|
+
@native.add_file(name.to_s, source.to_s, comment&.to_s)
|
|
68
|
+
@entries << Added.new(name: name.to_s, comment: comment&.to_s)
|
|
69
|
+
self
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Adds +data+ as +name+, dated now.
|
|
73
|
+
def add(name, data, comment: nil)
|
|
74
|
+
@native.add(name.to_s, data.to_s.b, comment&.to_s)
|
|
75
|
+
@entries << Added.new(name: name.to_s, comment: comment&.to_s)
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The archive's own comment, shown by most zip tools.
|
|
80
|
+
def comment=(comment)
|
|
81
|
+
@native.comment = comment.to_s
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# @api private
|
|
85
|
+
def finish = @native.finish
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# An archive's entries, by name; see {Zip.open}.
|
|
89
|
+
class Reader
|
|
90
|
+
# @return [Array<String>] the entries' names, in the archive's order
|
|
91
|
+
attr_reader :names
|
|
92
|
+
|
|
93
|
+
def initialize(path)
|
|
94
|
+
raise Errno::ENOENT, path.to_s unless File.file?(path.to_s)
|
|
95
|
+
|
|
96
|
+
@native = Native::ZipReader.new(path.to_s)
|
|
97
|
+
@names = @native.names.freeze
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Each entry's name, sizes, CRC-32 and comment, nothing decompressed.
|
|
101
|
+
#
|
|
102
|
+
# @return [Array<Entry>]
|
|
103
|
+
def entries
|
|
104
|
+
@native.entries.map { |row| Entry.new(*row) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The names matching a shell pattern, +**/+ spanning folders.
|
|
108
|
+
def glob(pattern) = names.select { File.fnmatch?(pattern, it, File::FNM_PATHNAME | File::FNM_EXTGLOB) }
|
|
109
|
+
|
|
110
|
+
# The entry's bytes, binary.
|
|
111
|
+
#
|
|
112
|
+
# @raise [KeyError] for an entry the archive does not have
|
|
113
|
+
# @raise [LimitExceeded] when it holds more than +max_bytes+
|
|
114
|
+
def read(name, max_bytes: MAX_ENTRY_BYTES)
|
|
115
|
+
@native.read(name.to_s, Integer(max_bytes)) or raise KeyError, "no entry #{name} in the archive"
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Writes the entry to +path+, streamed; returns +path+.
|
|
119
|
+
#
|
|
120
|
+
# @raise (see #read)
|
|
121
|
+
def extract(name, path, max_bytes: MAX_ENTRY_BYTES)
|
|
122
|
+
@native.extract(name.to_s, path.to_s, Integer(max_bytes)) or raise KeyError, "no entry #{name} in the archive"
|
|
123
|
+
path
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
data/lib/imprimatur.rb
CHANGED
|
@@ -9,6 +9,8 @@ require_relative 'imprimatur/validation'
|
|
|
9
9
|
require_relative 'imprimatur/xlsx'
|
|
10
10
|
require_relative 'imprimatur/document'
|
|
11
11
|
require_relative 'imprimatur/reader'
|
|
12
|
+
require_relative 'imprimatur/spreadsheet'
|
|
13
|
+
require_relative 'imprimatur/zip'
|
|
12
14
|
|
|
13
15
|
# Document authoring with a native core: XLSX workbooks, and PDF and DOCX from
|
|
14
16
|
# one document model.
|
|
@@ -24,7 +26,7 @@ require_relative 'imprimatur/reader'
|
|
|
24
26
|
module Imprimatur
|
|
25
27
|
class << self
|
|
26
28
|
# @see Xlsx.build
|
|
27
|
-
def xlsx(path = nil, &) = Xlsx.build(path, &)
|
|
29
|
+
def xlsx(path = nil, **, &) = Xlsx.build(path, **, &)
|
|
28
30
|
|
|
29
31
|
# @param expression [String] such as +"SUM(A1:A3)"+ or +"=A1*2"+
|
|
30
32
|
# @return [Formula]
|
|
@@ -46,7 +48,7 @@ module Imprimatur
|
|
|
46
48
|
# As {.pdf}, rendered as DOCX.
|
|
47
49
|
def docx(path = nil, **options, &) = Document.build(:docx, path, **options, &)
|
|
48
50
|
|
|
49
|
-
# A PDF, XLSX or DOCX as Markdown: a PDF's text layer (headings by font
|
|
51
|
+
# A PDF, workbook (XLSX, XLS, ODS) or DOCX as Markdown: a PDF's text layer (headings by font
|
|
50
52
|
# size, running headers and footers left out, pages marked with
|
|
51
53
|
# +<!-- page N -->+), each sheet of a workbook as a table, a Word
|
|
52
54
|
# document's headings, lists, emphasis, links and tables. The format is
|
|
@@ -67,7 +69,7 @@ module Imprimatur
|
|
|
67
69
|
# @return [String] Markdown, UTF-8
|
|
68
70
|
# @raise [NoTextLayer] a PDF of scanned pages: run OCR instead
|
|
69
71
|
# @raise [PasswordRequired] a PDF that needs a password to open
|
|
70
|
-
# @raise [UnsupportedDocument] not a PDF, XLSX or DOCX
|
|
72
|
+
# @raise [UnsupportedDocument] not a PDF, XLSX, XLS, ODS or DOCX
|
|
71
73
|
# @raise [LimitExceeded] the document is too large, too deep or too slow
|
|
72
74
|
# @raise [Error] a damaged document
|
|
73
75
|
def markdown(document, ...) = Reader.markdown(document, ...)
|
|
@@ -77,18 +79,29 @@ module Imprimatur
|
|
|
77
79
|
# (+heading+, +paragraph+) with their +:bounds+ in points from the
|
|
78
80
|
# page's top-left corner; a workbook's +:sheets+ of +:rows+ of typed
|
|
79
81
|
# +:cells+ (+number+, +date+, +bool+, +text+, +error+, +formula+) and
|
|
80
|
-
# +:comments
|
|
82
|
+
# +:comments+, and from an +.xlsx+ its +:columns+ widths and data
|
|
83
|
+
# +:validations+; a Word document's +:blocks+ (+heading+, +paragraph+,
|
|
81
84
|
# +list_item+, +table+), their bold, italic and linked +:marks+ as byte
|
|
82
85
|
# ranges. Dates are ISO 8601 strings: Excel's have no time zone.
|
|
83
86
|
#
|
|
84
87
|
# sheet = Imprimatur.content('sales.xlsx')[:sheets].first
|
|
85
88
|
# sheet[:rows][1][:cells] # => [{cell: "A2", type: "text", value: "North"}, ...]
|
|
86
89
|
#
|
|
87
|
-
# Takes the arguments of {.markdown}, and raises as it does.
|
|
90
|
+
# Takes the arguments of {.markdown}, and raises as it does. With
|
|
91
|
+
# +styles: true+ each sheet also lists its formatted cells' +:styles+:
|
|
92
|
+
# font, size, bold, italic, underline, ARGB color and fill, alignment,
|
|
93
|
+
# wrap, border and number format.
|
|
88
94
|
#
|
|
89
95
|
# @return [Hash] with symbol keys; +:format+ is +"pdf"+, +"xlsx"+ or +"docx"+
|
|
90
96
|
def content(document, ...) = Reader.content(document, ...)
|
|
91
97
|
|
|
98
|
+
# A workbook's sheets and their values, read from the same documents
|
|
99
|
+
# {.markdown} takes (+.xlsx+, +.xls+, +.ods+); see {Spreadsheet}.
|
|
100
|
+
#
|
|
101
|
+
# @return [Spreadsheet]
|
|
102
|
+
# @raise [UnsupportedDocument] a PDF, a Word document or another format
|
|
103
|
+
def spreadsheet(document, ...) = Spreadsheet.open(document, ...)
|
|
104
|
+
|
|
92
105
|
# A document's images, as files: a PDF's, once each, from the first page
|
|
93
106
|
# that draws them (JPEG and JPEG 2000 as stored, fax scans as TIFF, raw
|
|
94
107
|
# pixels as PNG); a Word or Excel document's media. A scanned PDF gives
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: imprimatur
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: x86_64-linux-gnu
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -11,7 +11,8 @@ date: 1980-01-02 00:00:00.000000000 Z
|
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: 'A Ruby DSL over a native core that writes Excel workbooks, and PDF and
|
|
13
13
|
Word documents from one model, with its own XML, zip and PDF writers: no third-party
|
|
14
|
-
format stack.
|
|
14
|
+
format stack. It reads PDF, XLSX, XLS, ODS and DOCX back: as Markdown, as typed
|
|
15
|
+
structure, and their images. Precompiled for Linux, macOS and FreeBSD; CRuby only.'
|
|
15
16
|
email:
|
|
16
17
|
- terminale@gmail.com
|
|
17
18
|
executables: []
|
|
@@ -28,10 +29,12 @@ files:
|
|
|
28
29
|
- lib/imprimatur/formula.rb
|
|
29
30
|
- lib/imprimatur/native.rb
|
|
30
31
|
- lib/imprimatur/reader.rb
|
|
32
|
+
- lib/imprimatur/spreadsheet.rb
|
|
31
33
|
- lib/imprimatur/style.rb
|
|
32
34
|
- lib/imprimatur/validation.rb
|
|
33
35
|
- lib/imprimatur/version.rb
|
|
34
36
|
- lib/imprimatur/xlsx.rb
|
|
37
|
+
- lib/imprimatur/zip.rb
|
|
35
38
|
homepage: https://github.com/seuros/imprimatur
|
|
36
39
|
licenses:
|
|
37
40
|
- BSD-3-Clause
|
|
@@ -59,5 +62,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
59
62
|
requirements: []
|
|
60
63
|
rubygems_version: 4.0.20
|
|
61
64
|
specification_version: 4
|
|
62
|
-
summary: XLSX, PDF and DOCX authoring
|
|
65
|
+
summary: XLSX, PDF and DOCX authoring, and office documents read back. Let it be printed.
|
|
63
66
|
test_files: []
|