imprimatur 0.2.0-aarch64-linux-gnu → 0.4.0-aarch64-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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a1dcd8e194d2f7cf4096350af23ef9a629d8b9bb0e32ec88cd60edbebb0e190b
4
- data.tar.gz: d5a960d8c7902c18b4be2b99553d50f95fdb1eb18bf8f674191de123a2f081b0
3
+ metadata.gz: 820b696bf9efd260711282d1ad700b843939f469797577f578db69490d90795c
4
+ data.tar.gz: a53d88596e77e76cfa13e967ff9a095f7f996d0d21ffb3aa2b1b6bf6bcfcba9a
5
5
  SHA512:
6
- metadata.gz: f27b7635c4c624b33adaedb532afd279d594117a2e596de2291d180158be82e90e34c5f19a1596c31a13b75c073f23331ec846f61751d8c70d48fcd9750bac5d
7
- data.tar.gz: 9a3d3ae03b027bb7e3044779b63760cfb99889664d21acb08a78c065181282cf6e35f9d6cab6bb9af2cb19efa649304d34db2713adc913759ee32677765bb939
6
+ metadata.gz: 0a0bec11ea0fe229a56b729d3c40f8ef8f397d4940780a0460d2772ef5cec8529f7ec97dfcd99759b7b5fd30fa6a026a62d97115a94c113857af06d563616800
7
+ data.tar.gz: a659697da26c4e7878a17f6d2442bc53403dbb926f91d930e026a9468f00437a4403b14325f00b3d9e6d63580074d8e83e1f4b22ef3a163b4797d80aa2ea9513
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/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.right_to_left! # column A on the right
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("מכירות") do |sheet| # Hebrew
146
- sheet.right_to_left!
147
- sheet.row "אזור", "צפון", style: rtl
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. `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.
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
- - 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).
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 (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,
219
- hyphenated line breaks rejoined, 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).
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
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Imprimatur
4
+ # A barcode: QR Code, Data Matrix, EAN-13, EAN-8, UPC-A or Code 128, and the
5
+ # GS1 forms of QR Code, Data Matrix and Code 128 (GS1-128). Built with
6
+ # {Imprimatur.barcode}; draw it with {#svg} or {#png}, or place it in a
7
+ # {Document} with {Document#barcode}. The class itself is native.
8
+ #
9
+ # qr = Imprimatur.barcode(:qr, 'https://id.gs1.org/01/09506000134352')
10
+ # File.write('qr.svg', qr.svg(module_size: 4))
11
+ #
12
+ # label = Imprimatur.barcode(:code128, '(00)106141412345678908', gs1: true)
13
+ # File.binwrite('label.png', label.png(module_size: 3))
14
+ #
15
+ # Imprimatur.barcode(:ean13, '950600013435').text # => "9506000134352"
16
+ class Barcode
17
+ SYMBOLOGIES = %i[qr datamatrix ean13 ean8 upca code128].freeze
18
+
19
+ # @param symbology [:qr, :datamatrix, :ean13, :ean8, :upca, :code128]
20
+ # @param data [String] text; a GTIN's digits for EAN and UPC, the check
21
+ # digit added when left out; with +gs1: true+, a GS1 element string
22
+ # such as "(01)09506000134352(17)251231(10)AB12"
23
+ # @param gs1 [Boolean] a GS1 QR Code, GS1 DataMatrix or GS1-128
24
+ # @param level [:l, :m, :q, :h, nil] a QR Code's error correction, :m unless set
25
+ # @return [Barcode]
26
+ # @raise [Imprimatur::Error] data the symbology cannot hold, an invalid
27
+ # element string, or a wrong check digit
28
+ # @raise [ArgumentError] an unknown symbology or level, or one that does not apply
29
+ def self.encode(symbology, data, gs1: false, level: nil)
30
+ __encode(symbology.to_s, String(data), gs1 ? true : false, level&.to_s)
31
+ end
32
+
33
+ private_class_method :__encode
34
+
35
+ # @param module_size [Numeric] user units (pixels) a module, 0.01 to 1000
36
+ # @param color [String] the bars, "#RRGGBB"
37
+ # @param background [String, nil] "#RRGGBB", or nil for none
38
+ # @param quiet_zone [Boolean] keep the blank margin scanners need
39
+ # @param bar_height [Integer, nil] a linear code's bar height in modules;
40
+ # the symbology's nominal height unless set
41
+ # @return [String] SVG markup
42
+ # @raise [Imprimatur::Error] a module size or color out of range
43
+ def svg(module_size: 4, color: '#000000', background: '#FFFFFF', quiet_zone: true, bar_height: nil)
44
+ __svg(Float(module_size), look(color, background, quiet_zone, bar_height))
45
+ end
46
+
47
+ # @param module_size [Integer] pixels a module
48
+ # @param (see #svg)
49
+ # @return [String] the PNG bytes (binary): one bit a pixel, the background
50
+ # transparent when nil
51
+ # @raise [Imprimatur::Error] a size of 0, an image above 65 536 pixels a side,
52
+ # or an invalid color
53
+ def png(module_size: 4, color: '#000000', background: '#FFFFFF', quiet_zone: true, bar_height: nil)
54
+ __png(Integer(module_size), look(color, background, quiet_zone, bar_height))
55
+ end
56
+
57
+ # @!method symbology
58
+ # @return [Symbol] one of {SYMBOLOGIES}
59
+ # @!method text
60
+ # @return [String] what it encodes: the data given, or a GTIN with its check digit
61
+ # @!method gs1?
62
+ # @return [Boolean] whether it carries a GS1 element string
63
+ # @!method width
64
+ # @return [Integer] modules across, without the quiet zone
65
+ # @!method height
66
+ # @return [Integer] modules down; 1 for a linear code
67
+ # @!method modules
68
+ # @return [Array<Array<Boolean>>] rows of modules, true for dark
69
+
70
+ def inspect = "#<#{self.class} #{symbology}#{' gs1' if gs1?} #{text.inspect}>"
71
+
72
+ private
73
+
74
+ def look(color, background, quiet_zone, bar_height)
75
+ [color.to_s, background&.to_s, quiet_zone ? true : false, bar_height && Integer(bar_height)]
76
+ end
77
+ end
78
+ end
@@ -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) that
5
- # renders to PDF and DOCX from the same content.
4
+ # A flowing document (headings, paragraphs, tables, pictures, barcodes, 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,51 @@ 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
- def initialize(page_size: :a4, page_numbers: false)
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
- @native.set_page(page_size.to_s, page_numbers ? true : false)
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
50
+ end
51
+
52
+ # A barcode in the flow: vector shapes in a PDF, a 300 dpi picture in DOCX.
53
+ # Drawn at +width+ or +height+ points (a QR Code or Data Matrix keeps
54
+ # square modules; a linear code's height is its bars'), or else at its
55
+ # symbology's usual module size; never larger than the page leaves room for.
56
+ # Its quiet zone is part of its size.
57
+ #
58
+ # @param barcode [Barcode] from {Imprimatur.barcode}
59
+ # @param align [:left, :center, :right, nil]
60
+ # @param color [String, nil] the bars, "#RRGGBB"; black unless set
61
+ # @return [self]
62
+ # @raise [Imprimatur::Error] a size out of range, or an invalid color
63
+ def barcode(barcode, width: nil, height: nil, align: nil, color: nil)
64
+ @native.barcode(barcode, [width && Float(width), height && Float(height)], align&.to_s, color&.to_s)
65
+ self
27
66
  end
28
67
 
29
68
  # @param text [String]
@@ -40,7 +79,8 @@ module Imprimatur
40
79
  #
41
80
  # @param text [String, nil] plain text, styled by the keywords below
42
81
  # @param align [:left, :center, :right, nil]
43
- # @param direction [:ltr, :rtl, nil] right to left aligns right unless told otherwise
82
+ # @param direction [:ltr, :rtl, nil] nil takes the direction of the first letter;
83
+ # right to left aligns right unless told otherwise
44
84
  # @yieldparam paragraph [Paragraph] add runs with {Paragraph#text}
45
85
  # @return [self]
46
86
  def paragraph(text = nil, align: nil, direction: nil, **look)
@@ -21,8 +21,8 @@ module Imprimatur
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Imprimatur
4
- VERSION = '0.2.0'
4
+ VERSION = '0.4.0'
5
5
  end
@@ -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
- def initialize
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)
data/lib/imprimatur.rb CHANGED
@@ -6,9 +6,11 @@ require_relative 'imprimatur/formula'
6
6
  require_relative 'imprimatur/native'
7
7
  require_relative 'imprimatur/style'
8
8
  require_relative 'imprimatur/validation'
9
+ require_relative 'imprimatur/barcode'
9
10
  require_relative 'imprimatur/xlsx'
10
11
  require_relative 'imprimatur/document'
11
12
  require_relative 'imprimatur/reader'
13
+ require_relative 'imprimatur/spreadsheet'
12
14
  require_relative 'imprimatur/zip'
13
15
 
14
16
  # Document authoring with a native core: XLSX workbooks, and PDF and DOCX from
@@ -25,7 +27,7 @@ require_relative 'imprimatur/zip'
25
27
  module Imprimatur
26
28
  class << self
27
29
  # @see Xlsx.build
28
- def xlsx(path = nil, &) = Xlsx.build(path, &)
30
+ def xlsx(path = nil, **, &) = Xlsx.build(path, **, &)
29
31
 
30
32
  # @param expression [String] such as +"SUM(A1:A3)"+ or +"=A1*2"+
31
33
  # @return [Formula]
@@ -37,6 +39,9 @@ module Imprimatur
37
39
  # @see Validation.build
38
40
  def validation(**) = Validation.build(**)
39
41
 
42
+ # @see Barcode.encode
43
+ def barcode(symbology, data, **) = Barcode.encode(symbology, data, **)
44
+
40
45
  # Builds a {Document} and renders it as PDF. With a path, saves it there
41
46
  # and returns the path; without, returns the bytes.
42
47
  #
@@ -78,18 +83,29 @@ module Imprimatur
78
83
  # (+heading+, +paragraph+) with their +:bounds+ in points from the
79
84
  # page's top-left corner; a workbook's +:sheets+ of +:rows+ of typed
80
85
  # +:cells+ (+number+, +date+, +bool+, +text+, +error+, +formula+) and
81
- # +:comments+; a Word document's +:blocks+ (+heading+, +paragraph+,
86
+ # +:comments+, and from an +.xlsx+ its +:columns+ widths and data
87
+ # +:validations+; a Word document's +:blocks+ (+heading+, +paragraph+,
82
88
  # +list_item+, +table+), their bold, italic and linked +:marks+ as byte
83
89
  # ranges. Dates are ISO 8601 strings: Excel's have no time zone.
84
90
  #
85
91
  # sheet = Imprimatur.content('sales.xlsx')[:sheets].first
86
92
  # sheet[:rows][1][:cells] # => [{cell: "A2", type: "text", value: "North"}, ...]
87
93
  #
88
- # Takes the arguments of {.markdown}, and raises as it does.
94
+ # Takes the arguments of {.markdown}, and raises as it does. With
95
+ # +styles: true+ each sheet also lists its formatted cells' +:styles+:
96
+ # font, size, bold, italic, underline, ARGB color and fill, alignment,
97
+ # wrap, border and number format.
89
98
  #
90
99
  # @return [Hash] with symbol keys; +:format+ is +"pdf"+, +"xlsx"+ or +"docx"+
91
100
  def content(document, ...) = Reader.content(document, ...)
92
101
 
102
+ # A workbook's sheets and their values, read from the same documents
103
+ # {.markdown} takes (+.xlsx+, +.xls+, +.ods+); see {Spreadsheet}.
104
+ #
105
+ # @return [Spreadsheet]
106
+ # @raise [UnsupportedDocument] a PDF, a Word document or another format
107
+ def spreadsheet(document, ...) = Spreadsheet.open(document, ...)
108
+
93
109
  # A document's images, as files: a PDF's, once each, from the first page
94
110
  # that draws them (JPEG and JPEG 2000 as stored, fax scans as TIFF, raw
95
111
  # 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.2.0
4
+ version: 0.4.0
5
5
  platform: aarch64-linux-gnu
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -24,11 +24,13 @@ files:
24
24
  - README.md
25
25
  - lib/imprimatur.rb
26
26
  - lib/imprimatur/4.0/imprimatur_native.so
27
+ - lib/imprimatur/barcode.rb
27
28
  - lib/imprimatur/document.rb
28
29
  - lib/imprimatur/error.rb
29
30
  - lib/imprimatur/formula.rb
30
31
  - lib/imprimatur/native.rb
31
32
  - lib/imprimatur/reader.rb
33
+ - lib/imprimatur/spreadsheet.rb
32
34
  - lib/imprimatur/style.rb
33
35
  - lib/imprimatur/validation.rb
34
36
  - lib/imprimatur/version.rb