pdfrb 0.8.5 → 0.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04a819d2ed2b54eb4ca8c0ac17ceb14cc1c45a79e359bb51c6b5194e2fdb7025
4
- data.tar.gz: bcb6a670c04b7e68b20b90f8ac344ac0672a76f1fd3e16306ac46b4c44f728ec
3
+ metadata.gz: b0edf5439bfe359af4b900aeb846047b168d741271eb78a7f78315ca5b0bba55
4
+ data.tar.gz: 0b30e0edb04dcc2a4d26a493ba782379a8a864904eafc72f07c0c49ca4a25e57
5
5
  SHA512:
6
- metadata.gz: f8eb286788ae52128019b39b18006776c9c9227ab92e76b0a72add85eb92b16daa390704d243627a0f79fcd8b9c0195a606f7f6878953db634ff1c9ad257b0f4
7
- data.tar.gz: 1940d86297e8e20ae06d33b23bd7b36815d5249fce28eb120262ab33e5350668fa015247444ed905ac1ba5b73295fa092b74d6e477c9fd74a507252a78cb110f
6
+ metadata.gz: bd9802dee13e72edb052c80bd4fea095eade14c95b0fe8cb31277c0a9c36de9f9c202d99bbc3a485911dc5035d6253589b3f50358f0d2b789f4756c19d211e72
7
+ data.tar.gz: c44636603bba32db00de362646b378238fcb884008b085627c1e19d66e899472d9c1a2539ffffd3eb076a58788afb3abec2eaecfb9b44274960e025d31c7ed34
data/lib/pdfrb/cli.rb CHANGED
@@ -334,10 +334,16 @@ module Pdfrb
334
334
 
335
335
  # Set up the handler so Writer can encrypt per-object.
336
336
  handler = Pdfrb::Encryption::StandardSecurityHandler.new(
337
- Encrypt: { V: v, R: r, Length: length, P: perms, O: o_entry, U: u_entry },
338
- ID: [id0.b, id0.b]
337
+ {
338
+ Encrypt: { V: v, R: r, Length: length, P: perms,
339
+ O: o_entry, U: u_entry },
340
+ ID: [id0.b, id0.b],
341
+ }
339
342
  )
340
- handler.verify_user_password(user_pw)
343
+ unless handler.verify_user_password?(user_pw)
344
+ raise Pdfrb::EncryptionError, "failed to derive encryption key"
345
+ end
346
+
341
347
  doc.config["encryption.handler"] = handler
342
348
  doc.config["encryption.password"] = user_pw
343
349
  end
@@ -96,8 +96,7 @@ module Pdfrb
96
96
 
97
97
  def inventory_fonts(doc)
98
98
  fonts = Set.new
99
- doc.each_indirect_object do |obj|
100
- next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary)
99
+ doc.each_indirect_of(Pdfrb::Model::Cos::Dictionary) do |obj|
101
100
  next unless obj[:Type] == :Font
102
101
 
103
102
  name = obj[:BaseFont] || obj[:Name] || "Unknown"
@@ -117,8 +116,7 @@ module Pdfrb
117
116
 
118
117
  def count_images(doc)
119
118
  count = 0
120
- doc.each_indirect_object do |obj|
121
- next unless obj.is_a?(Pdfrb::Model::Cos::Stream)
119
+ doc.each_indirect_of(Pdfrb::Model::Cos::Stream) do |obj|
122
120
  next unless obj[:Subtype] == :Image
123
121
 
124
122
  count += 1
@@ -31,8 +31,7 @@ module Pdfrb
31
31
  id: "6.1-4", description: "JavaScript prohibited",
32
32
  severity: :error, spec_clause: "ISO 19005-1 6.1",
33
33
  check: ->(doc) {
34
- doc.each_indirect_object do |obj|
35
- next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary)
34
+ doc.each_indirect_of(Pdfrb::Model::Cos::Dictionary) do |obj|
36
35
  if obj.value[:S] == :JavaScript
