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 +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +27 -5
- data/lib/imprimatur/4.0/imprimatur_native.so +0 -0
- data/lib/imprimatur/error.rb +4 -3
- data/lib/imprimatur/reader.rb +1 -1
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur/zip.rb +127 -0
- data/lib/imprimatur.rb +3 -2
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1dcd8e194d2f7cf4096350af23ef9a629d8b9bb0e32ec88cd60edbebb0e190b
|
|
4
|
+
data.tar.gz: d5a960d8c7902c18b4be2b99553d50f95fdb1eb18bf8f674191de123a2f081b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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,
|
|
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
|
-
`.
|
|
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
|
|
Binary file
|
data/lib/imprimatur/error.rb
CHANGED
|
@@ -9,11 +9,12 @@ module Imprimatur
|
|
|
9
9
|
# A PDF without a text layer: its pages are images (scans), which need OCR.
|
|
10
10
|
class NoTextLayer < Error; end
|
|
11
11
|
|
|
12
|
-
# A PDF encrypted with a user password
|
|
12
|
+
# A PDF encrypted with a user password (only an empty one is tried), or
|
|
13
|
+
# an encrypted legacy +.xls+.
|
|
13
14
|
class PasswordRequired < Error; end
|
|
14
15
|
|
|
15
|
-
# A format {Imprimatur.markdown} does not read: legacy +.
|
|
16
|
-
#
|
|
16
|
+
# A format {Imprimatur.markdown} does not read: legacy +.doc+, binary
|
|
17
|
+
# +.xlsb+, presentations, anything not a PDF, XLSX, XLS, ODS or DOCX.
|
|
17
18
|
class UnsupportedDocument < Error; end
|
|
18
19
|
|
|
19
20
|
# The read hit one of its limits (size, decompressed size, nesting, time),
|
data/lib/imprimatur/reader.rb
CHANGED
|
@@ -14,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
|
data/lib/imprimatur/version.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
64
|
+
summary: XLSX, PDF and DOCX authoring, and office documents read back. Let it be printed.
|
|
63
65
|
test_files: []
|