hexapdf 0.28.0 → 0.31.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.
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)