hexapdf 0.28.0 → 0.31.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +86 -10
  3. data/examples/024-digital-signatures.rb +23 -0
  4. data/lib/hexapdf/cli/command.rb +16 -1
  5. data/lib/hexapdf/cli/info.rb +9 -1
  6. data/lib/hexapdf/cli/inspect.rb +2 -2
  7. data/lib/hexapdf/composer.rb +76 -28
  8. data/lib/hexapdf/configuration.rb +29 -16
  9. data/lib/hexapdf/dictionary_fields.rb +13 -4
  10. data/lib/hexapdf/digital_signature/cms_handler.rb +137 -0
  11. data/lib/hexapdf/digital_signature/handler.rb +138 -0
  12. data/lib/hexapdf/digital_signature/pkcs1_handler.rb +96 -0
  13. data/lib/hexapdf/{type → digital_signature}/signature.rb +3 -8
  14. data/lib/hexapdf/digital_signature/signatures.rb +210 -0
  15. data/lib/hexapdf/digital_signature/signing/default_handler.rb +317 -0
  16. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +308 -0
  17. data/lib/hexapdf/digital_signature/signing/timestamp_handler.rb +148 -0
  18. data/lib/hexapdf/digital_signature/signing.rb +101 -0
  19. data/lib/hexapdf/{type/signature → digital_signature}/verification_result.rb +37 -41
  20. data/lib/hexapdf/digital_signature.rb +56 -0
  21. data/lib/hexapdf/document/pages.rb +31 -18
  22. data/lib/hexapdf/document.rb +29 -15
  23. data/lib/hexapdf/encryption/standard_security_handler.rb +4 -3
  24. data/lib/hexapdf/filter/flate_decode.rb +20 -8
  25. data/lib/hexapdf/layout/page_style.rb +144 -0
  26. data/lib/hexapdf/layout.rb +1 -0
  27. data/lib/hexapdf/task/optimize.rb +8 -6
  28. data/lib/hexapdf/type/font_simple.rb +14 -2
  29. data/lib/hexapdf/type/object_stream.rb +7 -2
  30. data/lib/hexapdf/type/outline.rb +1 -1
  31. data/lib/hexapdf/type/outline_item.rb +1 -1
  32. data/lib/hexapdf/type/page.rb +29 -8
  33. data/lib/hexapdf/type/xref_stream.rb +11 -4
  34. data/lib/hexapdf/type.rb +0 -1
  35. data/lib/hexapdf/version.rb +1 -1
  36. data/lib/hexapdf/writer.rb +1 -1
  37. data/test/hexapdf/{type/signature → digital_signature}/common.rb +31 -3
  38. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +162 -0
  39. data/test/hexapdf/digital_signature/signing/test_signed_data_creator.rb +225 -0
  40. data/test/hexapdf/digital_signature/signing/test_timestamp_handler.rb +88 -0
  41. data/test/hexapdf/{type/signature/test_adbe_pkcs7_detached.rb → digital_signature/test_cms_handler.rb} +7 -7
  42. data/test/hexapdf/{type/signature → digital_signature}/test_handler.rb +4 -4
  43. data/test/hexapdf/{type/signature/test_adbe_x509_rsa_sha1.rb → digital_signature/test_pkcs1_handler.rb} +3 -3
  44. data/test/hexapdf/{type → digital_signature}/test_signature.rb +7 -7
  45. data/test/hexapdf/digital_signature/test_signatures.rb +137 -0
  46. data/test/hexapdf/digital_signature/test_signing.rb +53 -0
  47. data/test/hexapdf/{type/signature → digital_signature}/test_verification_result.rb +7 -7
  48. data/test/hexapdf/document/test_pages.rb +25 -0
  49. data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -2
  50. data/test/hexapdf/filter/test_flate_decode.rb +19 -5
  51. data/test/hexapdf/layout/test_page_style.rb +70 -0
  52. data/test/hexapdf/task/test_optimize.rb +11 -9
  53. data/test/hexapdf/test_composer.rb +35 -10
  54. data/test/hexapdf/test_dictionary_fields.rb +9 -3
  55. data/test/hexapdf/test_document.rb +1 -1
  56. data/test/hexapdf/test_writer.rb +8 -8
  57. data/test/hexapdf/type/test_font_simple.rb +18 -6
  58. data/test/hexapdf/type/test_object_stream.rb +16 -7
  59. data/test/hexapdf/type/test_outline.rb +3 -1
  60. data/test/hexapdf/type/test_outline_item.rb +3 -1
  61. data/test/hexapdf/type/test_page.rb +42 -11
  62. data/test/hexapdf/type/test_xref_stream.rb +6 -1
  63. metadata +27 -15
  64. data/lib/hexapdf/document/signatures.rb +0 -546
  65. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +0 -135
  66. data/lib/hexapdf/type/signature/adbe_x509_rsa_sha1.rb +0 -95
  67. data/lib/hexapdf/type/signature/handler.rb +0 -140
  68. data/test/hexapdf/document/test_signatures.rb +0 -352
