hexapdf 0.46.0 → 0.47.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|