imprimatur 0.1.0-aarch64-linux-gnu → 0.2.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: 96b43fb07eaa3f15a32bc14ecfce9131ae1391369cec190e8b4458b56f40e14f
4
- data.tar.gz: fd7e35e9d7d87c1523cb563acd16b189a3168e816daeac1495bd507e0e4d6235
3
+ metadata.gz: a1dcd8e194d2f7cf4096350af23ef9a629d8b9bb0e32ec88cd60edbebb0e190b
4
+ data.tar.gz: d5a960d8c7902c18b4be2b99553d50f95fdb1eb18bf8f674191de123a2f081b0
5
5
  SHA512:
6
- metadata.gz: 5a5443f9a772157fea30caaa758cec11fdeaa42a262c7b0fe0f260d67d5cd0892ab96e02cb16bb69e5bd1316d7b5acf7f8aaebba01fcba8a40b5292f4f33f657
7
- data.tar.gz: e02d379d59b7682b24f86f51408f3f1a932b2f49579b57959cd3e8cc8954f4141533f0d46727c8887bb6e832c1ad1d5f56d1af3b745e2435a73d6fa76c1c428b
6
+ metadata.gz: f27b7635c4c624b33adaedb532afd279d594117a2e596de2291d180158be82e90e34c5f19a1596c31a13b75c073f23331ec846f61751d8c70d48fcd9750bac5d
7
+ data.tar.gz: 9a3d3ae03b027bb7e3044779b63760cfb99889664d21acb08a78c065181282cf6e35f9d6cab6bb9af2cb19efa649304d34db2713adc913759ee32677765bb939
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.0](https://github.com/seuros/imprimatur/compare/v0.1.0...v0.2.0) (2026-10-16)
4
+
5
+
6
+ ### Features
7
+
8
+ * read legacy XLS and ODS workbooks ([96b86fa](https://github.com/seuros/imprimatur/commit/96b86faab8eb04b80d33885c61b17ebad4f1f63a))
9
+ * zip archive comments and entry listings ([a87f223](https://github.com/seuros/imprimatur/commit/a87f22330859ee4a583943ddbf107571cca9e841))
10
+ * zip archives of any files ([d5ea579](https://github.com/seuros/imprimatur/commit/d5ea579ae1fc80a8ec6a1b213f442d490ededfba))
11
+
3
12
  ## 0.1.0 (2026-10-16)
4
13
 
5
14
 
data/README.md CHANGED
@@ -197,8 +197,8 @@ PDF, are the next step.
197
197
 
198
198
  ## Reading: documents as Markdown
199
199
 
200
- The other direction: a PDF, XLSX or DOCX printed as Markdown, for search
201
- indexes and language models.
200
+ The other direction: a PDF, a workbook (XLSX, legacy XLS, ODS) or a DOCX
201
+ printed as Markdown, for search indexes and language models.
202
202
 
203
203
  ```ruby
204
204
  Imprimatur.markdown("audit.pdf") # a path
@@ -215,8 +215,8 @@ end
215
215
  - **PDF**: the text layer, placed glyph by glyph (fonts, encodings and
216
216
  ToUnicode maps, forms, rotated pages), in reading order (right-to-left
217
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, hyphenated line breaks rejoined,
219
- presentation forms (`fi`, Arabic contextual forms) as plain letters, and
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
220
  running headers and footers left out. Pages after the first start with
221
221
  `<!-- page N -->`. Encrypted PDFs open when the user password is empty, as
222
222
  it is for documents that only restrict printing or copying (RC4, AES-128,
@@ -224,6 +224,9 @@ end
224
224
  - **XLSX**: each sheet as a table under its name, values as Excel shows them
225
225
  (dates as dates, percentages, 15 significant digits), formulas without a
226
226
  cached result as `=FORMULA`, then the sheet's comments.
227
+ - **XLS** (Excel 97–2003, and Excel 5.0/95 in Western European code pages)
228
+ and **ODS** (LibreOffice, OpenOffice): the same, sheet by sheet; an ODS
229
+ number keeps the text it shows. Legacy `.xls` comments are left out.
227
230
  - **DOCX**: headings (from styles and outline levels), bulleted and numbered
228
231
  lists, bold, italic, links, tables, text boxes; deleted text and field
229
232
  codes left out.
@@ -237,7 +240,7 @@ Documents are untrusted input, so reads are bounded: `max_bytes:` (512 MiB),
237
240
  `Imprimatur::LimitExceeded`. Reading runs without the GVL: other threads and
238
241
  fibers keep running, and `Timeout` or `Thread#raise` cancels it.
239
242
  `Imprimatur::PasswordRequired` and `Imprimatur::UnsupportedDocument` (legacy
240
- `.xls` and `.doc`, `.xlsb`, presentations) are the other errors to expect.
243
+ `.doc`, `.xlsb`, presentations) are the other errors to expect.
241
244
 
242
245
  ### Structure and images
243
246
 
@@ -270,6 +273,25 @@ Imprimatur.images("scan.pdf") do |image|
270
273
  end
271
274
  ```
272
275
 
276
+ ## Zip archives
277
+
278
+ Archives of any files, for what is not a document (an export of an order's
279
+ files, a download to unpack). Entries keep their modification times and
280
+ comments, files are streamed from disk, and archives past 4 GiB use ZIP64;
281
+ reading is bounded per entry. Both run without the GVL.
282
+
283
+ ```ruby
284
+ Imprimatur::Zip.write("export.zip") do |zip|
285
+ zip.add_file("contracts/0contract.pdf", "/tmp/contract.pdf", comment: "Contract")
286
+ zip.add("manifest.json", JSON.generate(manifest))
287
+ end
288
+
289
+ Imprimatur::Zip.open("geolite.zip") do |zip|
290
+ csv = zip.read(zip.glob("**/*-Locations-en.csv").first) # max_bytes: 1 GiB by default
291
+ zip.extract("README.txt", "/tmp/readme.txt")
292
+ end
293
+ ```
294
+
273
295
  ## Command line
274
296
 
275
297
  The same reader as a static binary, for scripts and agents that would
@@ -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; only an empty one is tried.
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 +.xls+ and +.doc+,
16
- # binary +.xlsb+, presentations, anything not a PDF, XLSX or DOCX.
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),
@@ -14,7 +14,7 @@ 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Imprimatur
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -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,7 @@ 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/zip'
12
13
 
13
14
  # Document authoring with a native core: XLSX workbooks, and PDF and DOCX from
14
15
  # one document model.
@@ -46,7 +47,7 @@ module Imprimatur
46
47
  # As {.pdf}, rendered as DOCX.
47
48
  def docx(path = nil, **options, &) = Document.build(:docx, path, **options, &)
48
49
 
49
- # A PDF, XLSX or DOCX as Markdown: a PDF's text layer (headings by font
50
+ # A PDF, workbook (XLSX, XLS, ODS) or DOCX as Markdown: a PDF's text layer (headings by font
50
51
  # size, running headers and footers left out, pages marked with
51
52
  # +<!-- page N -->+), each sheet of a workbook as a table, a Word
52
53
  # document's headings, lists, emphasis, links and tables. The format is
@@ -67,7 +68,7 @@ module Imprimatur
67
68
  # @return [String] Markdown, UTF-8
68
69
  # @raise [NoTextLayer] a PDF of scanned pages: run OCR instead
69
70
  # @raise [PasswordRequired] a PDF that needs a password to open
70
- # @raise [UnsupportedDocument] not a PDF, XLSX or DOCX
71
+ # @raise [UnsupportedDocument] not a PDF, XLSX, XLS, ODS or DOCX
71
72
  # @raise [LimitExceeded] the document is too large, too deep or too slow
72
73
  # @raise [Error] a damaged document
73
74
  def markdown(document, ...) = Reader.markdown(document, ...)
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.1.0
4
+ version: 0.2.0
5
5
  platform: aarch64-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. Precompiled for Linux, macOS and FreeBSD; CRuby only.'
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: []
@@ -32,6 +33,7 @@ files:
32
33
  - lib/imprimatur/validation.rb
33
34
  - lib/imprimatur/version.rb
34
35
  - lib/imprimatur/xlsx.rb
36
+ - lib/imprimatur/zip.rb
35
37
  homepage: https://github.com/seuros/imprimatur
36
38
  licenses:
37
39
  - BSD-3-Clause
@@ -59,5 +61,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
59
61
  requirements: []
60
62
  rubygems_version: 4.0.20
61
63
  specification_version: 4
62
- summary: XLSX, PDF and DOCX authoring with a Ruby DSL. Let it be printed.
64
+ summary: XLSX, PDF and DOCX authoring, and office documents read back. Let it be printed.
63
65
  test_files: []