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 +4 -4
- data/lib/pdfrb/cli.rb +9 -3
- data/lib/pdfrb/compare/comparator.rb +2 -4
- data/lib/pdfrb/conformance/pdf_a.rb +9 -19
- data/lib/pdfrb/conformance/pdf_x.rb +3 -13
- data/lib/pdfrb/content/canvas.rb +24 -32
- data/lib/pdfrb/content/hidden_text_detector.rb +1 -2
- data/lib/pdfrb/digital_signature.rb +8 -7
- data/lib/pdfrb/document.rb +10 -0
- data/lib/pdfrb/linearization/writer.rb +20 -5
- data/lib/pdfrb/pdf_version.rb +23 -0
- data/lib/pdfrb/serializer.rb +40 -6
- data/lib/pdfrb/task/optimize.rb +2 -6
- data/lib/pdfrb/version.rb +1 -1
- data/lib/pdfrb/writer.rb +36 -22
- data/lib/pdfrb.rb +1 -2
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b0edf5439bfe359af4b900aeb846047b168d741271eb78a7f78315ca5b0bba55
|
|
4
|
+
data.tar.gz: 0b30e0edb04dcc2a4d26a493ba782379a8a864904eafc72f07c0c49ca4a25e57
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
338
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
data/lib/pdfrb/content/canvas.rb
CHANGED
|
@@ -43,13 +43,11 @@ module Pdfrb
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def concat(a, b, c, d, e, f, &)
|
|
46
|
-
emit_op
|
|
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
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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:, **)
|
data/lib/pdfrb/document.rb
CHANGED
|
@@ -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::
|
|
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
|
-
|
|
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
|
data/lib/pdfrb/serializer.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
115
|
+
hash = dict_value.dup
|
|
82
116
|
existing_filter = hash[:Filter]
|
|
83
117
|
|
|
84
118
|
if should_compress?(stream_obj, payload, existing_filter)
|
data/lib/pdfrb/task/optimize.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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
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 =
|
|
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
|
-
**
|
|
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
|
|
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
|
-
|
|
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
|
|
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]) &&
|
|
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] &&
|
|
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.
|
|
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.
|
|
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-
|
|
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
|