@@ -0,0 +1,148 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2022 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ require 'openssl'
38
+ require 'net/http'
39
+ require 'hexapdf/error'
40
+ require 'stringio'
41
+
42
+ module HexaPDF
43
+ module DigitalSignature
44
+ module Signing
45
+
46
+ # This is a signing handler for adding a timestamp signature (a PDF2.0 feature) to a PDF
47
+ # document. It is registered under the :timestamp name.
48
+ #
49
+ # The timestamp is provided by a timestamp authority and establishes the document contents at
50
+ # the time indicated in the timestamp. Timestamping a PDF document is usually done in context
51
+ # of long term validation but can also be done standalone.
52
+ #
53
+ # == Usage
54
+ #
55
+ # It is necessary to provide at least the URL of the timestamp authority server (TSA) via
56
+ # #tsa_url, everything else is optional and uses default values. The TSA server must not use
57
+ # authentication to be usable.
58
+ #
59
+ # Example:
60
+ #
61
+ # document.sign("output.pdf", handler: :timestamp, tsa_url: 'https://freetsa.org/tsr')
62
+ class TimestampHandler
63
+
64
+ # The URL of the timestamp authority server.
65
+ #
66
+ # This value is required.
67
+ attr_accessor :tsa_url
68
+
69
+ # The hash algorithm to use for timestamping. Defaults to SHA512.
70
+ attr_accessor :tsa_hash_algorithm
71
+
72
+ # The policy OID to use for timestamping. Defaults to +nil+.
73
+ attr_accessor :tsa_policy_id
74
+
75
+ # The size of the serialized signature that should be reserved.
76
+ #
77
+ # If this attribute has not been set, an empty string will be signed using #sign to
78
+ # determine the signature size which will contact the TSA server
79
+ #
80
+ # The size needs to be at least as big as the final signature, otherwise signing results in
81
+ # an error.
82
+ attr_writer :signature_size
83
+
84
+ # The reason for timestamping. If used, will be set on the signature object.
85
+ attr_accessor :reason
86
+
87
+ # The timestamping location. If used, will be set on the signature object.
88
+ attr_accessor :location
89
+
90
+ # The contact information. If used, will be set on the signature object.
91
+ attr_accessor :contact_info
92
+
93
+ # Creates a new TimestampHandler with the given attributes.
94
+ def initialize(**arguments)
95
+ @signature_size = nil
96
+ arguments.each {|name, value| send("#{name}=", value) }
97
+ end
98
+
99
+ # Returns the size of the serialized signature that should be reserved.
100
+ def signature_size
101
+ @signature_size || (sign(StringIO.new, [0, 0, 0, 0]).size * 1.5).to_i
102
+ end
103
+
104
+ # Finalizes the signature field as well as the signature dictionary before writing.
105
+ def finalize_objects(_signature_field, signature)
106
+ signature.document.version = '2.0'
107
+ signature[:Type] = :DocTimeStamp
108
+ signature[:Filter] = :'Adobe.PPKLite'
109
+ signature[:SubFilter] = :'ETSI.RFC3161'
110
+ signature[:Reason] = reason if reason
111
+ signature[:Location] = location if location
112
+ signature[:ContactInfo] = contact_info if contact_info
113
+ end
114
+
115
+ # Returns the DER serialized OpenSSL::PKCS7 structure containing the timestamp token for the
116
+ # given IO byte ranges.
117
+ def sign(io, byte_range)
118
+ hash_algorithm = tsa_hash_algorithm || 'SHA512'
119
+ digest = OpenSSL::Digest.new(hash_algorithm)
120
+ io.pos = byte_range[0]
121
+ digest << io.read(byte_range[1])
122
+ io.pos = byte_range[2]
123
+ digest << io.read(byte_range[3])
124
+
125
+ req = OpenSSL::Timestamp::Request.new
126
+ req.algorithm = hash_algorithm
127
+ req.message_imprint = digest.digest
128
+ req.policy_id = tsa_policy_id if tsa_policy_id
129
+
130
+ http_response = Net::HTTP.post(URI(tsa_url), req.to_der,
131
+ 'content-type' => 'application/timestamp-query')
132
+ if http_response.kind_of?(Net::HTTPOK)
133
+ response = OpenSSL::Timestamp::Response.new(http_response.body)
134
+ if response.status == 0
135
+ response.token.to_der
136
+ else
137
+ raise HexaPDF::Error, "Timestamp token could not be created: #{response.failure_info}"
138
+ end
139
+ else
140
+ raise HexaPDF::Error, "Invalid TSA server response: #{http_response.body}"
141
+ end
142
+ end
143
+
144
+ end
145
+
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,101 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2022 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ require 'hexapdf/document'
38
+
39
+ module HexaPDF
40
+ module DigitalSignature
41
+
42
+ # This module contains everything related to the signing of a PDF document, i.e. signing
43
+ # handlers and the actual code for signing.
44
+ module Signing
45
+
46
+ autoload(:DefaultHandler, 'hexapdf/digital_signature/signing/default_handler')
47
+ autoload(:TimestampHandler, 'hexapdf/digital_signature/signing/timestamp_handler')
48
+ autoload(:SignedDataCreator, 'hexapdf/digital_signature/signing/signed_data_creator')
49
+
50
+ # Embeds the given +signature+ into the /Contents value of the newest signature dictionary of
51
+ # the PDF document given by the +io+ argument.
52
+ #
53
+ # This functionality can be used together with the support for external signing (see
54
+ # DefaultHandler and DefaultHandler#external_signing) to implement asynchronous signing.
55
+ #
56
+ # Note: This will, most probably, only work on documents prepared for external signing by
57
+ # HexaPDF and not by other libraries.
58
+ def self.embed_signature(io, signature)
59
+ doc = HexaPDF::Document.new(io: io)
60
+ signature_dict = doc.signatures.find {|sig| doc.revisions.current.object(sig) == sig }
61
+ signature_dict_offset, signature_dict_length = locate_signature_dict(
62
+ doc.revisions.current.xref_section,
63
+ doc.revisions.parser.startxref_offset,
64
+ signature_dict.oid
65
+ )
66
+ io.pos = signature_dict_offset
67
+ signature_data = io.read(signature_dict_length)
68
+ replace_signature_contents(signature_data, signature)
69
+ io.pos = signature_dict_offset
70
+ io.write(signature_data)
71
+ end
72
+
73
+ # Uses the information in the given cross-reference section as well as the byte offset of the
74
+ # cross-reference section to calculate the offset and length of the signature dictionary with
75
+ # the given object id.
76
+ def self.locate_signature_dict(xref_section, start_xref_position, signature_oid)
77
+ data = xref_section.map {|oid, _gen, entry| [entry.pos, oid] if entry.in_use? }.compact.sort <<
78
+ [start_xref_position, nil]
79
+ index = data.index {|_pos, oid| oid == signature_oid }
80
+ [data[index][0], data[index + 1][0] - data[index][0]]
81
+ end
82
+
83
+ # Replaces the value of the /Contents key in the serialized +signature_data+ with the value of
84
+ # +contents+.
85
+ def self.replace_signature_contents(signature_data, contents)
86
+ signature_data.sub!(/Contents(?:\(.*?\)|<.*?>)/) do |match|
87
+ length = match.size
88
+ result = "Contents<#{contents.unpack1('H*')}"
89
+ if length < result.size
90
+ raise HexaPDF::Error, "The reserved space for the signature was too small " \
91
+ "(#{(length - 10) / 2} vs #{(result.size - 10) / 2}) - use the handlers " \
92
+ "#signature_size method to increase the reserved space"
93
+ end
94
+ "#{result.ljust(length - 1, '0')}>"
95
+ end
96
+ end
97
+
98
+ end
99
+
100
+ end
101
+ end
@@ -34,59 +34,55 @@
34
34
  # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
