imprimatur 0.3.0-aarch64-linux-gnu → 0.5.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: cec8c0fa26e3628bd1d67a514909be24610c8d03ca7f087e422a0a05e66fb8ee
4
- data.tar.gz: d80d26028f03816a403fb1907f934ebd5d643d8d22bea5a0d7e2677a1963701d
3
+ metadata.gz: 5a6d6451a7a370921f43b37e27a8edb7d886a4923981afd28a256b424b85234e
4
+ data.tar.gz: e74c978a45ce43cc0856b186b71f38d3204aea8cb71c4f0ed5d961bd4f0dc874
5
5
  SHA512:
6
- metadata.gz: 7a9153c976c1571f303e65cd0e41bc9c9768122e57ad928dcf81ca8f973aca20b7812d42f82e1455d183deaa05ebdb707e0545302b84aad0cb3f2e0b03cd287f
7
- data.tar.gz: 94498b1778cc9cd937070971bec6f9261f5e4c4d5fb916f5a2ef0c8a62efb3187201b9d64ea92bfd241c35779fea50bf50624845d23a729f4dfcea602c1976a8
6
+ metadata.gz: bf5c3cc806be6379e109c335d65e4b1f84662cbdcb569db4ae0e9fd0e028d4185cf0f9f0b4cd7cc37ac00cc8a313878aaddaee276898b5835e4365f25fe0dfb4
7
+ data.tar.gz: 0ac1716d1208f48001683efafd3bba638ddc134ff1d67c545a7ad123f99213ee9ff83a96eb7603ca3f3f64ba1cdb8a14507128f530adf21c3e6a5ab989ebca95
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0](https://github.com/seuros/imprimatur/compare/v0.4.0...v0.5.0) (2026-10-18)
4
+
5
+
6
+ ### Features
7
+
8
+ * a binary String is always a document's bytes, a path must name a regular file, and reads stop at max_bytes ([3484a64](https://github.com/seuros/imprimatur/commit/3484a64b791beb2e9f90cd485441fae255e223f0))
9
+
10
+ ## [0.4.0](https://github.com/seuros/imprimatur/compare/v0.3.0...v0.4.0) (2026-10-18)
11
+
12
+
13
+ ### Features
14
+
15
+ * 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))
16
+ * **pdf:** add embedded TrueType font resources ([c0e3be4](https://github.com/seuros/imprimatur/commit/c0e3be4ef46ede94fcc840840e82d8ccb6679f3e))
17
+ * **pdf:** add embedded TrueType font resources ([313075f](https://github.com/seuros/imprimatur/commit/313075fe8ba1bedc9f4156060aeedcbb0e0a8bef))
18
+
3
19
  ## [0.3.0](https://github.com/seuros/imprimatur/compare/v0.2.0...v0.3.0) (2026-10-17)
4
20
 
5
21
 
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
 
@@ -223,6 +224,45 @@ dashes, the euro sign. Anything else (Arabic, Hebrew, Greek, CJK, emoji) raises
223
224
  script. Embedded TrueType fonts, and with them every script and proper RTL in
224
225
  PDF, are the next step.
225
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
+
226
266
  ## Reading: documents as Markdown
227
267
 
228
268
  The other direction: a PDF, a workbook (XLSX, legacy XLS, ODS) or a DOCX
@@ -278,6 +318,13 @@ fibers keep running, and `Timeout` or `Thread#raise` cancels it.
278
318
  `Imprimatur::PasswordRequired` and `Imprimatur::UnsupportedDocument` (legacy
279
319
  `.doc`, `.xlsb`, presentations) are the other errors to expect.
280
320
 
321
+ A binary String is always the document's bytes, never a path: an upload
322
+ holding `/etc/passwd` or `/dev/zero` is refused as `UnsupportedDocument`,
323
+ not read from the server. A path (a `Pathname`, or a String that is not
324
+ binary and has no document signature) must name a regular file; devices,
325
+ pipes and directories are refused. An IO or a path is read no further than
326
+ `max_bytes:`, so an endless stream or a huge file stops there.
327
+
281
328
  ### Structure and images
282
329
 
283
330
  Markdown is one rendering of what a read finds. `Imprimatur.content` gives
@@ -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,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Imprimatur
4
- # A flowing document (headings, paragraphs, tables, pictures, page breaks)
4
+ # A flowing document (headings, paragraphs, tables, pictures, barcodes, page breaks)
5
5
  # that renders to PDF and DOCX from the same content.
6
6
  #
7
7
  # doc = Imprimatur::Document.new(page_numbers: true)
@@ -49,6 +49,22 @@ module Imprimatur
49
49
  self
50
50
  end
51
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
66
+ end
67
+
52
68
  # @param text [String]
53
69
  # @param level [Integer] 1 (the title) to 3
54
70
  # @return [self]
@@ -12,22 +12,29 @@ module Imprimatur
12
12
  # Documents in: Markdown, their structure, their images. See
13
13
  # {Imprimatur.markdown}, {Imprimatur.content} and {Imprimatur.images}.
14
14
  module Reader
15
- # File signatures a document's bytes start with; a String that starts
16
- # with neither is taken for a path.
15
+ # File signatures a document's bytes start with; a String that is not
16
+ # binary and starts with neither is taken for a path.
17
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
+ # The largest document read in unless told, as the native core's own limit.
21
+ MAX_BYTES = 512 * 1024 * 1024
22
+ private_constant :MAX_BYTES
23
+
20
24
  # @see Imprimatur.markdown
21
- def self.markdown(document, **) = Native.markdown(read(document), *limits(**))
25
+ def self.markdown(document, max_bytes: nil, **)
26
+ Native.markdown(read(document, max_bytes), *limits(max_bytes:, **))
27
+ end
22
28
 
23
29
  # @see Imprimatur.content
24
- def self.content(document, styles: false, **)
25
- JSON.parse(Native.content(read(document), *limits(**), styles ? true : false), symbolize_names: true)
30
+ def self.content(document, styles: false, max_bytes: nil, **)
31
+ bytes = read(document, max_bytes)
32
+ JSON.parse(Native.content(bytes, *limits(max_bytes:, **), styles ? true : false), symbolize_names: true)
26
33
  end
27
34
 
28
35
  # @see Imprimatur.images
29
- def self.images(document, **)
30
- Native.images(read(document), *limits(**)).map do |data, description|
36
+ def self.images(document, max_bytes: nil, **)
37
+ Native.images(read(document, max_bytes), *limits(max_bytes:, **)).map do |data, description|
31
38
  found = JSON.parse(description, symbolize_names: true)
32
39
  Image.new(data:, **Image.members.drop(1).to_h { [it, found[it]] })
33
40
  end
@@ -38,22 +45,52 @@ module Imprimatur
38
45
  [max_bytes, max_inflated, timeout && Float(timeout)]
39
46
  end
40
47
 
48
+ # A path or an IO is read no further than +max_bytes+ (one byte past it
49
+ # tells it is passed): an endless stream or a huge file stops there.
50
+ #
41
51
  # @api private
42
- def self.read(document)
52
+ def self.read(document, max_bytes = nil)
53
+ limit = max_bytes.is_a?(Integer) && max_bytes >= 0 ? max_bytes : MAX_BYTES
43
54
  case document
44
- when Pathname then File.binread(document)
45
- when String then bytes?(document) ? document : File.binread(document)
55
+ when Pathname then file(document, limit)
56
+ when String then bytes?(document) ? document : file(document, limit)
46
57
  else
47
58
  raise TypeError, "expected a path, bytes or an IO, not #{document.class}" unless document.respond_to?(:read)
48
59
 
49
- document.read.to_s.b
60
+ within(document.read(limit + 1).to_s.b, limit)
50
61
  end
51
62
  end
52
63
 
64
+ # A binary String (what +File.binread+, a binary IO or a download give)
65
+ # is bytes, whatever it holds: uploaded bytes never name a file to read.
66
+ #
53
67
  # @api private
54
68
  def self.bytes?(string)
69
+ return true if string.encoding == Encoding::BINARY
70
+
55
71
  head = string.byteslice(0, 1024).b
56
72
  SIGNATURES.any? { head.start_with?(it) } || head.include?("%PDF-") || head.include?("\0")
57
73
  end
74
+
75
+ # A regular file's bytes. Opened without blocking and checked through
76
+ # the open file, so a pipe cannot hang the read, a device such as
77
+ # +/dev/zero+ is refused, and the file cannot be swapped in between.
78
+ #
79
+ # @api private
80
+ def self.file(path, limit)
81
+ File.open(path, File::RDONLY | File::NONBLOCK | File::BINARY) do |io|
82
+ stat = io.stat
83
+ raise UnsupportedDocument, "cannot read this document: #{path} is not a regular file" unless stat.file?
84
+
85
+ within(stat.size > limit ? '' : io.read(limit + 1).to_s, limit, stat.size)
86
+ end
87
+ end
88
+
89
+ # @api private
90
+ def self.within(bytes, limit, size = bytes.bytesize)
91
+ raise LimitExceeded, 'the document exceeds the input size limit' if [size, bytes.bytesize].max > limit
92
+
93
+ bytes
94
+ end
58
95
  end
59
96
  end
@@ -123,7 +123,7 @@ module Imprimatur
123
123
  # @raise [UnsupportedDocument] a PDF, a Word document or another format
124
124
  # @raise [Imprimatur::Error] a damaged workbook, or past a limit
125
125
  def self.open(document, styles: false, **limits)
126
- new(Native.sheets(Reader.read(document), *Reader.limits(**limits), styles ? true : false))
126
+ new(Native.sheets(Reader.read(document, limits[:max_bytes]), *Reader.limits(**limits), styles ? true : false))
127
127
  end
128
128
 
129
129
  # @return [Array<Sheet>] in the workbook's order, hidden ones included
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Imprimatur
4
- VERSION = '0.3.0'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/imprimatur.rb CHANGED
@@ -6,6 +6,7 @@ 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'
@@ -38,6 +39,9 @@ module Imprimatur
38
39
  # @see Validation.build
39
40
  def validation(**) = Validation.build(**)
40
41
 
42
+ # @see Barcode.encode
43
+ def barcode(symbology, data, **) = Barcode.encode(symbology, data, **)
44
+
41
45
  # Builds a {Document} and renders it as PDF. With a path, saves it there
42
46
  # and returns the path; without, returns the bytes.
43
47
  #
@@ -62,7 +66,8 @@ module Imprimatur
62
66
  # Imprimatur.markdown(File.open('sales.xlsx'))
63
67
  #
64
68
  # @param document [String, Pathname, #read] a path, the document's bytes
65
- # (told apart by their signature), or an IO
69
+ # (a binary String always is; another String only with a document's
70
+ # signature), or an IO. A path must name a regular file.
66
71
  # @param timeout [Numeric, nil] seconds the read may take (60), +nil+ for none
67
72
  # @param max_bytes [Integer, nil] the largest document accepted (512 MiB)
68
73
  # @param max_inflated [Integer, nil] decompressed bytes allowed in all (1 GiB)
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.3.0
4
+ version: 0.5.0
5
5
  platform: aarch64-linux-gnu
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -24,6 +24,7 @@ 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