hexapdf 0.26.2 → 0.28.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +115 -1
- data/README.md +1 -1
- data/examples/013-text_layouter_shapes.rb +8 -8
- data/examples/016-frame_automatic_box_placement.rb +3 -3
- data/examples/017-frame_text_flow.rb +3 -3
- data/examples/019-acro_form.rb +14 -3
- data/examples/020-column_box.rb +3 -3
- data/examples/023-images.rb +30 -0
- data/lib/hexapdf/cli/info.rb +5 -1
- data/lib/hexapdf/cli/inspect.rb +2 -2
- data/lib/hexapdf/cli/split.rb +8 -8
- data/lib/hexapdf/cli/watermark.rb +2 -2
- data/lib/hexapdf/configuration.rb +3 -2
- data/lib/hexapdf/content/canvas.rb +8 -3
- data/lib/hexapdf/dictionary.rb +4 -17
- data/lib/hexapdf/document/destinations.rb +42 -5
- data/lib/hexapdf/document/signatures.rb +265 -48
- data/lib/hexapdf/document.rb +6 -10
- data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
- data/lib/hexapdf/importer.rb +35 -27
- data/lib/hexapdf/layout/list_box.rb +1 -5
- data/lib/hexapdf/object.rb +5 -0
- data/lib/hexapdf/parser.rb +14 -0
- data/lib/hexapdf/revision.rb +15 -12
- data/lib/hexapdf/revisions.rb +7 -1
- data/lib/hexapdf/tokenizer.rb +15 -9
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
- data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
- data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/field.rb +11 -5
- data/lib/hexapdf/type/acro_form/form.rb +61 -8
- data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
- data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
- data/lib/hexapdf/type/annotations/widget.rb +3 -0
- data/lib/hexapdf/type/catalog.rb +1 -1
- data/lib/hexapdf/type/font_true_type.rb +14 -0
- data/lib/hexapdf/type/object_stream.rb +2 -2
- data/lib/hexapdf/type/outline.rb +19 -1
- data/lib/hexapdf/type/outline_item.rb +72 -14
- data/lib/hexapdf/type/page.rb +95 -64
- data/lib/hexapdf/type/resources.rb +13 -17
- data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +16 -2
- data/lib/hexapdf/type/signature.rb +10 -0
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +5 -3
- data/test/hexapdf/content/test_canvas.rb +5 -0
- data/test/hexapdf/document/test_destinations.rb +41 -0
- data/test/hexapdf/document/test_pages.rb +2 -2
- data/test/hexapdf/document/test_signatures.rb +139 -19
- data/test/hexapdf/encryption/test_aes.rb +1 -1
- data/test/hexapdf/filter/test_predictor.rb +0 -1
- data/test/hexapdf/layout/test_box.rb +2 -1
- data/test/hexapdf/layout/test_column_box.rb +1 -1
- data/test/hexapdf/layout/test_list_box.rb +1 -1
- data/test/hexapdf/test_document.rb +2 -8
- data/test/hexapdf/test_importer.rb +27 -6
- data/test/hexapdf/test_parser.rb +19 -2
- data/test/hexapdf/test_revision.rb +15 -14
- data/test/hexapdf/test_revisions.rb +63 -12
- data/test/hexapdf/test_stream.rb +1 -1
- data/test/hexapdf/test_tokenizer.rb +10 -1
- data/test/hexapdf/test_writer.rb +11 -3
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
- data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
- data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_field.rb +4 -4
- data/test/hexapdf/type/acro_form/test_form.rb +65 -0
- data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
- data/test/hexapdf/type/signature/common.rb +54 -0
- data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +21 -0
- data/test/hexapdf/type/test_catalog.rb +5 -2
- data/test/hexapdf/type/test_font_true_type.rb +20 -0
- data/test/hexapdf/type/test_object_stream.rb +2 -1
- data/test/hexapdf/type/test_outline.rb +4 -1
- data/test/hexapdf/type/test_outline_item.rb +62 -1
- data/test/hexapdf/type/test_page.rb +103 -45
- data/test/hexapdf/type/test_page_tree_node.rb +4 -2
- data/test/hexapdf/type/test_resources.rb +0 -5
- data/test/hexapdf/type/test_signature.rb +8 -0
- data/test/test_helper.rb +1 -1
- metadata +61 -4
data/lib/hexapdf/type/page.rb
CHANGED
@@ -187,8 +187,8 @@ module HexaPDF
|
|
187
187
|
end
|
188
188
|
|
189
189
|
# :call-seq:
|
190
|
-
# page.box(type = :
|
191
|
-
# page.box(type = :
|
190
|
+
# page.box(type = :crop) -> box
|
191
|
+
# page.box(type = :crop, rectangle) -> rectangle
|
192
192
|
#
|
193
193
|
# If no +rectangle+ is given, returns the rectangle defining a certain kind of box for the
|
194
194
|
# page. Otherwise sets the value for the given box type to +rectangle+ (an array with four
|
@@ -219,7 +219,7 @@ module HexaPDF
|
|
219
219
|
# author. The default is the crop box.
|
220
220
|
#
|
221
221
|
# See: PDF1.7 s14.11.2
|
222
|
-
def box(type = :
|
222
|
+
def box(type = :crop, rectangle = nil)
|
223
223
|
if rectangle
|
224
224
|
case type
|
225
225
|
when :media, :crop, :bleed, :trim, :art
|
@@ -240,9 +240,10 @@ module HexaPDF
|
|
240
240
|
end
|
241
241
|
end
|
242
242
|
|
243
|
-
# Returns the orientation of the
|
244
|
-
|
245
|
-
|
243
|
+
# Returns the orientation of the specified box (default is the crop box), either :portrait or
|
244
|
+
# :landscape.
|
245
|
+
def orientation(type = :crop)
|
246
|
+
box = self.box(type)
|
246
247
|
rotation = self[:Rotate]
|
247
248
|
if (box.height > box.width && (rotation == 0 || rotation == 180)) ||
|
248
249
|
(box.height < box.width && (rotation == 90 || rotation == 270))
|
@@ -266,27 +267,49 @@ module HexaPDF
|
|
266
267
|
raise ArgumentError, "Page rotation has to be multiple of 90 degrees"
|
267
268
|
end
|
268
269
|
|
270
|
+
# /Rotate and therefore cw_angle is angle in clockwise orientation
|
269
271
|
cw_angle = (self[:Rotate] - angle) % 360
|
270
272
|
|
271
273
|
if flatten
|
272
274
|
delete(:Rotate)
|
273
275
|
return if cw_angle == 0
|
274
276
|
|
275
|
-
matrix
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
[
|
285
|
-
|
277
|
+
matrix = case cw_angle
|
278
|
+
when 90 then Content::TransformationMatrix.new(0, -1, 1, 0, -box.bottom, box.right)
|
279
|
+
when 180 then Content::TransformationMatrix.new(-1, 0, 0, -1, box.right, box.top)
|
280
|
+
when 270 then Content::TransformationMatrix.new(0, 1, -1, 0, box.top, -box.left)
|
281
|
+
end
|
282
|
+
|
283
|
+
rotate_box = lambda do |box|
|
284
|
+
llx, lly, urx, ury = \
|
285
|
+
case cw_angle
|
286
|
+
when 90 then [box.right, box.bottom, box.left, box.top]
|
287
|
+
when 180 then [box.right, box.top, box.left, box.bottom]
|
288
|
+
when 270 then [box.left, box.top, box.right, box.bottom]
|
289
|
+
end
|
290
|
+
box.value.replace(matrix.evaluate(llx, lly).concat(matrix.evaluate(urx, ury)))
|
291
|
+
end
|
292
|
+
|
293
|
+
[:MediaBox, :CropBox, :BleedBox, :TrimBox, :ArtBox].each do |box_name|
|
294
|
+
next unless key?(box_name)
|
295
|
+
rotate_box.call(self[box_name])
|
296
|
+
end
|
297
|
+
|
298
|
+
each_annotation do |annot|
|
299
|
+
rotate_box.call(annot[:Rect])
|
300
|
+
if (quad_points = annot[:QuadPoints])
|
301
|
+
quad_points = quad_points.value if quad_points.respond_to?(:value)
|
302
|
+
result = []
|
303
|
+
quad_points.each_slice(2) {|x, y| result.concat(matrix.evaluate(x, y)) }
|
304
|
+
quad_points.replace(result)
|
305
|
+
end
|
306
|
+
if (appearance = annot.appearance)
|
307
|
+
appearance[:Matrix] = matrix.dup.premultiply(*appearance[:Matrix].value).to_a
|
308
|
+
end
|
309
|
+
if annot[:Subtype] == :Widget
|
310
|
+
app_ch = annot[:MK] ||= document.wrap({}, type: :XXAppearanceCharacteristics)
|
311
|
+
app_ch[:R] = (app_ch[:R] + 360 - cw_angle) % 360
|
286
312
|
end
|
287
|
-
[:MediaBox, :CropBox, :BleedBox, :TrimBox, :ArtBox].each do |box|
|
288
|
-
next unless key?(box)
|
289
|
-
self[box].value = matrix.evaluate(llx, lly).concat(matrix.evaluate(urx, ury))
|
290
313
|
end
|
291
314
|
|
292
315
|
before_contents = document.add({}, stream: " q #{matrix.to_a.join(' ')} cm ")
|
@@ -373,20 +396,33 @@ module HexaPDF
|
|
373
396
|
|
374
397
|
# Returns the requested type of canvas for the page.
|
375
398
|
#
|
376
|
-
#
|
377
|
-
#
|
378
|
-
#
|
379
|
-
#
|
380
|
-
#
|
381
|
-
# canvas. This means that the canvas' origin is always at the bottom left corner of the media
|
382
|
-
# box.
|
399
|
+
# There are potentially three different canvas objects, one for each of the types :underlay,
|
400
|
+
# :page, and :overlay. The canvas objects are cached once they are created so that their
|
401
|
+
# graphics states are correctly retained without the need for parsing the contents. This also
|
402
|
+
# means that on subsequent invocations the graphic states of the canvases might already be
|
403
|
+
# changed.
|
383
404
|
#
|
384
405
|
# type::
|
385
406
|
# Can either be
|
386
407
|
# * :page for getting the canvas for the page itself (only valid for initially empty pages)
|
387
408
|
# * :overlay for getting the canvas for drawing over the page contents
|
388
409
|
# * :underlay for getting the canvas for drawing unter the page contents
|
389
|
-
|
410
|
+
#
|
411
|
+
# translate_origin::
|
412
|
+
# Specifies whether the origin should automatically be translated into the lower-left
|
413
|
+
# corner of the crop box.
|
414
|
+
#
|
415
|
+
# Note that this argument is only used for the first invocation for every canvas type. So
|
416
|
+
# if a canvas was initially requested with this argument set to false and then with true,
|
417
|
+
# it won't have any effect as the cached canvas is returned.
|
418
|
+
#
|
419
|
+
# To check whether the origin has been translated or not, use
|
420
|
+
#
|
421
|
+
# canvas.graphics_state.ctm.evaluate(0, 0)
|
422
|
+
#
|
423
|
+
# and check whether the result is [0, 0]. If it is, then the origin has not been
|
424
|
+
# translated.
|
425
|
+
def canvas(type: :page, translate_origin: true)
|
390
426
|
unless [:page, :overlay, :underlay].include?(type)
|
391
427
|
raise ArgumentError, "Invalid value for 'type', expected: :page, :underlay or :overlay"
|
392
428
|
end
|
@@ -399,9 +435,10 @@ module HexaPDF
|
|
399
435
|
|
400
436
|
create_canvas = lambda do
|
401
437
|
Content::Canvas.new(self).tap do |canvas|
|
402
|
-
|
403
|
-
|
404
|
-
|
438
|
+
next unless translate_origin
|
439
|
+
crop_box = box(:crop)
|
440
|
+
if crop_box.left != 0 || crop_box.bottom != 0
|
441
|
+
canvas.translate(crop_box.left, crop_box.bottom)
|
405
442
|
end
|
406
443
|
end
|
407
444
|
end
|
@@ -497,17 +534,16 @@ module HexaPDF
|
|
497
534
|
# If an annotation is a form field widget, only the widget will be deleted but not the form
|
498
535
|
# field itself.
|
499
536
|
def flatten_annotations(annotations = self[:Annots])
|
500
|
-
|
537
|
+
not_flattened = (annotations || []).to_ary
|
538
|
+
return not_flattened unless key?(:Annots)
|
501
539
|
|
502
|
-
not_flattened = annotations.to_ary
|
503
540
|
annotations = not_flattened & self[:Annots] if annotations != self[:Annots]
|
504
541
|
return not_flattened if annotations.empty?
|
505
542
|
|
506
543
|
canvas = self.canvas(type: :overlay)
|
507
|
-
canvas.
|
508
|
-
|
509
|
-
|
510
|
-
canvas.translate(-media_box.left, -media_box.bottom) # revert initial translation of origin
|
544
|
+
if (pos = canvas.graphics_state.ctm.evaluate(0, 0)) != [0, 0]
|
545
|
+
canvas.save_graphics_state
|
546
|
+
canvas.translate(-pos[0], -pos[1])
|
511
547
|
end
|
512
548
|
|
513
549
|
to_delete = []
|
@@ -525,36 +561,31 @@ module HexaPDF
|
|
525
561
|
|
526
562
|
rect = annotation[:Rect]
|
527
563
|
box = appearance.box
|
528
|
-
matrix = appearance[:Matrix]
|
529
|
-
|
530
|
-
# Adjust position based on matrix
|
531
|
-
pos = [rect.left - matrix[4], rect.bottom - matrix[5]]
|
532
|
-
|
533
|
-
# In case of a rotation we need to counter the default translation in #xobject by adding
|
534
|
-
# box.left and box.bottom, and then translate the origin for the rotation
|
535
|
-
angle = (-Math.atan2(matrix[2], matrix[0]) * 180 / Math::PI).to_i
|
536
|
-
case angle
|
537
|
-
when 0
|
538
|
-
# Nothing to do, no rotation
|
539
|
-
when 90
|
540
|
-
pos[0] += box.top + box.left
|
541
|
-
pos[1] += -box.left + box.bottom
|
542
|
-
when -90
|
543
|
-
pos[0] += -box.bottom + box.left
|
544
|
-
pos[1] += box.right + box.bottom
|
545
|
-
when 180, -180
|
546
|
-
pos[0] += box.right + box.left
|
547
|
-
pos[1] += box.top + box.bottom
|
548
|
-
else
|
549
|
-
not_flattened << annotation
|
550
|
-
next
|
551
|
-
end
|
552
564
|
|
553
|
-
|
554
|
-
|
565
|
+
# PDF1.7 12.5.5 algorithm
|
566
|
+
# Step a) Calculate smallest rectangle containing transformed bounding box
|
567
|
+
matrix = HexaPDF::Content::TransformationMatrix.new(*appearance[:Matrix].value)
|
568
|
+
llx, lly = matrix.evaluate(box.left, box.bottom)
|
569
|
+
ulx, uly = matrix.evaluate(box.left, box.top)
|
570
|
+
lrx, lry = matrix.evaluate(box.right, box.bottom)
|
571
|
+
left, right = [llx, ulx, lrx, lrx + (ulx - llx)].minmax
|
572
|
+
bottom, top = [lly, uly, lry, lry + (uly - lly)].minmax
|
573
|
+
|
574
|
+
# Step b) Fit calculated rectangle to annotation rectangle by translating/scaling
|
575
|
+
a = HexaPDF::Content::TransformationMatrix.new
|
576
|
+
a.translate(rect.left - left, rect.bottom - bottom)
|
577
|
+
a.scale(rect.width.fdiv(right - left), rect.height.fdiv(top - bottom))
|
578
|
+
|
579
|
+
# Step c) Premultiply form matrix - done implicitly when drawing the XObject
|
580
|
+
|
581
|
+
canvas.transform(*a) do
|
582
|
+
# Use [box.left, box.bottom] to counter default translation in #xobject since that
|
583
|
+
# is already taken care of in matrix a
|
584
|
+
canvas.xobject(appearance, at: [box.left, box.bottom])
|
585
|
+
end
|
555
586
|
to_delete << annotation
|
556
587
|
end
|
557
|
-
canvas.restore_graphics_state
|
588
|
+
canvas.restore_graphics_state unless pos == [0, 0]
|
558
589
|
|
559
590
|
to_delete.each do |annotation|
|
560
591
|
if annotation[:Subtype] == :Widget
|
@@ -217,24 +217,20 @@ module HexaPDF
|
|
217
217
|
# Ensures that a valid procedure set is available.
|
218
218
|
def perform_validation
|
219
219
|
super
|
220
|
-
val = self[:ProcSet]
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
end
|
220
|
+
return unless (val = self[:ProcSet])
|
221
|
+
|
222
|
+
if val.kind_of?(Symbol)
|
223
|
+
yield("Procedure set is a single value instead of an Array", true)
|
224
|
+
val = value[:ProcSet] = [val]
|
225
|
+
end
|
226
|
+
val.reject! do |name|
|
227
|
+
case name
|
228
|
+
when :PDF, :Text, :ImageB, :ImageC, :ImageI
|
229
|
+
false
|
230
|
+
else
|
231
|
+
yield("Invalid page procedure set name /#{name}", true)
|
232
|
+
true
|
234
233
|
end
|
235
|
-
else
|
236
|
-
yield("No procedure set specified", true)
|
237
|
-
self[:ProcSet] = [:PDF, :Text, :ImageB, :ImageC, :ImageI]
|
238
234
|
end
|
239
235
|
end
|
240
236
|
|
@@ -104,8 +104,22 @@ module HexaPDF
|
|
104
104
|
result.log(:error, "Certificate key usage is missing 'Digital Signature'")
|
105
105
|
end
|
106
106
|
|
107
|
-
if
|
108
|
-
|
107
|
+
if signature_dict.signature_type == 'ETSI.RFC3161'
|
108
|
+
# Getting the needed values is not directly supported by Ruby OpenSSL
|
109
|
+
p7 = OpenSSL::ASN1.decode(signature_dict.contents.sub(/\x00*\z/, ''))
|
110
|
+
signed_data = p7.value[1].value[0]
|
111
|
+
content_info = signed_data.value[2]
|
112
|
+
content = OpenSSL::ASN1.decode(content_info.value[1].value[0].value)
|
113
|
+
digest_algorithm = content.value[2].value[0].value[0].value
|
114
|
+
original_hash = content.value[2].value[1].value
|
115
|
+
recomputed_hash = OpenSSL::Digest.digest(digest_algorithm, signature_dict.signed_data)
|
116
|
+
hash_valid = (original_hash == recomputed_hash)
|
117
|
+
else
|
118
|
+
data = signature_dict.signed_data
|
119
|
+
hash_valid = true # hash will be checked by @pkcs7.verify
|
120
|
+
end
|
121
|
+
if hash_valid && @pkcs7.verify(certificate_chain, store, data,
|
122
|
+
OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY)
|
109
123
|
result.log(:info, "Signature valid")
|
110
124
|
else
|
111
125
|
result.log(:error, "Signature verification failed")
|
@@ -230,6 +230,16 @@ module HexaPDF
|
|
230
230
|
signature_handler.verify(store, allow_self_signed: allow_self_signed)
|
231
231
|
end
|
232
232
|
|
233
|
+
private
|
234
|
+
|
235
|
+
def perform_validation #:nodoc:
|
236
|
+
if (self[:SubFilter] == :'ETSI.CAdES.detached' || self[:SubFilter] == :'ETSI.RFC3161') &&
|
237
|
+
document.version < '2.0'
|
238
|
+
yield("Signature handler needs at least PDF version 2.0", true)
|
239
|
+
document.version = '2.0'
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
233
243
|
end
|
234
244
|
|
235
245
|
end
|
data/lib/hexapdf/version.rb
CHANGED
data/lib/hexapdf/writer.rb
CHANGED
@@ -110,8 +110,11 @@ module HexaPDF
|
|
110
110
|
|
111
111
|
revision = Revision.new(@document.revisions.current.trailer)
|
112
112
|
@document.trailer.info[:Producer] = "HexaPDF version #{HexaPDF::VERSION}"
|
113
|
+
if parser.file_header_version < @document.version
|
114
|
+
@document.catalog[:Version] = @document.version.to_sym
|
115
|
+
end
|
113
116
|
@document.revisions.each do |rev|
|
114
|
-
rev.each_modified_object {|obj| revision.send(:add_without_check, obj) }
|
117
|
+
rev.each_modified_object(all: true) {|obj| revision.send(:add_without_check, obj) }
|
115
118
|
end
|
116
119
|
|
117
120
|
write_revision(revision, parser.startxref_offset)
|
@@ -132,8 +135,7 @@ module HexaPDF
|
|
132
135
|
|
133
136
|
revision = @document.revisions.add
|
134
137
|
@document.revisions.all[0..-2].each do |rev|
|
135
|
-
rev.each_modified_object {|obj| revision.send(:add_without_check, obj) }
|
136
|
-
rev.reset_objects
|
138
|
+
rev.each_modified_object(delete: true) {|obj| revision.send(:add_without_check, obj) }
|
137
139
|
end
|
138
140
|
@document.revisions.merge(-2..-1)
|
139
141
|
end
|
@@ -934,6 +934,11 @@ describe HexaPDF::Content::Canvas do
|
|
934
934
|
[:restore_graphics_state]])
|
935
935
|
end
|
936
936
|
|
937
|
+
it "correctly serializes the form when no transformation is needed" do
|
938
|
+
@canvas.image(@form, at: [100, 50])
|
939
|
+
assert_operators(@page.contents, [[:paint_xobject, [:XO1]]])
|
940
|
+
end
|
941
|
+
|
937
942
|
it "doesn't do anything if the form's width or height is zero" do
|
938
943
|
@form[:BBox] = [100, 50, 100, 200]
|
939
944
|
@canvas.xobject(@form, at: [0, 0])
|
@@ -29,6 +29,12 @@ describe HexaPDF::Document::Destinations::Destination do
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
+
it "accepts an array or a dictionary containing a /D entry as value" do
|
33
|
+
assert(destination([5, :Fit]).valid?)
|
34
|
+
assert(destination({D: [5, :Fit]}).valid?)
|
35
|
+
assert(destination(HexaPDF::Dictionary.new({D: [5, :Fit]})).valid?)
|
36
|
+
end
|
37
|
+
|
32
38
|
it "can be asked whether the referenced page is in a remote document" do
|
33
39
|
assert(destination([5, :Fit]).remote?)
|
34
40
|
refute(destination([HexaPDF::Dictionary.new({}), :Fit]).remote?)
|
@@ -42,6 +48,10 @@ describe HexaPDF::Document::Destinations::Destination do
|
|
42
48
|
assert(destination([5, :Fit]).valid?)
|
43
49
|
end
|
44
50
|
|
51
|
+
it "returns the destination array" do
|
52
|
+
assert_equal([5, :Fit], destination([5, :Fit]).value)
|
53
|
+
end
|
54
|
+
|
45
55
|
describe "type :xyz" do
|
46
56
|
before do
|
47
57
|
@dest = destination([:page, :XYZ, :left, :top, :zoom])
|
@@ -396,6 +406,37 @@ describe HexaPDF::Document::Destinations do
|
|
396
406
|
refute(@doc.destinations['abc'])
|
397
407
|
end
|
398
408
|
|
409
|
+
describe "resolve" do
|
410
|
+
it "resolves the named destination" do
|
411
|
+
@doc.catalog.names.destinations.add_entry("arr", [@page, :Fit])
|
412
|
+
@doc.catalog.names.destinations.add_entry("dict", {D: [@page, :Fit]})
|
413
|
+
assert_equal([@page, :Fit], @doc.destinations.resolve("arr").value)
|
414
|
+
assert_equal([@page, :Fit], @doc.destinations.resolve("dict").value)
|
415
|
+
end
|
416
|
+
|
417
|
+
it "returns nil if the named destination is not found" do
|
418
|
+
assert_nil(@doc.destinations.resolve("arr"))
|
419
|
+
end
|
420
|
+
|
421
|
+
it "resolves the old-style named destination" do
|
422
|
+
@doc.catalog[:Dests] = {arr: [@page, :Fit]}
|
423
|
+
assert_equal([@page, :Fit], @doc.destinations.resolve(:arr).value)
|
424
|
+
end
|
425
|
+
|
426
|
+
it "returns nil if the old-style named destination is not found" do
|
427
|
+
assert_nil(@doc.destinations.resolve(:arr))
|
428
|
+
end
|
429
|
+
|
430
|
+
it "uses a PDFArray or array argument directly" do
|
431
|
+
assert_equal([@page, :Fit], @doc.destinations.resolve([@page, :Fit]).value)
|
432
|
+
assert_equal([@page, :Fit], @doc.destinations.resolve(HexaPDF::PDFArray.new([@page, :Fit])).value)
|
433
|
+
end
|
434
|
+
|
435
|
+
it "returns nil if the resolved destination is not valid" do
|
436
|
+
assert_nil(@doc.destinations.resolve([@page, :Fitd]))
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
399
440
|
describe "each" do
|
400
441
|
before do
|
401
442
|
3.times {|i| @doc.destinations.add("abc#{i}", [:page, :Fit]) }
|
@@ -166,7 +166,7 @@ describe HexaPDF::Document::Pages do
|
|
166
166
|
it "works for a single page label entry" do
|
167
167
|
@doc.catalog[:PageLabels] = {Nums: [0, {S: :r}]}
|
168
168
|
result = @doc.pages.each_labelling_range.to_a
|
169
|
-
assert_equal([[0, 10, {S: :r}]], result.map {|s, c, l| [s, c, l.value]})
|
169
|
+
assert_equal([[0, 10, {S: :r}]], result.map {|s, c, l| [s, c, l.value] })
|
170
170
|
assert_equal(:lowercase_roman, result[0].last.numbering_style)
|
171
171
|
end
|
172
172
|
|
@@ -174,7 +174,7 @@ describe HexaPDF::Document::Pages do
|
|
174
174
|
@doc.catalog[:PageLabels] = {Nums: [0, {S: :r}, 2, {S: :d}, 7, {S: :A}]}
|
175
175
|
result = @doc.pages.each_labelling_range.to_a
|
176
176
|
assert_equal([[0, 2, {S: :r}], [2, 5, {S: :d}], [7, 3, {S: :A}]],
|
177
|
-
result.map {|s, c, l| [s, c, l.value]})
|
177
|
+
result.map {|s, c, l| [s, c, l.value] })
|
178
178
|
end
|
179
179
|
|
180
180
|
it "returns a zero or negative count for the last range if there aren't enough pages" do
|
@@ -19,17 +19,37 @@ describe HexaPDF::Document::Signatures do
|
|
19
19
|
)
|
20
20
|
end
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
22
|
+
it "allows embedding an external signature value" do
|
23
|
+
doc = HexaPDF::Document.new(io: StringIO.new(MINIMAL_PDF))
|
24
|
+
io = StringIO.new(''.b)
|
25
|
+
doc.signatures.add(io, @handler)
|
26
|
+
doc = HexaPDF::Document.new(io: io)
|
27
|
+
io = StringIO.new(''.b)
|
28
|
+
|
29
|
+
byte_range = nil
|
30
|
+
@handler.signature_size = 5000
|
31
|
+
@handler.external_signing = proc {|_, br| byte_range = br; "" }
|
32
|
+
doc.signatures.add(io, @handler)
|
33
|
+
|
34
|
+
io.pos = byte_range[0]
|
35
|
+
data = io.read(byte_range[1])
|
36
|
+
io.pos = byte_range[2]
|
37
|
+
data << io.read(byte_range[3])
|
38
|
+
contents = OpenSSL::PKCS7.sign(@handler.certificate, @handler.key, data, @handler.certificate_chain,
|
39
|
+
OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY).to_der
|
40
|
+
HexaPDF::Document::Signatures.embed_signature(io, contents)
|
41
|
+
doc = HexaPDF::Document.new(io: io)
|
42
|
+
assert_equal(2, doc.signatures.each.count)
|
43
|
+
doc.signatures.each do |signature|
|
44
|
+
assert(signature.verify(allow_self_signed: true).messages.find {|m| m.content == 'Signature valid' })
|
29
45
|
end
|
46
|
+
end
|
30
47
|
|
48
|
+
describe "DefaultHandler" do
|
31
49
|
it "returns the size of serialized signature" do
|
32
50
|
assert_equal(1310, @handler.signature_size)
|
51
|
+
@handler.signature_size = 100
|
52
|
+
assert_equal(100, @handler.signature_size)
|
33
53
|
end
|
34
54
|
|
35
55
|
it "allows setting the DocMDP permissions" do
|
@@ -56,16 +76,23 @@ describe HexaPDF::Document::Signatures do
|
|
56
76
|
assert_raises(ArgumentError) { @handler.doc_mdp_permissions = :other }
|
57
77
|
end
|
58
78
|
|
59
|
-
|
60
|
-
data
|
61
|
-
|
62
|
-
|
79
|
+
describe "sign" do
|
80
|
+
it "can sign the data using PKCS7" do
|
81
|
+
data = StringIO.new("data")
|
82
|
+
store = OpenSSL::X509::Store.new
|
83
|
+
store.add_cert(CERTIFICATES.ca_certificate)
|
84
|
+
|
85
|
+
pkcs7 = OpenSSL::PKCS7.new(@handler.sign(data, [0, 4, 0, 0]))
|
86
|
+
assert(pkcs7.detached?)
|
87
|
+
assert_equal([CERTIFICATES.signer_certificate, CERTIFICATES.ca_certificate],
|
88
|
+
pkcs7.certificates)
|
89
|
+
assert(pkcs7.verify([], store, data.string, OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY))
|
90
|
+
end
|
63
91
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
assert(pkcs7.verify([], store, data, OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY))
|
92
|
+
it "can use external signing" do
|
93
|
+
@handler.external_signing = proc { "hallo" }
|
94
|
+
assert_equal("hallo", @handler.sign(StringIO.new, [0, 0, 0, 0]))
|
95
|
+
end
|
69
96
|
end
|
70
97
|
|
71
98
|
describe "finalize_objects" do
|
@@ -80,6 +107,12 @@ describe HexaPDF::Document::Signatures do
|
|
80
107
|
assert(@obj.empty?)
|
81
108
|
end
|
82
109
|
|
110
|
+
it "adjust the /SubFilter if signature type is etsi" do
|
111
|
+
@handler.signature_type = :etsi
|
112
|
+
@handler.finalize_objects(@field, @obj)
|
113
|
+
assert_equal(:'ETSI.CAdES.detached', @obj[:SubFilter])
|
114
|
+
end
|
115
|
+
|
83
116
|
it "sets the reason, location and contact info fields" do
|
84
117
|
@handler.reason = 'Reason'
|
85
118
|
@handler.location = 'Location'
|
@@ -111,6 +144,87 @@ describe HexaPDF::Document::Signatures do
|
|
111
144
|
end
|
112
145
|
end
|
113
146
|
|
147
|
+
describe "TimestampHandler" do
|
148
|
+
before do
|
149
|
+
@handler = HexaPDF::Document::Signatures::TimestampHandler.new
|
150
|
+
end
|
151
|
+
|
152
|
+
it "allows setting the attributes in the constructor" do
|
153
|
+
handler = HexaPDF::Document::Signatures::TimestampHandler.new(
|
154
|
+
tsa_url: "url", tsa_hash_algorithm: "MD5", tsa_policy_id: "5",
|
155
|
+
reason: "Reason", location: "Location", contact_info: "Contact",
|
156
|
+
signature_size: 1_000
|
157
|
+
)
|
158
|
+
assert_equal("url", handler.tsa_url)
|
159
|
+
assert_equal("MD5", handler.tsa_hash_algorithm)
|
160
|
+
assert_equal("5", handler.tsa_policy_id)
|
161
|
+
assert_equal("Reason", handler.reason)
|
162
|
+
assert_equal("Location", handler.location)
|
163
|
+
assert_equal("Contact", handler.contact_info)
|
164
|
+
assert_equal(1_000, handler.signature_size)
|
165
|
+
end
|
166
|
+
|
167
|
+
it "finalizes the signature field and signature objects" do
|
168
|
+
@field = @doc.wrap({})
|
169
|
+
@sig = @doc.wrap({})
|
170
|
+
@handler.reason = 'Reason'
|
171
|
+
@handler.location = 'Location'
|
172
|
+
@handler.contact_info = 'Contact'
|
173
|
+
|
174
|
+
@handler.finalize_objects(@field, @sig)
|
175
|
+
assert_equal('2.0', @doc.version)
|
176
|
+
assert_equal(:DocTimeStamp, @sig[:Type])
|
177
|
+
assert_equal(:'ETSI.RFC3161', @sig[:SubFilter])
|
178
|
+
assert_equal('Reason', @sig[:Reason])
|
179
|
+
assert_equal('Location', @sig[:Location])
|
180
|
+
assert_equal('Contact', @sig[:ContactInfo])
|
181
|
+
end
|
182
|
+
|
183
|
+
it "returns the size of serialized signature" do
|
184
|
+
@handler.tsa_url = "http://127.0.0.1:34567"
|
185
|
+
CERTIFICATES.start_tsa_server
|
186
|
+
assert_equal(1420, @handler.signature_size)
|
187
|
+
end
|
188
|
+
|
189
|
+
describe "sign" do
|
190
|
+
before do
|
191
|
+
@data = StringIO.new("data")
|
192
|
+
@range = [0, 4, 0, 0]
|
193
|
+
@handler.tsa_url = "http://127.0.0.1:34567"
|
194
|
+
CERTIFICATES.start_tsa_server
|
195
|
+
end
|
196
|
+
|
197
|
+
it "respects the set hash algorithm and policy id" do
|
198
|
+
@handler.tsa_hash_algorithm = 'SHA256'
|
199
|
+
@handler.tsa_policy_id = '1.2.3.4.2'
|
200
|
+
token = OpenSSL::ASN1.decode(@handler.sign(@data, @range))
|
201
|
+
content = OpenSSL::ASN1.decode(token.value[1].value[0].value[2].value[1].value[0].value)
|
202
|
+
policy_id = content.value[1].value
|
203
|
+
digest_algorithm = content.value[2].value[0].value[0].value
|
204
|
+
assert_equal('SHA256', digest_algorithm)
|
205
|
+
assert_equal("1.2.3.4.2", policy_id)
|
206
|
+
end
|
207
|
+
|
208
|
+
it "returns the serialized timestamp token" do
|
209
|
+
token = OpenSSL::PKCS7.new(@handler.sign(@data, @range))
|
210
|
+
assert_equal(CERTIFICATES.ca_certificate.subject, token.signers[0].issuer)
|
211
|
+
assert_equal(CERTIFICATES.timestamp_certificate.serial, token.signers[0].serial)
|
212
|
+
end
|
213
|
+
|
214
|
+
it "fails if the timestamp token could not be created" do
|
215
|
+
@handler.tsa_hash_algorithm = 'SHA1'
|
216
|
+
msg = assert_raises(HexaPDF::Error) { @handler.sign(@data, @range) }
|
217
|
+
assert_match(/BAD_ALG/, msg.message)
|
218
|
+
end
|
219
|
+
|
220
|
+
it "fails if the timestamp server couldn't process the request" do
|
221
|
+
@handler.tsa_policy_id = '1.2.3.4.1'
|
222
|
+
msg = assert_raises(HexaPDF::Error) { @handler.sign(@data, @range) }
|
223
|
+
assert_match(/Invalid TSA server response/, msg.message)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
114
228
|
it "iterates over all signature dictionaries" do
|
115
229
|
assert_equal([], @doc.signatures.to_a)
|
116
230
|
@sig1.field_value = :sig1
|
@@ -164,7 +278,7 @@ describe HexaPDF::Document::Signatures do
|
|
164
278
|
sig = @doc.signatures.first
|
165
279
|
assert_equal(:'Adobe.PPKLite', sig[:Filter])
|
166
280
|
assert_equal(:'adbe.pkcs7.detached', sig[:SubFilter])
|
167
|
-
assert_equal([0,
|
281
|
+
assert_equal([0, 996, 3618, 2501], sig[:ByteRange].value)
|
168
282
|
assert_equal(:sig, sig[:key])
|
169
283
|
assert_equal(:sig_field, @doc.acro_form.each_field.first[:key])
|
170
284
|
assert(sig.key?(:Contents))
|
@@ -205,14 +319,14 @@ describe HexaPDF::Document::Signatures do
|
|
205
319
|
it "handles different xref section types correctly when determing the offsets" do
|
206
320
|
@doc.delete(7)
|
207
321
|
sig = @doc.signatures.add(@io, @handler, write_options: {update_fields: false})
|
208
|
-
assert_equal([0,
|
322
|
+
assert_equal([0, 988, 3610, 2483], sig[:ByteRange].value)
|
209
323
|
end
|
210
324
|
|
211
325
|
it "works if the signature object is the last object of the xref section" do
|
212
326
|
field = @doc.acro_form(create: true).create_signature_field('Signature2')
|
213
327
|
field.create_widget(@doc.pages[0], Rect: [0, 0, 0, 0])
|
214
328
|
sig = @doc.signatures.add(@io, @handler, signature: field, write_options: {update_fields: false})
|
215
|
-
assert_equal([0,
|
329
|
+
assert_equal([0, 3095, 5717, 380], sig[:ByteRange].value)
|
216
330
|
end
|
217
331
|
|
218
332
|
it "allows writing to a file in addition to writing to an IO" do
|
@@ -228,5 +342,11 @@ describe HexaPDF::Document::Signatures do
|
|
228
342
|
signed_doc = HexaPDF::Document.new(io: @io)
|
229
343
|
assert(signed_doc.signatures.first.verify)
|
230
344
|
end
|
345
|
+
|
346
|
+
it "fails if the reserved signature space is too small" do
|
347
|
+
def @handler.signature_size; 200; end
|
348
|
+
msg = assert_raises(HexaPDF::Error) { @doc.signatures.add(@io, @handler) }
|
349
|
+
assert_match(/space.*too small.*200 vs/, msg.message)
|
350
|
+
end
|
231
351
|
end
|
232
352
|
end
|
@@ -122,7 +122,7 @@ describe HexaPDF::Encryption::AES do
|
|
122
122
|
[4, 20, 40].each do |length|
|
123
123
|
assert_raises(HexaPDF::EncryptionError) do
|
124
124
|
collector(@algorithm_class.decryption_fiber('some' * 4,
|
125
|
-
|
125
|
+
Fiber.new { 'a' * length }))
|
126
126
|
end
|
127
127
|
end
|
128
128
|
end
|