stationery 0.12.0 → 0.12.1

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: '08814231457b8700e5c8a03e29c1234d219adcfbf29cbc49de819f05667bf994'
4
- data.tar.gz: 0cdb872927ead42db1e41ab558c828acabd73d7cba89c121edf46a58aafbf26d
3
+ metadata.gz: ab0f207d1a92bc13494a61e91f50d1af6df807978f878f4cf613536d96f072b6
4
+ data.tar.gz: feb1a51466ae5020b1afcc0f5e7be4fd0389c8217e14b06d18044f61a5f5e67b
5
5
  SHA512:
6
- metadata.gz: 583090cf9f95cecf4e53ed6a535c9e71d8c62797e455228f70036497554e0b0e594a8b3b74d60212cef2734334611379b8ac301a1411c6eee7597bcf58d720a0
7
- data.tar.gz: 42541eb732431081e0ff9a00d66e6d0071c46bed006402b5ebee3fc87d7cd679098b77f03692b00b275aa04f20eeefc82a2e50fb949af9ac0e27664dc472bde2
6
+ metadata.gz: 2707d1a55f7bef5d7f22a6a6117b45bb5f73c41641da1ae9786b37c463ff68323c24ec75d5cf3ab88274b5b8d600cde5fe49d456f776a29afdbf152624d15a28
7
+ data.tar.gz: f6954e7b30c7e211c0d9f83055b9cdac5cb628773c8aba34ec38fce37ba0bb39ac958e0a245fb840200be0da43722175ecb3615a48278958b300aa562fb91a6a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - A tagged render is written with object streams (#206): its structure elements, and every other object that is not a stream, are packed 200 to a deflated `/Type /ObjStm`, with a cross-reference stream in place of the table and trailer (ISO 32000-1, 7.5.7 and 7.5.8). A table of 300 rows, 8 pages, is 115 KB tagged where it was 501 KB, 36 objects where it was 4,544; the tagged examples are 9 to 65% smaller. It is on by default for a render that is tagged (`tagged`, `to_pdf(tagged: true)`, `conformance :pdf_ua1`), and `object_streams false` at class level or `to_pdf(object_streams: false)` writes the classic table for a reader that cannot read object streams; `to_pdf(object_streams: true)` packs an untagged render too. Streams, the `/Encrypt` dictionary and a signature's dictionary stay out, so an encrypted, a signed and an incremental render are packed too, and PDF/A-2b, PDF/A-3b and PDF/UA-1 pass veraPDF packed. `to_png` and `to_zpl` refuse `object_streams:` as a PDF option. `Document#page_count` is how many pages the last render laid out, which `stationery render` now reports. In `PDF::Writer` (`object_streams:`, `add_unpacked`) and `PDF::ObjectStreams`.
6
+ - **Behaviour changes:** a tagged render is packed: its catalog, pages, annotations and structure elements are inside deflated object streams, so a test or tool that searches the file's bytes for them (`/StructParent`, `/Type /Page`) finds nothing (`document.page_count` has the pages), and the file ends in a cross-reference stream, not `xref` and `trailer`. A reader of PDF 1.4 and earlier cannot open it; `object_streams: false` writes it as 0.12.0 did, byte for byte. A tagged render streamed to a block is a few bytes longer than the String, as each flush closes an object stream. Untagged documents are byte for byte what they were in 0.12.0.
7
+
3
8
  ## 0.12.0 (2026-09-29)
4
9
 
5
10
  Links that conform; labels and printing (page sizes in units, print hints, monochrome, page limits, `to_png`, JPEG pixels, `to_zpl` and barcodes); floats that keep to their page; speed and memory, with tables read as pages reach them; and AI first: a skill that ships with the gem, examples with their code, the docs over MCP, and `stationery inspect`.
data/README.md CHANGED
@@ -514,6 +514,8 @@ render Callout.new(color: "#F3F4F6") { text "Amount due" }
514
514
  are included. `to_pdf(strict: true)`, or `strict` at class level, raises `Stationery::WarningsError`
515
515
  (with `#warnings`) instead of writing a PDF that produced any; `to_pdf(strict: false)` opts one
516
516
  render out again.
517
+ - After a render, `document.page_count` is how many pages it laid out: a tagged file's page
518
+ dictionaries are packed into object streams, where a search of its bytes does not find them.
517
519
 
518
520
  ### Printing
519
521
 
@@ -672,7 +674,7 @@ ShelfLabel.new.to_png(monochrome: { dpi: 203, snap: true }) # one bit to a dot,
672
674
  - **Options.** `dpi:` (96, or the monochrome dpi), `pages:`, `monochrome:`, `debug:` (the layout
673
675
  rectangles), `strict:`, `shaper:` and `max_pages:` as `to_pdf` has them. What only a PDF has (`sign:`, `encrypt:`,
674
676
  `conformance:`, `attachments:`, `print:`, `tagged:`, `page_labels:`, `xmp:`, `factur_x:`,
675
- `incremental:`, `missing_glyphs:`) raises `ArgumentError` when it is passed, and is left alone when
677
+ `incremental:`, `missing_glyphs:`, `object_streams:`) raises `ArgumentError` when it is passed, and is left alone when
676
678
  the class declares it, so a signed or encrypted document still has pictures.
