imprimatur 0.3.0 → 0.5.0
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 +16 -0
- data/Cargo.lock +33 -3
- data/Cargo.toml +1 -1
- data/README.md +48 -1
- data/ext/imprimatur_native/cli/Cargo.toml +3 -1
- data/ext/imprimatur_native/core/Cargo.toml +15 -4
- data/ext/imprimatur_native/core/README.md +27 -4
- data/ext/imprimatur_native/core/src/barcode/code128.rs +162 -0
- data/ext/imprimatur_native/core/src/barcode/draw.rs +124 -0
- data/ext/imprimatur_native/core/src/barcode/ean.rs +91 -0
- data/ext/imprimatur_native/core/src/barcode/gs1.rs +188 -0
- data/ext/imprimatur_native/core/src/barcode/matrix.rs +130 -0
- data/ext/imprimatur_native/core/src/barcode.rs +314 -0
- data/ext/imprimatur_native/core/src/doc/symbol.rs +136 -0
- data/ext/imprimatur_native/core/src/doc.rs +11 -1
- data/ext/imprimatur_native/core/src/docx.rs +22 -10
- data/ext/imprimatur_native/core/src/error.rs +6 -0
- data/ext/imprimatur_native/core/src/font.rs +3 -1
- data/ext/imprimatur_native/core/src/lib.rs +3 -1
- data/ext/imprimatur_native/core/src/pdf/layout.rs +48 -14
- data/ext/imprimatur_native/core/src/pdf/truetype/descriptor.rs +129 -0
- data/ext/imprimatur_native/core/src/pdf/truetype/program.rs +70 -0
- data/ext/imprimatur_native/core/src/pdf/truetype.rs +149 -0
- data/ext/imprimatur_native/core/src/pdf.rs +9 -1
- data/ext/imprimatur_native/core/src/read/image.rs +17 -0
- data/ext/imprimatur_native/core/src/read.rs +3 -2
- data/ext/imprimatur_native/core/src/style.rs +11 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +1 -1
- data/ext/imprimatur_native/ffi/src/barcode.rs +147 -0
- data/ext/imprimatur_native/ffi/src/document.rs +29 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +5 -3
- data/lib/imprimatur/barcode.rb +78 -0
- data/lib/imprimatur/document.rb +17 -1
- data/lib/imprimatur/reader.rb +48 -11
- data/lib/imprimatur/spreadsheet.rb +1 -1
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur.rb +6 -1
- metadata +13 -2
- data/ext/imprimatur_native/core/src/test_support.rs +0 -251
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
use std::cell::RefCell;
|
|
4
4
|
|
|
5
|
-
use imprimatur::doc::{Document, PageSize, Paragraph, Picture, Run, Table};
|
|
5
|
+
use imprimatur::doc::{Document, PageSize, Paragraph, Picture, Run, Symbol, Table};
|
|
6
6
|
use imprimatur::style::Color;
|
|
7
7
|
use magnus::{Error, RModule, RString, Ruby, function, method, prelude::*};
|
|
8
8
|
|
|
9
|
+
use crate::barcode::Barcode;
|
|
9
10
|
use crate::{ALIGNS, DIRECTIONS, OrRaise, RubyBytes, borrow, keyword};
|
|
10
11
|
|
|
11
12
|
/// `[text, bold, italic, underline, color]`, as `Imprimatur::Paragraph` collects it.
|
|
@@ -88,6 +89,32 @@ impl NativeDocument {
|
|
|
88
89
|
Ok(())
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
/// A barcode, `width` and `height` in points, in a `color` "#RRGGBB".
|
|
93
|
+
fn barcode(
|
|
94
|
+
ruby: &Ruby,
|
|
95
|
+
rb_self: &Self,
|
|
96
|
+
barcode: &Barcode,
|
|
97
|
+
(width, height): (Option<f64>, Option<f64>),
|
|
98
|
+
align: Option<String>,
|
|
99
|
+
color: Option<String>,
|
|
100
|
+
) -> Result<(), Error> {
|
|
101
|
+
let mut symbol = Symbol::new(barcode.0.clone());
|
|
102
|
+
if let Some(width) = width {
|
|
103
|
+
symbol = symbol.width(width).or_raise(ruby)?;
|
|
104
|
+
}
|
|
105
|
+
if let Some(height) = height {
|
|
106
|
+
symbol = symbol.height(height).or_raise(ruby)?;
|
|
107
|
+
}
|
|
108
|
+
if let Some(align) = align {
|
|
109
|
+
symbol = symbol.align(keyword(ruby, "align", &align, &ALIGNS)?);
|
|
110
|
+
}
|
|
111
|
+
if let Some(color) = color {
|
|
112
|
+
symbol = symbol.color(Color::from_hex(&color).or_raise(ruby)?);
|
|
113
|
+
}
|
|
114
|
+
borrow(ruby, &rb_self.0, "document")?.barcode(symbol);
|
|
115
|
+
Ok(())
|
|
116
|
+
}
|
|
117
|
+
|
|
91
118
|
fn heading(ruby: &Ruby, rb_self: &Self, level: u8, text: String) -> Result<(), Error> {
|
|
92
119
|
borrow(ruby, &rb_self.0, "document")?.heading(level, text).or_raise(ruby)?;
|
|
93
120
|
Ok(())
|
|
@@ -175,6 +202,7 @@ pub fn define(ruby: &Ruby, native: RModule) -> Result<(), Error> {
|
|
|
175
202
|
class.define_method("set_page_points", method!(NativeDocument::set_page_points, 3))?;
|
|
176
203
|
class.define_method("set_margin", method!(NativeDocument::set_margin, 1))?;
|
|
177
204
|
class.define_method("picture", method!(NativeDocument::picture, 4))?;
|
|
205
|
+
class.define_method("barcode", method!(NativeDocument::barcode, 4))?;
|
|
178
206
|
class.define_method("heading", method!(NativeDocument::heading, 2))?;
|
|
179
207
|
class.define_method("paragraph", method!(NativeDocument::paragraph, 3))?;
|
|
180
208
|
class.define_method("table", method!(NativeDocument::table, 2))?;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
//! drives them; sheets cross as indices.
|
|
5
5
|
|
|
6
6
|
mod archive;
|
|
7
|
+
mod barcode;
|
|
7
8
|
mod blocking;
|
|
8
9
|
mod convert;
|
|
9
10
|
mod document;
|
|
@@ -202,16 +203,16 @@ impl std::io::Write for RubyBytes {
|
|
|
202
203
|
}
|
|
203
204
|
}
|
|
204
205
|
|
|
205
|
-
/// Mutable access to a wrapped value; Ruby code that calls back into the
|
|
206
|
-
/// same object mid-call gets a `RuntimeError`, not a panic.
|
|
207
206
|
/// A Ruby String's bytes, copied out.
|
|
208
207
|
fn bytes(string: RString) -> Vec<u8> {
|
|
208
|
+
#[allow(unsafe_code)]
|
|
209
209
|
// SAFETY: the bytes are copied out at once, while the GVL is held and
|
|
210
210
|
// before any Ruby code can run and move or free the string.
|
|
211
|
-
#[allow(unsafe_code)]
|
|
212
211
|
unsafe { string.as_slice() }.to_vec()
|
|
213
212
|
}
|
|
214
213
|
|
|
214
|
+
/// Mutable access to a wrapped value; Ruby code that calls back into the
|
|
215
|
+
/// same object mid-call gets a `RuntimeError`, not a panic.
|
|
215
216
|
fn borrow<'a, T>(ruby: &Ruby, cell: &'a RefCell<T>, what: &str) -> Result<RefMut<'a, T>, Error> {
|
|
216
217
|
cell.try_borrow_mut().map_err(|_| {
|
|
217
218
|
Error::new(
|
|
@@ -315,6 +316,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
315
316
|
style.define_singleton_method("__build", function!(Style::build, 13))?;
|
|
316
317
|
let validation = imprimatur.define_class("Validation", ruby.class_object())?;
|
|
317
318
|
validation.define_singleton_method("__build", function!(Validation::build, 8))?;
|
|
319
|
+
barcode::define(ruby, imprimatur)?;
|
|
318
320
|
|
|
319
321
|
let native = imprimatur.define_module("Native")?;
|
|
320
322
|
native.define_singleton_method("markdown", function!(read::markdown_native, 4))?;
|
|
@@ -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
|
data/lib/imprimatur/document.rb
CHANGED
|
@@ -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]
|
data/lib/imprimatur/reader.rb
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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
|
|
45
|
-
when String then bytes?(document) ? 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
|
data/lib/imprimatur/version.rb
CHANGED
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
|
-
# (
|
|
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.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -52,9 +52,16 @@ files:
|
|
|
52
52
|
- ext/imprimatur_native/core/LICENSE
|
|
53
53
|
- ext/imprimatur_native/core/README.md
|
|
54
54
|
- ext/imprimatur_native/core/src/archive.rs
|
|
55
|
+
- ext/imprimatur_native/core/src/barcode.rs
|
|
56
|
+
- ext/imprimatur_native/core/src/barcode/code128.rs
|
|
57
|
+
- ext/imprimatur_native/core/src/barcode/draw.rs
|
|
58
|
+
- ext/imprimatur_native/core/src/barcode/ean.rs
|
|
59
|
+
- ext/imprimatur_native/core/src/barcode/gs1.rs
|
|
60
|
+
- ext/imprimatur_native/core/src/barcode/matrix.rs
|
|
55
61
|
- ext/imprimatur_native/core/src/doc.rs
|
|
56
62
|
- ext/imprimatur_native/core/src/doc/paragraph.rs
|
|
57
63
|
- ext/imprimatur_native/core/src/doc/picture.rs
|
|
64
|
+
- ext/imprimatur_native/core/src/doc/symbol.rs
|
|
58
65
|
- ext/imprimatur_native/core/src/doc/table.rs
|
|
59
66
|
- ext/imprimatur_native/core/src/docx.rs
|
|
60
67
|
- ext/imprimatur_native/core/src/docx/picture.rs
|
|
@@ -69,6 +76,9 @@ files:
|
|
|
69
76
|
- ext/imprimatur_native/core/src/pdf/metrics.rs
|
|
70
77
|
- ext/imprimatur_native/core/src/pdf/objects.rs
|
|
71
78
|
- ext/imprimatur_native/core/src/pdf/picture.rs
|
|
79
|
+
- ext/imprimatur_native/core/src/pdf/truetype.rs
|
|
80
|
+
- ext/imprimatur_native/core/src/pdf/truetype/descriptor.rs
|
|
81
|
+
- ext/imprimatur_native/core/src/pdf/truetype/program.rs
|
|
72
82
|
- ext/imprimatur_native/core/src/read.rs
|
|
73
83
|
- ext/imprimatur_native/core/src/read/budget.rs
|
|
74
84
|
- ext/imprimatur_native/core/src/read/content.rs
|
|
@@ -108,7 +118,6 @@ files:
|
|
|
108
118
|
- ext/imprimatur_native/core/src/read/xlsx/formats.rs
|
|
109
119
|
- ext/imprimatur_native/core/src/script.rs
|
|
110
120
|
- ext/imprimatur_native/core/src/style.rs
|
|
111
|
-
- ext/imprimatur_native/core/src/test_support.rs
|
|
112
121
|
- ext/imprimatur_native/core/src/xlsx.rs
|
|
113
122
|
- ext/imprimatur_native/core/src/xlsx/autofit.rs
|
|
114
123
|
- ext/imprimatur_native/core/src/xlsx/cell.rs
|
|
@@ -128,6 +137,7 @@ files:
|
|
|
128
137
|
- ext/imprimatur_native/extconf.rb
|
|
129
138
|
- ext/imprimatur_native/ffi/Cargo.toml
|
|
130
139
|
- ext/imprimatur_native/ffi/src/archive.rs
|
|
140
|
+
- ext/imprimatur_native/ffi/src/barcode.rs
|
|
131
141
|
- ext/imprimatur_native/ffi/src/blocking.rs
|
|
132
142
|
- ext/imprimatur_native/ffi/src/convert.rs
|
|
133
143
|
- ext/imprimatur_native/ffi/src/document.rs
|
|
@@ -137,6 +147,7 @@ files:
|
|
|
137
147
|
- ext/imprimatur_native/ffi/src/style.rs
|
|
138
148
|
- ext/imprimatur_native/ffi/src/validation.rs
|
|
139
149
|
- lib/imprimatur.rb
|
|
150
|
+
- lib/imprimatur/barcode.rb
|
|
140
151
|
- lib/imprimatur/document.rb
|
|
141
152
|
- lib/imprimatur/error.rb
|
|
142
153
|
- lib/imprimatur/formula.rb
|
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
//! Helpers shared by the unit tests.
|
|
2
|
-
|
|
3
|
-
use std::io::{Cursor, Read};
|
|
4
|
-
|
|
5
|
-
/// Every entry of a zip archive as `(name, bytes)`, read with the `zip`
|
|
6
|
-
/// crate: an implementation independent of ours, which verifies each CRC.
|
|
7
|
-
pub fn read_zip(bytes: Vec<u8>) -> Vec<(String, Vec<u8>)> {
|
|
8
|
-
let mut archive = ::zip::ZipArchive::new(Cursor::new(bytes)).unwrap();
|
|
9
|
-
(0..archive.len())
|
|
10
|
-
.map(|index| {
|
|
11
|
-
let mut file = archive.by_index(index).unwrap();
|
|
12
|
-
let mut data = Vec::new();
|
|
13
|
-
file.read_to_end(&mut data).unwrap();
|
|
14
|
-
(file.name().unwrap().into_owned(), data)
|
|
15
|
-
})
|
|
16
|
-
.collect()
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/// Every entry of a zip archive as text, by name, read as [`read_zip`] does;
|
|
20
|
-
/// media come out garbled, for their names.
|
|
21
|
-
pub fn text_parts(bytes: Vec<u8>) -> Vec<(String, String)> {
|
|
22
|
-
read_zip(bytes)
|
|
23
|
-
.into_iter()
|
|
24
|
-
.map(|(name, data)| (name, String::from_utf8_lossy(&data).into_owned()))
|
|
25
|
-
.collect()
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/// The text of the part `name` among `parts`.
|
|
29
|
-
pub fn part<'a>(parts: &'a [(String, String)], name: &str) -> &'a str {
|
|
30
|
-
parts
|
|
31
|
-
.iter()
|
|
32
|
-
.find(|(part, _)| part == name)
|
|
33
|
-
.map_or_else(|| panic!("no part {name}"), |(_, xml)| xml)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/// A zip of `(name, text)` parts, written with our own writer.
|
|
37
|
-
pub fn zip_of(parts: &[(&str, &str)]) -> Vec<u8> {
|
|
38
|
-
let mut zip = crate::zip::ZipWriter::new(Vec::new());
|
|
39
|
-
for (name, text) in parts {
|
|
40
|
-
zip.add(name, text.as_bytes()).unwrap();
|
|
41
|
-
}
|
|
42
|
-
zip.finish().unwrap()
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/// A PDF of `objects`, numbered from 1 (the first is the catalog), with a
|
|
46
|
-
/// classic cross-reference table.
|
|
47
|
-
pub fn pdf_of(objects: &[Vec<u8>]) -> Vec<u8> {
|
|
48
|
-
use std::io::Write as _;
|
|
49
|
-
let mut out = b"%PDF-1.7\n%\xE2\xE3\xCF\xD3\n".to_vec();
|
|
50
|
-
let mut offsets = Vec::new();
|
|
51
|
-
for (number, body) in (1..).zip(objects) {
|
|
52
|
-
offsets.push(out.len());
|
|
53
|
-
writeln!(out, "{number} 0 obj").unwrap();
|
|
54
|
-
out.extend_from_slice(body);
|
|
55
|
-
out.extend_from_slice(b"\nendobj\n");
|
|
56
|
-
}
|
|
57
|
-
let xref = out.len();
|
|
58
|
-
write!(out, "xref\n0 {}\n0000000000 65535 f \n", objects.len() + 1).unwrap();
|
|
59
|
-
for offset in offsets {
|
|
60
|
-
writeln!(out, "{offset:010} 00000 n ").unwrap();
|
|
61
|
-
}
|
|
62
|
-
write!(
|
|
63
|
-
out,
|
|
64
|
-
"trailer\n<< /Size {} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n",
|
|
65
|
-
objects.len() + 1
|
|
66
|
-
)
|
|
67
|
-
.unwrap();
|
|
68
|
-
out
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/// A stream object: `dict` holds the entries besides `/Length`.
|
|
72
|
-
pub fn stream_object(dict: &str, data: &[u8]) -> Vec<u8> {
|
|
73
|
-
let mut out = format!("<< /Length {} {dict} >>\nstream\n", data.len()).into_bytes();
|
|
74
|
-
out.extend_from_slice(data);
|
|
75
|
-
out.extend_from_slice(b"\nendstream");
|
|
76
|
-
out
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/// One page of `content` with Helvetica as `/F1` and a 612 by 792 page.
|
|
80
|
-
pub fn page_pdf(content: &str, page_extra: &str) -> Vec<u8> {
|
|
81
|
-
pdf_of(&[
|
|
82
|
-
b"<< /Type /Catalog /Pages 2 0 R >>".to_vec(),
|
|
83
|
-
b"<< /Type /Pages /Kids [3 0 R] /Count 1 >>".to_vec(),
|
|
84
|
-
format!(
|
|
85
|
-
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] \
|
|
86
|
-
/Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R {page_extra} >>"
|
|
87
|
-
)
|
|
88
|
-
.into_bytes(),
|
|
89
|
-
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>"
|
|
90
|
-
.to_vec(),
|
|
91
|
-
stream_object("", content.as_bytes()),
|
|
92
|
-
])
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/// A PNG as `read::image::png` writes it, read back.
|
|
96
|
-
#[derive(Debug)]
|
|
97
|
-
pub struct Png {
|
|
98
|
-
pub width: u32,
|
|
99
|
-
pub height: u32,
|
|
100
|
-
pub bits: u8,
|
|
101
|
-
pub color: u8,
|
|
102
|
-
pub palette: Option<Vec<u8>>,
|
|
103
|
-
/// The rows, their filter bytes (always 0, none) taken out.
|
|
104
|
-
pub rows: Vec<u8>,
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/// Reads a PNG, checking each chunk's CRC and that every row is unfiltered.
|
|
108
|
-
pub fn png_of(data: &[u8]) -> Png {
|
|
109
|
-
let be = |at: usize| u32::from_be_bytes(data[at..at + 4].try_into().unwrap());
|
|
110
|
-
assert!(data.starts_with(b"\x89PNG\r\n\x1a\n"), "{data:?}");
|
|
111
|
-
let (mut header, mut palette, mut compressed) = (Vec::new(), None, Vec::new());
|
|
112
|
-
let mut at = 8;
|
|
113
|
-
while at < data.len() {
|
|
114
|
-
let length = be(at) as usize;
|
|
115
|
-
let (kind, body) = (&data[at + 4..at + 8], &data[at + 8..at + 8 + length]);
|
|
116
|
-
assert_eq!(
|
|
117
|
-
be(at + 8 + length),
|
|
118
|
-
crc32fast::hash(&data[at + 4..at + 8 + length]),
|
|
119
|
-
"{kind:?}"
|
|
120
|
-
);
|
|
121
|
-
match kind {
|
|
122
|
-
b"IHDR" => header = body.to_vec(),
|
|
123
|
-
b"PLTE" => palette = Some(body.to_vec()),
|
|
124
|
-
b"IDAT" => compressed.extend_from_slice(body),
|
|
125
|
-
_ => {}
|
|
126
|
-
}
|
|
127
|
-
at += 12 + length;
|
|
128
|
-
}
|
|
129
|
-
let (width, height) = (be(16), be(20));
|
|
130
|
-
let (bits, color) = (header[8], header[9]);
|
|
131
|
-
let channels = match color {
|
|
132
|
-
0 | 3 => 1,
|
|
133
|
-
4 => 2,
|
|
134
|
-
2 => 3,
|
|
135
|
-
_ => 4,
|
|
136
|
-
};
|
|
137
|
-
let stride = (width as usize * channels * usize::from(bits)).div_ceil(8);
|
|
138
|
-
let filtered = miniz_oxide::inflate::decompress_to_vec_zlib(&compressed).unwrap();
|
|
139
|
-
assert_eq!(filtered.len(), (stride + 1) * height as usize);
|
|
140
|
-
let mut rows = Vec::new();
|
|
141
|
-
for row in filtered.chunks(stride + 1) {
|
|
142
|
-
assert_eq!(row[0], 0, "filter type");
|
|
143
|
-
rows.extend_from_slice(&row[1..]);
|
|
144
|
-
}
|
|
145
|
-
Png { width, height, bits, color, palette, rows }
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/// A one-strip little-endian TIFF's tags, by number, and its strip.
|
|
149
|
-
pub fn tiff_of(data: &[u8]) -> (std::collections::BTreeMap<u16, u32>, &[u8]) {
|
|
150
|
-
let le16 = |at: usize| u16::from_le_bytes(data[at..at + 2].try_into().unwrap());
|
|
151
|
-
let le32 = |at: usize| u32::from_le_bytes(data[at..at + 4].try_into().unwrap());
|
|
152
|
-
assert!(data.starts_with(b"II*\0"));
|
|
153
|
-
let directory = le32(4) as usize;
|
|
154
|
-
let tags: std::collections::BTreeMap<u16, u32> = (0..usize::from(le16(directory)))
|
|
155
|
-
.map(|index| {
|
|
156
|
-
let entry = directory + 2 + index * 12;
|
|
157
|
-
let value =
|
|
158
|
-
if le16(entry + 2) == 3 { u32::from(le16(entry + 8)) } else { le32(entry + 8) };
|
|
159
|
-
(le16(entry), value)
|
|
160
|
-
})
|
|
161
|
-
.collect();
|
|
162
|
-
let (offset, length) = (tags[&273] as usize, tags[&279] as usize);
|
|
163
|
-
(tags, &data[offset..offset + length])
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/// A compound file (MS-CFB, version 3) holding one stream named `name` in
|
|
167
|
-
/// its root storage; a stream under 4096 bytes goes to the mini stream, as
|
|
168
|
-
/// writers put it.
|
|
169
|
-
pub fn ole_of(name: &str, stream: &[u8]) -> Vec<u8> {
|
|
170
|
-
const SECTOR: usize = 512;
|
|
171
|
-
const END: u32 = 0xFFFF_FFFE;
|
|
172
|
-
let small = stream.len() < 4096;
|
|
173
|
-
let pad = |data: &[u8], size: usize| {
|
|
174
|
-
let mut data = data.to_vec();
|
|
175
|
-
data.resize(data.len().div_ceil(size).max(1) * size, 0);
|
|
176
|
-
data
|
|
177
|
-
};
|
|
178
|
-
// Sectors: FAT, directory, mini FAT (when small), then the data: the
|
|
179
|
-
// stream itself, or the mini stream holding it.
|
|
180
|
-
let data = if small { pad(stream, 64) } else { stream.to_vec() };
|
|
181
|
-
let data_sectors = data.len().div_ceil(SECTOR).max(1);
|
|
182
|
-
let first_data = if small { 3 } else { 2 };
|
|
183
|
-
let mut fat = vec![0xFFFF_FFFD_u32, END];
|
|
184
|
-
if small {
|
|
185
|
-
fat.push(END);
|
|
186
|
-
}
|
|
187
|
-
fat.extend((0..data_sectors).map(|index| {
|
|
188
|
-
if index + 1 == data_sectors { END } else { u32::try_from(first_data + index + 1).unwrap() }
|
|
189
|
-
}));
|
|
190
|
-
fat.resize(SECTOR / 4, 0xFFFF_FFFF);
|
|
191
|
-
let mut header = vec![0_u8; SECTOR];
|
|
192
|
-
header[..8].copy_from_slice(&[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]);
|
|
193
|
-
let put = |bytes: &mut Vec<u8>, at: usize, value: u32| {
|
|
194
|
-
bytes[at..at + 4].copy_from_slice(&value.to_le_bytes());
|
|
195
|
-
};
|
|
196
|
-
header[0x18..0x1A].copy_from_slice(&0x3E_u16.to_le_bytes());
|
|
197
|
-
header[0x1A..0x1C].copy_from_slice(&3_u16.to_le_bytes());
|
|
198
|
-
header[0x1C..0x1E].copy_from_slice(&0xFFFE_u16.to_le_bytes());
|
|
199
|
-
header[0x1E..0x20].copy_from_slice(&9_u16.to_le_bytes());
|
|
200
|
-
header[0x20..0x22].copy_from_slice(&6_u16.to_le_bytes());
|
|
201
|
-
put(&mut header, 0x2C, 1);
|
|
202
|
-
put(&mut header, 0x30, 1);
|
|
203
|
-
put(&mut header, 0x38, 4096);
|
|
204
|
-
put(&mut header, 0x3C, if small { 2 } else { END });
|
|
205
|
-
put(&mut header, 0x40, u32::from(small));
|
|
206
|
-
put(&mut header, 0x44, END);
|
|
207
|
-
put(&mut header, 0x4C, 0);
|
|
208
|
-
for index in 1..109 {
|
|
209
|
-
put(&mut header, 0x4C + index * 4, 0xFFFF_FFFF);
|
|
210
|
-
}
|
|
211
|
-
let entry = |name: &str, kind: u8, child: u32, start: u32, size: usize| {
|
|
212
|
-
let mut bytes = vec![0_u8; 128];
|
|
213
|
-
let units: Vec<u16> = name.encode_utf16().collect();
|
|
214
|
-
for (index, unit) in units.iter().enumerate() {
|
|
215
|
-
bytes[index * 2..index * 2 + 2].copy_from_slice(&unit.to_le_bytes());
|
|
216
|
-
}
|
|
217
|
-
bytes[0x40..0x42]
|
|
218
|
-
.copy_from_slice(&u16::try_from(units.len() * 2 + 2).unwrap().to_le_bytes());
|
|
219
|
-
bytes[0x42] = kind;
|
|
220
|
-
for at in [0x44, 0x48, 0x4C] {
|
|
221
|
-
put(&mut bytes, at, 0xFFFF_FFFF);
|
|
222
|
-
}
|
|
223
|
-
put(&mut bytes, 0x4C, child);
|
|
224
|
-
put(&mut bytes, 0x74, start);
|
|
225
|
-
put(&mut bytes, 0x78, u32::try_from(size).unwrap());
|
|
226
|
-
bytes
|
|
227
|
-
};
|
|
228
|
-
let first = u32::try_from(first_data).unwrap();
|
|
229
|
-
let mut directory = if small {
|
|
230
|
-
let mut directory = entry("Root Entry", 5, 1, first, data.len());
|
|
231
|
-
directory.extend(entry(name, 2, 0xFFFF_FFFF, 0, stream.len()));
|
|
232
|
-
directory
|
|
233
|
-
} else {
|
|
234
|
-
let mut directory = entry("Root Entry", 5, 1, END, 0);
|
|
235
|
-
directory.extend(entry(name, 2, 0xFFFF_FFFF, first, stream.len()));
|
|
236
|
-
directory
|
|
237
|
-
};
|
|
238
|
-
directory.resize(SECTOR, 0);
|
|
239
|
-
let mut file = header;
|
|
240
|
-
file.extend(fat.iter().flat_map(|word| word.to_le_bytes()));
|
|
241
|
-
file.extend(directory);
|
|
242
|
-
if small {
|
|
243
|
-
let mut mini_fat: Vec<u32> =
|
|
244
|
-
(1..data.len() / 64).map(|next| u32::try_from(next).unwrap()).collect();
|
|
245
|
-
mini_fat.push(END);
|
|
246
|
-
mini_fat.resize(SECTOR / 4, 0xFFFF_FFFF);
|
|
247
|
-
file.extend(mini_fat.iter().flat_map(|word| word.to_le_bytes()));
|
|
248
|
-
}
|
|
249
|
-
file.extend(pad(&data, SECTOR));
|
|
250
|
-
file
|
|
251
|
-
}
|