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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/Cargo.lock +33 -3
  4. data/Cargo.toml +1 -1
  5. data/README.md +48 -1
  6. data/ext/imprimatur_native/cli/Cargo.toml +3 -1
  7. data/ext/imprimatur_native/core/Cargo.toml +15 -4
  8. data/ext/imprimatur_native/core/README.md +27 -4
  9. data/ext/imprimatur_native/core/src/barcode/code128.rs +162 -0
  10. data/ext/imprimatur_native/core/src/barcode/draw.rs +124 -0
  11. data/ext/imprimatur_native/core/src/barcode/ean.rs +91 -0
  12. data/ext/imprimatur_native/core/src/barcode/gs1.rs +188 -0
  13. data/ext/imprimatur_native/core/src/barcode/matrix.rs +130 -0
  14. data/ext/imprimatur_native/core/src/barcode.rs +314 -0
  15. data/ext/imprimatur_native/core/src/doc/symbol.rs +136 -0
  16. data/ext/imprimatur_native/core/src/doc.rs +11 -1
  17. data/ext/imprimatur_native/core/src/docx.rs +22 -10
  18. data/ext/imprimatur_native/core/src/error.rs +6 -0
  19. data/ext/imprimatur_native/core/src/font.rs +3 -1
  20. data/ext/imprimatur_native/core/src/lib.rs +3 -1
  21. data/ext/imprimatur_native/core/src/pdf/layout.rs +48 -14
  22. data/ext/imprimatur_native/core/src/pdf/truetype/descriptor.rs +129 -0
  23. data/ext/imprimatur_native/core/src/pdf/truetype/program.rs +70 -0
  24. data/ext/imprimatur_native/core/src/pdf/truetype.rs +149 -0
  25. data/ext/imprimatur_native/core/src/pdf.rs +9 -1
  26. data/ext/imprimatur_native/core/src/read/image.rs +17 -0
  27. data/ext/imprimatur_native/core/src/read.rs +3 -2
  28. data/ext/imprimatur_native/core/src/style.rs +11 -0
  29. data/ext/imprimatur_native/ffi/Cargo.toml +1 -1
  30. data/ext/imprimatur_native/ffi/src/barcode.rs +147 -0
  31. data/ext/imprimatur_native/ffi/src/document.rs +29 -1
  32. data/ext/imprimatur_native/ffi/src/lib.rs +5 -3
  33. data/lib/imprimatur/barcode.rb +78 -0
  34. data/lib/imprimatur/document.rb +17 -1
  35. data/lib/imprimatur/reader.rb +48 -11
  36. data/lib/imprimatur/spreadsheet.rb +1 -1
  37. data/lib/imprimatur/version.rb +1 -1
  38. data/lib/imprimatur.rb +6 -1
  39. metadata +13 -2
  40. 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
@@ -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: 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
- }