35
  #++
36
36
 
37
- require 'hexapdf/type/signature'
38
-
39
37
  module HexaPDF
40
- module Type
41
- class Signature
38
+ module DigitalSignature
42
39
 
43
- # Holds the result information when verifying a signature.
44
- class VerificationResult
40
+ # Holds the result information when verifying a signature.
41
+ class VerificationResult
45
42
 
46
- # :nodoc:
47
- MESSAGE_SORT_MAP = {
48
- info: {warning: 1, error: 1, info: 0},
49
- warning: {info: -1, error: 1, warning: 0},
50
- error: {info: -1, warning: -1, error: 0},
51
- }
43
+ # :nodoc:
44
+ MESSAGE_SORT_MAP = {
45
+ info: {warning: 1, error: 1, info: 0},
46
+ warning: {info: -1, error: 1, warning: 0},
47
+ error: {info: -1, warning: -1, error: 0},
48
+ }
52
49
 
53
- # This structure represents a single status message, containing the type (:info, :warning,
54
- # :error) and the content of the message.
55
- Message = Struct.new(:type, :content) do
56
- def <=>(other)
57
- MESSAGE_SORT_MAP[type][other.type]
58
- end
50
+ # This structure represents a single status message, containing the type (:info, :warning,
51
+ # :error) and the content of the message.
52
+ Message = Struct.new(:type, :content) do
53
+ def <=>(other)
54
+ MESSAGE_SORT_MAP[type][other.type]
59
55
  end
