hexapdf 1.10.0 → 1.11.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 +30 -0
- data/lib/hexapdf/cli/command.rb +1 -2
- data/lib/hexapdf/content/canvas.rb +2 -2
- data/lib/hexapdf/digital_signature/signatures.rb +105 -0
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +0 -2
- data/lib/hexapdf/document.rb +2 -2
- data/lib/hexapdf/layout/style.rb +2 -2
- data/lib/hexapdf/layout/text_shaper.rb +1 -57
- data/lib/hexapdf/task/import_pages.rb +41 -9
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +1 -1
- data/lib/hexapdf/type/acro_form/choice_field.rb +3 -1
- data/lib/hexapdf/type/acro_form/field.rb +1 -1
- data/lib/hexapdf/type/document_security_store.rb +14 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/content/test_canvas.rb +3 -3
- data/test/hexapdf/digital_signature/common.rb +209 -2
- data/test/hexapdf/digital_signature/test_signatures.rb +131 -0
- data/test/hexapdf/layout/test_style.rb +3 -1
- data/test/hexapdf/task/test_import_pages.rb +42 -1
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +1 -1
- data/test/hexapdf/type/acro_form/test_choice_field.rb +5 -0
- data/test/hexapdf/type/acro_form/test_field.rb +3 -1
- data/test/hexapdf/type/test_document_security_store.rb +30 -0
- data/test/test_helper.rb +7 -3
- metadata +939 -905
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: da17a2a9dd8459e6f4624f59621c84f1b0be73f4d23347bffcdd6c534b58680e
|
|
4
|
+
data.tar.gz: ecdcfedc4f89722f799d04fda467acbed24bf256a1fa626623fce1d6399d6d98
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a39a7635005c7ef8be1dc0a1efc21b1aca00fba3bedb80886e5d909d9fc0ebfed0f967fff6f10f96ed6b05a883fee0337fea3802bfcde3a93a644e512d0ee3b
|
|
7
|
+
data.tar.gz: 4e9ccdbe854d0e5fb00e7042d61c758da93c5b354982b48a20f4a6cec3a07d209dac5711d1eb9b3b6274d8662acac8851124a838bed5795cc6e40c924f4bba26
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
+
## 1.11.0 - 2026-09-22
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
* [HexaPDF::DigitalSignature::Signatures#add_ltv_information] for adding
|
|
6
|
+
long term validation information to signed documents
|
|
7
|
+
* [HexaPDF::Type::DocumentSecurityStore] convenience methods for accessing
|
|
8
|
+
entries
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
* [HexaPDF::Type::AcroForm::ChoiceField#list_box_top_index] to always return a
|
|
13
|
+
valid index
|
|
14
|
+
* [HexaPDF::Task::ImportPages] to allow resizing the pages to a target media box
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
* [HexaPDF::Content::Canvas#xobject] to correctly position the bottom-left
|
|
19
|
+
corner of Form XObjects in case of scaling
|
|
20
|
+
* Reading password from standard input for the hexapdf CLI
|
|
21
|
+
* Use Adobe (not PDF spec) order for link annotation's /QuadPoints value to
|
|
22
|
+
ensure compatibility with most major PDF viewers/software
|
|
23
|
+
* [HexaPDF::Type::AcroForm::AppearanceGenerator] to work for files where one or
|
|
24
|
+
more values are not in the set of available list box options
|
|
25
|
+
* [HexaPDF::Type::AcroForm::Field#embedded_widget?] to only return if the
|
|
26
|
+
/Subtype entry is actually /Widget
|
|
27
|
+
* [HexaPDF::Task::ImportPages] to take optional content groups in Form XObjects
|
|
28
|
+
into account
|
|
29
|
+
|
|
30
|
+
|
|
1
31
|
## 1.10.0 - 2026-08-21
|
|
2
32
|
|
|
3
33
|
### Added
|
data/lib/hexapdf/cli/command.rb
CHANGED
|
@@ -410,8 +410,7 @@ module HexaPDF
|
|
|
410
410
|
def read_from_console(prompt, noecho: false)
|
|
411
411
|
IO.console.write("#{prompt}: ")
|
|
412
412
|
if noecho
|
|
413
|
-
IO.console.noecho {|io| io.gets.chomp }
|
|
414
|
-
puts
|
|
413
|
+
IO.console.noecho {|io| io.gets.chomp }.tap { puts }
|
|
415
414
|
else
|
|
416
415
|
IO.console.gets.chomp
|
|
417
416
|
end
|
|
@@ -1763,8 +1763,8 @@ module HexaPDF
|
|
|
1763
1763
|
if obj[:Subtype] != :Image
|
|
1764
1764
|
width /= obj.box.width.to_f
|
|
1765
1765
|
height /= obj.box.height.to_f
|
|
1766
|
-
left -= obj.box.left
|
|
1767
|
-
bottom -= obj.box.bottom
|
|
1766
|
+
left -= obj.box.left * width
|
|
1767
|
+
bottom -= obj.box.bottom * height
|
|
1768
1768
|
end
|
|
1769
1769
|
|
|
1770
1770
|
if left == 0 && bottom == 0 && width == 1 && height == 1
|
|
@@ -34,8 +34,10 @@
|
|
|
34
34
|
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
|
35
35
|
#++
|
|
36
36
|
|
|
37
|
+
require 'net/http'
|
|
37
38
|
require 'openssl'
|
|
38
39
|
require 'stringio'
|
|
40
|
+
require 'uri'
|
|
39
41
|
require 'hexapdf/digital_signature'
|
|
40
42
|
require 'hexapdf/error'
|
|
41
43
|
|
|
@@ -175,6 +177,41 @@ module HexaPDF
|
|
|
175
177
|
io.close if io && io != file_or_io
|
|
176
178
|
end
|
|
177
179
|
|
|
180
|
+
# Adds long-term validation information to the document.
|
|
181
|
+
#
|
|
182
|
+
# For each certificate OCSP is tried as it is smaller. If OCSP is not available, the CRL is
|
|
183
|
+
# used. If a problem is encountered, an error is thrown.
|
|
184
|
+
def add_ltv_information
|
|
185
|
+
dss = @document.catalog.dss
|
|
186
|
+
|
|
187
|
+
each do |signature|
|
|
188
|
+
certificates = signature.signature_handler.certificate_chain.dup
|
|
189
|
+
if (tsa_token = signature.signature_handler.embedded_tsa_signature)
|
|
190
|
+
certificates.concat(tsa_token.certificates)
|
|
191
|
+
end
|
|
192
|
+
cert_from_subject = certificates.each_with_object({}) {|c, h| h[c.subject] = c }
|
|
193
|
+
|
|
194
|
+
certs = []
|
|
195
|
+
ocsps = []
|
|
196
|
+
crls = []
|
|
197
|
+
certificates.each do |cert|
|
|
198
|
+
certs << cert.to_der
|
|
199
|
+
next if cert.issuer == cert.subject # skip self-signed root CA
|
|
200
|
+
|
|
201
|
+
issuer = cert_from_subject[cert.issuer]
|
|
202
|
+
if issuer && (ocsp_der = fetch_ocsp_response(cert, issuer))
|
|
203
|
+
ocsps << ocsp_der
|
|
204
|
+
elsif (crl_der = fetch_crl(cert))
|
|
205
|
+
crls << crl_der
|
|
206
|
+
else
|
|
207
|
+
raise HexaPDF::Error, "No OCSP and CRL response could be fetched for #{cert.subject}"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
dss.add_vri(signature, certs: certs, ocsps: ocsps, crls: crls)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
178
215
|
# :call-seq:
|
|
179
216
|
# signatures.each {|signature| block } -> signatures
|
|
180
217
|
# signatures.each -> Enumerator
|
|
@@ -205,6 +242,74 @@ module HexaPDF
|
|
|
205
242
|
"Signature#{index}"
|
|
206
243
|
end
|
|
207
244
|
|
|
245
|
+
# Fetches an OCSP response for +cert+ issued by +issuer+. Returns the DER-encoded response, or
|
|
246
|
+
# +nil+ if no URL is found or the request fails.
|
|
247
|
+
def fetch_ocsp_response(cert, issuer)
|
|
248
|
+
url = cert.ocsp_uris&.first
|
|
249
|
+
return nil unless url
|
|
250
|
+
|
|
251
|
+
certificate_id = OpenSSL::OCSP::CertificateId.new(cert, issuer)
|
|
252
|
+
req = OpenSSL::OCSP::Request.new
|
|
253
|
+
req.add_certid(certificate_id)
|
|
254
|
+
req.add_nonce
|
|
255
|
+
|
|
256
|
+
url = URI(url)
|
|
257
|
+
http_request = Net::HTTP::Post.new(url, 'Content-Type' => 'application/ocsp-request')
|
|
258
|
+
http_request.body = req.to_der
|
|
259
|
+
http_response = Net::HTTP.start(url.hostname, url.port, use_ssl: (url.scheme == 'https')) do |http|
|
|
260
|
+
http.request(http_request)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
if http_response.kind_of?(Net::HTTPOK)
|
|
264
|
+
ocsp_der = http_response.body
|
|
265
|
+
response = OpenSSL::OCSP::Response.new(ocsp_der)
|
|
266
|
+
basic_response = response.basic
|
|
267
|
+
if response.status != OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL ||
|
|
268
|
+
req.check_nonce(basic_response) == 0
|
|
269
|
+
return nil
|
|
270
|
+
end
|
|
271
|
+
single_response = basic_response.find_response(certificate_id)
|
|
272
|
+
return nil unless single_response && single_response.check_validity
|
|
273
|
+
if single_response.cert_status != OpenSSL::OCSP::V_CERTSTATUS_GOOD
|
|
274
|
+
raise HexaPDF::Error, "OCSP response indicates that the certificate is not valid"
|
|
275
|
+
end
|
|
276
|
+
ocsp_der
|
|
277
|
+
else
|
|
278
|
+
nil
|
|
279
|
+
end
|
|
280
|
+
rescue HexaPDF::Error
|
|
281
|
+
raise
|
|
282
|
+
rescue
|
|
283
|
+
nil
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Fetches a CRL for +cert+. Returns the DER-encoded CRL, or +nil+ if no URL is found or the
|
|
287
|
+
# request fails.
|
|
288
|
+
def fetch_crl(cert)
|
|
289
|
+
url = cert.crl_uris&.first
|
|
290
|
+
return nil unless url
|
|
291
|
+
|
|
292
|
+
http_response = Net::HTTP.get_response(URI(url))
|
|
293
|
+
if http_response.kind_of?(Net::HTTPOK)
|
|
294
|
+
crl_der = http_response.body
|
|
295
|
+
cert_serial = cert.serial
|
|
296
|
+
crl = OpenSSL::X509::CRL.new(crl_der)
|
|
297
|
+
if crl_der.include?(OpenSSL::ASN1::Integer.new(cert_serial).to_der)
|
|
298
|
+
crl = OpenSSL::X509::CRL.new(crl_der)
|
|
299
|
+
if crl.revoked.find {|r| r.serial == cert_serial }
|
|
300
|
+
raise HexaPDF::Error, "CRL response indicates that the certificate is revoked"
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
crl_der
|
|
304
|
+
else
|
|
305
|
+
nil
|
|
306
|
+
end
|
|
307
|
+
rescue HexaPDF::Error
|
|
308
|
+
raise
|
|
309
|
+
rescue
|
|
310
|
+
nil
|
|
311
|
+
end
|
|
312
|
+
|
|
208
313
|
end
|
|
209
314
|
|
|
210
315
|
end
|
|
@@ -53,8 +53,6 @@ module HexaPDF
|
|
|
53
53
|
# As the created CMS object is only meant to be used in the context of PDF signing, it also
|
|
54
54
|
# restricts certain things, like allowing only a single signer.
|
|
55
55
|
#
|
|
56
|
-
# Additionally, only RSA signatures are currently supported!
|
|
57
|
-
#
|
|
58
56
|
# See: PDF2.0 s12.8.3.3, PDF2.0 s12.8.3.4, RFC5652, ETSI TS 102 778 Parts 1-4
|
|
59
57
|
class SignedDataCreator
|
|
60
58
|
|
data/lib/hexapdf/document.rb
CHANGED
|
@@ -693,8 +693,8 @@ module HexaPDF
|
|
|
693
693
|
# If not changed, the default signing handler is DigitalSignature::Signing::DefaultHandler.
|
|
694
694
|
#
|
|
695
695
|
# *Note*: Once signing is done the document cannot be changed anymore since it was written
|
|
696
|
-
# during the signing process. If a document needs to be signed multiple times
|
|
697
|
-
# loaded again afterwards.
|
|
696
|
+
# during the signing process. If a document needs to be signed multiple times or if LTV
|
|
697
|
+
# information needs to be added, it needs to be loaded again afterwards.
|
|
698
698
|
def sign(file_or_io, handler: :default, signature: nil, write_options: {}, **handler_options)
|
|
699
699
|
handler = signatures.signing_handler(name: handler, **handler_options)
|
|
700
700
|
signatures.add(file_or_io, handler, signature: signature, write_options: write_options)
|
data/lib/hexapdf/layout/style.rb
CHANGED
|
@@ -541,8 +541,8 @@ module HexaPDF
|
|
|
541
541
|
|
|
542
542
|
page = canvas.context
|
|
543
543
|
matrix = canvas.graphics_state.ctm
|
|
544
|
-
quad_points = [*matrix.evaluate(0,
|
|
545
|
-
*matrix.evaluate(
|
|
544
|
+
quad_points = [*matrix.evaluate(0, box.height), *matrix.evaluate(box.width, box.height),
|
|
545
|
+
*matrix.evaluate(0, 0), *matrix.evaluate(box.width, 0)]
|
|
546
546
|
x_minmax = quad_points.values_at(0, 2, 4, 6).minmax
|
|
547
547
|
y_minmax = quad_points.values_at(1, 3, 5, 7).minmax
|
|
548
548
|
border_color = case @border_color
|
|
@@ -43,62 +43,6 @@ HARFBUZZ_AVAILABLE = begin
|
|
|
43
43
|
rescue LoadError
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
if HARFBUZZ_AVAILABLE
|
|
47
|
-
class HarfBuzz::Buffer #:nodoc:
|
|
48
|
-
|
|
49
|
-
GLYPH_INFO_SIZE = HarfBuzz::C::HbGlyphInfoT.size
|
|
50
|
-
GLYPH_INFO_CODEPOINT_OFFSET = HarfBuzz::C::HbGlyphInfoT.offset_of(:codepoint)
|
|
51
|
-
GLYPH_INFO_CLUSTER_OFFSET = HarfBuzz::C::HbGlyphInfoT.offset_of(:cluster)
|
|
52
|
-
GLYPH_POS_SIZE = HarfBuzz::C::HbGlyphPositionT.size
|
|
53
|
-
GLYPH_POS_XADVANCE_OFFSET = HarfBuzz::C::HbGlyphPositionT.offset_of(:x_advance)
|
|
54
|
-
GLYPH_POS_YADVANCE_OFFSET = HarfBuzz::C::HbGlyphPositionT.offset_of(:y_advance)
|
|
55
|
-
GLYPH_POS_XOFFSET_OFFSET = HarfBuzz::C::HbGlyphPositionT.offset_of(:x_offset)
|
|
56
|
-
GLYPH_POS_YOFFSET_OFFSET = HarfBuzz::C::HbGlyphPositionT.offset_of(:y_offset)
|
|
57
|
-
|
|
58
|
-
# Iterates efficiently over the shaping result without creating intermediary objects.
|
|
59
|
-
def each_result
|
|
60
|
-
return enum_for(__method__) unless block_given?
|
|
61
|
-
|
|
62
|
-
length_ptr = FFI::MemoryPointer.new(:uint)
|
|
63
|
-
infos_ptr = HarfBuzz::C.hb_buffer_get_glyph_infos(@ptr, length_ptr)
|
|
64
|
-
length_ptr = FFI::MemoryPointer.new(:uint)
|
|
65
|
-
positions_ptr = HarfBuzz::C.hb_buffer_get_glyph_positions(@ptr, length_ptr)
|
|
66
|
-
length = length_ptr.read_uint
|
|
67
|
-
|
|
68
|
-
return if infos_ptr.null? || positions_ptr.null? || length.zero?
|
|
69
|
-
|
|
70
|
-
last_info_cluster_offset = (length - 1) * GLYPH_INFO_SIZE + GLYPH_INFO_CLUSTER_OFFSET
|
|
71
|
-
i = 0
|
|
72
|
-
while i < length
|
|
73
|
-
info_offset = i * GLYPH_INFO_SIZE
|
|
74
|
-
pos_offset = i * GLYPH_POS_SIZE
|
|
75
|
-
|
|
76
|
-
glyph_id = infos_ptr.get_uint32(info_offset + GLYPH_INFO_CODEPOINT_OFFSET)
|
|
77
|
-
cluster = infos_ptr.get_uint32(info_offset + GLYPH_INFO_CLUSTER_OFFSET)
|
|
78
|
-
|
|
79
|
-
next_cluster = nil
|
|
80
|
-
tmp_offset = info_offset + GLYPH_INFO_CLUSTER_OFFSET + GLYPH_INFO_SIZE
|
|
81
|
-
while tmp_offset <= last_info_cluster_offset &&
|
|
82
|
-
(next_cluster = infos_ptr.get_uint32(tmp_offset)) == cluster
|
|
83
|
-
tmp_offset += GLYPH_INFO_SIZE
|
|
84
|
-
next_cluster = nil
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
x_advance = positions_ptr.get_int32(pos_offset + GLYPH_POS_XADVANCE_OFFSET)
|
|
88
|
-
y_advance = positions_ptr.get_int32(pos_offset + GLYPH_POS_YADVANCE_OFFSET)
|
|
89
|
-
x_offset = positions_ptr.get_int32(pos_offset + GLYPH_POS_XOFFSET_OFFSET)
|
|
90
|
-
y_offset = positions_ptr.get_int32(pos_offset + GLYPH_POS_YOFFSET_OFFSET)
|
|
91
|
-
|
|
92
|
-
yield(glyph_id, cluster, next_cluster, x_advance, y_advance, x_offset, y_offset)
|
|
93
|
-
|
|
94
|
-
i += 1
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
self
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
|
|
102
46
|
module HexaPDF
|
|
103
47
|
module Layout
|
|
104
48
|
|
|
@@ -188,7 +132,7 @@ module HexaPDF
|
|
|
188
132
|
items = text_fragment.items.clear
|
|
189
133
|
last_cluster = nil
|
|
190
134
|
last_y_offset = 0
|
|
191
|
-
buffer.
|
|
135
|
+
buffer.each_glyph_with_clusters do |glyph_id, cluster, next_cluster, x_advance, y_advance, x_offset, y_offset|
|
|
192
136
|
advance = (x_advance - x_offset) * font.scaling_factor
|
|
193
137
|
|
|
194
138
|
# 1. Determine the source characters for each glyph via their cluster numbers. If two or
|
|
@@ -51,6 +51,9 @@ module HexaPDF
|
|
|
51
51
|
# Note that the /Order, /AS and /Locked fields of the default optional content configuration
|
|
52
52
|
# dictionary are not preserved.
|
|
53
53
|
#
|
|
54
|
+
# The imported page can optionally be resized to a given page size. Note, however, that this
|
|
55
|
+
# will make all interactive elements (e.g. annotation widgets and form fields) static.
|
|
56
|
+
#
|
|
54
57
|
# Example:
|
|
55
58
|
#
|
|
56
59
|
# doc.task(:import_pages, source: source_doc, pages: [1..-2])
|
|
@@ -87,7 +90,20 @@ module HexaPDF
|
|
|
87
90
|
#
|
|
88
91
|
# +:merge+:: Merge AcroForm fields using the MergeAcroForm task.
|
|
89
92
|
# +:ignore+:: Ignore AcroForm fields.
|
|
90
|
-
|
|
93
|
+
#
|
|
94
|
+
# +:resize_to+::
|
|
95
|
+
# Resizes the pages to the given page size (either a pre-defined name or an array
|
|
96
|
+
# specifying the media box), potentially deforming it. This is done by converting the page
|
|
97
|
+
# to a Form XObject and then painting it.
|
|
98
|
+
#
|
|
99
|
+
# Note 1: By specifying this argument, the argument +acro_form+ is handled as if it were
|
|
100
|
+
# :ignore.
|
|
101
|
+
#
|
|
102
|
+
# Note 2: Annotations without an appearance stream are ignored. If that is something that
|
|
103
|
+
# needs to be considered, +page.flatten_annotations+ should be called beforehand for each
|
|
104
|
+
# source page and the results handled.
|
|
105
|
+
def self.call(doc, source:, pages: :all, append: true, ocgs: :preserve, acro_form: :merge,
|
|
106
|
+
resize_to: nil)
|
|
91
107
|
# Retrieve all specified source pages
|
|
92
108
|
pages = if pages == :all
|
|
93
109
|
source.pages.each.to_a
|
|
@@ -106,6 +122,7 @@ module HexaPDF
|
|
|
106
122
|
# Import the source pages and optionally append them to the target page tree
|
|
107
123
|
pages = pages.map do |page|
|
|
108
124
|
imported_page = doc.import(page)
|
|
125
|
+
imported_page = resize_page(doc, imported_page, resize_to) if resize_to
|
|
109
126
|
doc.pages << imported_page if append
|
|
110
127
|
imported_page
|
|
111
128
|
end
|
|
@@ -128,17 +145,19 @@ module HexaPDF
|
|
|
128
145
|
ocgs.merge(obj[:OCGs].to_ary)
|
|
129
146
|
end
|
|
130
147
|
end
|
|
148
|
+
process_page_or_form = lambda do |page_or_form|
|
|
149
|
+
page_or_form.resources[:Properties]&.each do |name, obj|
|
|
150
|
+
process_ocg_or_ocmd.call(obj) if obj
|
|
151
|
+
end
|
|
152
|
+
page_or_form.resources[:XObject]&.each do |name, obj|
|
|
153
|
+
process_ocg_or_ocmd.call(obj[:OC]) if obj.key?(:OC)
|
|
154
|
+
process_page_or_form.call(obj) if obj[:Subtype] == :Form
|
|
155
|
+
end
|
|
156
|
+
end
|
|
131
157
|
seen_resources = {}
|
|
132
158
|
pages.each do |page|
|
|
133
159
|
unless seen_resources[page.resources] # handle case when pages share the resources dict
|
|
134
|
-
page
|
|
135
|
-
next unless obj
|
|
136
|
-
process_ocg_or_ocmd.call(obj)
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
page.resources[:XObject]&.each do |name, obj|
|
|
140
|
-
process_ocg_or_ocmd.call(obj[:OC]) if obj.key?(:OC)
|
|
141
|
-
end
|
|
160
|
+
process_page_or_form.call(page)
|
|
142
161
|
end
|
|
143
162
|
|
|
144
163
|
page.each_annotation do |annot|
|
|
@@ -178,6 +197,19 @@ module HexaPDF
|
|
|
178
197
|
(target_config[:RBGroups] ||= []) << result
|
|
179
198
|
end
|
|
180
199
|
end
|
|
200
|
+
private_class_method :preserve_ocgs
|
|
201
|
+
|
|
202
|
+
# Resizes the +page+ to the given +media_box+ exactly, potentially deforming it.
|
|
203
|
+
def self.resize_page(doc, page, media_box)
|
|
204
|
+
page.flatten_annotations
|
|
205
|
+
form = page.to_form_xobject
|
|
206
|
+
doc.delete(page)
|
|
207
|
+
page = doc.pages.create(media_box: media_box)
|
|
208
|
+
media_box = page.box(:media) # needed because before media_box could be e.g. :A4
|
|
209
|
+
page.canvas.xobject(form, at: [0, 0], width: media_box.width, height: media_box.height)
|
|
210
|
+
page
|
|
211
|
+
end
|
|
212
|
+
private_class_method :resize_page
|
|
181
213
|
|
|
182
214
|
end
|
|
183
215
|
|
|
@@ -505,7 +505,7 @@ module HexaPDF
|
|
|
505
505
|
top_index = @field.list_box_top_index
|
|
506
506
|
items = @document.layout.text_fragments(option_items[top_index..-1].join("\n"), style: style)
|
|
507
507
|
# Should use /I but if it differs from /V, we need to use /V; so just use /V...
|
|
508
|
-
indices = [@field.field_value].flatten.compact.
|
|
508
|
+
indices = [@field.field_value].flatten.compact.filter_map {|val| option_items.index(val) }
|
|
509
509
|
|
|
510
510
|
layouter = Layout::TextLayouter.new(style)
|
|
511
511
|
layouter.style.text_align(@field.text_alignment).line_spacing(:proportional, 1.25)
|
|
@@ -199,7 +199,9 @@ module HexaPDF
|
|
|
199
199
|
|
|
200
200
|
# Returns the index of the first visible option item of a list box.
|
|
201
201
|
def list_box_top_index
|
|
202
|
-
|
|
202
|
+
prepare_option_items
|
|
203
|
+
ti = self[:TI]
|
|
204
|
+
ti.kind_of?(Integer) && self[:Opt] && ti.between?(0, self[:Opt].length - 1) ? ti : 0
|
|
203
205
|
end
|
|
204
206
|
|
|
205
207
|
# Makes the option item referred to via the given +index+ the first visible option item of a
|
|
@@ -94,6 +94,11 @@ module HexaPDF
|
|
|
94
94
|
(self[:VRI] ||= {})[key] = document.add(vri)
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
# Returns the VRI dictionary for the given signature if it exists or else +nil+.
|
|
98
|
+
def vri_for(signature)
|
|
99
|
+
self[:VRI] && self[:VRI][OpenSSL::Digest::SHA1.hexdigest(signature.contents).upcase.to_sym]
|
|
100
|
+
end
|
|
101
|
+
|
|
97
102
|
# Adds the DER-encoded certificate to the /Certs array if not already present and returns
|
|
98
103
|
# the stream object containing it.
|
|
99
104
|
def add_cert(cert_der)
|
|
@@ -112,6 +117,15 @@ module HexaPDF
|
|
|
112
117
|
add_data_as_stream_to_field(crl_der, :CRLs)
|
|
113
118
|
end
|
|
114
119
|
|
|
120
|
+
# Returns all certificates in the /Certs array as OpenSSL::X509::Certificate objects.
|
|
121
|
+
def certificates
|
|
122
|
+
if key?(:Certs)
|
|
123
|
+
self[:Certs].length.times.map {|i| OpenSSL::X509::Certificate.new(self[:Certs][i].stream) }
|
|
124
|
+
else
|
|
125
|
+
[]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
115
129
|
private
|
|
116
130
|
|
|
117
131
|
# Adds the given string +data+ to the +field+ array as stream object and returns the resulting
|
data/lib/hexapdf/version.rb
CHANGED
|
@@ -942,7 +942,7 @@ describe HexaPDF::Content::Canvas do
|
|
|
942
942
|
it "correctly serializes the form with just the width given" do
|
|
943
943
|
@canvas.image(@form, at: [1, 2], width: 50)
|
|
944
944
|
assert_operators(@page.contents, [[:save_graphics_state],
|
|
945
|
-
[:concatenate_matrix, [0.5, 0, 0, 0.5, -
|
|
945
|
+
[:concatenate_matrix, [0.5, 0, 0, 0.5, -49, -23]],
|
|
946
946
|
[:paint_xobject, [:XO1]],
|
|
947
947
|
[:restore_graphics_state]])
|
|
948
948
|
end
|
|
@@ -950,7 +950,7 @@ describe HexaPDF::Content::Canvas do
|
|
|
950
950
|
it "correctly serializes the form with just the height given" do
|
|
951
951
|
@canvas.image(@form, at: [1, 2], height: 10)
|
|
952
952
|
assert_operators(@page.contents, [[:save_graphics_state],
|
|
953
|
-
[:concatenate_matrix, [0.2, 0, 0, 0.2, -
|
|
953
|
+
[:concatenate_matrix, [0.2, 0, 0, 0.2, -19, -8]],
|
|
954
954
|
[:paint_xobject, [:XO1]],
|
|
955
955
|
[:restore_graphics_state]])
|
|
956
956
|
end
|
|
@@ -958,7 +958,7 @@ describe HexaPDF::Content::Canvas do
|
|
|
958
958
|
it "correctly serializes the form with both width and height given" do
|
|
959
959
|
@canvas.image(@form, at: [1, 2], width: 50, height: 10)
|
|
960
960
|
assert_operators(@page.contents, [[:save_graphics_state],
|
|
961
|
-
[:concatenate_matrix, [0.5, 0, 0, 0.2, -
|
|
961
|
+
[:concatenate_matrix, [0.5, 0, 0, 0.2, -49, -8]],
|
|
962
962
|
[:paint_xobject, [:XO1]],
|
|
963
963
|
[:restore_graphics_state]])
|
|
964
964
|
end
|