gamba 2.0.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 +7 -0
- data/CHANGELOG.md +80 -0
- data/COPYING +2 -0
- data/GPLv2 +339 -0
- data/GPLv3 +674 -0
- data/LICENSE +65 -0
- data/LICENSE-prawn-svg +21 -0
- data/README.md +148 -0
- data/data/fonts/Courier-Bold.afm +342 -0
- data/data/fonts/Courier-BoldOblique.afm +342 -0
- data/data/fonts/Courier-Oblique.afm +342 -0
- data/data/fonts/Courier.afm +342 -0
- data/data/fonts/Helvetica-Bold.afm +2827 -0
- data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
- data/data/fonts/Helvetica-Oblique.afm +3051 -0
- data/data/fonts/Helvetica.afm +3051 -0
- data/data/fonts/MustRead.html +19 -0
- data/data/fonts/Symbol.afm +213 -0
- data/data/fonts/Times-Bold.afm +2588 -0
- data/data/fonts/Times-BoldItalic.afm +2384 -0
- data/data/fonts/Times-Italic.afm +2667 -0
- data/data/fonts/Times-Roman.afm +2419 -0
- data/data/fonts/ZapfDingbats.afm +225 -0
- data/lib/gamba/annotations.rb +21 -0
- data/lib/gamba/combine.rb +307 -0
- data/lib/gamba/document/bounding_box.rb +193 -0
- data/lib/gamba/document/column_box.rb +91 -0
- data/lib/gamba/document/internals.rb +264 -0
- data/lib/gamba/document/span.rb +25 -0
- data/lib/gamba/document.rb +268 -0
- data/lib/gamba/encoding.rb +13 -0
- data/lib/gamba/errors.rb +39 -0
- data/lib/gamba/font.rb +228 -0
- data/lib/gamba/fonts/afm.rb +171 -0
- data/lib/gamba/fonts/otf.rb +50 -0
- data/lib/gamba/fonts/ttf.rb +413 -0
- data/lib/gamba/graphic_state.rb +84 -0
- data/lib/gamba/graphics/color.rb +174 -0
- data/lib/gamba/graphics/patterns.rb +100 -0
- data/lib/gamba/graphics/style.rb +159 -0
- data/lib/gamba/graphics.rb +212 -0
- data/lib/gamba/grid.rb +216 -0
- data/lib/gamba/images/image.rb +271 -0
- data/lib/gamba/images/png_decoder.rb +65 -0
- data/lib/gamba/images.rb +115 -0
- data/lib/gamba/measurement_extensions.rb +40 -0
- data/lib/gamba/measurements.rb +62 -0
- data/lib/gamba/outline.rb +148 -0
- data/lib/gamba/page.rb +125 -0
- data/lib/gamba/page_geometry.rb +27 -0
- data/lib/gamba/pdf/decryption.rb +165 -0
- data/lib/gamba/pdf/outline_node.rb +53 -0
- data/lib/gamba/pdf/reader.rb +616 -0
- data/lib/gamba/pdf/reference.rb +31 -0
- data/lib/gamba/pdf/writer.rb +105 -0
- data/lib/gamba/pdf.rb +87 -0
- data/lib/gamba/pdf_a/srgb_profile.rb +61 -0
- data/lib/gamba/pdf_a.rb +132 -0
- data/lib/gamba/repeater.rb +79 -0
- data/lib/gamba/security.rb +111 -0
- data/lib/gamba/stamp.rb +76 -0
- data/lib/gamba/table/cell/kinds.rb +246 -0
- data/lib/gamba/table/cell.rb +331 -0
- data/lib/gamba/table/cells.rb +208 -0
- data/lib/gamba/table.rb +298 -0
- data/lib/gamba/text/box.rb +16 -0
- data/lib/gamba/text/formatted/arranger.rb +217 -0
- data/lib/gamba/text/formatted/box.rb +312 -0
- data/lib/gamba/text/formatted/fragment.rb +205 -0
- data/lib/gamba/text/formatted/line_wrap.rb +205 -0
- data/lib/gamba/text/formatted/parser.rb +150 -0
- data/lib/gamba/text/formatted/wrap.rb +103 -0
- data/lib/gamba/text.rb +310 -0
- data/lib/gamba/transformation_stack.rb +43 -0
- data/lib/gamba/true_type/cff_subset.rb +157 -0
- data/lib/gamba/true_type/file.rb +372 -0
- data/lib/gamba/true_type/open_type_layout.rb +189 -0
- data/lib/gamba/true_type/simple_subsets.rb +108 -0
- data/lib/gamba/true_type/subset.rb +206 -0
- data/lib/gamba/version.rb +10 -0
- data/lib/gamba/view.rb +28 -0
- data/lib/gamba.rb +124 -0
- data/lib/pdf/core.rb +4 -0
- data/lib/prawn/measurement_extensions.rb +4 -0
- data/lib/prawn/svg/attributes/clip_path.rb +46 -0
- data/lib/prawn/svg/attributes/mask.rb +16 -0
- data/lib/prawn/svg/attributes/opacity.rb +30 -0
- data/lib/prawn/svg/attributes/space.rb +10 -0
- data/lib/prawn/svg/attributes/stroke.rb +50 -0
- data/lib/prawn/svg/attributes/transform.rb +10 -0
- data/lib/prawn/svg/attributes.rb +6 -0
- data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
- data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
- data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
- data/lib/prawn/svg/calculators/path_length.rb +168 -0
- data/lib/prawn/svg/calculators/pixels.rb +57 -0
- data/lib/prawn/svg/color.rb +240 -0
- data/lib/prawn/svg/css/font_face_parser.rb +72 -0
- data/lib/prawn/svg/css/font_family_parser.rb +39 -0
- data/lib/prawn/svg/css/font_parser.rb +46 -0
- data/lib/prawn/svg/css/selector_parser.rb +180 -0
- data/lib/prawn/svg/css/stylesheets.rb +233 -0
- data/lib/prawn/svg/css/values_parser.rb +68 -0
- data/lib/prawn/svg/document.rb +169 -0
- data/lib/prawn/svg/elements/anchor.rb +10 -0
- data/lib/prawn/svg/elements/base.rb +315 -0
- data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
- data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
- data/lib/prawn/svg/elements/circle.rb +19 -0
- data/lib/prawn/svg/elements/clip_path.rb +26 -0
- data/lib/prawn/svg/elements/container.rb +10 -0
- data/lib/prawn/svg/elements/direct_render_base.rb +28 -0
- data/lib/prawn/svg/elements/ellipse.rb +20 -0
- data/lib/prawn/svg/elements/gradient.rb +183 -0
- data/lib/prawn/svg/elements/ignored.rb +5 -0
- data/lib/prawn/svg/elements/image.rb +178 -0
- data/lib/prawn/svg/elements/line.rb +27 -0
- data/lib/prawn/svg/elements/marker.rb +72 -0
- data/lib/prawn/svg/elements/mask.rb +65 -0
- data/lib/prawn/svg/elements/path.rb +294 -0
- data/lib/prawn/svg/elements/pattern.rb +150 -0
- data/lib/prawn/svg/elements/polygon.rb +25 -0
- data/lib/prawn/svg/elements/polyline.rb +23 -0
- data/lib/prawn/svg/elements/rect.rb +64 -0
- data/lib/prawn/svg/elements/root.rb +22 -0
- data/lib/prawn/svg/elements/switch.rb +92 -0
- data/lib/prawn/svg/elements/text.rb +99 -0
- data/lib/prawn/svg/elements/text_component.rb +352 -0
- data/lib/prawn/svg/elements/text_node.rb +308 -0
- data/lib/prawn/svg/elements/text_path.rb +153 -0
- data/lib/prawn/svg/elements/use.rb +142 -0
- data/lib/prawn/svg/elements/viewport.rb +29 -0
- data/lib/prawn/svg/elements.rb +46 -0
- data/lib/prawn/svg/extension.rb +25 -0
- data/lib/prawn/svg/font.rb +20 -0
- data/lib/prawn/svg/font_metrics.rb +144 -0
- data/lib/prawn/svg/font_registry.rb +315 -0
- data/lib/prawn/svg/funciri.rb +14 -0
- data/lib/prawn/svg/gradient_renderer.rb +332 -0
- data/lib/prawn/svg/gradients.rb +53 -0
- data/lib/prawn/svg/interface.rb +72 -0
- data/lib/prawn/svg/length.rb +43 -0
- data/lib/prawn/svg/link_renderer.rb +40 -0
- data/lib/prawn/svg/loaders/data.rb +17 -0
- data/lib/prawn/svg/loaders/file.rb +111 -0
- data/lib/prawn/svg/loaders/web.rb +24 -0
- data/lib/prawn/svg/paint.rb +67 -0
- data/lib/prawn/svg/pathable.rb +153 -0
- data/lib/prawn/svg/pattern_renderer.rb +112 -0
- data/lib/prawn/svg/pdfmatrix.rb +39 -0
- data/lib/prawn/svg/percentage.rb +24 -0
- data/lib/prawn/svg/properties.rb +329 -0
- data/lib/prawn/svg/renderer.rb +249 -0
- data/lib/prawn/svg/state.rb +24 -0
- data/lib/prawn/svg/transform_parser.rb +78 -0
- data/lib/prawn/svg/ttc.rb +35 -0
- data/lib/prawn/svg/ttf.rb +95 -0
- data/lib/prawn/svg/url_loader.rb +46 -0
- data/lib/prawn/svg/version.rb +5 -0
- data/lib/prawn/table.rb +4 -0
- data/lib/prawn-svg.rb +52 -0
- data/lib/prawn.rb +3 -0
- metadata +263 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
require "zlib"
|
|
5
|
+
|
|
6
|
+
module PDF
|
|
7
|
+
module Core
|
|
8
|
+
# Reads a PDF written by someone else, versions 1.0 to 1.7 and 2.0: xref tables and cross-reference streams,
|
|
9
|
+
# object streams, incremental updates (ISO 32000-1 §7.5). Objects are parsed when first asked for. When the
|
|
10
|
+
# cross-reference data is missing or points to the wrong bytes, the reader rebuilds it by scanning the file, as
|
|
11
|
+
# viewers do. Streams keep their encoded bytes: only object and cross-reference streams are decoded.
|
|
12
|
+
class Reader
|
|
13
|
+
Ref = Data.define(:number, :generation)
|
|
14
|
+
Stream = Data.define(:dictionary, :data)
|
|
15
|
+
|
|
16
|
+
WHITESPACE = /(?:[\x00\t\n\f\r ]+|%[^\r\n]*)*/n
|
|
17
|
+
NAME_BODY = %r{[^\x00\t\n\f\r ()<>\[\]{}/%]*}n
|
|
18
|
+
NUMBER = /[+-]?(?:\d+\.?\d*|\.\d+)/n
|
|
19
|
+
REFERENCE_TAIL = /[\x00\t\n\f\r ]+(\d+)[\x00\t\n\f\r ]+R(?![^\x00\t\n\f\r ()<>\[\]{}\/%])/n
|
|
20
|
+
OBJECT_HEADER = /[\x00\t\n\f\r ]*(\d+)[\x00\t\n\f\r ]+(\d+)[\x00\t\n\f\r ]+obj/n
|
|
21
|
+
OBJECT_HEADER_ANYWHERE = /(?<![0-9])(\d+)[\x00\t\n\f\r ]+(\d+)[\x00\t\n\f\r ]+obj(?![^\x00\t\n\f\r ()<>\[\]{}\/%])/n
|
|
22
|
+
STREAM_KEYWORD = /stream(?:\r\n|\n|\r)?/n
|
|
23
|
+
KEYWORD = /[^\x00\t\n\f\r ()<>\[\]{}\/%]+/n
|
|
24
|
+
LITERAL_ESCAPES = {"n" => "\n", "r" => "\r", "t" => "\t", "b" => "\b", "f" => "\f"}.freeze
|
|
25
|
+
INHERITED_PAGE_KEYS = %i[Resources MediaBox CropBox Rotate].freeze
|
|
26
|
+
TYPE_PAGES = Name.new(value: "Pages")
|
|
27
|
+
TYPE_OBJECT_STREAM = Name.new(value: "ObjStm")
|
|
28
|
+
FLATE = Name.new(value: "FlateDecode")
|
|
29
|
+
TYPE_CATALOG = Name.new(value: "Catalog")
|
|
30
|
+
# Limits on what a hostile file can make the reader spend. Raised past them, LimitExceeded is never repaired
|
|
31
|
+
# around: the file is refused.
|
|
32
|
+
LimitExceeded = Class.new(Prawn::Errors::MalformedPDF)
|
|
33
|
+
# Arrays and dictionaries inside one another; real files nest a handful of levels, PDFium stops at 64 too.
|
|
34
|
+
MAX_NESTING_DEPTH = 64
|
|
35
|
+
# Objects loading one another: a /Length or an object stream in an object stream. Real files need 3.
|
|
36
|
+
MAX_RESOLUTION_DEPTH = 32
|
|
37
|
+
# Object and cross-reference streams hold dictionaries and offsets, which Flate packs 5 to 10 times: 100 times
|
|
38
|
+
# the whole file, decoded, leaves room for any real one; 256 MiB caps the memory a large file may claim.
|
|
39
|
+
DECODED_BYTES_PER_FILE_BYTE = 100
|
|
40
|
+
MAX_DECODED_BYTES = 256 * 1024 * 1024
|
|
41
|
+
# Bytes parsed, per byte of file or decoded stream: a damaged encrypted file is read about 4 times over (first
|
|
42
|
+
# pass, rebuild, restart once decrypting), objects whose bytes overlap many more.
|
|
43
|
+
SCANS_PER_BYTE = 16
|
|
44
|
+
INFLATE_CHUNK_BYTES = 1024
|
|
45
|
+
# PNG predictor parameters MuPDF accepts (ISO 32000-1 table 8 sets no maximum to /Colors; 32 is the most
|
|
46
|
+
# colorants a colour space has, Annex C).
|
|
47
|
+
PREDICTOR_BITS_PER_COMPONENT = [1, 2, 4, 8, 16].freeze
|
|
48
|
+
PREDICTOR_COLORS = 1..32
|
|
49
|
+
STRAY_DELIMITERS = /(?:[\x00\t\n\f\r ]+|%[^\r\n]*|[)>\]{}])*/n
|
|
50
|
+
|
|
51
|
+
attr_reader :version
|
|
52
|
+
|
|
53
|
+
def initialize(bytes)
|
|
54
|
+
@bytes = bytes.b
|
|
55
|
+
@scanner = StringScanner.new(@bytes)
|
|
56
|
+
@objects = {}
|
|
57
|
+
@object_streams = {}
|
|
58
|
+
@nesting_depth = 0
|
|
59
|
+
@resolution_depth = 0
|
|
60
|
+
@decoded_bytes_left = [@bytes.bytesize * DECODED_BYTES_PER_FILE_BYTE, MAX_DECODED_BYTES].min
|
|
61
|
+
@scanned_bytes_left = @bytes.bytesize * SCANS_PER_BYTE
|
|
62
|
+
raise Prawn::Errors::MalformedPDF, "Not a PDF file: no %PDF- header" unless (header = @bytes.index("%PDF-")) && header < 1024
|
|
63
|
+
|
|
64
|
+
@version = @bytes.byteslice(header + 5, 3).to_f
|
|
65
|
+
begin
|
|
66
|
+
read_cross_references
|
|
67
|
+
raise Prawn::Errors::MalformedPDF, "The trailer has no /Root" unless @trailer[:Root]
|
|
68
|
+
rescue LimitExceeded
|
|
69
|
+
raise
|
|
70
|
+
rescue Prawn::Errors::MalformedPDF
|
|
71
|
+
rebuild_cross_references
|
|
72
|
+
end
|
|
73
|
+
decrypt_from_now_on if @trailer[:Encrypt] && !@decryption
|
|
74
|
+
catalog_version = resolve(catalog[:Version])
|
|
75
|
+
@version = [@version, catalog_version.value.to_f].max if catalog_version.is_a?(Name)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A count or a size read from the file: anything but an integer reads as the default.
|
|
79
|
+
def self.integer(value, default = 0)
|
|
80
|
+
value.is_a?(Integer) ? value : default
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def catalog
|
|
84
|
+
catalog = resolve(@trailer[:Root])
|
|
85
|
+
catalog.is_a?(Hash) ? catalog : {}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def resolve(value)
|
|
89
|
+
value.is_a?(Ref) ? object(value.number) : value
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def object(number)
|
|
93
|
+
return @objects[number] if @objects.key?(number)
|
|
94
|
+
|
|
95
|
+
raise LimitExceeded, "Objects nested deeper than #{MAX_RESOLUTION_DEPTH} levels" if @resolution_depth >= MAX_RESOLUTION_DEPTH
|
|
96
|
+
|
|
97
|
+
@objects[number] = nil # a reference loop resolves to null instead of recursing forever
|
|
98
|
+
@resolution_depth += 1
|
|
99
|
+
begin
|
|
100
|
+
@objects[number] = load_object(number)
|
|
101
|
+
ensure
|
|
102
|
+
@resolution_depth -= 1
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Each page with the attributes it inherits from its ancestors (ISO 32000-1 §7.7.3.4): [reference, dictionary].
|
|
107
|
+
def pages
|
|
108
|
+
pages = []
|
|
109
|
+
visited = {}
|
|
110
|
+
pending = [[catalog[:Pages], {}]]
|
|
111
|
+
until pending.empty?
|
|
112
|
+
reference, inherited = pending.pop
|
|
113
|
+
if reference.is_a?(Ref)
|
|
114
|
+
next if visited[reference.number]
|
|
115
|
+
|
|
116
|
+
visited[reference.number] = true
|
|
117
|
+
end
|
|
118
|
+
node = resolve(reference)
|
|
119
|
+
next unless node.is_a?(Hash)
|
|
120
|
+
|
|
121
|
+
kids = resolve(node[:Kids])
|
|
122
|
+
if node[:Type] == TYPE_PAGES || (node[:Type].nil? && kids.is_a?(Array))
|
|
123
|
+
inherited = inherited.merge(node.slice(*INHERITED_PAGE_KEYS))
|
|
124
|
+
Array(kids).reverse_each { |kid| pending << [kid, inherited] }
|
|
125
|
+
else
|
|
126
|
+
pages << [reference, inherited.merge(node)]
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
pages
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
# Objects read so far were read as stored: forget them, so they are read again and decrypted.
|
|
135
|
+
def decrypt_from_now_on
|
|
136
|
+
encryption = resolve(@trailer[:Encrypt])
|
|
137
|
+
raise Prawn::Errors::MalformedPDF, "The /Encrypt entry is not a dictionary" unless encryption.is_a?(Hash)
|
|
138
|
+
|
|
139
|
+
@decryption = Decryption.new(encryption, Array(resolve(@trailer[:ID])).first.to_s.b,
|
|
140
|
+
(@trailer[:Encrypt].number if @trailer[:Encrypt].is_a?(Ref)))
|
|
141
|
+
@objects.clear
|
|
142
|
+
@object_streams.clear
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The last startxref points to the newest section; each section's /Prev to the one it updates.
|
|
146
|
+
def read_cross_references
|
|
147
|
+
@locations = {}
|
|
148
|
+
startxref = @bytes.rindex("startxref", -1) || raise(Prawn::Errors::MalformedPDF, "No startxref")
|
|
149
|
+
@scanner.pos = startxref + 9
|
|
150
|
+
skip_whitespace
|
|
151
|
+
offset = @scanner.scan(/\d+/n)&.to_i || raise(Prawn::Errors::MalformedPDF, "No offset after startxref")
|
|
152
|
+
visited = {}
|
|
153
|
+
until offset.nil? || visited[offset]
|
|
154
|
+
visited[offset] = true
|
|
155
|
+
section_trailer = read_cross_reference_section(offset)
|
|
156
|
+
@trailer ||= section_trailer
|
|
157
|
+
read_cross_reference_section(section_trailer[:XRefStm]) if section_trailer[:XRefStm].is_a?(Integer)
|
|
158
|
+
offset = section_trailer[:Prev].is_a?(Integer) ? section_trailer[:Prev] : nil
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def read_cross_reference_section(offset)
|
|
163
|
+
raise Prawn::Errors::MalformedPDF, "startxref points outside the file" unless offset.between?(0, @bytes.bytesize - 1)
|
|
164
|
+
|
|
165
|
+
@scanner.pos = offset
|
|
166
|
+
skip_whitespace
|
|
167
|
+
return read_cross_reference_table if @scanner.skip(/xref/n)
|
|
168
|
+
|
|
169
|
+
number, _, value = parse_indirect_object_at(@scanner.pos)
|
|
170
|
+
raise Prawn::Errors::MalformedPDF, "startxref points to neither a table nor a stream" unless value.is_a?(Stream)
|
|
171
|
+
|
|
172
|
+
read_cross_reference_stream(number, value)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def read_cross_reference_table
|
|
176
|
+
loop do
|
|
177
|
+
skip_whitespace
|
|
178
|
+
break unless (first = @scanner.scan(/\d+/n))
|
|
179
|
+
|
|
180
|
+
skip_whitespace
|
|
181
|
+
count = @scanner.scan(/\d+/n).to_i
|
|
182
|
+
first = first.to_i
|
|
183
|
+
count.times do |index|
|
|
184
|
+
skip_whitespace
|
|
185
|
+
@scanner.skip(/(\d+) +(\d+) +([nf])/n) || raise(Prawn::Errors::MalformedPDF, "Truncated xref table")
|
|
186
|
+
# A subsection starting at 1 whose first entry is the free head of the list: the writer meant 0.
|
|
187
|
+
first -= 1 if index.zero? && first == 1 && @scanner[1].to_i.zero? && @scanner[2] == "65535"
|
|
188
|
+
next unless @scanner[3] == "n"
|
|
189
|
+
|
|
190
|
+
@locations[first + index] ||= [:offset, @scanner[1].to_i]
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
skip_whitespace
|
|
194
|
+
raise Prawn::Errors::MalformedPDF, "No trailer after the xref table" unless @scanner.skip(/trailer/n)
|
|
195
|
+
|
|
196
|
+
dictionary = parse_value
|
|
197
|
+
raise Prawn::Errors::MalformedPDF, "The trailer is not a dictionary" unless dictionary.is_a?(Hash)
|
|
198
|
+
|
|
199
|
+
dictionary
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Rows of /W widths: type (default 1), then offset or object stream, then generation or index (§7.5.8.3).
|
|
203
|
+
def read_cross_reference_stream(stream_number, stream)
|
|
204
|
+
dictionary = stream.dictionary
|
|
205
|
+
widths = Array(dictionary[:W]).map { Reader.integer(_1, -1) }
|
|
206
|
+
row_width = widths.sum
|
|
207
|
+
raise Prawn::Errors::MalformedPDF, "Bad /W in a cross-reference stream" unless widths.size == 3 && widths.all? { _1.between?(0, 8) } && row_width.positive?
|
|
208
|
+
|
|
209
|
+
rows = decode(stream)
|
|
210
|
+
index = Array(dictionary[:Index] || [0, Reader.integer(dictionary[:Size])]).each_slice(2)
|
|
211
|
+
position = 0
|
|
212
|
+
index.each do |first, count|
|
|
213
|
+
Reader.integer(count).times do |offset|
|
|
214
|
+
break if position + row_width > rows.bytesize
|
|
215
|
+
|
|
216
|
+
type = widths[0].zero? ? 1 : field(rows, position, widths[0])
|
|
217
|
+
second = field(rows, position + widths[0], widths[1])
|
|
218
|
+
third = field(rows, position + widths[0] + widths[1], widths[2])
|
|
219
|
+
position += row_width
|
|
220
|
+
number = Reader.integer(first) + offset
|
|
221
|
+
next if @locations.key?(number)
|
|
222
|
+
|
|
223
|
+
case type
|
|
224
|
+
when 1 then @locations[number] = [:offset, second]
|
|
225
|
+
when 2 then @locations[number] = [:compressed, second, third]
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
@locations[stream_number] ||= [:offset, nil]
|
|
230
|
+
dictionary
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def field(bytes, position, width)
|
|
234
|
+
value = 0
|
|
235
|
+
width.times { |index| value = (value << 8) | bytes.getbyte(position + index) }
|
|
236
|
+
value
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Every "N G obj" in the file, the last one winning as in an incremental update; then the objects packed in
|
|
240
|
+
# object streams, and the trailer from the last trailer dictionary or cross-reference stream holding a /Root.
|
|
241
|
+
def rebuild_cross_references
|
|
242
|
+
@rebuilt = true
|
|
243
|
+
@locations = {}
|
|
244
|
+
@objects.clear
|
|
245
|
+
@object_streams.clear
|
|
246
|
+
@trailer = {}
|
|
247
|
+
position = 0
|
|
248
|
+
while (match = OBJECT_HEADER_ANYWHERE.match(@bytes, position))
|
|
249
|
+
@locations[match[1].to_i] = [:offset, match.begin(0)]
|
|
250
|
+
position = match.end(0)
|
|
251
|
+
end
|
|
252
|
+
position = 0
|
|
253
|
+
while (start = @bytes.index("trailer", position))
|
|
254
|
+
@scanner.pos = start + 7
|
|
255
|
+
dictionary = begin
|
|
256
|
+
parse_top_level_value
|
|
257
|
+
rescue LimitExceeded
|
|
258
|
+
raise
|
|
259
|
+
rescue Prawn::Errors::MalformedPDF
|
|
260
|
+
nil
|
|
261
|
+
end
|
|
262
|
+
@trailer.merge!(dictionary) if dictionary.is_a?(Hash)
|
|
263
|
+
position = start + 7
|
|
264
|
+
end
|
|
265
|
+
@locations.keys.each do |number|
|
|
266
|
+
value = object(number)
|
|
267
|
+
@trailer.merge!(value.dictionary.slice(:Root, :Info, :ID, :Encrypt)) if value.is_a?(Stream) && value.dictionary[:Root]
|
|
268
|
+
end
|
|
269
|
+
# Object streams are encrypted as a whole: they can only be opened once the key is known.
|
|
270
|
+
decrypt_from_now_on if @trailer[:Encrypt] && !@decryption
|
|
271
|
+
@locations.keys.each do |number|
|
|
272
|
+
value = object(number)
|
|
273
|
+
next unless value.is_a?(Stream) && value.dictionary[:Type] == TYPE_OBJECT_STREAM
|
|
274
|
+
|
|
275
|
+
offsets = begin
|
|
276
|
+
object_stream(number)[2]
|
|
277
|
+
rescue LimitExceeded
|
|
278
|
+
raise
|
|
279
|
+
rescue Prawn::Errors::MalformedPDF
|
|
280
|
+
next # a damaged object stream loses its own objects, not the file
|
|
281
|
+
end
|
|
282
|
+
offsets.each_with_index { |(packed, _), index| @locations[packed] ||= [:compressed, number, index] }
|
|
283
|
+
end
|
|
284
|
+
@trailer[:Root] ||= @locations.keys.find { |number| (catalog = object(number)).is_a?(Hash) && catalog[:Type] == TYPE_CATALOG }&.then { |number| Ref.new(number:, generation: 0) }
|
|
285
|
+
raise Prawn::Errors::MalformedPDF, "No document catalog found" unless @trailer[:Root]
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def load_object(number)
|
|
289
|
+
type, field, index = @locations[number]
|
|
290
|
+
case type
|
|
291
|
+
when :offset
|
|
292
|
+
return nil if field.nil?
|
|
293
|
+
|
|
294
|
+
found, generation, value = parse_indirect_object_at(field)
|
|
295
|
+
return @decryption ? @decryption.decrypt(value, number, generation) : value if found == number
|
|
296
|
+
return nil if @rebuilt
|
|
297
|
+
|
|
298
|
+
rebuild_cross_references
|
|
299
|
+
object(number)
|
|
300
|
+
when :compressed
|
|
301
|
+
object_from_stream(field, index, number)
|
|
302
|
+
end
|
|
303
|
+
rescue LimitExceeded
|
|
304
|
+
raise
|
|
305
|
+
rescue Prawn::Errors::MalformedPDF
|
|
306
|
+
nil
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def parse_indirect_object_at(offset)
|
|
310
|
+
raise Prawn::Errors::MalformedPDF, "Offset #{offset} is outside the file" unless offset.between?(0, @bytes.bytesize)
|
|
311
|
+
|
|
312
|
+
@scanner.pos = offset
|
|
313
|
+
raise Prawn::Errors::MalformedPDF, "No object at offset #{offset}" unless @scanner.skip(OBJECT_HEADER)
|
|
314
|
+
|
|
315
|
+
charging_scanned_bytes(offset) do
|
|
316
|
+
number = @scanner[1].to_i
|
|
317
|
+
generation = @scanner[2].to_i
|
|
318
|
+
value = parse_value
|
|
319
|
+
skip_whitespace
|
|
320
|
+
value = Stream.new(dictionary: value, data: stream_data(value)) if value.is_a?(Hash) && @scanner.skip(STREAM_KEYWORD)
|
|
321
|
+
[number, generation, value]
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def parse_top_level_value
|
|
326
|
+
charging_scanned_bytes(@scanner.pos) { parse_value }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Offsets may point anywhere, inside one another's bytes: parsing the same bytes over and over is refused.
|
|
330
|
+
def charging_scanned_bytes(start)
|
|
331
|
+
yield
|
|
332
|
+
ensure
|
|
333
|
+
@scanned_bytes_left -= @scanner.pos - start
|
|
334
|
+
raise LimitExceeded, "The file makes the reader parse its bytes over and over" if @scanned_bytes_left.negative?
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# /Length is trusted when "endstream" follows it; otherwise the data runs to the next "endstream".
|
|
338
|
+
def stream_data(dictionary)
|
|
339
|
+
start = @scanner.pos
|
|
340
|
+
length = dictionary[:Length]
|
|
341
|
+
length = load_length(length) if length.is_a?(Ref)
|
|
342
|
+
if length.is_a?(Integer) && length >= 0 && start + length <= @bytes.bytesize
|
|
343
|
+
@scanner.pos = start + length
|
|
344
|
+
skip_whitespace
|
|
345
|
+
return @bytes.byteslice(start, length) if @scanner.match?(/endstream/n)
|
|
346
|
+
end
|
|
347
|
+
finish = @bytes.index("endstream", start) || @bytes.bytesize
|
|
348
|
+
finish -= 1 if finish > start && @bytes.getbyte(finish - 1) == 10
|
|
349
|
+
finish -= 1 if finish > start && @bytes.getbyte(finish - 1) == 13
|
|
350
|
+
@scanner.pos = finish # charged as parsed: the search for endstream read these bytes
|
|
351
|
+
@bytes.byteslice(start, finish - start)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def load_length(reference)
|
|
355
|
+
saved = @scanner.pos
|
|
356
|
+
object(reference.number)
|
|
357
|
+
ensure
|
|
358
|
+
@scanner.pos = saved
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def object_from_stream(stream_number, index, number)
|
|
362
|
+
stream = object_stream(stream_number)
|
|
363
|
+
return nil unless stream
|
|
364
|
+
|
|
365
|
+
data, first, offsets, offset_by_number = stream
|
|
366
|
+
found, offset = offsets[index]
|
|
367
|
+
offset = offset_by_number[number] unless found == number
|
|
368
|
+
return nil unless offset && (first + offset).between?(0, data.bytesize)
|
|
369
|
+
|
|
370
|
+
scanner = @scanner
|
|
371
|
+
@scanner = StringScanner.new(data)
|
|
372
|
+
@scanner.pos = first + offset
|
|
373
|
+
parse_top_level_value
|
|
374
|
+
ensure
|
|
375
|
+
@scanner = scanner if scanner
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# [decoded bytes, /First, [[number, offset], ...], {number => offset}], inflated once per object stream; false
|
|
379
|
+
# when it is not a stream. The first offset of a number wins, as a scan of the header would find it.
|
|
380
|
+
def object_stream(stream_number)
|
|
381
|
+
@object_streams[stream_number] ||= begin
|
|
382
|
+
value = object(stream_number)
|
|
383
|
+
if value.is_a?(Stream)
|
|
384
|
+
data = decode(value)
|
|
385
|
+
first = Reader.integer(value.dictionary[:First])
|
|
386
|
+
offsets = data.byteslice(0, first).to_s.scan(/\d+/n).map(&:to_i).each_slice(2).to_a
|
|
387
|
+
offset_by_number = {}
|
|
388
|
+
offsets.each { |number, offset| offset_by_number[number] = offset unless offset_by_number.key?(number) }
|
|
389
|
+
[data, first, offsets, offset_by_number]
|
|
390
|
+
else
|
|
391
|
+
false
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Object and cross-reference streams only: Flate, optionally with a PNG predictor (§7.4.4.4). Every filter's
|
|
397
|
+
# output counts against what the file may decode in all.
|
|
398
|
+
def decode(stream)
|
|
399
|
+
filters = [resolve(stream.dictionary[:Filter])].flatten
|
|
400
|
+
filters.compact!
|
|
401
|
+
parameters = [resolve(stream.dictionary[:DecodeParms])].flatten
|
|
402
|
+
filters.each_with_index.reduce(stream.data) do |data, (filter, position)|
|
|
403
|
+
raise Prawn::Errors::MalformedPDF, "Unsupported filter #{filter.inspect} on an object stream" unless filter == FLATE
|
|
404
|
+
|
|
405
|
+
inflated = inflate(data)
|
|
406
|
+
@decoded_bytes_left -= inflated.bytesize
|
|
407
|
+
@scanned_bytes_left += inflated.bytesize * SCANS_PER_BYTE
|
|
408
|
+
unpredict(inflated, resolve(parameters[position]))
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Input fed a chunk at a time, output taken as it comes, so that a bomb stops at the ceiling instead of filling
|
|
413
|
+
# memory. A truncated or padded stream keeps what inflated before the damage.
|
|
414
|
+
def inflate(data)
|
|
415
|
+
zstream = Zlib::Inflate.new
|
|
416
|
+
output = +"".b
|
|
417
|
+
collect = lambda do |chunk|
|
|
418
|
+
output << chunk
|
|
419
|
+
raise LimitExceeded, "Object streams inflate to more than the file may decode" if output.bytesize > @decoded_bytes_left
|
|
420
|
+
end
|
|
421
|
+
0.step(data.bytesize - 1, INFLATE_CHUNK_BYTES) { |offset| zstream.inflate(data.byteslice(offset, INFLATE_CHUNK_BYTES), &collect) }
|
|
422
|
+
zstream.finish(&collect)
|
|
423
|
+
output
|
|
424
|
+
rescue Zlib::Error => error
|
|
425
|
+
raise Prawn::Errors::MalformedPDF, "Corrupt object stream: #{error.message}" if output.empty?
|
|
426
|
+
|
|
427
|
+
output
|
|
428
|
+
ensure
|
|
429
|
+
zstream&.reset # closing an unfinished stream warns otherwise
|
|
430
|
+
zstream&.close
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def unpredict(data, parameters)
|
|
434
|
+
return data if data.empty? || !parameters.is_a?(Hash) || Reader.integer(parameters[:Predictor]) < 10
|
|
435
|
+
|
|
436
|
+
colors = Reader.integer(parameters[:Colors], 1)
|
|
437
|
+
bits = Reader.integer(parameters[:BitsPerComponent], 8)
|
|
438
|
+
columns = Reader.integer(parameters[:Columns], 1)
|
|
439
|
+
raise Prawn::Errors::MalformedPDF, "Bad predictor parameters #{parameters.inspect[0, 100]}" unless PREDICTOR_COLORS.cover?(colors) && PREDICTOR_BITS_PER_COMPONENT.include?(bits) && columns.positive?
|
|
440
|
+
|
|
441
|
+
pixel_width = [(colors * bits + 7) / 8, 1].max
|
|
442
|
+
row_width = (colors * bits * columns + 7) / 8
|
|
443
|
+
return +"".b if 1 + row_width > data.bytesize
|
|
444
|
+
|
|
445
|
+
output = +"".b
|
|
446
|
+
previous = Array.new(row_width, 0)
|
|
447
|
+
position = 0
|
|
448
|
+
while position + 1 + row_width <= data.bytesize
|
|
449
|
+
filter = data.getbyte(position)
|
|
450
|
+
row = data.byteslice(position + 1, row_width).bytes
|
|
451
|
+
row_width.times do |index|
|
|
452
|
+
left = (index >= pixel_width) ? row[index - pixel_width] : 0
|
|
453
|
+
up = previous[index]
|
|
454
|
+
up_left = (index >= pixel_width) ? previous[index - pixel_width] : 0
|
|
455
|
+
predicted = case filter
|
|
456
|
+
when 1 then left
|
|
457
|
+
when 2 then up
|
|
458
|
+
when 3 then (left + up) / 2
|
|
459
|
+
when 4 then paeth(left, up, up_left)
|
|
460
|
+
else 0
|
|
461
|
+
end
|
|
462
|
+
row[index] = (row[index] + predicted) & 0xFF
|
|
463
|
+
end
|
|
464
|
+
output << row.pack("C*")
|
|
465
|
+
previous = row
|
|
466
|
+
position += 1 + row_width
|
|
467
|
+
end
|
|
468
|
+
output
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def paeth(left, up, up_left)
|
|
472
|
+
estimate = left + up - up_left
|
|
473
|
+
distances = [(estimate - left).abs, (estimate - up).abs, (estimate - up_left).abs]
|
|
474
|
+
return left if distances[0] <= distances[1] && distances[0] <= distances[2]
|
|
475
|
+
|
|
476
|
+
(distances[1] <= distances[2]) ? up : up_left
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def skip_whitespace
|
|
480
|
+
@scanner.skip(WHITESPACE)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# StringScanner#peek_byte, which strscan only has from Ruby 3.4.
|
|
484
|
+
def next_byte
|
|
485
|
+
@scanner.string.getbyte(@scanner.pos)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# A stray delimiter, ")", ">", "]", "{" or "}", is skipped as viewers do.
|
|
489
|
+
def parse_value
|
|
490
|
+
@scanner.skip(STRAY_DELIMITERS)
|
|
491
|
+
case next_byte
|
|
492
|
+
when 47 then Name.new(value: name_bytes)
|
|
493
|
+
when 40 then parse_literal_string
|
|
494
|
+
when 60
|
|
495
|
+
@scanner.match?(/<</n) ? parse_dictionary : parse_hex_string
|
|
496
|
+
when 91 then parse_array
|
|
497
|
+
when 43, 45, 46, 48..57 then parse_number
|
|
498
|
+
when nil then raise Prawn::Errors::MalformedPDF, "Unexpected end of data"
|
|
499
|
+
else parse_keyword
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def name_bytes
|
|
504
|
+
@scanner.pos += 1 # the slash, which the callers have seen
|
|
505
|
+
name = @scanner.scan(NAME_BODY)
|
|
506
|
+
name.include?("#") ? name.gsub(/#(\h\h)/n) { |escape| escape[1, 2].hex.chr } : name
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def parse_number
|
|
510
|
+
token = @scanner.scan(NUMBER) || raise(Prawn::Errors::MalformedPDF, "Bad number at #{@scanner.pos}")
|
|
511
|
+
return Real.new(source: token) if token.include?(".")
|
|
512
|
+
|
|
513
|
+
integer = token.to_i
|
|
514
|
+
return Ref.new(number: integer, generation: @scanner[1].to_i) if !token.start_with?("+", "-") && @scanner.skip(REFERENCE_TAIL)
|
|
515
|
+
|
|
516
|
+
integer
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def parse_keyword
|
|
520
|
+
keyword = @scanner.scan(KEYWORD)
|
|
521
|
+
case keyword
|
|
522
|
+
when "true" then true
|
|
523
|
+
when "false" then false
|
|
524
|
+
when "null" then nil
|
|
525
|
+
else raise Prawn::Errors::MalformedPDF, "Unexpected #{keyword[0, 20].inspect} at #{@scanner.pos}"
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def parse_dictionary
|
|
530
|
+
@scanner.pos += 2
|
|
531
|
+
nest
|
|
532
|
+
dictionary = {}
|
|
533
|
+
loop do
|
|
534
|
+
skip_whitespace
|
|
535
|
+
break if @scanner.skip(/>>/n)
|
|
536
|
+
raise Prawn::Errors::MalformedPDF, "Unterminated dictionary" if @scanner.eos?
|
|
537
|
+
|
|
538
|
+
unless next_byte == 47
|
|
539
|
+
parse_value # a value without its key: dropped, as viewers do
|
|
540
|
+
next
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
key = name_bytes.to_sym
|
|
544
|
+
skip_whitespace
|
|
545
|
+
break if @scanner.skip(/>>/n)
|
|
546
|
+
|
|
547
|
+
dictionary[key] = parse_value
|
|
548
|
+
end
|
|
549
|
+
dictionary
|
|
550
|
+
ensure
|
|
551
|
+
@nesting_depth -= 1
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def parse_array
|
|
555
|
+
@scanner.pos += 1
|
|
556
|
+
nest
|
|
557
|
+
array = []
|
|
558
|
+
loop do
|
|
559
|
+
skip_whitespace
|
|
560
|
+
break if @scanner.skip(/\]/n)
|
|
561
|
+
raise Prawn::Errors::MalformedPDF, "Unterminated array" if @scanner.eos?
|
|
562
|
+
|
|
563
|
+
array << parse_value
|
|
564
|
+
end
|
|
565
|
+
array
|
|
566
|
+
ensure
|
|
567
|
+
@nesting_depth -= 1
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def nest
|
|
571
|
+
@nesting_depth += 1
|
|
572
|
+
raise LimitExceeded, "Arrays and dictionaries nested deeper than #{MAX_NESTING_DEPTH} levels" if @nesting_depth > MAX_NESTING_DEPTH
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def parse_hex_string
|
|
576
|
+
@scanner.pos += 1
|
|
577
|
+
hex = @scanner.scan(/[^>]*/n).delete("^0-9A-Fa-f")
|
|
578
|
+
@scanner.skip(/>/n)
|
|
579
|
+
ByteString.new([hex].pack("H*"))
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def parse_literal_string
|
|
583
|
+
@scanner.pos += 1
|
|
584
|
+
output = +"".b
|
|
585
|
+
depth = 1
|
|
586
|
+
loop do
|
|
587
|
+
output << @scanner.scan(/[^\\()]*/n)
|
|
588
|
+
case @scanner.getch
|
|
589
|
+
when "("
|
|
590
|
+
depth += 1
|
|
591
|
+
output << "("
|
|
592
|
+
when ")"
|
|
593
|
+
depth -= 1
|
|
594
|
+
break if depth.zero?
|
|
595
|
+
|
|
596
|
+
output << ")"
|
|
597
|
+
when "\\" then output << literal_escape
|
|
598
|
+
when nil then break
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
ByteString.new(output)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
def literal_escape
|
|
605
|
+
if (octal = @scanner.scan(/[0-7]{1,3}/n))
|
|
606
|
+
(octal.to_i(8) & 0xFF).chr
|
|
607
|
+
elsif @scanner.skip(/\r\n|\r|\n/n)
|
|
608
|
+
""
|
|
609
|
+
else
|
|
610
|
+
character = @scanner.getch.to_s
|
|
611
|
+
LITERAL_ESCAPES.fetch(character, character)
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
end
|
|
615
|
+
end
|
|
616
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PDF
|
|
4
|
+
module Core
|
|
5
|
+
# An indirect object: a number, its value and, for a stream, its bytes.
|
|
6
|
+
class Reference
|
|
7
|
+
attr_reader :identifier
|
|
8
|
+
attr_accessor :data, :uncompressed
|
|
9
|
+
attr_writer :stream
|
|
10
|
+
|
|
11
|
+
def initialize(identifier, data, stream = nil)
|
|
12
|
+
@identifier = identifier
|
|
13
|
+
@data = data
|
|
14
|
+
@stream = stream
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_s
|
|
18
|
+
"#{identifier} 0 R"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# A reference becomes a stream once bytes are written to it: prawn-svg appends to ref!({}).stream.
|
|
22
|
+
def stream
|
|
23
|
+
@stream ||= +"".b
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def stream?
|
|
27
|
+
!@stream.nil? && !@stream.empty?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|