56
+ end
60
57
 
61
- # An array with all result messages.
62
- attr_reader :messages
63
-
64
- # Creates an empty result object.
65
- def initialize
66
- @messages = []
67
- end
58
+ # An array with all result messages.
59
+ attr_reader :messages
68
60
 
69
- # Returns +true+ if there are no error messages.
70
- def success?
71
- @messages.none? {|message| message.type == :error }
72
- end
61
+ # Creates an empty result object.
62
+ def initialize
63
+ @messages = []
64
+ end
73
65
 
74
- # Returns +true+ if there is at least one error message.
75
- def failure?
76
- !success?
77
- end
66
+ # Returns +true+ if there are no error messages.
67
+ def success?
68
+ @messages.none? {|message| message.type == :error }
69
+ end
78
70
 
79
- # Adds a new message of the given type to this result object.
80
- #
81
- # +type+:: One of :info, :warning or :error.
82
- #
83
- # +content+:: The log message.
84
- def log(type, content)
85
- @messages << Message.new(type, content)
86
- end
71
+ # Returns +true+ if there is at least one error message.
72
+ def failure?
73
+ !success?
74
+ end
87
75
 
76
+ # Adds a new message of the given type to this result object.
77
+ #
78
+ # +type+:: One of :info, :warning or :error.
79
+ #
80
+ # +content+:: The log message.
81
+ def log(type, content)
82
+ @messages << Message.new(type, content)
88
83
  end
89
84
 
90
85
  end
86
+
91
87
  end
92
88
  end
@@ -0,0 +1,56 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2022 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ module HexaPDF
38
+
39
+ # PDF documents can be signed using digital signatures. Such a signature can be used to
40
+ # authenticate the identity of the signer and the contents of the documents.
41
+ #
42
+ # This module contains all code related to digital signatures in PDF.
43
+ #
44
+ # See: PDF1.7/2.0 s12.8
45
+ module DigitalSignature
46
+
47
+ autoload(:Signatures, 'hexapdf/digital_signature/signatures')
48
+ autoload(:Signature, "hexapdf/digital_signature/signature")
49
+ autoload(:Handler, 'hexapdf/digital_signature/handler')
50
+ autoload(:CMSHandler, "hexapdf/digital_signature/cms_handler")
51
+ autoload(:PKCS1Handler, "hexapdf/digital_signature/pkcs1_handler")
52
+ autoload(:VerificationResult, 'hexapdf/digital_signature/verification_result')
53
+ autoload(:Signing, 'hexapdf/digital_signature/signing')
54
+
55
+ end
56
+ end
@@ -75,28 +75,41 @@ module HexaPDF
75
75
  @document.catalog.pages