677
679
  - **Speed.** A 100 × 150 mm label at 203 dpi (800 × 1200 dots) takes about 30 ms, a one-page invoice
678
680
  at 96 dpi about 0.1 s, a page of dithered photographs at 203 dpi about 0.7 s, a label with a
@@ -875,6 +877,13 @@ and across page breaks: a paragraph continued on the next page stays one `P`.
875
877
  `canvas.tag(element) { … }`.
876
878
  - A missing `lang` is a `Warnings::MissingLanguage`. All four are warnings, so `strict` catches them,
877
879
  and `conformance :pdf_ua1` raises on the first three.
880
+ - A tagged render packs its structure elements, and every other object that is not a stream, into
881
+ deflated object streams (`/Type /ObjStm`, 200 objects each) with a cross-reference stream in place
882
+ of the table and trailer (ISO 32000-1, 7.5.7 and 7.5.8). A table of 300 rows is 115 KB instead of
883
+ 501 KB. Streams, the `/Encrypt` dictionary and a signature dictionary stay out; PDF/A-2, PDF/A-3 and
884
+ PDF/UA-1 allow them. `object_streams false` at class level, or `to_pdf(object_streams: false)`,
885
+ writes the classic table for a reader that cannot read them; `to_pdf(object_streams: true)` packs
886
+ an untagged render too.
878
887
  - Untagged documents (the default) are written exactly as before.
879
888
 
