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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +10 -1
- data/lib/stationery/cli/render.rb +7 -6
- data/lib/stationery/document.rb +32 -7
- data/lib/stationery/forms/acro_form.rb +1 -1
- data/lib/stationery/pdf/assembler.rb +5 -3
- data/lib/stationery/pdf/object_streams.rb +41 -0
- data/lib/stationery/pdf/writer.rb +59 -5
- data/lib/stationery/raster/render.rb +1 -1
- data/lib/stationery/version.rb +1 -1
- data/lib/stationery.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ab0f207d1a92bc13494a61e91f50d1af6df807978f878f4cf613536d96f072b6
|
|
4
|
+
data.tar.gz: feb1a51466ae5020b1afcc0f5e7be4fd0389c8217e14b06d18044f61a5f5e67b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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 =
|
|
166
|
+
count = @options[:zpl] ? output.scan("^XA").size : document.page_count
|
|
166
167
|
"#{count} #{name}#{"s" unless count == 1}"
|
|
167
168
|
end
|
|
168
169
|
|
data/lib/stationery/document.rb
CHANGED
|
@@ -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
|
-
|
|
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],
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
data/lib/stationery/version.rb
CHANGED
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.
|
|
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
|