76
76
  end
77
77
 
78
- # :call-seq:
79
- # pages.add -> new_page
80
- # pages.add(media_box, orientation: :portrait) -> new_page
81
- # pages.add(page) -> page
78
+ # Creates a page object and returns it *without* adding it to the page tree.
79
+ #
80
+ # +media_box+::
81
+ # If this argument is +nil+/not specified, the value is taken from the configuration
82
+ # option 'page.default_media_box'.
83
+ #
84
+ # If the resulting value is an array with four numbers (specifying the media box), the new
85
+ # page will have these exact dimensions.
82
86
  #
83
- # Adds the page or a new empty page at the end and returns it.
87
+ # If the value is a symbol, it is taken as a reference to a pre-defined media box in
88
+ # HexaPDF::Type::Page::PAPER_SIZE. The +orientation+ can then be used to specify the page
89
+ # orientation.
84
90
  #
85
- # If no argument is given, a new page with the default dimensions (see configuration option
86
- # 'page.default_media_box') is used.
91
+ # +orientation+::
92
+ # If this argument is not specified, it is taken from 'page.default_media_orientation'. It
93
+ # is only used if +media_box+ is a symbol and not an array.
94
+ def create(media_box: nil, orientation: nil)
95
+ media_box ||= @document.config['page.default_media_box']
96
+ orientation ||= @document.config['page.default_media_orientation']
97
+ box = Type::Page.media_box(media_box, orientation: orientation)
98
+ @document.add({Type: :Page, MediaBox: box})
99
+ end
100
+
101
+ # :call-seq:
102
+ # pages.add -> new_page
103
+ # pages.add(page) -> page
104
+ # pages.add(media_box, orientation: nil) -> new_page
87
105
  #
88
- # If the single argument is an array with four numbers (specifying the media box), the new
89
- # page will have these dimensions.
106
+ # Adds the given page or a new empty page at the end and returns it.
90
107
  #
91
- # If the single argument is a symbol, it is taken as referencing a pre-defined media box in
92
- # HexaPDF::Type::Page::PAPER_SIZE for the new page. The optional argument +orientation+ can be
93
- # used to change the orientation to :landscape if needed.
94
- def add(page = nil, orientation: :portrait)
95
- if page.kind_of?(Array)
96
- page = @document.add({Type: :Page, MediaBox: page})
97
- elsif page.kind_of?(Symbol)
98
- box = Type::Page.media_box(page, orientation: orientation)
99
- page = @document.add({Type: :Page, MediaBox: box})
108
+ # If called with a page object as argument, that page object is used. Otherwise #create is
109
+ # called with the arguments +media_box+ and +orientation+ to create a new page.
110
+ def add(page = nil, orientation: nil)
111
+ unless page.kind_of?(HexaPDF::Type::Page)
112
+ page = create(media_box: page, orientation: orientation)
100
113
  end
101
114
  @document.catalog.pages.add_page(page)
102
115
  end
@@ -52,6 +52,7 @@ require 'hexapdf/importer'
52
52
  require 'hexapdf/image_loader'
53
53
  require 'hexapdf/font_loader'
54
54
  require 'hexapdf/layout'
55
+ require 'hexapdf/digital_signature'
55
56
 
56
57
  begin
57
58
  require 'hexapdf/cext'
@@ -105,7 +106,6 @@ module HexaPDF
105
106
  autoload(:Fonts, 'hexapdf/document/fonts')
106
107
  autoload(:Images, 'hexapdf/document/images')
107
108
  autoload(:Files, 'hexapdf/document/files')
