hexapdf 0.46.0 → 0.47.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 +33 -0
- data/lib/hexapdf/configuration.rb +11 -0
- data/lib/hexapdf/encryption/standard_security_handler.rb +32 -26
- data/lib/hexapdf/importer.rb +1 -1
- data/lib/hexapdf/layout/table_box.rb +57 -10
- data/lib/hexapdf/task/optimize.rb +4 -4
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +8 -4
- data/lib/hexapdf/type/acro_form/form.rb +8 -5
- data/lib/hexapdf/type/acro_form/signature_field.rb +2 -1
- data/lib/hexapdf/type/acro_form/variable_text_field.rb +11 -3
- data/lib/hexapdf/type/annotations/widget.rb +4 -2
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +1 -0
- data/test/data/standard-security-handler/bothpwd-aes-256bit-V5-R5.pdf +43 -0
- data/test/data/standard-security-handler/nopwd-aes-256bit-V5-R5.pdf +44 -0
- data/test/data/standard-security-handler/ownerpwd-aes-256bit-V5-R5.pdf +43 -0
- data/test/data/standard-security-handler/userpwd-aes-256bit-V5-R5.pdf +0 -0
- data/test/hexapdf/digital_signature/test_signatures.rb +4 -4
- data/test/hexapdf/encryption/test_standard_security_handler.rb +5 -2
- data/test/hexapdf/layout/test_table_box.rb +52 -0
- data/test/hexapdf/task/test_optimize.rb +2 -0
- data/test/hexapdf/test_document.rb +3 -3
- data/test/hexapdf/test_importer.rb +7 -0
- data/test/hexapdf/test_writer.rb +11 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +22 -5
- data/test/hexapdf/type/acro_form/test_form.rb +0 -5
- data/test/hexapdf/type/acro_form/test_signature_field.rb +3 -1
- data/test/hexapdf/type/acro_form/test_variable_text_field.rb +14 -1
- data/test/hexapdf/type/annotations/test_widget.rb +4 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 15f4c7590b5f2ce321519ac0b4871971c12073a9d4b0df36ab2f2e3c156f09ab
|
|
4
|
+
data.tar.gz: 76dac6196e06e80fd88dade3f831b7744c2b763f004d3c389d3efebcace3be82
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8ea19ed17370cad1a1fa48a7e0cb85c16e3ff62b344d2a9a90c42d2ac98d5ce5f75c356facf6bb8276822db460e2d9f912f523783c82b3539200e33f9de9f383
|
|
7
|
+
data.tar.gz: bdf2d93feade9f0654366bbf89711b670cbd03ee88b81f197f756c2032305f9ee7b9265973f31c5a4e18e7afd3e4f9dfd08b46a0483502b896774494294c111a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,36 @@
|
|
|
1
|
+
## 0.47.0 - 2024-09-07
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
* Configuration option 'acro_form.fallback_default_appearance' to allow setting
|
|
6
|
+
a standard default appearance string for a variable text field if none is
|
|
7
|
+
found
|
|
8
|
+
* Support for decrypting files with the proprietary algorithm /R 5
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
* [HexaPDF::Task::Optimize] to not remove optional /Type entries containing
|
|
13
|
+
default values
|
|
14
|
+
* Validation of [HexaPDF::Type::AcroForm::Form] to not add a /DA entry
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
* [HexaPDF::Layout::TableBox] to correctly calculcate and distribute row
|
|
19
|
+
heights when row spans are involved
|
|
20
|
+
* [HexaPDF::Type::AcroForm::AppearanceGenerator] to work for files where check
|
|
21
|
+
boxes don't define the name of the on state
|
|
22
|
+
* [HexaPDF::Importer#import] to handle null values in all cases
|
|
23
|
+
* [HexaPDF::Type::AcroForm::VariableTextField] to handle parsing of invalid PDFs
|
|
24
|
+
with symbolic appearance strings
|
|
25
|
+
* [HexaPDF::Type::Annotations::Widget#marker_style] to handle invalid /DA values
|
|
26
|
+
with missing font size or color information
|
|
27
|
+
* [HexaPDF::Type::AcroForm::SignatureField#field_value] to always return a
|
|
28
|
+
correctly wrapped object
|
|
29
|
+
* [HexaPDF::Writer] to remove /Type entry from trailer
|
|
30
|
+
* [HexaPDF::Type::AcroForm::AppearanceGenerator#create_text_appearances] to
|
|
31
|
+
handle invalid appearance streams that are not correct Form XObjects
|
|
32
|
+
|
|
33
|
+
|
|
1
34
|
## 0.46.0 - 2024-08-11
|
|
2
35
|
|
|
3
36
|
### Added
|
|
@@ -182,6 +182,16 @@ module HexaPDF
|
|
|
182
182
|
# acro_form.default_font_size::
|
|
183
183
|
# A number specifying the default font size of AcroForm text fields which should be auto-sized.
|
|
184
184
|
#
|
|
185
|
+
# acro_form.fallback_default_appearance::
|
|
186
|
+
# A hash containging arguments for
|
|
187
|
+
# HexaPDF::Type::AcroForm::VariableTextField#set_defaut_appearance_string which is used as
|
|
188
|
+
# fallback for fields without a default appearance.
|
|
189
|
+
#
|
|
190
|
+
# If this value is set to +nil+, an error is raised in case a variable text field cannot
|
|
191
|
+
# resolve a default appearance string.
|
|
192
|
+
#
|
|
193
|
+
# The default is the empty hash meaning the defaults from the method are used.
|
|
194
|
+
#
|
|
185
195
|
# acro_form.fallback_font::
|
|
186
196
|
# The font that should be used when a variable text field references a font that cannot be used.
|
|
187
197
|
#
|
|
@@ -485,6 +495,7 @@ module HexaPDF
|
|
|
485
495
|
Configuration.new('acro_form.appearance_generator' => 'HexaPDF::Type::AcroForm::AppearanceGenerator',
|
|
486
496
|
'acro_form.create_appearances' => true,
|
|
487
497
|
'acro_form.default_font_size' => 10,
|
|
498
|
+
'acro_form.fallback_default_appearance' => {},
|
|
488
499
|
'acro_form.fallback_font' => 'Helvetica',
|
|
489
500
|
'acro_form.on_invalid_value' => proc do |field, value|
|
|
490
501
|
raise HexaPDF::Error, "Invalid value #{value.inspect} for " \
|
|
@@ -106,6 +106,10 @@ module HexaPDF
|
|
|
106
106
|
# password is supplied. To open such an encrypted PDF file, the +decryption_opts+ provided to
|
|
107
107
|
# HexaPDF::Document.new needs to contain a :password key with the password.
|
|
108
108
|
#
|
|
109
|
+
# **Note**: While HexaPDF supports reading files encrypted with revision 5, it doesn't support
|
|
110
|
+
# writing such files. This is no problem in practice since revision 5 was an inofficial Adobe
|
|
111
|
+
# extension to PDF 1.7 and revision 6 specified in PDF 2.0 is practically the same.
|
|
112
|
+
#
|
|
109
113
|
# See: PDF2.0 s7.6.4
|
|
110
114
|
class StandardSecurityHandler < SecurityHandler
|
|
111
115
|
|
|
@@ -340,13 +344,13 @@ module HexaPDF
|
|
|
340
344
|
# Uses the given password (or the default password if none given) to retrieve the encryption
|
|
341
345
|
# key.
|
|
342
346
|
#
|
|
343
|
-
# If the optional +check_permissions+ argument is +true+, the permissions for files
|
|
344
|
-
#
|
|
347
|
+
# If the optional +check_permissions+ argument is +true+, the permissions for files encrypted
|
|
348
|
+
# with revision 5 or 6 are checked. Otherwise, permission changes are ignored.
|
|
345
349
|
def prepare_decryption(password: '', check_permissions: true)
|
|
346
350
|
if dict[:Filter] != :Standard
|
|
347
351
|
raise(HexaPDF::UnsupportedEncryptionError,
|
|
348
352
|
"Invalid /Filter value #{dict[:Filter]} for standard security handler")
|
|
349
|
-
elsif ![2, 3, 4, 6].include?(dict[:R])
|
|
353
|
+
elsif ![2, 3, 4, 5, 6].include?(dict[:R])
|
|
350
354
|
raise(HexaPDF::UnsupportedEncryptionError,
|
|
351
355
|
"Invalid /R value #{dict[:R]} for standard security handler")
|
|
352
356
|
elsif dict[:R] <= 4 && !document.trailer[:ID].kind_of?(PDFArray)
|
|
@@ -369,7 +373,7 @@ module HexaPDF
|
|
|
369
373
|
raise HexaPDF::EncryptionError, "Invalid password specified"
|
|
370
374
|
end
|
|
371
375
|
|
|
372
|
-
check_perms_field(encryption_key) if check_permissions && dict[:R]
|
|
376
|
+
check_perms_field(encryption_key) if check_permissions && dict[:R] >= 5
|
|
373
377
|
|
|
374
378
|
encryption_key
|
|
375
379
|
end
|
|
@@ -396,8 +400,8 @@ module HexaPDF
|
|
|
396
400
|
# For revisions <= 4 this is the *only* way for generating the encryption key needed to
|
|
397
401
|
# encrypt or decrypt a file.
|
|
398
402
|
#
|
|
399
|
-
# For revision 6 the file encryption key is a string of random bytes that has been
|
|
400
|
-
# with the user password. If the password is the owner password,
|
|
403
|
+
# For revision 5 and 6 the file encryption key is a string of random bytes that has been
|
|
404
|
+
# encrypted with the user password. If the password is the owner password,
|
|
401
405
|
# #compute_owner_encryption_key has to be used instead.
|
|
402
406
|
#
|
|
403
407
|
# See: PDF2.0 s7.6.4.3.2 (algorithm 2), PDF2.0 s7.6.4.3.3 (algorithm 2.A (a)-(b),(e))
|
|
@@ -416,7 +420,7 @@ module HexaPDF
|
|
|
416
420
|
end
|
|
417
421
|
|
|
418
422
|
data[0, n]
|
|
419
|
-
elsif dict[:R]
|
|
423
|
+
elsif dict[:R] <= 6
|
|
420
424
|
key = compute_hash(password, dict[:U][40, 8])
|
|
421
425
|
aes_algorithm.new(key, "\0" * 16, :decrypt).process(dict[:UE])
|
|
422
426
|
end
|
|
@@ -427,15 +431,15 @@ module HexaPDF
|
|
|
427
431
|
# For revisions <= 4 this is done by first retrieving the user password through the use of
|
|
428
432
|
# the owner password and then using the #compute_user_encryption_key method.
|
|
429
433
|
#
|
|
430
|
-
# For
|
|
431
|
-
# with the owner password. If the password is the user password,
|
|
432
|
-
# has to be used.
|
|
434
|
+
# For revisions 5 and 6 the file encryption key is a string of random bytes that has been
|
|
435
|
+
# encrypted with the owner password. If the password is the user password,
|
|
436
|
+
# #compute_user_encryption_key has to be used.
|
|
433
437
|
#
|
|
434
438
|
# See: PDF2.0 s7.6.4.3.2 (algorithm 2.A (a)-(d))
|
|
435
439
|
def compute_owner_encryption_key(password)
|
|
436
440
|
if dict[:R] <= 4
|
|
437
441
|
compute_user_encryption_key(user_password_from_owner_password(password))
|
|
438
|
-
elsif dict[:R]
|
|
442
|
+
elsif dict[:R] <= 6
|
|
439
443
|
key = compute_hash(password, dict[:O][40, 8], dict[:U])
|
|
440
444
|
aes_algorithm.new(key, "\0" * 16, :decrypt).process(dict[:OE])
|
|
441
445
|
end
|
|
@@ -447,7 +451,7 @@ module HexaPDF
|
|
|
447
451
|
# the owner password. For revision 6 the /O value is a hash computed from the password and
|
|
448
452
|
# the /U value with added validation and key salts.
|
|
449
453
|
#
|
|
450
|
-
# *Attention*: If revision 6 is used, the /U value has to be computed and set before this
|
|
454
|
+
# *Attention*: If revision 5 or 6 is used, the /U value has to be computed and set before this
|
|
451
455
|
# method is used, otherwise the return value is incorrect!
|
|
452
456
|
#
|
|
453
457
|
# See: PDF2.0 s7.6.4.4.2 (algorithm 3), PDF2.0 s7.6.4.4.8 (algorithm 9 (a))
|
|
@@ -465,14 +469,14 @@ module HexaPDF
|
|
|
465
469
|
end
|
|
466
470
|
|
|
467
471
|
data
|
|
468
|
-
elsif dict[:R]
|
|
472
|
+
elsif dict[:R] <= 6
|
|
469
473
|
validation_salt = random_bytes(8)
|
|
470
474
|
key_salt = random_bytes(8)
|
|
471
475
|
compute_hash(owner_password, validation_salt, dict[:U]) << validation_salt << key_salt
|
|
472
476
|
end
|
|
473
477
|
end
|
|
474
478
|
|
|
475
|
-
# Computes the encryption dictionary's /OE (owner encryption key) value (for
|
|
479
|
+
# Computes the encryption dictionary's /OE (owner encryption key) value (for revisions 5 and 6
|
|
476
480
|
# only).
|
|
477
481
|
#
|
|
478
482
|
# Short explanation: Encrypts the file encryption key with a key based on the password and
|
|
@@ -487,7 +491,7 @@ module HexaPDF
|
|
|
487
491
|
# Computes the encryption dictionary's /U (user password) value.
|
|
488
492
|
#
|
|
489
493
|
# Short explanation: For revisions <= 4, the password padding string is encrypted with a key
|
|
490
|
-
# based on the user password. For
|
|
494
|
+
# based on the user password. For revisions 5 and 6 the /U value is a hash computed from the
|
|
491
495
|
# password with added validation and key salts.
|
|
492
496
|
#
|
|
493
497
|
# See: PDF2.0 s7.6.4.4.3 (algorithm 4 for R=2), PDF s7.6.4.4.4 (algorithm 5 for R=3 and R=4)
|
|
@@ -502,14 +506,14 @@ module HexaPDF
|
|
|
502
506
|
data = arc4_algorithm.encrypt(key, data)
|
|
503
507
|
19.times {|i| data = arc4_algorithm.encrypt(xor_key(key, i + 1), data) }
|
|
504
508
|
data << "hexapdfhexapdfhe"
|
|
505
|
-
elsif dict[:R]
|
|
509
|
+
elsif dict[:R] <= 6
|
|
506
510
|
validation_salt = random_bytes(8)
|
|
507
511
|
key_salt = random_bytes(8)
|
|
508
512
|
compute_hash(password, validation_salt) << validation_salt << key_salt
|
|
509
513
|
end
|
|
510
514
|
end
|
|
511
515
|
|
|
512
|
-
# Computes the encryption dictionary's /UE (user encryption key) value (for revision 6
|
|
516
|
+
# Computes the encryption dictionary's /UE (user encryption key) value (for revision 5 and 6
|
|
513
517
|
# only).
|
|
514
518
|
#
|
|
515
519
|
# Short explanation: Encrypts the file encryption key with a key based on the password and
|
|
@@ -521,7 +525,8 @@ module HexaPDF
|
|
|
521
525
|
aes_algorithm.new(key, "\0" * 16, :encrypt).process(file_encryption_key)
|
|
522
526
|
end
|
|
523
527
|
|
|
524
|
-
# Computes the encryption dictionary's /Perms (permissions) value (for
|
|
528
|
+
# Computes the encryption dictionary's /Perms (permissions) value (for revisions 5 and 6
|
|
529
|
+
# only).
|
|
525
530
|
#
|
|
526
531
|
# Uses /P and /EncryptMetadata values, so these have to be set beforehand.
|
|
527
532
|
#
|
|
@@ -543,7 +548,7 @@ module HexaPDF
|
|
|
543
548
|
compute_u_field(password) == dict[:U]
|
|
544
549
|
elsif dict[:R] <= 4
|
|
545
550
|
compute_u_field(password)[0, 16] == dict[:U][0, 16]
|
|
546
|
-
elsif dict[:R]
|
|
551
|
+
elsif dict[:R] <= 6
|
|
547
552
|
compute_hash(password, dict[:U][32, 8]) == dict[:U][0, 32]
|
|
548
553
|
end
|
|
549
554
|
end
|
|
@@ -554,14 +559,14 @@ module HexaPDF
|
|
|
554
559
|
def owner_password_valid?(password)
|
|
555
560
|
if dict[:R] <= 4
|
|
556
561
|
user_password_valid?(user_password_from_owner_password(password))
|
|
557
|
-
elsif dict[:R]
|
|
562
|
+
elsif dict[:R] <= 6
|
|
558
563
|
compute_hash(password, dict[:O][32, 8], dict[:U]) == dict[:O][0, 32]
|
|
559
564
|
end
|
|
560
565
|
end
|
|
561
566
|
|
|
562
567
|
# Checks if the decrypted /Perms entry matches the /P and /EncryptMetadata entries.
|
|
563
568
|
#
|
|
564
|
-
# This method can only be used for
|
|
569
|
+
# This method can only be used for revisions 5 and 6.
|
|
565
570
|
#
|
|
566
571
|
# See: PDF2.0 s7.6.4.4.12 (algorithm 13)
|
|
567
572
|
def check_perms_field(encryption_key)
|
|
@@ -596,17 +601,18 @@ module HexaPDF
|
|
|
596
601
|
end
|
|
597
602
|
|
|
598
603
|
# Computes a hash that is used extensively for all operations in security handlers of
|
|
599
|
-
# revision 6.
|
|
604
|
+
# revision 5 and 6.
|
|
600
605
|
#
|
|
601
606
|
# Note: The original input (as defined by the spec) is calculated as
|
|
602
607
|
# "#{password}#{salt}#{user_key}" where +user_key+ has to be empty when doing operations
|
|
603
608
|
# with the user password.
|
|
604
609
|
#
|
|
605
|
-
# See: PDF2.0 s7.6.4.3.4 (algorithm 2.B)
|
|
610
|
+
# See: PDF2.0 s7.6.4.3.4 (algorithm 2.B) and ADB Extension Level 3 s3.5.2
|
|
606
611
|
def compute_hash(password, salt, user_key = '')
|
|
607
612
|
k = Digest::SHA256.digest("#{password}#{salt}#{user_key}")
|
|
608
|
-
|
|
613
|
+
return k if dict[:R] == 5
|
|
609
614
|
|
|
615
|
+
e = ''
|
|
610
616
|
i = 0
|
|
611
617
|
while i < 64 || e.getbyte(-1) > i - 32
|
|
612
618
|
k1 = "#{password}#{k}#{user_key}" * 64
|
|
@@ -627,7 +633,7 @@ module HexaPDF
|
|
|
627
633
|
# * For revisions <= 4, the password is converted into ISO-8859-1 encoding, padded with
|
|
628
634
|
# PASSWORD_PADDING and truncated to a maximum of 32 bytes.
|
|
629
635
|
#
|
|
630
|
-
# * For revision 6 the password is converted into UTF-8 encoding that is normalized
|
|
636
|
+
# * For revision 5 and 6 the password is converted into UTF-8 encoding that is normalized
|
|
631
637
|
# according to the PDF2.0 specification.
|
|
632
638
|
#
|
|
633
639
|
# See: PDF2.0 s7.6.4.3.2 (algorithm 2 step a)),
|
|
@@ -636,7 +642,7 @@ module HexaPDF
|
|
|
636
642
|
if dict[:R] <= 4
|
|
637
643
|
password.to_s[0, 32].encode(Encoding::ISO_8859_1).force_encoding(Encoding::BINARY).
|
|
638
644
|
ljust(32, PASSWORD_PADDING)
|
|
639
|
-
elsif dict[:R]
|
|
645
|
+
elsif dict[:R] <= 6
|
|
640
646
|
password.to_s.encode(Encoding::UTF_8).force_encoding(Encoding::BINARY)[0, 127]
|
|
641
647
|
end
|
|
642
648
|
rescue Encoding::UndefinedConversionError => e
|
data/lib/hexapdf/importer.rb
CHANGED
|
@@ -141,7 +141,7 @@ module HexaPDF
|
|
|
141
141
|
internal_import(wrapper.source.object(object), wrapper)
|
|
142
142
|
when HexaPDF::Object
|
|
143
143
|
wrapper.source ||= object.document
|
|
144
|
-
if !@allow_all && (object.type == :Catalog || object.type == :Pages)
|
|
144
|
+
if object.null? || (!@allow_all && (object.type == :Catalog || object.type == :Pages))
|
|
145
145
|
@mapper[object.data] = nil
|
|
146
146
|
elsif (mapped_object = @mapper[object.data]&.__getobj__) && !mapped_object.null?
|
|
147
147
|
mapped_object
|
|
@@ -382,7 +382,14 @@ module HexaPDF
|
|
|
382
382
|
def fit_rows(start_row, available_height, column_info, frame)
|
|
383
383
|
height = available_height
|
|
384
384
|
last_fitted_row_index = -1
|
|
385
|
+
row_heights = {}
|
|
386
|
+
zero_height_rows = {}
|
|
387
|
+
row_spans = []
|
|
388
|
+
|
|
385
389
|
@cells[start_row..-1].each.with_index(start_row) do |columns, row_index|
|
|
390
|
+
# 1. Fit all columns of the row and record the max height of all non-row-span cells. If
|
|
391
|
+
# a row has zero height (usually because it only has row-span cells), record that
|
|
392
|
+
# information. Additionally store all cells with row-spans.
|
|
386
393
|
row_fit = true
|
|
387
394
|
row_height = 0
|
|
388
395
|
columns.each_with_index do |cell, col_index|
|
|
@@ -396,27 +403,67 @@ module HexaPDF
|
|
|
396
403
|
row_fit = false
|
|
397
404
|
break
|
|
398
405
|
end
|
|
399
|
-
cell.
|
|
400
|
-
|
|
401
|
-
|
|
406
|
+
if row_height < cell.preferred_height && cell.row_span == 1
|
|
407
|
+
row_height = cell.preferred_height
|
|
408
|
+
end
|
|
409
|
+
row_spans << cell if cell.row_span > 1
|
|
402
410
|
end
|
|
403
411
|
|
|
404
|
-
if
|
|
405
|
-
seen = {}
|
|
406
|
-
columns.each do |cell|
|
|
407
|
-
next if seen[cell]
|
|
408
|
-
cell.update_height(cell.row == row_index ? row_height : cell.height + row_height)
|
|
409
|
-
seen[cell] = true
|
|
410
|
-
end
|
|
412
|
+
zero_height_rows[row_index] = true if row_height == 0
|
|
411
413
|
|
|
414
|
+
if row_fit
|
|
415
|
+
# 2. If all cells of the row fit, we subtract the recorded row height of the
|
|
416
|
+
# non-row-span cells from the available height for the next pass.
|
|
412
417
|
last_fitted_row_index = row_index
|
|
418
|
+
row_heights[row_index] = row_height
|
|
413
419
|
available_height -= row_height
|
|
420
|
+
|
|
421
|
+
# 3. We look at all row-span cells that end at the current row index. If the row-span
|
|
422
|
+
# cell is larger than the sum of the row heights, we proportionally enlarge the
|
|
423
|
+
# stored height of each spanned row and subtract the difference from the available
|
|
424
|
+
# height for the next pass. If the row span contains initially zero-height rows,
|
|
425
|
+
# only those rows are enlarged. Row-span cells themselves are not updated at this
|
|
426
|
+
# point!
|
|
427
|
+
row_spans.each do |cell|
|
|
428
|
+
upper_row_index = cell.row + cell.row_span - 1
|
|
429
|
+
next unless upper_row_index == row_index
|
|
430
|
+
|
|
431
|
+
rows = cell.row.upto(upper_row_index)
|
|
432
|
+
row_span_height = rows.sum {|ri| row_heights[ri] }
|
|
433
|
+
if row_span_height < cell.preferred_height
|
|
434
|
+
zero_height_rows_in_span = rows.select {|ri| zero_height_rows[ri] }
|
|
435
|
+
rows = zero_height_rows_in_span if zero_height_rows_in_span.size > 0
|
|
436
|
+
adjustment = (cell.preferred_height - row_span_height) / rows.size.to_f
|
|
437
|
+
rows.each {|ri| row_heights[ri] += adjustment }
|
|
438
|
+
available_height -= cell.preferred_height - row_span_height
|
|
439
|
+
end
|
|
440
|
+
end
|
|
414
441
|
else
|
|
415
442
|
last_fitted_row_index = columns.min_by(&:row).row - 1 if height != available_height
|
|
416
443
|
break
|
|
417
444
|
end
|
|
418
445
|
end
|
|
419
446
|
|
|
447
|
+
if last_fitted_row_index >= 0
|
|
448
|
+
# 4. Once all possible rows have been fitted and the heights of the rows are fixed, the
|
|
449
|
+
# final height and top-left corner of each cell needs to be set.
|
|
450
|
+
running_height = 0
|
|
451
|
+
@cells[start_row..last_fitted_row_index].each.with_index(start_row) do |columns, row_index|
|
|
452
|
+
columns.each_with_index do |cell, col_index|
|
|
453
|
+
next if cell.row != row_index || cell.column != col_index
|
|
454
|
+
cell.left = column_info[cell.column].first
|
|
455
|
+
cell.top = running_height
|
|
456
|
+
if cell.row_span == 1
|
|
457
|
+
cell.update_height(row_heights[row_index])
|
|
458
|
+
else
|
|
459
|
+
new_height = cell.row.upto(cell.row + cell.row_span - 1).sum {|ri| row_heights[ri] }
|
|
460
|
+
cell.update_height(new_height)
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
running_height += row_heights[row_index]
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
420
467
|
[height - available_height, last_fitted_row_index < start_row ? -1 : last_fitted_row_index]
|
|
421
468
|
end
|
|
422
469
|
|
|
@@ -214,13 +214,13 @@ module HexaPDF
|
|
|
214
214
|
end
|
|
215
215
|
end
|
|
216
216
|
|
|
217
|
-
# Deletes field entries of the object that are optional and currently set
|
|
218
|
-
# value.
|
|
217
|
+
# Deletes field entries (except for /Type) of the object that are optional and currently set
|
|
218
|
+
# to their default value.
|
|
219
219
|
def self.delete_fields_with_defaults(obj)
|
|
220
220
|
return unless obj.kind_of?(HexaPDF::Dictionary) && !obj.null?
|
|
221
221
|
obj.each do |name, value|
|
|
222
|
-
if (field = obj.class.field(name)) && !field.required? &&
|
|
223
|
-
|
|
222
|
+
if name != :Type && (field = obj.class.field(name)) && !field.required? &&
|
|
223
|
+
field.default? && value == field.default
|
|
224
224
|
obj.delete(name)
|
|
225
225
|
end
|
|
226
226
|
end
|
|
@@ -134,11 +134,12 @@ module HexaPDF
|
|
|
134
134
|
if !normal_appearance.kind_of?(HexaPDF::Dictionary) || normal_appearance.kind_of?(HexaPDF::Stream)
|
|
135
135
|
(@widget[:AP] ||= {})[:N] = {Off: nil}
|
|
136
136
|
normal_appearance = @widget[:AP][:N]
|
|
137
|
-
normal_appearance[@field
|
|
137
|
+
normal_appearance[@field.field_value&.to_sym || :Yes] = nil
|
|
138
138
|
end
|
|
139
139
|
on_name = (normal_appearance.value.keys - [:Off]).first
|
|
140
140
|
unless on_name
|
|
141
|
-
|
|
141
|
+
on_name = @field.field_value&.to_sym || :Yes
|
|
142
|
+
normal_appearance[on_name] = nil
|
|
142
143
|
end
|
|
143
144
|
|
|
144
145
|
@widget[:AS] = (@field[:V] == on_name ? on_name : :Off)
|
|
@@ -226,8 +227,11 @@ module HexaPDF
|
|
|
226
227
|
|
|
227
228
|
form = (@widget[:AP] ||= {})[:N] ||= @document.add({Type: :XObject, Subtype: :Form})
|
|
228
229
|
# Wrap existing object in Form class in case the PDF writer didn't include the /Subtype
|
|
229
|
-
# key; we can do this since we know this has to be a
|
|
230
|
-
|
|
230
|
+
# key or the type of the object is wrong; we can do this since we know this has to be a
|
|
231
|
+
# Form object
|
|
232
|
+
unless form.type == :XObject && form[:Subtype] == :Form
|
|
233
|
+
form = @document.wrap(form, type: :XObject, subtype: :Form)
|
|
234
|
+
end
|
|
231
235
|
form.value.replace({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height],
|
|
232
236
|
Matrix: matrix, Resources: HexaPDF::Object.deep_copy(default_resources)})
|
|
233
237
|
form.contents = ''
|
|
@@ -186,9 +186,14 @@ module HexaPDF
|
|
|
186
186
|
# or +font_color+ is specified but +font+ isn't, the font Helvetica is used.
|
|
187
187
|
#
|
|
188
188
|
# If no font is set on the text field, the default font properties of the AcroForm form
|
|
189
|
-
# are used. Note that field specific or form specific font properties have to be
|
|
190
|
-
# Otherwise there
|
|
191
|
-
#
|
|
189
|
+
# are used. Note that field specific or form specific font properties have to be
|
|
190
|
+
# set. Otherwise there might be problems when creating a visual appearance with other
|
|
191
|
+
# PDF libraries/viewers.
|
|
192
|
+
#
|
|
193
|
+
# If HexaPDF is used to create a visual appearance of the field value and neither field
|
|
194
|
+
# specific nor form specific font properties are available, the configuration option
|
|
195
|
+
# 'acro_form.fallback_default_appearance' defines whether and which field specific font
|
|
196
|
+
# properties are set and used.
|
|
192
197
|
#
|
|
193
198
|
# +font_options+::
|
|
194
199
|
# A hash with font options like :variant that should be used.
|
|
@@ -625,8 +630,6 @@ module HexaPDF
|
|
|
625
630
|
if font_name && !(self[:DR][:Font] && self[:DR][:Font][font_name])
|
|
626
631
|
yield("The font specified in /DA is not in the /DR resource dictionary")
|
|
627
632
|
end
|
|
628
|
-
else
|
|
629
|
-
set_default_appearance_string
|
|
630
633
|
end
|
|
631
634
|
|
|
632
635
|
create_appearances if document.config['acro_form.create_appearances']
|
|
@@ -199,7 +199,8 @@ module HexaPDF
|
|
|
199
199
|
|
|
200
200
|
# Returns the associated signature dictionary or +nil+ if the signature is not filled in.
|
|
201
201
|
def field_value
|
|
202
|
-
self[:V]
|
|
202
|
+
val = self[:V]
|
|
203
|
+
val.instance_of?(Dictionary) ? document.wrap(val, type: :Sig) : val
|
|
203
204
|
end
|
|
204
205
|
|
|
205
206
|
# Sets the signature dictionary as value of this signature field.
|
|
@@ -106,7 +106,7 @@ module HexaPDF
|
|
|
106
106
|
font_params[2] = HexaPDF::Content::ColorSpace.prenormalized_device_color(params)
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
|
-
HexaPDF::Content::Parser.parse(appearance_string.sub(/\/\//, '/'), &block)
|
|
109
|
+
HexaPDF::Content::Parser.parse(appearance_string.to_s.sub(/\/\//, '/'), &block)
|
|
110
110
|
block_given? ? nil : font_params
|
|
111
111
|
end
|
|
112
112
|
|
|
@@ -154,13 +154,21 @@ module HexaPDF
|
|
|
154
154
|
# font_size, font_color].
|
|
155
155
|
#
|
|
156
156
|
# The default appearance string is taken from the given +widget+ of the field, falls back to
|
|
157
|
-
# the field itself
|
|
157
|
+
# the field itself and then the default appearance string of the form. If it still not
|
|
158
|
+
# available, a standard default appearance string is set (see
|
|
159
|
+
# #set_default_appearance_string) and used.
|
|
158
160
|
#
|
|
159
161
|
# The reason why a specific widget of the field can be specified is because the widgets of a
|
|
160
162
|
# field might differ in their visual representation.
|
|
161
163
|
def parse_default_appearance_string(widget = self)
|
|
162
164
|
da = widget[:DA] || self[:DA] || (document.acro_form && document.acro_form[:DA])
|
|
163
|
-
|
|
165
|
+
unless da
|
|
166
|
+
if (args = document.config['acro_form.fallback_default_appearance'])
|
|
167
|
+
da = set_default_appearance_string(**args)
|
|
168
|
+
else
|
|
169
|
+
raise HexaPDF::Error, "No default appearance string set"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
164
172
|
self.class.parse_appearance_string(da)
|
|
165
173
|
end
|
|
166
174
|
|
|
@@ -259,7 +259,7 @@ module HexaPDF
|
|
|
259
259
|
# auto-sized checkmark (i.e. :check for for check boxes) or circle (:circle for radio
|
|
260
260
|
# buttons). This also means that multiple invocations will reset *all* prior values.
|
|
261
261
|
#
|
|
262
|
-
# Note: The marker is called "normal caption" in the PDF
|
|
262
|
+
# Note: The marker is called "normal caption" in the PDF 2.0 spec and the /CA entry of the
|
|
263
263
|
# associated appearance characteristics dictionary. The marker size and color are set using
|
|
264
264
|
# the /DA key on the widget (although /DA is not defined for widget, this is how Acrobat
|
|
265
265
|
# does it).
|
|
@@ -305,7 +305,9 @@ module HexaPDF
|
|
|
305
305
|
size = 0
|
|
306
306
|
color = HexaPDF::Content::ColorSpace.prenormalized_device_color([0])
|
|
307
307
|
if (da = self[:DA] || field[:DA])
|
|
308
|
-
_,
|
|
308
|
+
_, da_size, da_color = AcroForm::VariableTextField.parse_appearance_string(da)
|
|
309
|
+
size = da_size || size
|
|
310
|
+
color = da_color || color
|
|
309
311
|
end
|
|
310
312
|
|
|
311
313
|
MarkerStyle.new(style, size, color)
|
data/lib/hexapdf/version.rb
CHANGED
data/lib/hexapdf/writer.rb
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
%PDF-1.7
|
|
2
|
+
%����
|
|
3
|
+
1 0 obj
|
|
4
|
+
<< /Extensions << /ADBE << /BaseVersion /1.7 /ExtensionLevel 3 >> >> /Pages 3 0 R /Type /Catalog >>
|
|
5
|
+
endobj
|
|
6
|
+
2 0 obj
|
|
7
|
+
<< /ModDate <aa1d1637459ea17c7fc9709f0c0c7b64761ff74e905b3765f33425776aa3010163cd5e898cfa7c5fc16159359375c323> >>
|
|
8
|
+
endobj
|
|
9
|
+
3 0 obj
|
|
10
|
+
<< /Count 1 /Kids [ 4 0 R ] /MediaBox [ 0 0 612 446 ] /Type /Pages >>
|
|
11
|
+
endobj
|
|
12
|
+
4 0 obj
|
|
13
|
+
<< /Contents 5 0 R /Parent 3 0 R /Resources 6 0 R /Type /Page >>
|
|
14
|
+
endobj
|
|
15
|
+
5 0 obj
|
|
16
|
+
<< /Length 80 /Filter /FlateDecode >>
|
|
17
|
+
stream
|
|
18
|
+
J<~S�)��9�M�$S�z��g�j���r�.R�"PO���_���X���^�GP�&z�g�*$�2SΈ�z�Kl��5ĩendstream
|
|
19
|
+
endobj
|
|
20
|
+
6 0 obj
|
|
21
|
+
<< /Font << /F1 7 0 R >> /ProcSet [ /PDF /Text ] >>
|
|
22
|
+
endobj
|
|
23
|
+
7 0 obj
|
|
24
|
+
<< /BaseFont /Helvetica /Name /F1 /Subtype /Type1 /Type /Font >>
|
|
25
|
+
endobj
|
|
26
|
+
8 0 obj
|
|
27
|
+
<< /CF << /StdCF << /AuthEvent /DocOpen /CFM /AESV3 /Length 32 >> >> /Filter /Standard /Length 256 /O <c60934c0925e8d3ceebd0609102d9407dcf22d7fb3d87d030fce633af6d2ed99c1c3382bee5e8afc0d55ff8bca441d48> /OE <3fa2e3ecf34ddcb38b44af372a43268dda32111f58dc79da74d960b8fa206ead> /P -4 /Perms <b6c6ab65529f0a3322f03909e8a5547a> /R 5 /StmF /StdCF /StrF /StdCF /U <18fbd94777c28531c495a3116d273de9f8f3ed338b31c07687ca0ba06812842843765a66484d098194bcef0f9ecaaace> /UE <496a81bbb3207dfd12ddca4c16b60a411c6a18e638205824295e09afde826b4a> /V 5 >>
|
|
28
|
+
endobj
|
|
29
|
+
xref
|
|
30
|
+
0 9
|
|
31
|
+
0000000000 65535 f
|
|
32
|
+
0000000015 00000 n
|
|
33
|
+
0000000130 00000 n
|
|
34
|
+
0000000259 00000 n
|
|
35
|
+
0000000344 00000 n
|
|
36
|
+
0000000424 00000 n
|
|
37
|
+
0000000574 00000 n
|
|
38
|
+
0000000641 00000 n
|
|
39
|
+
0000000721 00000 n
|
|
40
|
+
trailer << /Info 2 0 R /Root 1 0 R /Size 9 /ID [<6790ffa610024e78369114311fc0df96><ed6c0810cb0d19599ac62042a0487749>] /Encrypt 8 0 R >>
|
|
41
|
+
startxref
|
|
42
|
+
1268
|
|
43
|
+
%%EOF
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
%PDF-1.7
|
|
2
|
+
%����
|
|
3
|
+
1 0 obj
|
|
4
|
+
<< /Extensions << /ADBE << /BaseVersion /1.7 /ExtensionLevel 3 >> >> /Pages 3 0 R /Type /Catalog >>
|
|
5
|
+
endobj
|
|
6
|
+
2 0 obj
|
|
7
|
+
<< /ModDate <0a09febf75d913e5eff6197f52f28b31321a43f49e0735a25a93dd77b1a2701e73ae05be0803747ca030ee4917544cbb> >>
|
|
8
|
+
endobj
|
|
9
|
+
3 0 obj
|
|
10
|
+
<< /Count 1 /Kids [ 4 0 R ] /MediaBox [ 0 0 612 446 ] /Type /Pages >>
|
|
11
|
+
endobj
|
|
12
|
+
4 0 obj
|
|
13
|
+
<< /Contents 5 0 R /Parent 3 0 R /Resources 6 0 R /Type /Page >>
|
|
14
|
+
endobj
|
|
15
|
+
5 0 obj
|
|
16
|
+
<< /Length 80 /Filter /FlateDecode >>
|
|
17
|
+
stream
|
|
18
|
+
\��sy%1����a��
|
|
19
|
+
�/���F�.�L:pb���C�`��D�5 �� �کU"�ʇ�3�Fy/�9�<V��ڦ;�?5�S�8endstream
|
|
20
|
+
endobj
|
|
21
|
+
6 0 obj
|
|
22
|
+
<< /Font << /F1 7 0 R >> /ProcSet [ /PDF /Text ] >>
|
|
23
|
+
endobj
|
|
24
|
+
7 0 obj
|
|
25
|
+
<< /BaseFont /Helvetica /Name /F1 /Subtype /Type1 /Type /Font >>
|
|
26
|
+
endobj
|
|
27
|
+
8 0 obj
|
|
28
|
+
<< /CF << /StdCF << /AuthEvent /DocOpen /CFM /AESV3 /Length 32 >> >> /Filter /Standard /Length 256 /O <8034f99b1eff9e91054d7ee490155e22f65170f607d6a614236b089e602517d9a22abe7c1ea53f52dbc9ae8d9701a065> /OE <bdc5c4b46519af7b4041b7341f70e4d8b1c6087dc12d3f16f060313f18e73386> /P -4 /Perms <64028d6bceb0d927477ecf45e6be7f3f> /R 5 /StmF /StdCF /StrF /StdCF /U <5db7b7b5bd8bbe9fac28477756a930cb079e1dcbdcd3b50c1817d3de4ce5184b5189848832df0043f8e1fe19ff696a36> /UE <dc3fed7b473148238ba0689409edd3d80c91bd26802003c152594aa27a50a81e> /V 5 >>
|
|
29
|
+
endobj
|
|
30
|
+
xref
|
|
31
|
+
0 9
|
|
32
|
+
0000000000 65535 f
|
|
33
|
+
0000000015 00000 n
|
|
34
|
+
0000000130 00000 n
|
|
35
|
+
0000000259 00000 n
|
|
36
|
+
0000000344 00000 n
|
|
37
|
+
0000000424 00000 n
|
|
38
|
+
0000000574 00000 n
|
|
39
|
+
0000000641 00000 n
|
|
40
|
+
0000000721 00000 n
|
|
41
|
+
trailer << /Info 2 0 R /Root 1 0 R /Size 9 /ID [<6790ffa610024e78369114311fc0df96><da8e0d03302398724f68ef24a831285e>] /Encrypt 8 0 R >>
|
|
42
|
+
startxref
|
|
43
|
+
1268
|
|
44
|
+
%%EOF
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
%PDF-1.7
|
|
2
|
+
%����
|
|
3
|
+
1 0 obj
|
|
4
|
+
<< /Extensions << /ADBE << /BaseVersion /1.7 /ExtensionLevel 3 >> >> /Pages 3 0 R /Type /Catalog >>
|
|
5
|
+
endobj
|
|
6
|
+
2 0 obj
|
|
7
|
+
<< /ModDate <d2aa32b8d87666bd29df499a424d2e6d23c0b475ebb67598733b5f06429470a37062d3d6a32fc9603301c5eb99a20605> >>
|
|
8
|
+
endobj
|
|
9
|
+
3 0 obj
|
|
10
|
+
<< /Count 1 /Kids [ 4 0 R ] /MediaBox [ 0 0 612 446 ] /Type /Pages >>
|
|
11
|
+
endobj
|
|
12
|
+
4 0 obj
|
|
13
|
+
<< /Contents 5 0 R /Parent 3 0 R /Resources 6 0 R /Type /Page >>
|
|
14
|
+
endobj
|
|
15
|
+
5 0 obj
|
|
16
|
+
<< /Length 80 /Filter /FlateDecode >>
|
|
17
|
+
stream
|
|
18
|
+
j��D3�(���"�ڛ�������xvY$B\4^Y����$��
|
|
19
|
+
endobj
|
|
20
|
+
6 0 obj
|
|
21
|
+
<< /Font << /F1 7 0 R >> /ProcSet [ /PDF /Text ] >>
|
|
22
|
+
endobj
|
|
23
|
+
7 0 obj
|
|
24
|
+
<< /BaseFont /Helvetica /Name /F1 /Subtype /Type1 /Type /Font >>
|
|
25
|
+
endobj
|
|
26
|
+
8 0 obj
|
|
27
|
+
<< /CF << /StdCF << /AuthEvent /DocOpen /CFM /AESV3 /Length 32 >> >> /Filter /Standard /Length 256 /O <95d20f6277a6955bf4bda243c2144e94889bd5fa4225a4cf4e0d496fa1ffa1d991e1a37d46e4afe9e2dfc207eba9ec53> /OE <432a7d086b4338f1020bbc5847e6dd4f49ce586ab2c5de0f5301450e45e3bb3e> /P -4 /Perms <7df865105074d365f2ce50407e8b6dc8> /R 5 /StmF /StdCF /StrF /StdCF /U <930a631e8b2b95ff6b024e9bde92cb73c5c43f0106ec4fb1c336e49608c0740d87395c6ea79b99ee07eeae5ffbacd031> /UE <3f7eb6b9a897049bfca85a5ae71470eca3dfedbe9101e8532b217e4d95bcf51a> /V 5 >>
|
|
28
|
+
endobj
|
|
29
|
+
xref
|
|
30
|
+
0 9
|
|
31
|
+
0000000000 65535 f
|
|
32
|
+
0000000015 00000 n
|
|
33
|
+
0000000130 00000 n
|
|
34
|
+
0000000259 00000 n
|
|
35
|
+
0000000344 00000 n
|
|
36
|
+
0000000424 00000 n
|
|
37
|
+
0000000574 00000 n
|
|
38
|
+
0000000641 00000 n
|
|
39
|
+
0000000721 00000 n
|
|
40
|
+
trailer << /Info 2 0 R /Root 1 0 R /Size 9 /ID [<6790ffa610024e78369114311fc0df96><dd2e6bd65a9735c0bef37d88d5291ff1>] /Encrypt 8 0 R >>
|
|
41
|
+
startxref
|
|
42
|
+
1268
|
|
43
|
+
%%EOF
|
|
Binary file
|
|
@@ -16,13 +16,13 @@ describe HexaPDF::DigitalSignature::Signatures do
|
|
|
16
16
|
|
|
17
17
|
it "iterates over all signature dictionaries" do
|
|
18
18
|
assert_equal([], @doc.signatures.to_a)
|
|
19
|
-
@sig1.field_value = :sig1
|
|
20
|
-
@sig2.field_value = :sig2
|
|
21
|
-
assert_equal([:sig1, :sig2], @doc.signatures.to_a)
|
|
19
|
+
@sig1.field_value = {k: :sig1}
|
|
20
|
+
@sig2.field_value = {k: :sig2}
|
|
21
|
+
assert_equal([{k: :sig1}, {k: :sig2}], @doc.signatures.to_a)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
it "returns the number of signature dictionaries" do
|
|
25
|
-
@sig1.field_value = :sig1
|
|
25
|
+
@sig1.field_value = {k: :sig1}
|
|
26
26
|
assert_equal(1, @doc.signatures.count)
|
|
27
27
|
end
|
|
28
28
|
|
|
@@ -228,11 +228,14 @@ describe HexaPDF::Encryption::StandardSecurityHandler do
|
|
|
228
228
|
end
|
|
229
229
|
|
|
230
230
|
it "fails if the /R value is incorrect" do
|
|
231
|
+
HexaPDF::Encryption::StandardEncryptionDictionary.field(:R).allowed_values << 7
|
|
231
232
|
exp = assert_raises(HexaPDF::UnsupportedEncryptionError) do
|
|
232
|
-
@handler.set_up_decryption({Filter: :Standard, V: 2, R:
|
|
233
|
+
@handler.set_up_decryption({Filter: :Standard, V: 2, R: 7, O: 't' * 32, U: 't' * 32, P: 0,
|
|
233
234
|
Length: 128})
|
|
234
235
|
end
|
|
235
|
-
assert_match(/Invalid \/R value
|
|
236
|
+
assert_match(/Invalid \/R value 7/i, exp.message)
|
|
237
|
+
ensure
|
|
238
|
+
HexaPDF::Encryption::StandardEncryptionDictionary.field(:R).allowed_values.pop
|
|
236
239
|
end
|
|
237
240
|
|
|
238
241
|
it "fails if the supplied password is invalid" do
|
|
@@ -511,6 +511,58 @@ describe HexaPDF::Layout::TableBox do
|
|
|
511
511
|
[0, 66, 39.75, 22], [39.75, 66, 39.75, 22], [79.5, 44, 39.75, 44], [119.25, 66, 39.75, 22]])
|
|
512
512
|
end
|
|
513
513
|
|
|
514
|
+
describe "row spans" do
|
|
515
|
+
# ----------
|
|
516
|
+
# | a | b |
|
|
517
|
+
# | a | |
|
|
518
|
+
# | a |----|
|
|
519
|
+
# | a | c |
|
|
520
|
+
# | a | |
|
|
521
|
+
# ----------
|
|
522
|
+
it "works if content of a row span cell is larger than the rows" do
|
|
523
|
+
cells = [[{row_span: 2, content: @fixed_size_boxes[0..2]}, @fixed_size_boxes[3]],
|
|
524
|
+
[@fixed_size_boxes[4]]]
|
|
525
|
+
check_box(create_box(cells: cells, cell_style: {padding: 0, border: {width: 0}}),
|
|
526
|
+
:success, 160, 30,
|
|
527
|
+
[[0, 0, 80, 30], [80, 0, 80, 15], [0, 0, 80, 30], [80, 15, 80, 15]])
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# ----------
|
|
531
|
+
# | a | b |
|
|
532
|
+
# | |----|
|
|
533
|
+
# | | c |
|
|
534
|
+
# ----------
|
|
535
|
+
it "works if content of a row span cell is smaller than the rows" do
|
|
536
|
+
cells = [[{row_span: 2, content: @fixed_size_boxes[0]}, @fixed_size_boxes[3]],
|
|
537
|
+
[@fixed_size_boxes[4]]]
|
|
538
|
+
check_box(create_box(cells: cells, cell_style: {padding: 0, border: {width: 0}}),
|
|
539
|
+
:success, 160, 20,
|
|
540
|
+
[[0, 0, 80, 20], [80, 0, 80, 10], [0, 0, 80, 20], [80, 10, 80, 10]])
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
# -----------------
|
|
544
|
+
# | a | b | c | d |
|
|
545
|
+
# | a | b |---| d |
|
|
546
|
+
# | a | b | e | |
|
|
547
|
+
# | a | | e | |
|
|
548
|
+
# --------| e | |
|
|
549
|
+
# | f | g | e | |
|
|
550
|
+
# ----------------
|
|
551
|
+
it "works if multiple, possibly overlapping row spans are involved" do
|
|
552
|
+
cells = [[{row_span: 2, content: @fixed_size_boxes[0..2]},
|
|
553
|
+
{row_span: 2, content: @fixed_size_boxes[3..4]},
|
|
554
|
+
@fixed_size_boxes[5],
|
|
555
|
+
{row_span: 3, content: @fixed_size_boxes[6..7]}],
|
|
556
|
+
[{row_span: 2, content: @fixed_size_boxes[8, 3]}],
|
|
557
|
+
[@fixed_size_boxes[11], @fixed_size_boxes[12]]]
|
|
558
|
+
check_box(create_box(cells: cells, cell_style: {padding: 0, border: {width: 0}}),
|
|
559
|
+
:success, 160, 40,
|
|
560
|
+
[[0, 0, 40, 30], [40, 0, 40, 30], [80, 0, 40, 10], [120, 0, 40, 40],
|
|
561
|
+
[0, 0, 40, 30], [40, 0, 40, 30], [80, 10, 40, 30], [120, 0, 40, 40],
|
|
562
|
+
[0, 30, 40, 10], [40, 30, 40, 10], [80, 10, 40, 30], [120, 0, 40, 40]])
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
514
566
|
it "fits a table with header rows" do
|
|
515
567
|
result = [[0, 0, 80, 10], [80, 0, 80, 10], [0, 10, 80, 10], [80, 10, 80, 10]]
|
|
516
568
|
header = lambda {|_| [@fixed_size_boxes[10, 2], @fixed_size_boxes[12, 2]] }
|
|
@@ -8,6 +8,7 @@ describe HexaPDF::Task::Optimize do
|
|
|
8
8
|
class TestType < HexaPDF::Dictionary
|
|
9
9
|
|
|
10
10
|
define_type :Test
|
|
11
|
+
define_field :Type, type: Symbol, default: type
|
|
11
12
|
define_field :Optional, type: Symbol, default: :Optional
|
|
12
13
|
|
|
13
14
|
end
|
|
@@ -46,6 +47,7 @@ describe HexaPDF::Task::Optimize do
|
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def assert_default_deleted
|
|
50
|
+
assert(@doc.object(1).key?(:Type))
|
|
49
51
|
refute(@doc.object(1).key?(:Optional))
|
|
50
52
|
end
|
|
51
53
|
|
|
@@ -497,10 +497,10 @@ describe HexaPDF::Document do
|
|
|
497
497
|
|
|
498
498
|
it "returns all signature fields of the document" do
|
|
499
499
|
form = @doc.acro_form(create: true)
|
|
500
|
-
sig1 = @doc.add({FT: :Sig, T: 'sig1', V: :sig1})
|
|
501
|
-
sig2 = @doc.add({FT: :Sig, T: 'sig2', V: :sig2})
|
|
500
|
+
sig1 = @doc.add({FT: :Sig, T: 'sig1', V: {k: :sig1}})
|
|
501
|
+
sig2 = @doc.add({FT: :Sig, T: 'sig2', V: {k: :sig2}})
|
|
502
502
|
form.root_fields << sig1 << sig2
|
|
503
|
-
assert_equal([:sig1, :sig2], @doc.signatures.to_a)
|
|
503
|
+
assert_equal([{k: :sig1}, {k: :sig2}], @doc.signatures.to_a)
|
|
504
504
|
end
|
|
505
505
|
|
|
506
506
|
it "allows to conveniently sign a document" do
|
|
@@ -147,6 +147,13 @@ describe HexaPDF::Importer do
|
|
|
147
147
|
assert_nil(obj[:pages])
|
|
148
148
|
end
|
|
149
149
|
|
|
150
|
+
it "handles null values correctly" do
|
|
151
|
+
@source.add(@hash)
|
|
152
|
+
@source.delete(@hash)
|
|
153
|
+
obj = @importer.import(@obj)
|
|
154
|
+
assert_nil(obj[:hash])
|
|
155
|
+
end
|
|
156
|
+
|
|
150
157
|
it "imports Page objects correctly by copying the inherited values" do
|
|
151
158
|
page = @importer.import(@source.pages[0])
|
|
152
159
|
assert_equal(90, page[:Rotate])
|
data/test/hexapdf/test_writer.rb
CHANGED
|
@@ -64,7 +64,7 @@ describe HexaPDF::Writer do
|
|
|
64
64
|
20
|
|
65
65
|
endobj
|
|
66
66
|
3 0 obj
|
|
67
|
-
<</Type/XRef/
|
|
67
|
+
<</Size 6/Type/XRef/W[1 1 2]/Index[0 4 5 1]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 31>>stream
|
|
68
68
|
x\xDAcb`\xF8\xFF\x9F\x89\x89\x95\x91\x91\xE9\x7F\x19\x03\x03\x13\x83\x10\x88he`\x00\x00B4\x04\x1E
|
|
69
69
|
endstream
|
|
70
70
|
endobj
|
|
@@ -80,7 +80,7 @@ describe HexaPDF::Writer do
|
|
|
80
80
|
endstream
|
|
81
81
|
endobj
|
|
82
82
|
4 0 obj
|
|
83
|
-
<</
|
|
83
|
+
<</Size 7/Root<</Type/Catalog>>/Info 6 0 R/Prev 141/Type/XRef/W[1 2 2]/Index[2 1 4 1 6 1]/Filter/FlateDecode/DecodeParms<</Columns 5/Predictor 12>>/Length 22>>stream
|
|
84
84
|
x\xDAcbdlg``b`\xB0\x04\x93\x93\x18\x18\x00\f\e\x01[
|
|
85
85
|
endstream
|
|
86
86
|
endobj
|
|
@@ -270,4 +270,13 @@ describe HexaPDF::Writer do
|
|
|
270
270
|
doc = HexaPDF::Document.new(io: io)
|
|
271
271
|
refute(doc.trailer.key?(:XRefStm))
|
|
272
272
|
end
|
|
273
|
+
|
|
274
|
+
it "removes the /Type entry in a non-xref stream trailer" do
|
|
275
|
+
io = StringIO.new
|
|
276
|
+
doc = HexaPDF::Document.new
|
|
277
|
+
doc.trailer[:Type] = :XRef
|
|
278
|
+
doc.write(io)
|
|
279
|
+
doc = HexaPDF::Document.new(io: io)
|
|
280
|
+
refute(doc.trailer.key?(:Type))
|
|
281
|
+
end
|
|
273
282
|
end
|
|
@@ -291,6 +291,22 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
|
291
291
|
assert_equal(:XObject, @widget[:AP][:N][:Other].type)
|
|
292
292
|
end
|
|
293
293
|
|
|
294
|
+
it "uses the field's value or :Yes for the on state if the appearance dictionary doesn't contain a name for it" do
|
|
295
|
+
@widget[:AP][:N].delete(:Yes)
|
|
296
|
+
@generator.create_appearances
|
|
297
|
+
assert_equal(:XObject, @widget[:AP][:N][:Yes].type)
|
|
298
|
+
|
|
299
|
+
@widget[:AP][:N].delete(:Yes)
|
|
300
|
+
@field[:V] = nil
|
|
301
|
+
@generator.create_appearances
|
|
302
|
+
assert_equal(:XObject, @widget[:AP][:N][:Yes].type)
|
|
303
|
+
|
|
304
|
+
@widget[:AP][:N].delete(:Yes)
|
|
305
|
+
@field[:V] = "other" # some PDFs use a string instead of the correct symbol
|
|
306
|
+
@generator.create_appearances
|
|
307
|
+
assert_equal(:XObject, @widget[:AP][:N][:other].type)
|
|
308
|
+
end
|
|
309
|
+
|
|
294
310
|
it "creates the needed appearance streams" do
|
|
295
311
|
@widget[:AP][:N].delete(:Off)
|
|
296
312
|
@generator.create_appearances
|
|
@@ -327,11 +343,6 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
|
327
343
|
[:end_text],
|
|
328
344
|
[:restore_graphics_state]])
|
|
329
345
|
end
|
|
330
|
-
|
|
331
|
-
it "fails if the appearance dictionary doesn't contain a name for the on state" do
|
|
332
|
-
@widget[:AP][:N].delete(:Yes)
|
|
333
|
-
assert_raises(HexaPDF::Error) { @generator.create_appearances }
|
|
334
|
-
end
|
|
335
346
|
end
|
|
336
347
|
|
|
337
348
|
describe "radio button" do
|
|
@@ -441,6 +452,12 @@ describe HexaPDF::Type::AcroForm::AppearanceGenerator do
|
|
|
441
452
|
assert_equal(form, @widget[:AP][:N])
|
|
442
453
|
refute(form.key?(:key))
|
|
443
454
|
assert_match(/test1/, form.contents)
|
|
455
|
+
|
|
456
|
+
form.delete(:Type)
|
|
457
|
+
@widget[:AP][:N] = @doc.wrap(form, type: HexaPDF::Type::Annotation)
|
|
458
|
+
@field[:V] = 'test2'
|
|
459
|
+
@generator.create_appearances
|
|
460
|
+
assert_match(/test2/, form.contents)
|
|
444
461
|
end
|
|
445
462
|
|
|
446
463
|
describe "takes the rotation into account" do
|
|
@@ -507,11 +507,6 @@ describe HexaPDF::Type::AcroForm::Form do
|
|
|
507
507
|
assert(@acro_form.validate)
|
|
508
508
|
end
|
|
509
509
|
|
|
510
|
-
it "set the default appearance string, though optional, to a valid value to avoid problems" do
|
|
511
|
-
assert(@acro_form.validate)
|
|
512
|
-
assert_equal("0.0 g /F1 0 Tf", @acro_form[:DA])
|
|
513
|
-
end
|
|
514
|
-
|
|
515
510
|
describe "field hierarchy validation" do
|
|
516
511
|
before do
|
|
517
512
|
@acro_form[:Fields] = [
|
|
@@ -31,7 +31,9 @@ describe HexaPDF::Type::AcroForm::SignatureField do
|
|
|
31
31
|
|
|
32
32
|
it "gets the field value" do
|
|
33
33
|
@field[:V] = {Empty: :True}
|
|
34
|
-
|
|
34
|
+
value = @field.field_value
|
|
35
|
+
assert_kind_of(HexaPDF::DigitalSignature::Signature, value)
|
|
36
|
+
assert_equal({Empty: :True}, value)
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
it "validates the value of the /FT field" do
|
|
@@ -102,9 +102,22 @@ describe HexaPDF::Type::AcroForm::VariableTextField do
|
|
|
102
102
|
@field.parse_default_appearance_string)
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
-
it "
|
|
105
|
+
it "sets a standard /DA value if no other /DA is found" do
|
|
106
106
|
@doc.acro_form.delete(:DA)
|
|
107
|
+
assert_equal([:F1, 0, HexaPDF::Content::ColorSpace.prenormalized_device_color([0])],
|
|
108
|
+
@field.parse_default_appearance_string)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "converts the /DA to a string in case an invalid PDF uses a Symbol" do
|
|
112
|
+
@field[:DA] = :"1 g /F1 20 Tf"
|
|
113
|
+
assert_equal([:F1, 20, @color], @field.parse_default_appearance_string)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it "fails if no /DA value is set and no default appearance string should be set" do
|
|
117
|
+
@doc.acro_form.delete(:DA)
|
|
118
|
+
@doc.config['acro_form.fallback_default_appearance'] = nil
|
|
107
119
|
assert_raises(HexaPDF::Error) { @field.parse_default_appearance_string }
|
|
108
120
|
end
|
|
121
|
+
|
|
109
122
|
end
|
|
110
123
|
end
|
|
@@ -194,6 +194,8 @@ describe HexaPDF::Type::Annotations::Widget do
|
|
|
194
194
|
|
|
195
195
|
it "returns the default size if none is set" do
|
|
196
196
|
assert_equal(0, @widget.marker_style.size)
|
|
197
|
+
@widget.form_field[:DA] = "0.0 g"
|
|
198
|
+
assert_equal(0, @widget.marker_style.size)
|
|
197
199
|
end
|
|
198
200
|
|
|
199
201
|
it "sets the given size" do
|
|
@@ -212,6 +214,8 @@ describe HexaPDF::Type::Annotations::Widget do
|
|
|
212
214
|
|
|
213
215
|
it "returns the default color if none is set" do
|
|
214
216
|
assert_equal([0], @widget.marker_style.color.components)
|
|
217
|
+
@widget.form_field[:DA] = "/ZaDb 10 Tfg"
|
|
218
|
+
assert_equal([0], @widget.marker_style.color.components)
|
|
215
219
|
end
|
|
216
220
|
|
|
217
221
|
it "sets the given color" do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hexapdf
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.47.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Thomas Leitner
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2024-
|
|
11
|
+
date: 2024-09-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: cmdparse
|
|
@@ -594,21 +594,25 @@ files:
|
|
|
594
594
|
- test/data/minimal.pdf
|
|
595
595
|
- test/data/standard-security-handler/README
|
|
596
596
|
- test/data/standard-security-handler/bothpwd-aes-128bit-V4.pdf
|
|
597
|
+
- test/data/standard-security-handler/bothpwd-aes-256bit-V5-R5.pdf
|
|
597
598
|
- test/data/standard-security-handler/bothpwd-aes-256bit-V5.pdf
|
|
598
599
|
- test/data/standard-security-handler/bothpwd-arc4-128bit-V2.pdf
|
|
599
600
|
- test/data/standard-security-handler/bothpwd-arc4-128bit-V4.pdf
|
|
600
601
|
- test/data/standard-security-handler/bothpwd-arc4-40bit-V1.pdf
|
|
601
602
|
- test/data/standard-security-handler/nopwd-aes-128bit-V4.pdf
|
|
603
|
+
- test/data/standard-security-handler/nopwd-aes-256bit-V5-R5.pdf
|
|
602
604
|
- test/data/standard-security-handler/nopwd-aes-256bit-V5.pdf
|
|
603
605
|
- test/data/standard-security-handler/nopwd-arc4-128bit-V2.pdf
|
|
604
606
|
- test/data/standard-security-handler/nopwd-arc4-128bit-V4.pdf
|
|
605
607
|
- test/data/standard-security-handler/nopwd-arc4-40bit-V1.pdf
|
|
606
608
|
- test/data/standard-security-handler/ownerpwd-aes-128bit-V4.pdf
|
|
609
|
+
- test/data/standard-security-handler/ownerpwd-aes-256bit-V5-R5.pdf
|
|
607
610
|
- test/data/standard-security-handler/ownerpwd-aes-256bit-V5.pdf
|
|
608
611
|
- test/data/standard-security-handler/ownerpwd-arc4-128bit-V2.pdf
|
|
609
612
|
- test/data/standard-security-handler/ownerpwd-arc4-128bit-V4.pdf
|
|
610
613
|
- test/data/standard-security-handler/ownerpwd-arc4-40bit-V1.pdf
|
|
611
614
|
- test/data/standard-security-handler/userpwd-aes-128bit-V4.pdf
|
|
615
|
+
- test/data/standard-security-handler/userpwd-aes-256bit-V5-R5.pdf
|
|
612
616
|
- test/data/standard-security-handler/userpwd-aes-256bit-V5.pdf
|
|
613
617
|
- test/data/standard-security-handler/userpwd-arc4-128bit-V2.pdf
|
|
614
618
|
- test/data/standard-security-handler/userpwd-arc4-128bit-V4.pdf
|