imprimatur 0.4.0-aarch64-linux-gnu → 0.6.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: 820b696bf9efd260711282d1ad700b843939f469797577f578db69490d90795c
4
- data.tar.gz: a53d88596e77e76cfa13e967ff9a095f7f996d0d21ffb3aa2b1b6bf6bcfcba9a
3
+ metadata.gz: 60b0c7b03b95799ce827fc16a4c288dc929647e0fc94c6672bac555d75b85140
4
+ data.tar.gz: 11b2699bbe2481da51deeeeada85731c85d7577d3cfd06551093866edff29ab1
5
5
  SHA512:
6
- metadata.gz: 0a0bec11ea0fe229a56b729d3c40f8ef8f397d4940780a0460d2772ef5cec8529f7ec97dfcd99759b7b5fd30fa6a026a62d97115a94c113857af06d563616800
7
- data.tar.gz: a659697da26c4e7878a17f6d2442bc53403dbb926f91d930e026a9468f00437a4403b14325f00b3d9e6d63580074d8e83e1f4b22ef3a163b4797d80aa2ea9513
6
+ metadata.gz: e710314613e0b8fde5895a7b751d6458fd52749cd44167828f7a3cee6e9abdebf98befe86c78a832913e3b362e0cbc0b57072ed35742b5bab1dd11e64918623e
7
+ data.tar.gz: 1a5eeda8859cafa2ae3ac1b3bac3b50954d4bfbcb7b4562d6c0c04280f5195192eb20fa07aaf167f364e7022e9d5b338ceab269852be9bebd0d40bf9ceaf6d11
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - Add native PDF metadata, bounded page reads with explicit failure states,
6
+ and optional pure-Rust full-page PNG rendering.
7
+ - Keep scanned and blank page structure available without a text layer.
8
+ - Render PDFs through a bundled Rust worker with time, memory and output limits;
9
+ cancellation kills and reaps it. macOS uses an RSS watchdog, not a hard cap.
10
+
11
+ ## [0.6.0](https://github.com/seuros/imprimatur/compare/v0.5.0...v0.6.0) (2026-10-25)
12
+
13
+
14
+ ### Features
15
+
16
+ * bound PDF reads and rendering; deduplicate Rust ([420e096](https://github.com/seuros/imprimatur/commit/420e096591ef08190427c7b090840dcd761010b4))
17
+ * **doc:** set the body font size ([44f7617](https://github.com/seuros/imprimatur/commit/44f76170fd1ad29f143c1ef22c355fd1c1cbf8e1))
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * enforce render limits across Unix platforms ([5c7f0ba](https://github.com/seuros/imprimatur/commit/5c7f0bab2076681782c6bfa2de6062ad53c0e7f9))
23
+
24
+ ## [0.5.0](https://github.com/seuros/imprimatur/compare/v0.4.0...v0.5.0) (2026-10-18)
25
+
26
+
27
+ ### Features
28
+
29
+ * 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))
30
+
3
31
  ## [0.4.0](https://github.com/seuros/imprimatur/compare/v0.3.0...v0.4.0) (2026-10-18)
4
32
 
5
33
 
data/README.md CHANGED
@@ -7,10 +7,9 @@ a 40,000-character cell is refused at the door, not discovered by Excel's
7
7
 
8
8
  - **A Ruby DSL over a native core.** The core is also published on its own,
9
9
  as `imprimatur` on crates.io; the gem wraps it.
10
- - **No third-party format stack.** The XML writer and the zip container are
11
- written here, from ECMA-376 and the zip specification; only deflate
12
- (`miniz_oxide`) and CRC-32 (`crc32fast`) come from crates, and QR Code and
13
- Data Matrix encoding (`qrcode`, `datamatrix`) for barcodes.
10
+ - **Own readers and writers.** XML, zip and PDF reading/writing live here.
11
+ Optional full-page PDF rendering uses Hayro, a pure-Rust CPU renderer;
12
+ no Poppler, PDFium, GPU or system fonts are required.
14
13
  - **Deterministic.** The same document always gives the same bytes.
15
14
  - **Native only.** CRuby 4.0+. No pure Ruby fallback, no JRuby, no TruffleRuby.
16
15
 
@@ -178,7 +177,7 @@ Not yet: merged cells, frozen panes, streaming for very large sheets.
178
177
  One document, two formats:
179
178
 
180
179
  ```ruby
181
- doc = Imprimatur::Document.new(page_size: :a4, page_numbers: true)
180
+ doc = Imprimatur::Document.new(page_size: :a4, font_size: 10, page_numbers: true)
182
181
  doc.heading "Quarterly report"
183
182
  doc.heading "Sales", level: 2
184
183
  doc.paragraph do |p|
@@ -195,12 +194,14 @@ doc.paragraph "Appendix."
195
194
  doc.save_pdf("report.pdf")
196
195
  doc.save_docx("report.docx")
197
196
 
198
- Imprimatur.pdf("memo.pdf") { it.paragraph "Or in one go." } # Imprimatur.docx alike
197
+ Imprimatur.pdf("memo.pdf", font_size: 10) { it.paragraph "Or in one go." } # Imprimatur.docx alike
199
198
  Imprimatur.pdf(page_size: [200, 200], margin: 10) { it.image qr_png, width: 180 }
200
199
  ```
201
200
 
202
201
  - Headings 1 to 3 (real Word heading styles in DOCX), paragraphs of runs
203
202
  (bold, italic, underline, color), alignment, `\n` line breaks and tabs.
203
+ - Body text at `font_size:` points (1 to 1638, in steps of 0.5; 11 unless
204
+ set), in both formats; headings keep their own sizes.
204
205
  - Tables: equal columns, borders, a shaded header row that repeats on every
205
206
  page; cells are converted with `to_s`.
206
207
  - Pictures: PNG or JPEG (a path, the bytes or an IO), at `width:` or
@@ -318,6 +319,13 @@ fibers keep running, and `Timeout` or `Thread#raise` cancels it.
318
319
  `Imprimatur::PasswordRequired` and `Imprimatur::UnsupportedDocument` (legacy
319
320
  `.doc`, `.xlsb`, presentations) are the other errors to expect.
320
321
 
322
+ A binary String is always the document's bytes, never a path: an upload
323
+ holding `/etc/passwd` or `/dev/zero` is refused as `UnsupportedDocument`,
324
+ not read from the server. A path (a `Pathname`, or a String that is not
325
+ binary and has no document signature) must name a regular file; devices,
326
+ pipes and directories are refused. An IO or a path is read no further than
327
+ `max_bytes:`, so an endless stream or a huge file stops there.
328
+
321
329
  ### Structure and images
322
330
 
323
331
  Markdown is one rendering of what a read finds. `Imprimatur.content` gives
@@ -326,6 +334,50 @@ pages of headings and paragraphs with their position, a workbook's typed
326
334
  cells and comments, a Word document's blocks with their bold, italic and
327
335
  linked stretches.
328
336
 
337
+ For a PDF without text, `content` still returns every page, its empty
338
+ `blocks` and its `images` count. `markdown` and the text-oriented CLI commands
339
+ still report `NoTextLayer`;
340
+ OCR callers need the structure of scanned and blank pages too.
341
+
342
+ ### Bounded PDF pages and OCR rendering
343
+
344
+ ```ruby
345
+ Imprimatur.pdf_info(upload.download) # => {page_count: 20, encrypted: false}
346
+ result = Imprimatur.pdf_pages(upload.download, first_page: 2, last_page: 10, max_pages: 5)
347
+ result[:pages].each { |page| puts [page[:page], page[:status], page[:text]] }
348
+ png = Imprimatur.render_pdf_page(upload.download, page: 2, max_side: 1568)
349
+ ```
350
+
351
+ `pdf_info` counts actual pages without interpreting their content. `pdf_pages`
352
+ only interprets the selected streams, at most 500 pages by default. Each page
353
+ has `status` (`text`, `empty`, `failed`, `skipped`), `text`, `images` (nil if
354
+ unknown), `truncated` and `reason`. It keeps healthy pages when another is
355
+ damaged. Unlike the tolerant Markdown reader, corrupt Flate streams fail
356
+ explicitly. Limits are 256 KiB of text per page and 8 MiB total
357
+ (`max_page_bytes:`, `max_output_bytes:`); truncation respects UTF-8.
358
+
359
+ `render_pdf_page` returns an opaque grayscale PNG of the **whole page**, not
360
+ just its scan image; `grayscale: false` keeps color. It bounds the output surface
361
+ before allocation (`max_pixels:`, 4 million) and encoded output
362
+ (`max_output_bytes:`, 8 MiB). Preflight rejects pages drawing more than 40 million
363
+ source-image pixels. Renderer warnings fail instead of silently dropping content.
364
+ The crate exposes `read::pdf::{info, pages, render_page}` (`render` feature) and
365
+ `read::pdf::isolation::render_page_isolated` (`isolation` feature). The latter
366
+ takes an absolute path to the trusted `imprimatur-render-worker` executable.
367
+ The gem bundles that worker and always uses isolated rendering; it never falls
368
+ back to an in-process render or searches PATH.
369
+
370
+ All three accept the normal read limits. Rendering also accepts `memory_bytes:`
371
+ (1 GiB by default): Linux/FreeBSD enforce an address-space cap; macOS uses a
372
+ sampled RSS watchdog, which can briefly overshoot. A worker is killed and reaped
373
+ on timeout, cancellation or supervisor failure. Rendering has a mandatory
374
+ 60-second process ceiling even with `timeout: nil`; a shorter `timeout:` still
375
+ applies. CPU time, output files and core dumps are also limited.
376
+
377
+ This is resource containment, **not a filesystem/network security sandbox**.
378
+ The raw crate `render_page` remains in-process and cannot interrupt Hayro.
379
+ `max_inflated` bounds our preflight, not Hayro's internal decoders.
380
+
329
381
  ```ruby
330
382
  sheet = Imprimatur.content("sales.xlsx")[:sheets].first
331
383
  sheet[:rows][1][:cells]
@@ -434,7 +486,7 @@ The core works without Ruby too; see
434
486
 
435
487
  ```sh
436
488
  bundle install
437
- rake test:all # cargo test, then the Ruby suite on the compiled extension
489
+ rake test:all # core, CLI and Ruby suites on the compiled extension
438
490
  rake lint # dictator, cargo fmt --check, clippy
439
491
  rake gems:native # the precompiled gem for this machine, in pkg/
440
492
  ```
@@ -21,10 +21,11 @@ module Imprimatur
21
21
  # @param page_size [:a4, :letter, Array(Numeric, Numeric)] or its width
22
22
  # and height in points, 36 to 1584 a side
23
23
  # @param margin [Numeric, nil] points on every side, one inch (72) unless set
24
+ # @param font_size [Numeric, nil] body text in points, 1 to 1638 in steps of 0.5; 11 unless set
24
25
  # @param page_numbers [Boolean] number pages "1 / N" in the footer
25
26
  # @raise [ArgumentError] an unknown page size
26
- # @raise [Imprimatur::Error] a page size or margin out of range
27
- def initialize(page_size: :a4, margin: nil, page_numbers: false)
27
+ # @raise [Imprimatur::Error] a page size, margin or font size out of range
28
+ def initialize(page_size: :a4, margin: nil, font_size: nil, page_numbers: false)
28
29
  @native = Native::Document.new
29
30
  numbers = page_numbers ? true : false
30
31
  if page_size.is_a?(Array)
@@ -34,6 +35,7 @@ module Imprimatur
34
35
  @native.set_page(page_size.to_s, numbers)
35
36
  end
36
37
  @native.set_margin(Float(margin)) if margin
38
+ @native.set_font_size(Float(font_size)) if font_size
37
39
  end
38
40
 
39
41
  # A PNG or JPEG in the flow, at +width+ or +height+ points (the other
@@ -15,3 +15,10 @@ rescue LoadError => e
15
15
  raise LoadError, "imprimatur: the native extension did not load (#{e.message}). " \
16
16
  'Install a platform gem, or build it with `rake compile` (toolchain: https://rustup.rs).'
17
17
  end
18
+
19
+ module Imprimatur
20
+ # Always use the companion of the loaded extension, never PATH.
21
+ RENDER_WORKER = File.join(File.dirname($LOADED_FEATURES.find do |path|
22
+ path.end_with?("/imprimatur_native.#{RbConfig::CONFIG['DLEXT']}")
23
+ end), 'imprimatur-render-worker').freeze
24
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Imprimatur
4
+ # Keyword handling only; parsing, limits and rendering live in the native core.
5
+ module PdfReader
6
+ def self.info(document, max_bytes: nil, **limits)
7
+ JSON.parse(Native.pdf_info(Reader.read(document, max_bytes), Reader.limits(max_bytes:, **limits)),
8
+ symbolize_names: true)
9
+ end
10
+
11
+ def self.pages(document, first_page: 1, last_page: 0xFFFFFFFF, max_pages: 500,
12
+ max_page_bytes: 256 * 1024, max_output_bytes: 8 * 1024 * 1024,
13
+ max_bytes: nil, **limits)
14
+ options = [first_page, last_page, max_pages, max_page_bytes, max_output_bytes]
15
+ JSON.parse(Native.pdf_pages(Reader.read(document, max_bytes), Reader.limits(max_bytes:, **limits), options),
16
+ symbolize_names: true)
17
+ end
18
+
19
+ def self.render(document, page:, max_side: 1568, max_pixels: 4_000_000,
20
+ max_output_bytes: 8 * 1024 * 1024, grayscale: true,
21
+ memory_bytes: 1024 * 1024 * 1024, max_bytes: nil, **limits)
22
+ Native.render_pdf_page(Reader.read(document, max_bytes), Reader.limits(max_bytes:, **limits),
23
+ [page, max_side, max_pixels, max_output_bytes, grayscale],
24
+ RENDER_WORKER, memory_bytes)
25
+ end
26
+ end
27
+ end
@@ -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.4.0'
4
+ VERSION = '0.6.0'
5
5
  end
data/lib/imprimatur.rb CHANGED
@@ -10,6 +10,7 @@ require_relative 'imprimatur/barcode'
10
10
  require_relative 'imprimatur/xlsx'
11
11
  require_relative 'imprimatur/document'
12
12
  require_relative 'imprimatur/reader'
13
+ require_relative 'imprimatur/pdf_reader'
13
14
  require_relative 'imprimatur/spreadsheet'
14
15
  require_relative 'imprimatur/zip'
15
16
 
@@ -45,7 +46,7 @@ module Imprimatur
45
46
  # Builds a {Document} and renders it as PDF. With a path, saves it there
46
47
  # and returns the path; without, returns the bytes.
47
48
  #
48
- # @param options [Hash] +page_size:+ and +page_numbers:+, see {Document#initialize}
49
+ # @param options [Hash] +page_size:+, +margin:+, +font_size:+ and +page_numbers:+, see {Document#initialize}
49
50
  # @yieldparam document [Document]
50
51
  def pdf(path = nil, **options, &) = Document.build(:pdf, path, **options, &)
51
52
 
@@ -66,7 +67,8 @@ module Imprimatur
66
67
  # Imprimatur.markdown(File.open('sales.xlsx'))
67
68
  #
68
69
  # @param document [String, Pathname, #read] a path, the document's bytes
69
- # (told apart by their signature), or an IO
70
+ # (a binary String always is; another String only with a document's
71
+ # signature), or an IO. A path must name a regular file.
70
72
  # @param timeout [Numeric, nil] seconds the read may take (60), +nil+ for none
71
73
  # @param max_bytes [Integer, nil] the largest document accepted (512 MiB)
72
74
  # @param max_inflated [Integer, nil] decompressed bytes allowed in all (1 GiB)
@@ -91,7 +93,11 @@ module Imprimatur
91
93
  # sheet = Imprimatur.content('sales.xlsx')[:sheets].first
92
94
  # sheet[:rows][1][:cells] # => [{cell: "A2", type: "text", value: "North"}, ...]
93
95
  #
94
- # Takes the arguments of {.markdown}, and raises as it does. With
96
+ # Unlike {.markdown}, a PDF without text still returns every page, with
97
+ # empty +:blocks+ and its image count, rather than raising {NoTextLayer}.
98
+ # This keeps scanned and blank pages visible to OCR callers.
99
+ #
100
+ # Takes the arguments of {.markdown}, and raises as it does otherwise. With
95
101
  # +styles: true+ each sheet also lists its formatted cells' +:styles+:
96
102
  # font, size, bold, italic, underline, ARGB color and fill, alignment,
97
103
  # wrap, border and number format.
@@ -99,6 +105,25 @@ module Imprimatur
99
105
  # @return [Hash] with symbol keys; +:format+ is +"pdf"+, +"xlsx"+ or +"docx"+
100
106
  def content(document, ...) = Reader.content(document, ...)
101
107
 
108
+ # PDF metadata without reading page content.
109
+ # @return [Hash] +page_count:+ and +encrypted:+
110
+ def pdf_info(document, ...) = PdfReader.info(document, ...)
111
+
112
+ # A bounded, one-based page selection. Each page has its status, text,
113
+ # image count (nil when unknown), truncation flag and failure reason.
114
+ # Takes +first_page:+, +last_page:+, +max_pages:+ (500),
115
+ # +max_page_bytes:+ (256 KiB), +max_output_bytes:+ (8 MiB), and read limits.
116
+ # @return [Hash] +page_count:+, +encrypted:+ and +pages:+
117
+ def pdf_pages(document, ...) = PdfReader.pages(document, ...)
118
+
119
+ # A full page as an opaque PNG, not just its embedded images.
120
+ # Takes +page:+ (one-based), +max_side:+ (1568), +max_pixels:+ (4 million),
121
+ # +max_output_bytes:+ (8 MiB), +grayscale:+ (true), and read limits.
122
+ # Rendering checks time/cancellation before and after Hayro, not during it.
123
+ # Untrusted documents still need an externally limited worker process.
124
+ # @return [String] PNG bytes
125
+ def render_pdf_page(document, ...) = PdfReader.render(document, ...)
126
+
102
127
  # A workbook's sheets and their values, read from the same documents
103
128
  # {.markdown} takes (+.xlsx+, +.xls+, +.ods+); see {Spreadsheet}.
104
129
  #
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.0
4
+ version: 0.6.0
5
5
  platform: aarch64-linux-gnu
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -10,9 +10,10 @@ cert_chain: []
10
10
  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
- Word documents from one model, with its own XML, zip and PDF writers: no third-party
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.'
13
+ Word documents from one model, with its own XML, zip and PDF readers and writers.
14
+ It reads PDF, XLSX, XLS, ODS and DOCX back: as Markdown, as typed structure, and
15
+ their images. Full-page PDF rendering uses the pure-Rust Hayro renderer. Precompiled
16
+ for Linux, macOS and FreeBSD; CRuby only.'
16
17
  email:
17
18
  - terminale@gmail.com
18
19
  executables: []
@@ -23,12 +24,14 @@ files:
23
24
  - LICENSE.txt
24
25
  - README.md
25
26
  - lib/imprimatur.rb
27
+ - lib/imprimatur/4.0/imprimatur-render-worker
26
28
  - lib/imprimatur/4.0/imprimatur_native.so
27
29
  - lib/imprimatur/barcode.rb
28
30
  - lib/imprimatur/document.rb
29
31
  - lib/imprimatur/error.rb
30
32
  - lib/imprimatur/formula.rb
31
33
  - lib/imprimatur/native.rb
34
+ - lib/imprimatur/pdf_reader.rb
32
35
  - lib/imprimatur/reader.rb
33
36
  - lib/imprimatur/spreadsheet.rb
34
37
  - lib/imprimatur/style.rb