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.
Files changed (163) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/COPYING +2 -0
  4. data/GPLv2 +339 -0
  5. data/GPLv3 +674 -0
  6. data/LICENSE +65 -0
  7. data/LICENSE-prawn-svg +21 -0
  8. data/README.md +148 -0
  9. data/data/fonts/Courier-Bold.afm +342 -0
  10. data/data/fonts/Courier-BoldOblique.afm +342 -0
  11. data/data/fonts/Courier-Oblique.afm +342 -0
  12. data/data/fonts/Courier.afm +342 -0
  13. data/data/fonts/Helvetica-Bold.afm +2827 -0
  14. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  15. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  16. data/data/fonts/Helvetica.afm +3051 -0
  17. data/data/fonts/MustRead.html +19 -0
  18. data/data/fonts/Symbol.afm +213 -0
  19. data/data/fonts/Times-Bold.afm +2588 -0
  20. data/data/fonts/Times-BoldItalic.afm +2384 -0
  21. data/data/fonts/Times-Italic.afm +2667 -0
  22. data/data/fonts/Times-Roman.afm +2419 -0
  23. data/data/fonts/ZapfDingbats.afm +225 -0
  24. data/lib/gamba/annotations.rb +21 -0
  25. data/lib/gamba/combine.rb +307 -0
  26. data/lib/gamba/document/bounding_box.rb +193 -0
  27. data/lib/gamba/document/column_box.rb +91 -0
  28. data/lib/gamba/document/internals.rb +264 -0
  29. data/lib/gamba/document/span.rb +25 -0
  30. data/lib/gamba/document.rb +268 -0
  31. data/lib/gamba/encoding.rb +13 -0
  32. data/lib/gamba/errors.rb +39 -0
  33. data/lib/gamba/font.rb +228 -0
  34. data/lib/gamba/fonts/afm.rb +171 -0
  35. data/lib/gamba/fonts/otf.rb +50 -0
  36. data/lib/gamba/fonts/ttf.rb +413 -0
  37. data/lib/gamba/graphic_state.rb +84 -0
  38. data/lib/gamba/graphics/color.rb +174 -0
  39. data/lib/gamba/graphics/patterns.rb +100 -0
  40. data/lib/gamba/graphics/style.rb +159 -0
  41. data/lib/gamba/graphics.rb +212 -0
  42. data/lib/gamba/grid.rb +216 -0
  43. data/lib/gamba/images/image.rb +271 -0
  44. data/lib/gamba/images/png_decoder.rb +65 -0
  45. data/lib/gamba/images.rb +115 -0
  46. data/lib/gamba/measurement_extensions.rb +40 -0
  47. data/lib/gamba/measurements.rb +62 -0
  48. data/lib/gamba/outline.rb +148 -0
  49. data/lib/gamba/page.rb +125 -0
  50. data/lib/gamba/page_geometry.rb +27 -0
  51. data/lib/gamba/pdf/decryption.rb +165 -0
  52. data/lib/gamba/pdf/outline_node.rb +53 -0
  53. data/lib/gamba/pdf/reader.rb +616 -0
  54. data/lib/gamba/pdf/reference.rb +31 -0
  55. data/lib/gamba/pdf/writer.rb +105 -0
  56. data/lib/gamba/pdf.rb +87 -0
  57. data/lib/gamba/pdf_a/srgb_profile.rb +61 -0
  58. data/lib/gamba/pdf_a.rb +132 -0
  59. data/lib/gamba/repeater.rb +79 -0
  60. data/lib/gamba/security.rb +111 -0
  61. data/lib/gamba/stamp.rb +76 -0
  62. data/lib/gamba/table/cell/kinds.rb +246 -0
  63. data/lib/gamba/table/cell.rb +331 -0
  64. data/lib/gamba/table/cells.rb +208 -0
  65. data/lib/gamba/table.rb +298 -0
  66. data/lib/gamba/text/box.rb +16 -0
  67. data/lib/gamba/text/formatted/arranger.rb +217 -0
  68. data/lib/gamba/text/formatted/box.rb +312 -0
  69. data/lib/gamba/text/formatted/fragment.rb +205 -0
  70. data/lib/gamba/text/formatted/line_wrap.rb +205 -0
  71. data/lib/gamba/text/formatted/parser.rb +150 -0
  72. data/lib/gamba/text/formatted/wrap.rb +103 -0
  73. data/lib/gamba/text.rb +310 -0
  74. data/lib/gamba/transformation_stack.rb +43 -0
  75. data/lib/gamba/true_type/cff_subset.rb +157 -0
  76. data/lib/gamba/true_type/file.rb +372 -0
  77. data/lib/gamba/true_type/open_type_layout.rb +189 -0
  78. data/lib/gamba/true_type/simple_subsets.rb +108 -0
  79. data/lib/gamba/true_type/subset.rb +206 -0
  80. data/lib/gamba/version.rb +10 -0
  81. data/lib/gamba/view.rb +28 -0
  82. data/lib/gamba.rb +124 -0
  83. data/lib/pdf/core.rb +4 -0
  84. data/lib/prawn/measurement_extensions.rb +4 -0
  85. data/lib/prawn/svg/attributes/clip_path.rb +46 -0
  86. data/lib/prawn/svg/attributes/mask.rb +16 -0
  87. data/lib/prawn/svg/attributes/opacity.rb +30 -0
  88. data/lib/prawn/svg/attributes/space.rb +10 -0
  89. data/lib/prawn/svg/attributes/stroke.rb +50 -0
  90. data/lib/prawn/svg/attributes/transform.rb +10 -0
  91. data/lib/prawn/svg/attributes.rb +6 -0
  92. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
  93. data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
  94. data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
  95. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  96. data/lib/prawn/svg/calculators/pixels.rb +57 -0
  97. data/lib/prawn/svg/color.rb +240 -0
  98. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  99. data/lib/prawn/svg/css/font_family_parser.rb +39 -0
  100. data/lib/prawn/svg/css/font_parser.rb +46 -0
  101. data/lib/prawn/svg/css/selector_parser.rb +180 -0
  102. data/lib/prawn/svg/css/stylesheets.rb +233 -0
  103. data/lib/prawn/svg/css/values_parser.rb +68 -0
  104. data/lib/prawn/svg/document.rb +169 -0
  105. data/lib/prawn/svg/elements/anchor.rb +10 -0
  106. data/lib/prawn/svg/elements/base.rb +315 -0
  107. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  108. data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
  109. data/lib/prawn/svg/elements/circle.rb +19 -0
  110. data/lib/prawn/svg/elements/clip_path.rb +26 -0
  111. data/lib/prawn/svg/elements/container.rb +10 -0
  112. data/lib/prawn/svg/elements/direct_render_base.rb +28 -0
  113. data/lib/prawn/svg/elements/ellipse.rb +20 -0
  114. data/lib/prawn/svg/elements/gradient.rb +183 -0
  115. data/lib/prawn/svg/elements/ignored.rb +5 -0
  116. data/lib/prawn/svg/elements/image.rb +178 -0
  117. data/lib/prawn/svg/elements/line.rb +27 -0
  118. data/lib/prawn/svg/elements/marker.rb +72 -0
  119. data/lib/prawn/svg/elements/mask.rb +65 -0
  120. data/lib/prawn/svg/elements/path.rb +294 -0
  121. data/lib/prawn/svg/elements/pattern.rb +150 -0
  122. data/lib/prawn/svg/elements/polygon.rb +25 -0
  123. data/lib/prawn/svg/elements/polyline.rb +23 -0
  124. data/lib/prawn/svg/elements/rect.rb +64 -0
  125. data/lib/prawn/svg/elements/root.rb +22 -0
  126. data/lib/prawn/svg/elements/switch.rb +92 -0
  127. data/lib/prawn/svg/elements/text.rb +99 -0
  128. data/lib/prawn/svg/elements/text_component.rb +352 -0
  129. data/lib/prawn/svg/elements/text_node.rb +308 -0
  130. data/lib/prawn/svg/elements/text_path.rb +153 -0
  131. data/lib/prawn/svg/elements/use.rb +142 -0
  132. data/lib/prawn/svg/elements/viewport.rb +29 -0
  133. data/lib/prawn/svg/elements.rb +46 -0
  134. data/lib/prawn/svg/extension.rb +25 -0
  135. data/lib/prawn/svg/font.rb +20 -0
  136. data/lib/prawn/svg/font_metrics.rb +144 -0
  137. data/lib/prawn/svg/font_registry.rb +315 -0
  138. data/lib/prawn/svg/funciri.rb +14 -0
  139. data/lib/prawn/svg/gradient_renderer.rb +332 -0
  140. data/lib/prawn/svg/gradients.rb +53 -0
  141. data/lib/prawn/svg/interface.rb +72 -0
  142. data/lib/prawn/svg/length.rb +43 -0
  143. data/lib/prawn/svg/link_renderer.rb +40 -0
  144. data/lib/prawn/svg/loaders/data.rb +17 -0
  145. data/lib/prawn/svg/loaders/file.rb +111 -0
  146. data/lib/prawn/svg/loaders/web.rb +24 -0
  147. data/lib/prawn/svg/paint.rb +67 -0
  148. data/lib/prawn/svg/pathable.rb +153 -0
  149. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  150. data/lib/prawn/svg/pdfmatrix.rb +39 -0
  151. data/lib/prawn/svg/percentage.rb +24 -0
  152. data/lib/prawn/svg/properties.rb +329 -0
  153. data/lib/prawn/svg/renderer.rb +249 -0
  154. data/lib/prawn/svg/state.rb +24 -0
  155. data/lib/prawn/svg/transform_parser.rb +78 -0
  156. data/lib/prawn/svg/ttc.rb +35 -0
  157. data/lib/prawn/svg/ttf.rb +95 -0
  158. data/lib/prawn/svg/url_loader.rb +46 -0
  159. data/lib/prawn/svg/version.rb +5 -0
  160. data/lib/prawn/table.rb +4 -0
  161. data/lib/prawn-svg.rb +52 -0
  162. data/lib/prawn.rb +3 -0
  163. 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