hexapdf 0.26.2 → 0.28.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +115 -1
  3. data/README.md +1 -1
  4. data/examples/013-text_layouter_shapes.rb +8 -8
  5. data/examples/016-frame_automatic_box_placement.rb +3 -3
  6. data/examples/017-frame_text_flow.rb +3 -3
  7. data/examples/019-acro_form.rb +14 -3
  8. data/examples/020-column_box.rb +3 -3
  9. data/examples/023-images.rb +30 -0
  10. data/lib/hexapdf/cli/info.rb +5 -1
  11. data/lib/hexapdf/cli/inspect.rb +2 -2
  12. data/lib/hexapdf/cli/split.rb +8 -8
  13. data/lib/hexapdf/cli/watermark.rb +2 -2
  14. data/lib/hexapdf/configuration.rb +3 -2
  15. data/lib/hexapdf/content/canvas.rb +8 -3
  16. data/lib/hexapdf/dictionary.rb +4 -17
  17. data/lib/hexapdf/document/destinations.rb +42 -5
  18. data/lib/hexapdf/document/signatures.rb +265 -48
  19. data/lib/hexapdf/document.rb +6 -10
  20. data/lib/hexapdf/filter/ascii85_decode.rb +1 -1
  21. data/lib/hexapdf/importer.rb +35 -27
  22. data/lib/hexapdf/layout/list_box.rb +1 -5
  23. data/lib/hexapdf/object.rb +5 -0
  24. data/lib/hexapdf/parser.rb +14 -0
  25. data/lib/hexapdf/revision.rb +15 -12
  26. data/lib/hexapdf/revisions.rb +7 -1
  27. data/lib/hexapdf/tokenizer.rb +15 -9
  28. data/lib/hexapdf/type/acro_form/appearance_generator.rb +174 -128
  29. data/lib/hexapdf/type/acro_form/button_field.rb +5 -3
  30. data/lib/hexapdf/type/acro_form/choice_field.rb +2 -0
  31. data/lib/hexapdf/type/acro_form/field.rb +11 -5
  32. data/lib/hexapdf/type/acro_form/form.rb +61 -8
  33. data/lib/hexapdf/type/acro_form/signature_field.rb +2 -0
  34. data/lib/hexapdf/type/acro_form/text_field.rb +12 -2
  35. data/lib/hexapdf/type/annotations/widget.rb +3 -0
  36. data/lib/hexapdf/type/catalog.rb +1 -1
  37. data/lib/hexapdf/type/font_true_type.rb +14 -0
  38. data/lib/hexapdf/type/object_stream.rb +2 -2
  39. data/lib/hexapdf/type/outline.rb +19 -1
  40. data/lib/hexapdf/type/outline_item.rb +72 -14
  41. data/lib/hexapdf/type/page.rb +95 -64
  42. data/lib/hexapdf/type/resources.rb +13 -17
  43. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +16 -2
  44. data/lib/hexapdf/type/signature.rb +10 -0
  45. data/lib/hexapdf/version.rb +1 -1
  46. data/lib/hexapdf/writer.rb +5 -3
  47. data/test/hexapdf/content/test_canvas.rb +5 -0
  48. data/test/hexapdf/document/test_destinations.rb +41 -0
  49. data/test/hexapdf/document/test_pages.rb +2 -2
  50. data/test/hexapdf/document/test_signatures.rb +139 -19
  51. data/test/hexapdf/encryption/test_aes.rb +1 -1
  52. data/test/hexapdf/filter/test_predictor.rb +0 -1
  53. data/test/hexapdf/layout/test_box.rb +2 -1
  54. data/test/hexapdf/layout/test_column_box.rb +1 -1
  55. data/test/hexapdf/layout/test_list_box.rb +1 -1
  56. data/test/hexapdf/test_document.rb +2 -8
  57. data/test/hexapdf/test_importer.rb +27 -6
  58. data/test/hexapdf/test_parser.rb +19 -2
  59. data/test/hexapdf/test_revision.rb +15 -14
  60. data/test/hexapdf/test_revisions.rb +63 -12
  61. data/test/hexapdf/test_stream.rb +1 -1
  62. data/test/hexapdf/test_tokenizer.rb +10 -1
  63. data/test/hexapdf/test_writer.rb +11 -3
  64. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +135 -56
  65. data/test/hexapdf/type/acro_form/test_button_field.rb +6 -1
  66. data/test/hexapdf/type/acro_form/test_choice_field.rb +4 -0
  67. data/test/hexapdf/type/acro_form/test_field.rb +4 -4
  68. data/test/hexapdf/type/acro_form/test_form.rb +65 -0
  69. data/test/hexapdf/type/acro_form/test_signature_field.rb +4 -0
  70. data/test/hexapdf/type/acro_form/test_text_field.rb +13 -0
  71. data/test/hexapdf/type/signature/common.rb +54 -0
  72. data/test/hexapdf/type/signature/test_adbe_pkcs7_detached.rb +21 -0
  73. data/test/hexapdf/type/test_catalog.rb +5 -2
  74. data/test/hexapdf/type/test_font_true_type.rb +20 -0
  75. data/test/hexapdf/type/test_object_stream.rb +2 -1
  76. data/test/hexapdf/type/test_outline.rb +4 -1
  77. data/test/hexapdf/type/test_outline_item.rb +62 -1
  78. data/test/hexapdf/type/test_page.rb +103 -45
  79. data/test/hexapdf/type/test_page_tree_node.rb +4 -2
  80. data/test/hexapdf/type/test_resources.rb +0 -5
  81. data/test/hexapdf/type/test_signature.rb +8 -0
  82. data/test/test_helper.rb +1 -1
  83. metadata +61 -4