108
- autoload(:Signatures, 'hexapdf/document/signatures')
109
109
  autoload(:Destinations, 'hexapdf/document/destinations')
110
110
  autoload(:Layout, 'hexapdf/document/layout')
111
111
 
@@ -152,15 +152,19 @@ module HexaPDF
152
152
  #
153
153
  # Options:
154
154
  #
155
- # io:: If an IO object is provided, then this document can read PDF objects from this IO
156
- # object, otherwise it can only contain created PDF objects.
155
+ # io::
156
+ # If an IO object is provided, then this document can read PDF objects from this IO object,
157
+ # otherwise it can only contain created PDF objects.
157
158
  #
158
- # decryption_opts:: A hash with options for decrypting the PDF objects loaded from the IO.
159
+ # decryption_opts::
160
+ # A hash with options for decrypting the PDF objects loaded from the IO. The PDF standard
161
+ # security handler expects a :password key to be set to either the user or owner password of
162
+ # the PDF file.
159
163
  #
160
- # config:: A hash with configuration options that is deep-merged into the default configuration
161
- # (see
162
- # HexaPDF::DefaultDocumentConfiguration[../index.html#DefaultDocumentConfiguration],
163
- # meaning that direct sub-hashes are merged instead of overwritten.
164
+ # config::
165
+ # A hash with configuration options that is deep-merged into the default configuration (see
166
+ # HexaPDF::DefaultDocumentConfiguration[../index.html#DefaultDocumentConfiguration], meaning
167
+ # that direct sub-hashes are merged instead of overwritten.
164
168
  def initialize(io: nil, decryption_opts: {}, config: {})
165
169
  @config = Configuration.with_defaults(config)
166
170
  @version = '1.2'
@@ -321,7 +325,14 @@ module HexaPDF
321
325
  type = (klass <= HexaPDF::Dictionary ? klass.type : nil)
322
326
  else
323
327
  type ||= deref(data.value[:Type]) if data.value.kind_of?(Hash)
324
- klass = GlobalConfiguration.constantize('object.type_map', type) { nil } if type
328
+ if type
329
+ klass = GlobalConfiguration.constantize('object.type_map', type) { nil }
330
+ if (type == :ObjStm || type == :XRef) &&
331
+ klass.each_field.any? {|name, field| field.required? && !data.value.key?(name) }
332
+ data.value.delete(:Type)
333
+ klass = nil
334
+ end
335
+ end
325
336
  end
326
337
 
327
338
  if data.value.kind_of?(Hash)
@@ -585,25 +596,28 @@ module HexaPDF
585
596
  acro_form&.signature_flag?(:signatures_exist)
586
597
  end
587
598
 
588
- # Returns an array with the digital signatures of this document.
599
+ # Returns a DigitalSignature::Signatures object that allows working with the digital signatures
600
+ # of this document.
589
601
  def signatures
590
- @signatures ||= Signatures.new(self)
602
+ @signatures ||= DigitalSignature::Signatures.new(self)
591
603
  end
592
604
 
593
605
  # Signs the document and writes it to the given file or IO object.
594
606
  #
595
607
  # For details on the arguments +file_or_io+, +signature+ and +write_options+ see
596
- # HexaPDF::Document::Signatures#add.
608
+ # HexaPDF::DigitalSignature::Signatures#add.
597
609
  #
598
610
  # The signing handler to be used is determined by the +handler+ argument together with the rest
599
- # of the keyword arguments (see HexaPDF::Document::Signatures#handler for details).
611
+ # of the keyword arguments (see HexaPDF::DigitalSignature::Signatures#signing_handler for
612
+ # details).
600
613
  #
601
- # If not changed, the default signing handler is HexaPDF::Document::Signatures::DefaultHandler.
614
+ # If not changed, the default signing handler is
615
+ # HexaPDF::DigitalSignature::Signing::DefaultHandler.
602
616
  #
603
617
  # *Note*: Once signing is done the document cannot be changed anymore since it was written. If a
604
618
  # document needs to be signed multiple times, it needs to be loaded again after writing.
605
619
  def sign(file_or_io, handler: :default, signature: nil, write_options: {}, **handler_options)