37
36
  return Violation.new(rule_id: "6.1-4", message: "PDF/A prohibits JavaScript actions",
38
37
  object: "JavaScript action", severity: :error,
@@ -201,8 +200,7 @@ module Pdfrb
201
200
  id: "a1-1", description: "JPEG2000 not allowed in A-1",
202
201
  severity: :error, spec_clause: "ISO 19005-1 6.2.4",
203
202
  check: ->(doc) {
204
- doc.each_indirect_object do |obj|
205
- next unless obj.is_a?(Pdfrb::Model::Cos::Stream)
203
+ doc.each_indirect_of(Pdfrb::Model::Cos::Stream) do |obj|
206
204
  next unless obj[:Filter] == :JPXDecode
207
205
 
208
206
  return Violation.new(rule_id: "a1-1",
@@ -217,8 +215,7 @@ module Pdfrb
217
215
  id: "a1-2", description: "Object streams not allowed in A-1",
218
216
  severity: :error, spec_clause: "ISO 19005-1 6.2.4",
219
217
  check: ->(doc) {
220
- doc.each_indirect_object do |obj|
221
- next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary)
218
+ doc.each_indirect_of(Pdfrb::Model::Cos::Dictionary) do |obj|
222
219
  next unless obj.value[:Type] == :ObjStm
223
220
 
224
221
  return Violation.new(rule_id: "a1-2",
@@ -247,9 +244,7 @@ module Pdfrb
247
244
  id: "a1-4", description: "LZWDecode not allowed in A-1",
248
245
  severity: :error, spec_clause: "ISO 19005-1 6.2.4",
249
246
  check: ->(doc) {
250
- doc.each_indirect_object do |obj|
251
- next unless obj.is_a?(Pdfrb::Model::Cos::Stream)
252
-
247
+ doc.each_indirect_of(Pdfrb::Model::Cos::Stream) do |obj|
253
248
  f = obj[:Filter]
254
249
  fs = f.is_a?(::Array) ? f : [f]
255
250
  next unless fs.include?(:LZWDecode)
@@ -269,8 +264,7 @@ module Pdfrb
269
264
  id: "a2-1", description: "PostScript XObjects prohibited",
270
265
  severity: :warning, spec_clause: "ISO 19005-2 6.2.4",
271
266
  check: ->(doc) {
272
- doc.each_indirect_object do |obj|
273
- next unless obj.is_a?(Pdfrb::Model::Cos::Stream)
267
+ doc.each_indirect_of(Pdfrb::Model::Cos::Stream) do |obj|
274
268
  next unless [:PS, :PSXObject].include?(obj[:Subtype])
275
269
 
276
270
  return Violation.new(rule_id: "a2-1",
@@ -285,8 +279,7 @@ module Pdfrb
285
279
  id: "a2-2", description: "Embedded files not allowed in A-2",
286
280
  severity: :error, spec_clause: "ISO 19005-2 6.2",
287
281
  check: ->(doc) {
288
- doc.each_indirect_object do |obj|
289
- next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary)
282
+ doc.each_indirect_of(Pdfrb::Model::Cos::Dictionary) do |obj|
290
283
  next unless obj.value[:Type] == :EmbeddedFile
291
284
 
292
285
  return Violation.new(rule_id: "a2-2",
@@ -316,8 +309,7 @@ module Pdfrb
316
309
  severity: :error, spec_clause: "ISO 19005-3 6.2",
317
310
  check: ->(doc) {
318
311
  vs = []
319
- doc.each_indirect_object do |obj|
320
- next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary)
312
+ doc.each_indirect_of(Pdfrb::Model::Cos::Dictionary) do |obj|
321
313
  next unless obj.value[:Type] == :Filespec
322
314
  next if obj.value[:AFRelationship]
323
315
 
@@ -348,8 +340,7 @@ module Pdfrb
348
340
  severity: :error, spec_clause: "ISO 19005-4 6.2",
349
341
  check: ->(doc) {
350
342
  vs = []
351
- doc.each_indirect_object do |obj|
352
- next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary)
343
+ doc.each_indirect_of(Pdfrb::Model::Cos::Dictionary) do |obj|
353
344
  next unless obj.value[:Type] == :Filespec
354
345
  next if obj.value[:AFRelationship]
355
346
 
@@ -366,8 +357,7 @@ module Pdfrb
366
357
  severity: :error, spec_clause: "ISO 19005-4 6.2.3",
367
358
  check: ->(doc) {
368
359
  vs = []
369
- doc.each_indirect_object do |annot|
370
- next unless annot.is_a?(Pdfrb::Model::Cos::Dictionary)
360
+ doc.each_indirect_of(Pdfrb::Model::Cos::Dictionary) do |annot|
371
361
  next unless annot.value[:Type] == :Annot
372
362
 
373
363
  st = annot.value[:Subtype]
@@ -150,9 +150,7 @@ module Pdfrb
150
150
  spec_clause: "ISO 15930 §6.5",
151
151
  check: ->(doc) {
152
152
  violations = []
153
- doc.each_indirect_object do |obj|
154
- next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary)
155
-
153
+ doc.each_indirect_of(Pdfrb::Model::Cos::Dictionary) do |obj|
156
154
  s = obj[:S]
157
155
  next unless [:JavaScript, :JS, :Launch, :URI].include?(s)
158
156
 
@@ -231,7 +229,7 @@ module Pdfrb
231
229
  spec_clause: "ISO 15930-11 6.1",
232
230
  check: ->(doc) {
233
231
  v = doc.version.to_s
234
- next nil if compare_versions(v, "2.0") >= 0
232
+ next nil if Pdfrb::PdfVersion.compare(v, "2.0") >= 0
235
233
 
236
234
  Violation.new(
237
235
  rule_id: "x6-1",
@@ -280,16 +278,8 @@ module Pdfrb
280
278
  rs.register(X6_OUTPUT_INTENT_RULE)
281
279
  end
282
280
 
283
- def compare_versions(a, b)
284
- aa = a.to_s.split(".").map(&:to_i)
285
- bb = b.to_s.split(".").map(&:to_i)
286
- (aa <=> bb) || 0
287
- end
288
-
289
281
  def scan_color_spaces(doc, &block)
290
- doc.each_indirect_object do |obj|
291
- next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary)
292
-
282
+ doc.each_indirect_of(Pdfrb::Model::Cos::Dictionary) do |obj|
293
283
  cs = obj.value[:ColorSpace]
294
284
  next unless cs
295
285
 
@@ -43,13 +43,11 @@ module Pdfrb
43
43
  end
44
44
 
45
45
  def concat(a, b, c, d, e, f, &)
46
- emit_op Pdfrb::Content::Operator::ConcatMatrix, a, b, c, d, e, f
47
- return self unless block_given?
46
+ return emit_op(Pdfrb::Content::Operator::ConcatMatrix, a, b, c, d, e, f) unless block_given?
48
47
 
49
- begin
48
+ save_graphics_state do
49
+ emit_op(Pdfrb::Content::Operator::ConcatMatrix, a, b, c, d, e, f)
50
50
  yield self
51
- ensure
52
- emit_op Pdfrb::Content::Operator::SaveGraphicsState
53
51
  end
54
52
  self
55
53
  end
@@ -282,41 +280,28 @@ module Pdfrb
282
280
  create_ext_g_state(BM: mode.to_s))
283
281
  end
284
282
 
285
- def with_transparency(opacity: 1.0, blend_mode: nil)
286
- save_graphics_state
287
- self.opacity = opacity if opacity < 1.0
288
- self.blend_mode = blend_mode if blend_mode
289
-
290
- begin
283
+ def with_transparency(opacity: 1.0, blend_mode: nil, &)
284
+ save_graphics_state do
285
+ self.opacity = opacity if opacity < 1.0
286
+ self.blend_mode = blend_mode if blend_mode
291
287
  yield self
292
- ensure
293
- emit_op(Pdfrb::Content::Operator::RestoreGraphicsState)
294
288
  end
295
289
  self
296
290
  end
297
291
 
298
292
  def draw_image(name, at: nil, width: nil, height: nil, matrix: nil)
299
- @used_xobjects[name] = true
300
- save_graphics_state do
301
- if matrix
302
- a, b, c, d, e, f = matrix
303
- concat(a, b, c, d, e, f)
304
- emit_op(Pdfrb::Content::Operator::InvokeXObject, name)
305
- else
306
- translate(at[0], at[1])
307
- concat(width, 0, 0, height, 0, 0)
308
- append(" /#{name} Do\n")
309
- end
293
+ if matrix
294
+ a, b, c, d, e, f = matrix
295
+ invoke_xobject(name, matrix: [a, b, c, d, e, f])
296
+ else
297
+ invoke_xobject(name, matrix: [width, 0, 0, height, 0, 0],
298
+ pre_translate: at)
310
299
  end
311
300
  self
312
301
  end
313
302
 
314
303
  def draw_image_matrix(name, a:, b:, c:, d:, e:, f:)
315
- @used_xobjects[name] = true
316
- save_graphics_state do
317
- concat(a, b, c, d, e, f)
318
- emit_op(Pdfrb::Content::Operator::InvokeXObject, name)
319
- end
304
+ invoke_xobject(name, matrix: [a, b, c, d, e, f])
320
305
  self
321
306
  end
322
307
 
@@ -327,13 +312,20 @@ module Pdfrb
327
312
  # @param at [Array<Numeric>] x, y translation.
328
313
  # @param matrix [Array<Numeric>, nil] 6-element transform matrix.
329
314
  def draw_form_xobject(name, at: [0, 0], matrix: nil)
315
+ translate = matrix.nil? ? at : nil
316
+ invoke_xobject(name, matrix: matrix, pre_translate: translate)
317
+ self
318
+ end
319
+
320
+ # Invoke an XObject (/Do) inside a saved graphics state, applying
321
+ # an optional translation then a 6-element transform matrix.
322
+ def invoke_xobject(name, matrix: nil, pre_translate: nil)
330
323
  @used_xobjects[name] = true
331
324
  save_graphics_state do
325
+ translate(pre_translate[0], pre_translate[1]) if pre_translate
332
326
  if matrix
333
327
  a, b, c, d, e, f = matrix
334
- concat(a, b, c, d, e, f)
335
- elsif at
336
- translate(at[0], at[1])
328
+ emit_op(Pdfrb::Content::Operator::ConcatMatrix, a, b, c, d, e, f)
337
329
  end
338
330
  emit_op(Pdfrb::Content::Operator::InvokeXObject, name)
339
331
  end
@@ -55,8 +55,7 @@ module Pdfrb
55
55
  end
56
56
 
57
57
  def find_page(stream_obj)
58
- @document.each_indirect_object do |obj|
59
- next unless obj.is_a?(Pdfrb::Model::Cos::Dictionary)
58
+ @document.each_indirect_of(Pdfrb::Model::Cos::Dictionary) do |obj|
60
59
  next unless obj[:Type] == :Page
61
60
 
62
61
  contents = obj[:Contents]
@@ -13,16 +13,17 @@ module Pdfrb
13
13
  autoload :TimestampHandler, "pdfrb/digital_signature/timestamp_handler"
14
14
  autoload :TimestampClient, "pdfrb/digital_signature/timestamp_client"
15
15
 
16
- HANDLERS = {
17
- "adbe.pkcs7.detached": CmsHandler,
18
- "adbe.pkcs7.sha1": CmsHandler,
19
- "adbe.revision": TimestampHandler,
20
- }.freeze
21
-
22
16
  module_function
23
17
 
18
+ # Resolve the handler class for +sub_filter+. Constant references
19
+ # live inside the case so the autoloads above defer loading the
20
+ # handlers (and their OpenSSL surface) until an actual
21
+ # sign/verify call needs them.
24
22
  def handler_for(sub_filter)
25
- HANDLERS[sub_filter.to_sym]
23
+ case sub_filter.to_sym
24
+ when :"adbe.pkcs7.detached", :"adbe.pkcs7.sha1" then CmsHandler
25
+ when :"adbe.revision" then TimestampHandler
26
+ end
26
27
  end
27
28
 
28
29
  def sign(document, cert:, key:, **)
@@ -282,6 +282,16 @@ module Pdfrb
282
282
  end
283
283
  end
284
284
 
285
+ # Yield every indirect object of +klass+ (e.g. all Streams, all
286
+ # Dictionaries). Enumerator when blockless.
287
+ def each_indirect_of(klass)
288
+ return enum_for(:each_indirect_of, klass) unless block_given?
289
+
290
+ each_indirect_object do |obj|
291
+ yield obj if obj.is_a?(klass)
292
+ end
293
+ end
294
+
285
295
  # Walk every revision in the document's incremental-update chain,
286
296
  # from the latest (most recent) revision backward via /Prev.
287
297
  # Each revision yields a (revision_index, xref, trailer) tuple
@@ -30,7 +30,7 @@ module Pdfrb
30
30
 
31
31
  def initialize(document)
32
32
  @document = document
33
- @serializer = Pdfrb::Serializer.new
33
+ @serializer = Pdfrb::Writer.serializer_for(document)
34
34
  end
35
35
 
36
36
  # @param io [IO] target output.
@@ -70,7 +70,8 @@ module Pdfrb
70
70
 
71
71
  ordered.each do |obj|
72
72
  offsets[obj.oid] = temp.pos
73
- temp << @serializer.serialize_indirect(obj)
73
+ temp << @serializer.serialize_indirect(obj,
74
+ skip_string_encryption: encryption_dict?(obj))
74
75
  end
75
76
  first_page_end = temp.pos if first_page_objects.any?
76
77
 
@@ -79,7 +80,6 @@ module Pdfrb
79
80
  hint_compressed = ::Zlib::Deflate.deflate(hint_data)
80
81
  hint_offset = temp.pos
81
82
  hint_dict = @serializer.serialize(
82
- Type: :XRef,
83
83
  S: HintStream::DEFAULT_ITEM_BITS,
84
84
  Filter: :FlateDecode,
85
85
  Length: hint_compressed.bytesize
@@ -132,7 +132,8 @@ module Pdfrb
132
132
  buf << lin_padded
133
133
 
134
134
  layout.ordered_objects.each do |obj|
135
- buf << @serializer.serialize_indirect(obj)
135
+ buf << @serializer.serialize_indirect(obj,
136
+ skip_string_encryption: encryption_dict?(obj))
136
137
  end
137
138
 
138
139
  buf << "#{layout.hint_oid} 0 obj\n"
@@ -243,7 +244,16 @@ module Pdfrb
243
244
  end
244
245
  size = [@document.next_oid || 1, 2].max
245
246
 
246
- trailer_str = @serializer.serialize(Size: size, Root: root_ref)
247
+ trailer_hash = {}
248
+ (@document.trailer || {}).each do |k, v|
249
+ next if Pdfrb::Writer::TRAILER_OVERRIDDEN_KEYS.include?(k)
250
+
251
+ trailer_hash[k] = v
252
+ end
253
+ trailer_hash[:Size] = size
254
+ trailer_hash[:Root] = root_ref if root_ref
255
+
256
+ trailer_str = @serializer.serialize(trailer_hash)
247
257
  "trailer\n#{trailer_str}\nstartxref\n#{xref_offset}\n%%EOF\n"
248
258
  end
249
259
 
@@ -251,6 +261,11 @@ module Pdfrb
251
261
  "%PDF-1.7\n%\xE2\xE3\xCF\xD3\n"
252
262
  end
253
263
 
264
+ # The /Encrypt dictionary's own strings must stay in cleartext.
265
+ def encryption_dict?(obj)
266
+ (@document.trailer || {})[:Encrypt]&.oid == obj.oid
267
+ end
268
+
254
269
  def write_non_linearized(io)
255
270
  Pdfrb::Writer.write(@document, io)
256
271
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pdfrb
4
+ # PDF header version comparisons (ISO 32000-2, s7.2). Version strings
5
+ # are dot-separated numerics ("1.4", "2.0"); comparison is
6
+ # numeric-per-component, never lexical ("1.10" > "1.9").
7
+ module PdfVersion
8
+ module_function
9
+
10
+ # @return [Integer] -1, 0, or 1 as +a+ is earlier, equal, or later
11
+ # than +b+.
12
+ def compare(a, b)
13
+ aa = a.to_s.split(".").map(&:to_i)
14
+ bb = b.to_s.split(".").map(&:to_i)
15
+ (aa <=> bb) || 0
16
+ end
17
+
18
+ # @return [Boolean] whether +version+ meets or exceeds +minimum+.
19
+ def at_least?(version, minimum)
20
+ compare(version, minimum) >= 0
21
+ end
22
+ end
23
+ end
@@ -50,21 +50,28 @@ module Pdfrb
50
50
  end
51
51
 
52
52
  # Serialise an indirect object's body, including `oid gen obj`
53
- # header and (for streams) the stream payload.
54
- def serialize_indirect(obj)
53
+ # header and (for streams) the stream payload. When an encrypter
54
+ # is set, all strings in the object's dictionaries and the stream
55
+ # payload are encrypted with the per-object key (s7.6.1) — pass
56
+ # +skip_string_encryption:+ for the /Encrypt dictionary itself,
57
+ # whose entries must stay readable.
58
+ def serialize_indirect(obj, skip_string_encryption: false)
55
59
  raise ArgumentError, "not indirect: #{obj.inspect}" unless obj.indirect?
56
60
 
61
+ encrypt_strings = !skip_string_encryption && encrypter
57
62
  buffer = +""
58
63
  buffer << "#{obj.oid} #{obj.gen} obj\n"
59
64
  case obj
60
65
  when Pdfrb::Model::Cos::Stream
61
- dict, payload = emit_stream(obj)
66
+ dict_value = encrypt_strings ? encrypt_value_strings(obj.value, obj.oid, obj.gen) : obj.value
67
+ dict, payload = emit_stream(obj, dict_value)
62
68
  buffer << dict
63
69
  buffer << "\nstream\n"
64
70
  buffer << (encrypter ? encrypter.encrypt(payload, obj.oid, obj.gen) : payload)
65
71
  buffer << "\nendstream\n"
66
72
  else
67
- buffer << serialize(obj)
73
+ value = encrypt_strings ? encrypt_value_strings(obj.value, obj.oid, obj.gen) : obj.value
74
+ buffer << serialize(value)
68
75
  buffer << "\n"
69
76
  end
70
77
  buffer << "endobj\n"
@@ -73,12 +80,39 @@ module Pdfrb
73
80
 
74
81
  private
75
82
 
83
+ # Deep-transform +value+ so every literal string carries the
84
+ # per-object ciphertext (s7.6.1: strings AND streams encrypt).
85
+ def encrypt_value_strings(value, oid, gen)
86
+ case value
87
+ when ::Hash
88
+ value.each_with_object({}) do |(k, v), h|
89
+ h[k] = encrypt_value_strings(v, oid, gen)
90
+ end
91
+ when ::Array
92
+ value.map { |v| encrypt_value_strings(v, oid, gen) }
93
+ when Pdfrb::Model::PdfArray
94
+ Pdfrb::Model::PdfArray.new(value.map { |v| encrypt_value_strings(v, oid, gen) })
95
+ when Pdfrb::Model::Object
96
+ Pdfrb::Model::Object.new(encrypt_value_strings(value.value, oid, gen),
97
+ oid: value.oid, gen: value.gen)
98
+ when ::String
99
+ bytes = if value.encoding == Encoding::UTF_8
100
+ Pdfrb::Model::Cos::StringEncoding.encode_text(value)
101
+ else
102
+ value.dup.force_encoding(Encoding::BINARY)
103
+ end
104
+ encrypter.encrypt(bytes, oid, gen)
105
+ else
106
+ value
107
+ end
108
+ end
109
+
76
110
  # Build the dict body + payload for a stream object, applying
77
111
  # FlateDecode compression when enabled and the stream is large
78
112
  # enough to benefit. Returns [dict_bytes, payload_bytes].
79
- def emit_stream(stream_obj)
113
+ def emit_stream(stream_obj, dict_value = stream_obj.value)
80
114
  payload = stream_obj.stream
81
- hash = stream_obj.value.dup
115
+ hash = dict_value.dup
82
116
  existing_filter = hash[:Filter]
83
117
 
84
118
  if should_compress?(stream_obj, payload, existing_filter)
@@ -55,9 +55,7 @@ module Pdfrb
55
55
  # {duplicate_oid => canonical_oid} that was applied.
56
56
  def dedup_streams!(document)
57
57
  canonical_for = {}
58
- document.each_indirect_object do |obj|
59
- next unless obj.is_a?(Pdfrb::Model::Cos::Stream)
60
- next unless obj.indirect?
58
+ document.each_indirect_of(Pdfrb::Model::Cos::Stream) do |obj|
61
59
  next if obj.value[:Type] == :ObjStm
62
60
 
63
61
  key = stream_dedup_key(obj)
@@ -65,9 +63,7 @@ module Pdfrb
65
63
  end
66
64
 
67
65
  replacements = {}
68
- document.each_indirect_object do |obj|
69
- next unless obj.is_a?(Pdfrb::Model::Cos::Stream)
70
- next unless obj.indirect?
66
+ document.each_indirect_of(Pdfrb::Model::Cos::Stream) do |obj|
71
67
  next if obj.value[:Type] == :ObjStm
72
68
 
73
69
  key = stream_dedup_key(obj)
data/lib/pdfrb/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pdfrb
4
- VERSION = "0.8.5"
4
+ VERSION = "0.8.7"
5
5
  end
data/lib/pdfrb/writer.rb CHANGED
@@ -17,24 +17,37 @@ module Pdfrb
17
17
  class Writer
18
18
  DEFAULT_VERSION = "1.4"
19
19
 
20
+ # Trailer keys each writer recomputes itself; existing values
21
+ # for these are never carried through from the parsed trailer.
22
+ TRAILER_OVERRIDDEN_KEYS = %i[Size Root Prev XRefStm].freeze
23
+
20
24
  attr_reader :document, :io, :serializer
21
25
 
22
26
  def initialize(document, io)
23
27
  @document = document
24
28
  @io = io
25
- @serializer = Serializer.new(
29
+ @serializer = Writer.serializer_for(document)
30
+ @xref_offsets = {}
31
+ end
32
+
33
+ # Build a Serializer configured for +document+: stream compression
34
+ # from config, and an encrypter derived from the trailer /Encrypt.
35
+ # Raises EncryptionError when the document is encrypted but the
36
+ # configured password does not open it — writing must never
37
+ # silently fall back to plaintext.
38
+ def self.serializer_for(document)
39
+ Serializer.new(
26
40
  compress_streams: document.config["writer.compress_streams"],
27
41
  compress_min_size: document.config["writer.compress_min_size"],
28
- **serializer_encrypter_opts
42
+ **serializer_encrypter_opts_for(document)
29
43
  )
30
- @xref_offsets = {}
31
44
  end
32
45
 
33
46
  # Build the encrypter option for the Serializer from the document's
34
47
  # /Encrypt dict. If the document isn't encrypted, returns empty hash.
35
48
  # The SecurityHandler's encrypt_data method becomes the Serializer's
36
49
  # encrypter, which encrypts string/stream payloads per-object.
37
- def serializer_encrypter_opts
50
+ def self.serializer_encrypter_opts_for(document)
38
51
  # Use a pre-configured handler if the document provides one
39
52
  # (e.g., set by the encryption CLI).
40
53
  handler = document.config["encryption.handler"]
@@ -50,15 +63,21 @@ module Pdfrb
50
63
  return {} unless encrypt_dict
51
64
 
52
65
  handler = Pdfrb::Encryption::StandardSecurityHandler.new(
53
- Encrypt: encrypt_dict.value,
54
- ID: trailer[:ID]
66
+ { Encrypt: encrypt_dict.value, ID: trailer[:ID] }
55
67
  )
56
68
  password = document.config["encryption.password"] || ""
57
- handler.verify_user_password(password)
69
+ unless handler.verify_user_password?(password)
70
+ raise Pdfrb::EncryptionError,
71
+ "cannot write encrypted document: password does not verify"
72
+ end
58
73
 
59
74
  { encrypter: handler }
60
- rescue StandardError
61
- {}
75
+ end
76
+
77
+ # The /Encrypt dictionary's own strings (U, O, UE, OE, Perms) must
78
+ # stay in cleartext — they carry the password hashes.
79
+ def encryption_dict?(obj)
80
+ (document.trailer || {})[:Encrypt]&.oid == obj.oid
62
81
  end
63
82
 
64
83
  def self.write(document, io)
@@ -84,7 +103,8 @@ module Pdfrb
84
103
  next if packed.key?(obj.oid)
85
104
 
86
105
  @xref_offsets[obj.oid] = @io.pos
87
- @io << @serializer.serialize_indirect(obj)
106
+ @io << @serializer.serialize_indirect(obj,
107
+ skip_string_encryption: encryption_dict?(obj))
88
108
  end
89
109
 
90
110
  xref_pos = if use_stream
@@ -118,7 +138,8 @@ module Pdfrb
118
138
  next unless obj.indirect?
119
139
 
120
140
  @xref_offsets[obj.oid] = @io.pos
121
- @io << @serializer.serialize_indirect(obj)
141
+ @io << @serializer.serialize_indirect(obj,
142
+ skip_string_encryption: encryption_dict?(obj))
122
143
  end
123
144
  xref_pos = write_xref(prev: previous_xref_offset)
124
145
  write_trailer(xref_pos, prev: previous_xref_offset)
@@ -293,7 +314,7 @@ module Pdfrb
293
314
  existing = document.trailer || {}
294
315
  trailer_hash = {}
295
316
  existing.each do |k, v|
296
- next if %i[Size Root Prev XRefStm].include?(k)
317
+ next if TRAILER_OVERRIDDEN_KEYS.include?(k)
297
318
 
298
319
  trailer_hash[k] = v
299
320
  end
@@ -392,26 +413,19 @@ module Pdfrb
392
413
  return current unless catalog
393
414
 
394
415
  v = current
395
- if (catalog.value[:AF] || catalog.value[:Collection]) && compare_versions(v, "2.0").negative?
416
+ if (catalog.value[:AF] || catalog.value[:Collection]) && Pdfrb::PdfVersion.compare(v, "2.0").negative?
396
417
  v = "2.0"
397
418
  end
398
- if catalog.value[:OCProperties] && compare_versions(v, "1.5").negative?
419
+ if catalog.value[:OCProperties] && Pdfrb::PdfVersion.compare(v, "1.5").negative?
399
420
  v = "1.5"
400
421
  end
401
422
  document.version = v
402
423
  v
403
424
  end
404
425
 
405
- def compare_versions(a, b)
406
- aa = a.to_s.split(".").map(&:to_i)
407
- bb = b.to_s.split(".").map(&:to_i)
408
- (aa <=> bb) || 0
409
- end
410
-
411
426
  def compress_content_streams
412
427
  require "zlib"
413
- document.each_indirect_object do |obj|
414
- next unless obj.is_a?(Pdfrb::Model::Cos::Stream)
428
+ document.each_indirect_of(Pdfrb::Model::Cos::Stream) do |obj|
415
429
  next if obj.value[:Filter]
416
430
  next unless obj.stream
417
431
  next if obj.value[:Type] == :Metadata
data/lib/pdfrb.rb CHANGED
@@ -18,6 +18,7 @@ module Pdfrb
18
18
  autoload :Configuration, "pdfrb/configuration"
19
19
  autoload :DataDir, "pdfrb/data_dir"
20
20
  autoload :PdfConstants, "pdfrb/pdf_constants"
21
+ autoload :PdfVersion, "pdfrb/pdf_version"
21
22
 
22
23
  autoload :Source, "pdfrb/source"
23
24
  autoload :Model, "pdfrb/model"
@@ -46,8 +47,6 @@ module Pdfrb
46
47
  autoload :Serializer, "pdfrb/serializer"
47
48
  autoload :Writer, "pdfrb/writer"
48
49
  autoload :Importer, "pdfrb/importer"
49
- autoload :Revision, "pdfrb/revision"
50
- autoload :Revisions, "pdfrb/revisions"
51
50
  autoload :XrefSection, "pdfrb/xref_section"
52
51
  autoload :Document, "pdfrb/document"
53
52
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pdfrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.5
4
+ version: 0.8.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-29 00:00:00.000000000 Z
11
+ date: 2026-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: benchmark
@@ -1261,6 +1261,7 @@ files:
1261
1261
  - lib/pdfrb/model/type/xobject_image.rb
1262
1262
  - lib/pdfrb/model/type/xref_stream.rb
1263
1263
  - lib/pdfrb/pdf_constants.rb
1264
+ - lib/pdfrb/pdf_version.rb
1264
1265
  - lib/pdfrb/serializer.rb
1265
1266
  - lib/pdfrb/source.rb
1266
1267
  - lib/pdfrb/source/header_reader.rb