@@ -187,8 +187,8 @@ module HexaPDF
187
187
  end
188
188
 
189
189
  # :call-seq:
190
- # page.box(type = :media) -> box
191
- # page.box(type = :media, rectangle) -> rectangle
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 = :media, rectangle = nil)
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 media box, either :portrait or :landscape.
244
- def orientation
245
- box = self[:MediaBox]
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, llx, lly, urx, ury = \
276
- case cw_angle
277
- when 90
278
- [HexaPDF::Content::TransformationMatrix.new(0, -1, 1, 0),
279
- box.right, box.bottom, box.left, box.top]
280
- when 180
281
- [HexaPDF::Content::TransformationMatrix.new(-1, 0, 0, -1),
282
- box.right, box.top, box.left, box.bottom]
283
- when 270
284
- [HexaPDF::Content::TransformationMatrix.new(0, 1, -1, 0),
285
- box.left, box.top, box.right, box.bottom]
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
- # The canvas object is cached once it is created so that its graphics state is correctly
377
- # retained without the need for parsing its contents.
378
- #
379
- # If the media box of the page doesn't have its origin at (0, 0), the canvas origin is
380
- # translated into the bottom left corner so that this detail doesn't matter when using the
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
- def canvas(type: :page)
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
- media_box = box(:media)
403
- if media_box.left != 0 || media_box.bottom != 0
404
- canvas.translate(media_box.left, media_box.bottom)
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
- return [] unless key?(:Annots)
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.save_graphics_state
508
- media_box = box(:media)
509
- if media_box.left != 0 || media_box.bottom != 0
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
- width, height = (angle.abs == 90 ? [rect.height, rect.width] : [rect.width, rect.height])
554
- canvas.xobject(appearance, at: pos, width: width, height: height)
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
- if val
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
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 @pkcs7.verify(certificate_chain, store, signature_dict.signed_data,
108
- OpenSSL::PKCS7::DETACHED | OpenSSL::PKCS7::BINARY)
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
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.26.2'
40
+ VERSION = '0.28.0'
41
41
 
42
42
  end
@@ -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
- describe "DefaultHandler" do
23
- it "returns the filter name" do
24
- assert_equal(:'Adobe.PPKLite', @handler.filter_name)
25
- end
26
-
27
- it "returns the sub filter algorithm name" do
28
- assert_equal(:'adbe.pkcs7.detached', @handler.sub_filter_name)
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
- it "can sign the data using PKCS7" do
60
- data = "data"
61
- store = OpenSSL::X509::Store.new
62
- store.add_cert(CERTIFICATES.ca_certificate)
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
- pkcs7 = OpenSSL::PKCS7.new(@handler.sign(data))
65
- assert(pkcs7.detached?)
66
- assert_equal([CERTIFICATES.signer_certificate, CERTIFICATES.ca_certificate],
67
- pkcs7.certificates)
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, 968, 3590, 2517], sig[:ByteRange].value)
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, 968, 3590, 2491], sig[:ByteRange].value)
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, 3063, 5685, 400], sig[:ByteRange].value)
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
- Fiber.new { 'a' * length }))
125
+ Fiber.new { 'a' * length }))
126
126
  end
127
127
  end
128
128
  end