606
- handler = signatures.handler(name: handler, **handler_options)
620
+ handler = signatures.signing_handler(name: handler, **handler_options)
607
621
  signatures.add(file_or_io, handler, signature: signature, write_options: write_options)
608
622
  end
609
623
 
@@ -97,7 +97,8 @@ module HexaPDF
97
97
  # a user is allowed to do with a PDF file.
98
98
  #
99
99
  # When a user or owner password is specified, a PDF file can only be opened when the correct
100
- # password is supplied.
100
+ # password is supplied. To open such an encrypted PDF file, the +decryption_opts+ provided to
101
+ # HexaPDF::Document.new needs to contain a :password key with the password.
101
102
  #
102
103
  # See: PDF1.7 s7.6.3, PDF2.0 s7.6.3
103
104
  class StandardSecurityHandler < SecurityHandler
@@ -323,10 +324,10 @@ module HexaPDF
323
324
  def prepare_decryption(password: '', check_permissions: true)
324
325
  if dict[:Filter] != :Standard
325
326
  raise(HexaPDF::UnsupportedEncryptionError,
326
- "Invalid /Filter value for standard security handler")
327
+ "Invalid /Filter value #{dict[:Filter]} for standard security handler")
327
328
  elsif ![2, 3, 4, 6].include?(dict[:R])
328
329
  raise(HexaPDF::UnsupportedEncryptionError,
329
- "Invalid /R value for standard security handler")
330
+ "Invalid /R value #{dict[:R]} for standard security handler")
330
331
  elsif dict[:R] <= 4 && !document.trailer[:ID].kind_of?(PDFArray)
331
332
  document.trailer[:ID] = ['', '']
332
333
  end
@@ -55,21 +55,33 @@ module HexaPDF
55
55
  def self.decoder(source, options = nil)
56
56
  fib = Fiber.new do
57
57
  inflater = Zlib::Inflate.new
58
+ error_raised = nil
59
+
58
60
  while source.alive? && (data = source.resume)
59
61
  next if data.empty?
60
62
  begin
61
- data = inflater.inflate(data)
62
- rescue StandardError => e
63
- raise FilterError, "Problem while decoding Flate encoded stream: #{e}"
63
+ Fiber.yield(inflater.inflate(data))
64
+ rescue Zlib::DataError, Zlib::BufError => e
65
+ # Only swallow the error if it appears at the end of the stream
66
+ if error_raised || HexaPDF::GlobalConfiguration['filter.flate.on_error'].call(inflater, e)
67
+ raise FilterError, "Problem while decoding Flate encoded stream: #{e}"
68
+ else
69
+ Fiber.yield(inflater.flush_next_out)
70
+ error_raised = e
71
+ end
64
72
  end
65
- Fiber.yield(data)
66
73
  end
74
+
67
75
  begin
68
76
  data = inflater.total_in == 0 || (data = inflater.finish).empty? ? nil : data
69
77
  inflater.close
70
78
  data
71
- rescue StandardError => e
72
- raise FilterError, "Problem while decoding Flate encoded stream: #{e}"
79
+ rescue Zlib::DataError, Zlib::BufError => e
80
+ if HexaPDF::GlobalConfiguration['filter.flate.on_error'].call(inflater, e)
81
+ raise FilterError, "Problem while decoding Flate encoded stream: #{e}"
82
+ else
83
+ Fiber.yield(inflater.flush_next_out)
84
+ end
73
85
  end
74
86
  end
75
87
 
@@ -87,9 +99,9 @@ module HexaPDF
87
99
  end
88
100
 
89
101
  Fiber.new do
90
- deflater = Zlib::Deflate.new(HexaPDF::GlobalConfiguration['filter.flate_compression'],
102
+ deflater = Zlib::Deflate.new(HexaPDF::GlobalConfiguration['filter.flate.compression'],
91
103
  Zlib::MAX_WBITS,
92
- HexaPDF::GlobalConfiguration['filter.flate_memory'])
104
+ HexaPDF::GlobalConfiguration['filter.flate.memory'])
93
105
  while source.alive? && (data = source.resume)
94
106
  data = deflater.deflate(data)
95
107
  Fiber.yield(data)