hexapdf 1.9.1 → 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 +55 -0
- data/lib/hexapdf/cli/command.rb +1 -2
- data/lib/hexapdf/configuration.rb +2 -2
- data/lib/hexapdf/content/canvas.rb +2 -2
- data/lib/hexapdf/content/smart_text_extractor.rb +3 -2
- data/lib/hexapdf/dictionary_fields.rb +7 -0
- data/lib/hexapdf/digital_signature/cms_handler.rb +27 -2
- data/lib/hexapdf/digital_signature/pkcs1_handler.rb +5 -0
- 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/encryption/security_handler.rb +3 -1
- data/lib/hexapdf/layout/style.rb +2 -2
- data/lib/hexapdf/layout/text_fragment.rb +5 -1
- data/lib/hexapdf/layout/text_shaper.rb +1 -57
- data/lib/hexapdf/parser.rb +1 -2
- data/lib/hexapdf/serializer.rb +1 -1
- 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/catalog.rb +8 -1
- data/lib/hexapdf/type/document_security_store.rb +81 -13
- data/lib/hexapdf/type/font_type0.rb +1 -1
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/content/test_canvas.rb +3 -3
- data/test/hexapdf/content/test_smart_text_extractor.rb +7 -0
- data/test/hexapdf/digital_signature/common.rb +209 -2
- data/test/hexapdf/digital_signature/test_cms_handler.rb +4 -2
- data/test/hexapdf/digital_signature/test_pkcs1_handler.rb +4 -0
- data/test/hexapdf/digital_signature/test_signatures.rb +131 -0
- data/test/hexapdf/encryption/test_security_handler.rb +12 -0
- data/test/hexapdf/layout/test_style.rb +3 -1
- data/test/hexapdf/layout/test_text_fragment.rb +11 -4
- data/test/hexapdf/task/test_import_pages.rb +42 -1
- data/test/hexapdf/test_dictionary_fields.rb +8 -0
- data/test/hexapdf/test_parser.rb +25 -9
- data/test/hexapdf/test_serializer.rb +7 -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_catalog.rb +10 -0
- data/test/hexapdf/type/test_document_security_store.rb +120 -0
- data/test/test_helper.rb +7 -3
- metadata +939 -904
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,58 @@
|
|
|
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
|
+
|
|
31
|
+
## 1.10.0 - 2026-08-21
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
* Support for PDF 2.0 UTF-8 strings to
|
|
36
|
+
[HexaPDF::DictionaryFields::StringConverter]
|
|
37
|
+
* [HexaPDF::Type::Catalog#dss] for returning the document security store
|
|
38
|
+
* [HexaPDF::DigitalSignature::PKCS1Handler#embedded_tsa_signature]
|
|
39
|
+
* [HexaPDF::Type::DocumentSecurityStore] convenience methods for adding entries
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
* Removed invalid dictionary fields from [HexaPDF::Type::DocumentSecurityStore]
|
|
44
|
+
* [HexaPDF::DigitalSignature::CMSHandler] to handle padding of /Contents
|
|
45
|
+
correctly
|
|
46
|
+
* Regression in [HexaPDF::Layout::TextFragment::create_with_fallback_glyphs]
|
|
47
|
+
with respect to handling `\r\n` as a single newline
|
|
48
|
+
* Double decryption when reconstructing invalid files
|
|
49
|
+
* Parsing encrypted indirect objects that should be streams but aren't
|
|
50
|
+
* Serializing of large floats that used scientific instead of fixed-point
|
|
51
|
+
notation
|
|
52
|
+
* [HexaPDF::Content::SmartTextExtractor::layout_text_runs] to work in case of
|
|
53
|
+
degenerate input where the majority of text runs have zero height
|
|
54
|
+
|
|
55
|
+
|
|
1
56
|
## 1.9.1 - 2026-06-09
|
|
2
57
|
|
|
3
58
|
### Fixed
|
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
|
|
@@ -254,8 +254,8 @@ module HexaPDF
|
|
|
254
254
|
# In nearly all cases this option should not be changed from its default setting!
|
|
255
255
|
#
|
|
256
256
|
# document.on_invalid_string::
|
|
257
|
-
# A callable object that takes the invalid UTF-16BE encoded string and returns a valid
|
|
258
|
-
# encoded string.
|
|
257
|
+
# A callable object that takes the invalid UTF-16BE or UTF-8 encoded string and returns a valid
|
|
258
|
+
# UTF-8 encoded string.
|
|
259
259
|
#
|
|
260
260
|
# The default is to remove all invalid characters.
|
|
261
261
|
#
|
|
@@ -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
|
|
@@ -172,8 +172,9 @@ module HexaPDF
|
|
|
172
172
|
return '' if text_runs.empty?
|
|
173
173
|
|
|
174
174
|
# Use the median height of all text runs as an approximation of the main font size used on
|
|
175
|
-
# the page.
|
|
176
|
-
|
|
175
|
+
# the page. In case the majority of text runs have a height of 0, use a non-zero height
|
|
176
|
+
# value. The line tolerance uses a hard floor for small fonts.
|
|
177
|
+
median_height = [median(text_runs.map(&:height).sort), 1].max
|
|
177
178
|
line_tolerance = [median_height * line_tolerance_factor, 2].max
|
|
178
179
|
|
|
179
180
|
# Group the text runs into lines which are sorted top to bottom. Text runs are pre-sorted by
|
|
@@ -262,6 +262,13 @@ module HexaPDF
|
|
|
262
262
|
else
|
|
263
263
|
document.config['document.on_invalid_string'].call(str)
|
|
264
264
|
end
|
|
265
|
+
elsif str.getbyte(0) == 239 && str.getbyte(1) == 187 && str.getbyte(2) == 191
|
|
266
|
+
str = str[3..-1].force_encoding(Encoding::UTF_8)
|
|
267
|
+
if str.valid_encoding?
|
|
268
|
+
str
|
|
269
|
+
else
|
|
270
|
+
document.config['document.on_invalid_string'].call(str)
|
|
271
|
+
end
|
|
265
272
|
else
|
|
266
273
|
Utils::PDFDocEncoding.convert_to_utf8(str)
|
|
267
274
|
end
|
|
@@ -92,7 +92,7 @@ module HexaPDF
|
|
|
92
92
|
return @embedded_tsa_signature if defined?(@embedded_tsa_signature)
|
|
93
93
|
|
|
94
94
|
@embedded_tsa_signature = nil
|
|
95
|
-
p7 =
|
|
95
|
+
p7 = decode_asn1(signature_dict.contents)
|
|
96
96
|
signed_data = p7.value[1].value[0]
|
|
97
97
|
signer_info = signed_data.value[-1].value[0] # first (and only) signer info
|
|
98
98
|
return unless signer_info.value[-1].tag == 1 # check for unsigned attributes
|
|
@@ -140,7 +140,7 @@ module HexaPDF
|
|
|
140
140
|
|
|
141
141
|
if signature_dict.signature_type == 'ETSI.RFC3161'
|
|
142
142
|
# Getting the needed values is not directly supported by Ruby OpenSSL
|
|
143
|
-
p7 =
|
|
143
|
+
p7 = decode_asn1(signature_dict.contents)
|
|
144
144
|
signed_data = p7.value[1].value[0]
|
|
145
145
|
content_info = signed_data.value[2]
|
|
146
146
|
content = OpenSSL::ASN1.decode(content_info.value[1].value[0].value)
|
|
@@ -175,6 +175,31 @@ module HexaPDF
|
|
|
175
175
|
result
|
|
176
176
|
end
|
|
177
177
|
|
|
178
|
+
private
|
|
179
|
+
|
|
180
|
+
# Decode the first data structure in the given +data+ binary string.
|
|
181
|
+
#
|
|
182
|
+
# Since ASN1.decode raises an error if there are trailing bytes in +data+, we need to try
|
|
183
|
+
# several things to get the first data structure out of +data+ that is possibly zero-padded
|
|
184
|
+
# (due to definite and indefinite encodings; \x00\x00 is the EOD marker for indefinite
|
|
185
|
+
# encodings complicating things).
|
|
186
|
+
def decode_asn1(data)
|
|
187
|
+
length = OpenSSL::ASN1.traverse(data) do |_depth, _offset, header_length, length, *|
|
|
188
|
+
break length > 0 ? header_length + length : 0
|
|
189
|
+
end
|
|
190
|
+
if length > 0
|
|
191
|
+
OpenSSL::ASN1.decode(data[0, length])
|
|
192
|
+
else
|
|
193
|
+
begin
|
|
194
|
+
OpenSSL::ASN1.decode(data)
|
|
195
|
+
rescue OpenSSL::ASN1::ASN1Error => e
|
|
196
|
+
length = e.message.scan(/Total bytes read: (\d+)/)&.first&.first.to_i
|
|
197
|
+
data = data[0, length]
|
|
198
|
+
retry
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
178
203
|
end
|
|
179
204
|
|
|
180
205
|
end
|
|
@@ -59,6 +59,11 @@ module HexaPDF
|
|
|
59
59
|
certificate_chain.first
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
# Returns +nil+.
|
|
63
|
+
def embedded_tsa_signature
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
62
67
|
# Verifies the signature using the provided OpenSSL::X509::Store object.
|
|
63
68
|
def verify(store, allow_self_signed: false)
|
|
64
69
|
result = super
|
|
@@ -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)
|
|
@@ -276,7 +276,9 @@ module HexaPDF
|
|
|
276
276
|
str.replace(string_algorithm.decrypt(key, str, &error_proc))
|
|
277
277
|
end
|
|
278
278
|
|
|
279
|
-
|
|
279
|
+
# The (obj.raw_stream == '') case may occur for PDFs where a typed object that should be a
|
|
280
|
+
# stream isn't one. For example, if a /Type /Form object doesn't have stream ... endstream.
|
|
281
|
+
if obj.kind_of?(HexaPDF::Stream) && obj.raw_stream != '' && obj.raw_stream.filter[0] != :Crypt
|
|
280
282
|
unless string_algorithm == stream_algorithm
|
|
281
283
|
key = object_key(obj.oid, obj.gen, stream_algorithm)
|
|
282
284
|
end
|
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
|
|
@@ -119,7 +119,11 @@ module HexaPDF
|
|
|
119
119
|
items = []
|
|
120
120
|
end
|
|
121
121
|
if glyph.control_char?
|
|
122
|
-
result.
|
|
122
|
+
if result.last&.style == style
|
|
123
|
+
result.last.items << glyph
|
|
124
|
+
else
|
|
125
|
+
result.append(new([glyph], style))
|
|
126
|
+
end
|
|
123
127
|
else
|
|
124
128
|
fallback = yield(codepoint, glyph)
|
|
125
129
|
unless fallback.empty?
|
|
@@ -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
|
data/lib/hexapdf/parser.rb
CHANGED
|
@@ -531,8 +531,7 @@ module HexaPDF
|
|
|
531
531
|
|
|
532
532
|
loader = lambda do |xref_entry|
|
|
533
533
|
obj, oid, gen, stream = parse_indirect_object(xref_entry.pos)
|
|
534
|
-
|
|
535
|
-
@document.security_handler ? @document.security_handler.decrypt(obj) : obj
|
|
534
|
+
@document.wrap(obj, oid: oid, gen: gen, stream: stream)
|
|
536
535
|
end
|
|
537
536
|
|
|
538
537
|
@in_reconstruct_revision = false
|
data/lib/hexapdf/serializer.rb
CHANGED
|
@@ -206,7 +206,7 @@ module HexaPDF
|
|
|
206
206
|
if -0.0001 < obj && obj < 0.0001 && obj != 0
|
|
207
207
|
sprintf("%.6f", obj)
|
|
208
208
|
elsif obj.finite?
|
|
209
|
-
obj.round(6).to_s
|
|
209
|
+
obj.clamp(-999999999999999, 999999999999999).round(6).to_s
|
|
210
210
|
else
|
|
211
211
|
raise HexaPDF::Error, "Can't serialize special floating point number #{obj}"
|
|
212
212
|
end
|
|
@@ -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
|
data/lib/hexapdf/type/catalog.rb
CHANGED
|
@@ -84,7 +84,7 @@ module HexaPDF
|
|
|
84
84
|
define_field :Requirements, type: PDFArray, version: '1.7'
|
|
85
85
|
define_field :Collection, type: Dictionary, version: '1.7'
|
|
86
86
|
define_field :NeedsRendering, type: Boolean, version: '1.7'
|
|
87
|
-
define_field :DSS, type:
|
|
87
|
+
define_field :DSS, type: :DSS, version: '2.0'
|
|
88
88
|
define_field :AF, type: PDFArray, version: '2.0'
|
|
89
89
|
define_field :DPartRoot, type: Dictionary, version: '2.0'
|
|
90
90
|
|
|
@@ -124,6 +124,13 @@ module HexaPDF
|
|
|
124
124
|
self[:OCProperties] ||= document.add({OCGs: [], D: {Creator: 'HexaPDF'}}, type: :XXOCProperties)
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
+
# Returns the document security store, creating it if needed.
|
|
128
|
+
#
|
|
129
|
+
# See: DocumentSecurityStore
|
|
130
|
+
def dss
|
|
131
|
+
self[:DSS] ||= document.add({}, type: :DSS)
|
|
132
|
+
end
|
|
133
|
+
|
|
127
134
|
# Returns the main AcroForm object.
|
|
128
135
|
#
|
|
129
136
|
# * If an AcroForm object exists, the +create+ argument is not used.
|