880
889
  Check the tree in tests with `have_structure` and `have_tagged_content` (see [Testing](#testing)):
@@ -67,7 +67,7 @@ module Stationery
67
67
  output = @options[:zpl] ? to_zpl(document) : to_pdf(document)
68
68
  return FAILURE unless warnings_ok?(document)
69
69
 
70
- write(output, target)
70
+ write(output, target, document)
71
71
  pictures(document, target) if @options[:png]
72
72
  OK
73
73
  end
@@ -149,20 +149,21 @@ module Stationery
149
149
  false
150
150
  end
151
151
 
152
- def write(pdf, target)
152
+ def write(pdf, target, document)
153
153
  if target == "-"
154
154
  @out.binmode
155
155
  @out.write(pdf)
156
156
  else
157
157
  File.binwrite(target, pdf)
158
- @out.puts "wrote #{target} (#{count(pdf)}, #{pdf.bytesize} bytes)"
158
+ @out.puts "wrote #{target} (#{count(pdf, document)}, #{pdf.bytesize} bytes)"
159
159
  end
160
160
  end
161
161
 
162
- # "1 page", "3 labels": what the file holds.
163
- def count(output)
162
+ # "1 page", "3 labels": what the file holds. The pages are the
163
+ # render's: a tagged file's are packed, and encrypted too with it.
164
+ def count(output, document)
164
165
  name = @options[:zpl] ? "label" : "page"
165
- count = output.scan(@options[:zpl] ? "^XA" : %r{/Type /Page\b}).size
166
+ count = @options[:zpl] ? output.scan("^XA").size : document.page_count
166
167
  "#{count} #{name}#{"s" unless count == 1}"
167
168
  end
168
169
 
@@ -131,6 +131,15 @@ module Stationery
131
131
  config[:tagged] = value
132
132
  end
133
133
 
134
+ # Packs every object that is not a stream (the structure elements of a
135
+ # tagged PDF, most of all) into deflated object streams, with a
136
+ # cross-reference stream: PDF 1.5, which PDF/A-2, PDF/A-3 and PDF/UA-1
137
+ # allow. On for a tagged render unless set; `object_streams false`
138
+ # writes the classic table for a reader that cannot read them.
139
+ def object_streams(value = true) # rubocop:disable Style/OptionalBooleanParameter
140
+ config[:object_streams] = value
141
+ end
142
+
134
143
  # Claims PDF/A-2b, PDF/A-3b and/or PDF/UA-1 (`conformance :pdf_a3b,
135
144
  # :pdf_ua1`) and writes what the level asks for; a render that cannot
136
145
  # keep the claim raises. See PDF::Conformance. `missing_glyphs:` is what
@@ -197,8 +206,9 @@ module Stationery
197
206
  end
198
207
  end
199
208
 
200
- # `fields` is every form field's name and value from the last render.
201
- attr_reader :warnings, :fields
209
+ # `fields` is every form field's name and value from the last render;
210
+ # `page_count` is how many pages it laid out.
211
+ attr_reader :warnings, :fields, :page_count
202
212
 
203
213
  def page_options = self.class.config[:page]
204
214
  def metadata = self.class.config[:metadata]
@@ -225,13 +235,17 @@ module Stationery
225
235
  #
226
236
  # `max_pages:` replaces the class's limit (see .max_pages) for one
227
237
  # render; nil takes it away.
238
+ #
239
+ # `object_streams:` packs what is not a stream into object streams (see
240
+ # .object_streams); nil, the default, packs a tagged render.
228
241
  def to_pdf(target = nil, strict: self.class.config[:strict], debug: false, encrypt: self.class.config[:encrypt],
229
242
  tagged: self.class.config[:tagged], page_labels: self.class.config[:page_labels], attachments: [],
230
243
  xmp: metadata[:xmp] != false, conformance: self.class.config[:conformance],
231
244
  factur_x: self.class.config[:factur_x], sign: self.class.config[:sign],
232
245
  shaper: self.class.config[:shaping][:shaper], incremental: self.class.config[:incremental],
233
246
  print: PDF::PrintHints::NONE, missing_glyphs: self.class.config[:missing_glyphs],
234
- monochrome: self.class.config[:monochrome], max_pages: self.class.config[:max_pages], &block)
247
+ monochrome: self.class.config[:monochrome], max_pages: self.class.config[:max_pages],
248
+ object_streams: self.class.config[:object_streams], &block)
235
249
  invoice = PDF::FacturX.for(factur_x, self)
236
250
  attachments = PDF::Attachments.merge(self.class.config[:attachments], attachments, invoice&.attachment)
237
251
  conformance = PDF::Conformance.for(invoice ? invoice.conformance(conformance) : conformance, missing_glyphs:)
@@ -243,6 +257,7 @@ module Stationery
243
257
  raise ArgumentError, "pass a target or a block, not both" if target && block
244
258
 
245
259
  options = { strict:, debug:, encrypt:, tagged: tagged || conformance&.pdf_ua?, page_labels:, attachments:,
260
+ object_streams: packed?(object_streams, tagged, conformance),
246
261
  xmp: xmp || !conformance.nil?, conformance:, invoice:, signature:, sink: block, shaper:, print:,
247
262
  incremental: incremental && !conformance && !signature && !(strict && block),
248
263
  monochrome: Monochrome.for(self.class.config[:monochrome], monochrome), max_pages: }
@@ -297,6 +312,7 @@ module Stationery
297
312
  destinations = Structure.resolve(pages, warnings:, book:, canvases:)
298
313
  yield builder.outline.resolve(destinations) if block_given?
299
314
  @warnings = warnings
315
+ @page_count = pages.size
300
316
  pages
301
317
  end
302
318
 
@@ -359,8 +375,8 @@ module Stationery
359
375
  end
360
376
 
361
377
  def assemble(pages, resources, outline, encrypt:, tagging:, page_labels:, attachments:, xmp:, conformance:,
362
- invoice:, signature:, sink:, writer:, print:)
363
- assembler = PDF::Assembler.new(pages:, resources:, info:, outline:, tagging:, xmp:, writer:,
378
+ invoice:, signature:, sink:, writer:, print:, object_streams:)
379
+ assembler = PDF::Assembler.new(pages:, resources:, info:, outline:, tagging:, xmp:, writer:, object_streams:,
364
380
  encryption: writer ? nil : encryption(encrypt),
365
381
  lang: metadata[:lang], page_labels: PDF::PageLabels.entries(page_labels),
366
382
  attachments:, conformance:, xmp_extensions: invoice&.xmp_extensions || {},
@@ -370,6 +386,13 @@ module Stationery
370
386
  end
371
387
  end
372
388
 
389
+ # Object streams when asked for, else when the render is tagged.
390
+ def packed?(object_streams, tagged, conformance)
391
+ return object_streams unless object_streams.nil?
392
+
393
+ tagged || conformance&.pdf_ua? || false
394
+ end
395
+
373
396
  def byte_count(pdf) = pdf.is_a?(String) ? pdf.bytesize : pdf
374
397
  def encryption(options) = options && PDF::Encryption::StandardSecurity.new(**options)
375
398
 
@@ -377,8 +400,10 @@ module Stationery
377
400
  # every body at once, to the writer the rest of the file follows on; any
378
401
  # other seals the pages nothing paints on again (no header, footer or
379
402
  # page template, and no conformance audit to read them).
380
- def sealer_for(incremental, conformance, encrypt:, sink:, **)
381
- return PDF::PageSealer.new(writer: PDF::Writer.new(encryption: encryption(encrypt), sink:)) if incremental
403
+ def sealer_for(incremental, conformance, encrypt:, sink:, object_streams:, **)
404
+ if incremental
405
+ return PDF::PageSealer.new(writer: PDF::Writer.new(encryption: encryption(encrypt), sink:, object_streams:))
406
+ end
382
407
 
383
408
  config = self.class.config
384
409
  PDF::PageSealer.new(final: conformance.nil? && config[:templates].empty? && config[:regions].empty?)
@@ -93,7 +93,7 @@ module Stationery
93
93
  # known to exist; nil without a signature.
94
94
  def signed
95
95
  return unless @signature
96
- return @writer.add(@signature.dictionary) if @widgets.any? { |widget| signs?(widget) }
96
+ return @writer.add_unpacked(@signature.dictionary) if @widgets.any? { |widget| signs?(widget) }
97
97
 
98
98
  raise ArgumentError, %(sign field: names "#{@signature.field}", but the document has no such signature_field)
99
99
  end
@@ -17,13 +17,15 @@ module Stationery
17
17
  # finished file, so it cannot be streamed. `writer:` is the Writer that
18
18
  # already holds the bodies of pages sealed as they were painted; it
19
19
  # brings its own encryption and sink. `print:` are the hints of
20
- # PrintHints for the catalog.
20
+ # PrintHints for the catalog. `object_streams:` (true) packs what is not
21
+ # a stream into object streams (see Writer).
21
22
  def initialize(pages:, resources:, info: {}, outline: [], encryption: nil, tagging: nil, lang: nil,
22
23
  page_labels: nil, attachments: [], xmp: true, xmp_extensions: {}, xmp_schemas: [],
23
- conformance: nil, signature: nil, sink: nil, writer: nil, print: nil)
24
+ conformance: nil, signature: nil, sink: nil, writer: nil, print: nil, object_streams: false)
24
25
  raise ArgumentError, "a signed document cannot be streamed: sign needs the whole file" if signature && sink
25
26
 
26
27
  @writer = writer
28
+ @object_streams = object_streams
27
29
  @print = print
28
30
  @sink = sink
29
31
  @xmp_schemas = xmp_schemas
@@ -44,7 +46,7 @@ module Stationery
44
46
 
45
47
  # The file as a String, or the number of bytes streamed to the sink.
46
48
  def render
47
- writer = @writer || Writer.new(encryption: @encryption, sink: @sink)
49
+ writer = @writer || Writer.new(encryption: @encryption, sink: @sink, object_streams: @object_streams)
48
50
  tree = writer.reserve
49
51
  refs = @resources.build(writer)
50
52
  @form = Forms::AcroForm.new(writer, fonts: refs.fetch(:Font), signature: @signature,
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stationery
4
+ module PDF
5
+ # The two stream objects of a compressed file (ISO 32000-1, 7.5.7 and
6
+ # 7.5.8): an object stream holding objects that are not streams, and the
7
+ # cross-reference stream that replaces the table and the trailer.
8
+ module ObjectStreams
9
+ # Objects in one object stream: a few hundred deflate to a tenth of
10
+ # their size, and a reader inflates the whole stream to reach one.
11
+ CAPACITY = 200
12
+ # Bytes of an entry's type, offset or object stream number, and
13
+ # generation or index.
14
+ WIDTHS = [1, 4, 2].freeze
15
+ FREE = [0, 0, 0xFFFF].freeze
16
+
17
+ module_function
18
+
19
+ # The object stream of `batch`, [number, value] pairs, its values
20
+ # written without encryption: the stream is encrypted whole.
21
+ def pack(batch)
22
+ header = String.new(encoding: Encoding::BINARY)
23
+ body = String.new(encoding: Encoding::BINARY)
24
+ batch.each do |number, value|
25
+ header << "#{number} #{body.bytesize} "
26
+ body << Serializer.dump(value) << "\n"
27
+ end
28
+ Stream.new(header + body, { Type: :ObjStm, N: batch.size, First: header.bytesize })
29
+ end
30
+
31
+ # The cross-reference stream of `locations`, one per object from 1: an
32
+ # Integer offset in the file, or [object stream, index] for a packed
33
+ # object. `trailer` is what the trailer dictionary would hold.
34
+ def xref(locations, trailer)
35
+ fields = FREE.dup
36
+ locations.each { |at| at.is_a?(Integer) ? fields.push(1, at, 0) : fields.push(2, at[0], at[1]) }
37
+ Stream.new(fields.pack("CNn" * (fields.size / 3)), trailer.merge(Type: :XRef, W: WIDTHS))
38
+ end
39
+ end
40
+ end
41
+ end
@@ -5,7 +5,8 @@ require "digest/md5"
5
5
  module Stationery
6
6
  module PDF
7
7
  # Collects numbered objects and writes them as a PDF file with a classic
8
- # cross-reference table. `render` returns the file as a String with the
8
+ # cross-reference table, or with `object_streams: true` packed into
9
+ # object streams with a cross-reference stream (see ObjectStreams). `render` returns the file as a String with the
9
10
  # objects in numbered order. With a `sink:` (anything answering
10
11
  # `call(bytes)`) the file goes there in pieces instead: `flush` writes every
11
12
  # object set so far and drops it, so objects leave in the order they were
@@ -16,8 +17,14 @@ module Stationery
16
17
 
17
18
  # `encryption` (an Encryption::StandardSecurity) encrypts every string
18
19
  # and stream except those of its own /Encrypt dictionary.
19
- def initialize(encryption: nil, sink: nil)
20
+ #
21
+ # With `object_streams: true` every object that is not a stream goes
22
+ # into a deflated object stream, but for the Encrypt dictionary and an
23
+ # object added with #add_unpacked (a signature dictionary, which is
24
+ # filled in once the file is written and found by its bytes).
25
+ def initialize(encryption: nil, sink: nil, object_streams: false)
20
26
  @objects = []
27
+ @loose = [] if object_streams
21
28
  @offsets = []
22
29
  @encryption = encryption
23
30
  @sink = sink
@@ -45,6 +52,13 @@ module Stationery
45
52
  set(reserve, value)
46
53
  end
47
54
 
55
+ # Adds an object that stays out of the object streams.
56
+ def add_unpacked(value)
57
+ ref = add(value)
58
+ @loose << ref.id if @loose
59
+ ref
60
+ end
61
+
48
62
  # With a sink: writes every object set and not yet written, lowest number
49
63
  # first, and drops it. References to objects still unset stay valid, as
50
64
  # the cross-reference table is written last. Without a sink it does
@@ -66,9 +80,10 @@ module Stationery
66
80
  end
67
81
 
68
82
  write_pending
69
- xref = @size
70
83
  id = HexString.new(file_id)
84
+ return write_xref_stream(root, info, id) if @loose
71
85
 
86
+ xref = @size
72
87
  emit("xref\n0 #{@objects.size + 1}\n0000000000 65535 f \n")
73
88
  @offsets.each { |offset| emit(format("%010d 00000 n \n", offset)) }
74
89
  emit("trailer\n#{Serializer.dump(trailer(root, info, id))}")
@@ -79,6 +94,8 @@ module Stationery
79
94
  private
80
95
 
81
96
  def write_pending
97
+ return pack_pending if @loose
98
+
82
99
  @objects.each_with_index do |object, index|
83
100
  next if object.nil? || object.equal?(WRITTEN)
84
101
 
@@ -87,14 +104,51 @@ module Stationery
87
104
  end
88
105
  end
89
106
 
107
+ # Streams, the Encrypt dictionary and what was added with #add_unpacked
108
+ # are written as they are, the rest into object streams of at most
109
+ # ObjectStreams::CAPACITY objects; the last one is closed with the flush.
110
+ def pack_pending
111
+ batch = []
112
+ @objects.each_with_index do |object, index|
113
+ next if object.nil? || object.equal?(WRITTEN)
114
+
115
+ number = index + 1
116
+ if object.is_a?(Stream) || @loose.include?(number) || number == @encrypt&.id
117
+ write_object(number, object)
118
+ else
119
+ batch << [number, object]
120
+ batch = write_object_stream(batch) if batch.size == ObjectStreams::CAPACITY
121
+ end
122
+ @objects[index] = WRITTEN
123
+ end
124
+ write_object_stream(batch) if batch.any?
125
+ end
126
+
127
+ def write_object_stream(batch)
128
+ @objects << WRITTEN
129
+ number = @objects.size
130
+ batch.each_with_index { |(packed, _), index| @offsets[packed - 1] = [number, index] }
131
+ write_object(number, ObjectStreams.pack(batch))
132
+ []
133
+ end
134
+
135
+ # The cross-reference stream, its own entry among them, never encrypted.
136
+ def write_xref_stream(root, info, id)
137
+ @objects << WRITTEN
138
+ number = @objects.size
139
+ xref = @offsets[number - 1] = @size
140
+ write_object(number, ObjectStreams.xref(@offsets, trailer(root, info, id)), nil)
141
+ emit("startxref\n#{xref}\n%%EOF\n")
142
+ @buffer || @size
143
+ end
144
+
90
145
  def trailer(root, info, id)
91
146
  trailer = { Size: @objects.size + 1, Root: root, Info: info, ID: [id, id] }
92
147
  @encrypt ? trailer.merge(Encrypt: @encrypt) : trailer
93
148
  end
94
149
 
95
- def write_object(number, object)
150
+ def write_object(number, object, crypt = crypt_for(number))
96
151
  @offsets[number - 1] = @size
97
- crypt = crypt_for(number)
98
152
  emit("#{number} 0 obj\n")
99
153
  if object.is_a?(Stream)
100
154
  data = crypt ? crypt.call(object.data) : object.data
@@ -16,7 +16,7 @@ module Stationery
16
16
  # What only a PDF has: refused when asked for, left alone when the
17
17
  # class declares it.
18
18
  PDF_ONLY = %i[sign encrypt conformance attachments print tagged page_labels xmp factur_x incremental
19
- missing_glyphs].freeze
19
+ missing_glyphs object_streams].freeze
20
20
  GREY = "\x01-\xFE".b
21
21
  DARK = "\x00-\x7F".b
22
22
  LIGHT = "\x80-\xFF".b
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stationery
4
- VERSION = "0.12.0"
4
+ VERSION = "0.12.1"
5
5
  end
data/lib/stationery.rb CHANGED
@@ -11,6 +11,7 @@ require_relative "stationery/pdf/encryption/aes"
11
11
  require_relative "stationery/pdf/encryption/revision4"
12
12
  require_relative "stationery/pdf/encryption/revision6"
13
13
  require_relative "stationery/pdf/encryption/standard_security"
14
+ require_relative "stationery/pdf/object_streams"
14
15
  require_relative "stationery/pdf/writer"
15
16
  require_relative "stationery/fonts/sfnt_metrics"
16
17
  require_relative "stationery/fonts/woff"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stationery
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.12.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -254,6 +254,7 @@ files:
254
254
  - lib/stationery/pdf/factur_x.rb
255
255
  - lib/stationery/pdf/icc/LICENSE.md
256
256
  - lib/stationery/pdf/icc/sRGB2014.icc
257
+ - lib/stationery/pdf/object_streams.rb
257
258
  - lib/stationery/pdf/outline_writer.rb
258
259
  - lib/stationery/pdf/page_labels.rb
259
260
  - lib/stationery/pdf/page_sealer.rb