hexapdf 0.26.2 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|