hexapdf 0.14.4 → 0.15.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 +38 -0
- data/lib/hexapdf/cli/form.rb +30 -8
- data/lib/hexapdf/configuration.rb +18 -3
- data/lib/hexapdf/error.rb +4 -3
- data/lib/hexapdf/parser.rb +18 -6
- data/lib/hexapdf/revision.rb +16 -0
- data/lib/hexapdf/type/acro_form.rb +1 -0
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +29 -17
- data/lib/hexapdf/type/acro_form/button_field.rb +8 -4
- data/lib/hexapdf/type/acro_form/field.rb +1 -0
- data/lib/hexapdf/type/acro_form/form.rb +37 -0
- data/lib/hexapdf/type/acro_form/signature_field.rb +223 -0
- data/lib/hexapdf/type/annotation.rb +13 -9
- data/lib/hexapdf/type/annotations/widget.rb +3 -1
- data/lib/hexapdf/type/font_descriptor.rb +9 -2
- data/lib/hexapdf/type/page.rb +81 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/test_parser.rb +23 -3
- data/test/hexapdf/test_revision.rb +21 -0
- data/test/hexapdf/test_writer.rb +2 -2
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +21 -2
- data/test/hexapdf/type/acro_form/test_button_field.rb +13 -7
- data/test/hexapdf/type/acro_form/test_field.rb +5 -0
- data/test/hexapdf/type/acro_form/test_form.rb +46 -2
- data/test/hexapdf/type/acro_form/test_signature_field.rb +38 -0
- data/test/hexapdf/type/annotations/test_widget.rb +2 -0
- data/test/hexapdf/type/test_annotation.rb +20 -10
- data/test/hexapdf/type/test_font_descriptor.rb +7 -0
- data/test/hexapdf/type/test_page.rb +187 -49
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bcd3bc77b70872416b1377b4fdf97804de083cf4d7213dfd200738fd8b2adae7
|
4
|
+
data.tar.gz: 53a8a850610a744570999cf56c656d6bd65c8ab691a5658b81172111bdd44804
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 54c99dbd44c4ae146496912f295982d47bb5ca297d4d2b76475c1f3151670068cd3be0c2dedee413b0a52e493383229bbd36d819ff2db2a9c04b377731cc107e
|
7
|
+
data.tar.gz: 8426e942709633b921f7a644e01b645d555cfcdecbdbd25bae03d0154cf07df3a3cd1f108adb16a8b449801c203014698a6b382133bbb63033a1d590351b61f7
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,41 @@
|
|
1
|
+
## 0.15.0 - 2021-04-12
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* [HexaPDF::Type::Page#flatten_annotations] for flattening the annotations of a
|
6
|
+
page
|
7
|
+
* [HexaPDF::Type::AcroForm::Form#flatten] for flattening interactive forms
|
8
|
+
* [HexaPDF::Revision#update] for updating the stored wrapper class of a PDF
|
9
|
+
object
|
10
|
+
* [HexaPDF::Type::AcroForm::SignatureField] for working with AcroForm signature
|
11
|
+
fields
|
12
|
+
* Support for form field flattening to the `hexapdf form` CLI command
|
13
|
+
|
14
|
+
### Changed
|
15
|
+
|
16
|
+
* **Breaking change**: Overhauled the interface for accessing appearances of
|
17
|
+
annotations to make it more convenient
|
18
|
+
* Validation of [HexaPDF::Type::FontDescriptor] to delete invalid `/FontWeight`
|
19
|
+
value
|
20
|
+
* [HexaPDF::MalformedPDFError#pos] an accessor instead of a reader and update
|
21
|
+
the exception message
|
22
|
+
* Configuration option 'acro_form.fallback_font' to allow a callable object for
|
23
|
+
more advanced fallback font handling
|
24
|
+
|
25
|
+
### Fixed
|
26
|
+
|
27
|
+
* [HexaPDF::Type::Annotations::Widget#background_color] to correctly handle
|
28
|
+
empty background color arrays
|
29
|
+
* [HexaPDF::Type::AcroForm::Field#delete_widget] to update the wrapper object
|
30
|
+
stored in the document in case the widget is embedded
|
31
|
+
* Processing of invalid PDF files containing a space,CR,LF combination after
|
32
|
+
the 'stream' keyword
|
33
|
+
* Cross-reference stream reconstruction with respect to detection of linearized
|
34
|
+
files
|
35
|
+
* Detection of existing appearances for AcroForm push button fields when
|
36
|
+
creating appearances
|
37
|
+
|
38
|
+
|
1
39
|
## 0.14.4 - 2021-02-27
|
2
40
|
|
3
41
|
### Added
|
data/lib/hexapdf/cli/form.rb
CHANGED
@@ -52,18 +52,26 @@ module HexaPDF
|
|
52
52
|
If the the output file name is not given, all form fields are listed in page order. Use
|
53
53
|
the global --verbose option to show additional information like field type and location.
|
54
54
|
|
55
|
-
If the output file name is given, the fields can be
|
56
|
-
|
57
|
-
|
55
|
+
If the output file name is given, the fields can be filled out interactively, via a
|
56
|
+
template or just flattened by using the respective options. Form field flattening can also
|
57
|
+
be activated in addition to filling out the form. If neither --fill, --template nor
|
58
|
+
--flatten is specified, --fill is implied.
|
58
59
|
EOF
|
59
60
|
|
60
61
|
options.on("--password PASSWORD", "-p", String,
|
61
62
|
"The password for decryption. Use - for reading from standard input.") do |pwd|
|
62
63
|
@password = (pwd == '-' ? read_password : pwd)
|
63
64
|
end
|
65
|
+
options.on("--fill", "Fill out the form") do
|
66
|
+
@fill = true
|
67
|
+
end
|
64
68
|
options.on("--template TEMPLATE_FILE", "-t TEMPLATE_FILE",
|
65
|
-
"Use the template file for the field values") do |template|
|
69
|
+
"Use the template file for the field values (implies --fill)") do |template|
|
66
70
|
@template = template
|
71
|
+
@fill = true
|
72
|
+
end
|
73
|
+
options.on('--flatten', 'Flatten the form fields') do
|
74
|
+
@flatten = true
|
67
75
|
end
|
68
76
|
options.on("--[no-]viewer-override", "Let the PDF viewer override the visual " \
|
69
77
|
"appearance. Default: use setting from input PDF") do |need_appearances|
|
@@ -75,6 +83,8 @@ module HexaPDF
|
|
75
83
|
end
|
76
84
|
|
77
85
|
@password = nil
|
86
|
+
@fill = false
|
87
|
+
@flatten = false
|
78
88
|
@template = nil
|
79
89
|
@need_appearances = nil
|
80
90
|
@incremental = true
|
@@ -82,16 +92,28 @@ module HexaPDF
|
|
82
92
|
|
83
93
|
def execute(in_file, out_file = nil) #:nodoc:
|
84
94
|
maybe_raise_on_existing_file(out_file) if out_file
|
95
|
+
if (@fill || @flatten) && !out_file
|
96
|
+
raise "Output file missing"
|
97
|
+
end
|
85
98
|
with_document(in_file, password: @password, out_file: out_file,
|
86
99
|
incremental: @incremental) do |doc|
|
87
100
|
if !doc.acro_form
|
88
101
|
raise "This PDF doesn't contain an interactive form"
|
89
102
|
elsif out_file
|
90
103
|
doc.acro_form[:NeedAppearances] = @need_appearances unless @need_appearances.nil?
|
91
|
-
if @
|
92
|
-
|
93
|
-
|
94
|
-
|
104
|
+
if @fill || !@flatten
|
105
|
+
if @template
|
106
|
+
fill_form_with_template(doc)
|
107
|
+
else
|
108
|
+
fill_form(doc)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
if @flatten
|
112
|
+
unless doc.acro_form.flatten.empty?
|
113
|
+
$stderr.puts "Warning: Not all form fields could be flattened"
|
114
|
+
doc.catalog.delete(:AcroForm)
|
115
|
+
doc.delete(doc.acro_form)
|
116
|
+
end
|
95
117
|
end
|
96
118
|
else
|
97
119
|
list_form_fields(doc)
|
@@ -164,9 +164,20 @@ module HexaPDF
|
|
164
164
|
# acro_form.fallback_font::
|
165
165
|
# The font that should be used when a variable text field references a font that cannot be used.
|
166
166
|
#
|
167
|
-
# Can
|
168
|
-
#
|
169
|
-
#
|
167
|
+
# Can be one of the following:
|
168
|
+
#
|
169
|
+
# * The name of a font, like 'Helvetica'.
|
170
|
+
#
|
171
|
+
# * An array consisting of the font name and a hash of font options, like ['Helvetica',
|
172
|
+
# variant: :italic].
|
173
|
+
#
|
174
|
+
# * A callable object receiving the field and the font object (or +nil+ if no valid font object
|
175
|
+
# was found) and which has to return either a font name or an array consisting of the font
|
176
|
+
# name and a hash of font options. This way the response can be different depending on the
|
177
|
+
# original font and it would also allow e.g. modifying the configured fonts to add custom
|
178
|
+
# ones.
|
179
|
+
#
|
180
|
+
# If set to +nil+, the use of the fallback font is disabled.
|
170
181
|
#
|
171
182
|
# Default is 'Helvetica'.
|
172
183
|
#
|
@@ -516,6 +527,9 @@ module HexaPDF
|
|
516
527
|
XXAcroFormField: 'HexaPDF::Type::AcroForm::Field',
|
517
528
|
XXAppearanceDictionary: 'HexaPDF::Type::Annotation::AppearanceDictionary',
|
518
529
|
Border: 'HexaPDF::Type::Annotation::Border',
|
530
|
+
SigFieldLock: 'HexaPDF::Type::AcroForm::SignatureField::LockDictionary',
|
531
|
+
SV: 'HexaPDF::Type::AcroForm::SignatureField::SeedValueDictionary',
|
532
|
+
SVCert: 'HexaPDF::Type::AcroForm::SignatureField::CertificateSeedValueDictionary',
|
519
533
|
},
|
520
534
|
'object.subtype_map' => {
|
521
535
|
nil => {
|
@@ -561,6 +575,7 @@ module HexaPDF
|
|
561
575
|
Tx: 'HexaPDF::Type::AcroForm::TextField',
|
562
576
|
Btn: 'HexaPDF::Type::AcroForm::ButtonField',
|
563
577
|
Ch: 'HexaPDF::Type::AcroForm::ChoiceField',
|
578
|
+
Sig: 'HexaPDF::Type::AcroForm::SignatureField',
|
564
579
|
},
|
565
580
|
})
|
566
581
|
|
data/lib/hexapdf/error.rb
CHANGED
@@ -43,18 +43,19 @@ module HexaPDF
|
|
43
43
|
class MalformedPDFError < Error
|
44
44
|
|
45
45
|
# The byte position in the PDF file where the error occured.
|
46
|
-
|
46
|
+
attr_accessor :pos
|
47
47
|
|
48
48
|
# Creates a new malformed PDF error object for the given exception message.
|
49
49
|
#
|
50
|
-
# The byte position where the error occured can be given via the +pos+ argument
|
50
|
+
# The byte position where the error occured can either be given via the +pos+ argument or later
|
51
|
+
# via the #pos accessor but must be set before the exception message is retrieved.
|
51
52
|
def initialize(message, pos: nil)
|
52
53
|
super(message)
|
53
54
|
@pos = pos
|
54
55
|
end
|
55
56
|
|
56
57
|
def message # :nodoc:
|
57
|
-
"PDF malformed
|
58
|
+
"PDF malformed around position #{pos}: #{super}"
|
58
59
|
end
|
59
60
|
|
60
61
|
end
|
data/lib/hexapdf/parser.rb
CHANGED
@@ -140,11 +140,13 @@ module HexaPDF
|
|
140
140
|
raise_malformed("A stream needs a dictionary, not a(n) #{object.class}", pos: offset)
|
141
141
|
end
|
142
142
|
tok1 = @tokenizer.next_byte
|
143
|
-
|
143
|
+
if tok1 == 32 # space
|
144
|
+
maybe_raise("Keyword stream followed by space instead of LF or CR/LF", pos: @tokenizer.pos)
|
145
|
+
tok1 = @tokenizer.next_byte
|
146
|
+
end
|
147
|
+
tok2 = @tokenizer.next_byte if tok1 == 13 # CR
|
144
148
|
if tok1 != 10 && tok1 != 13
|
145
|
-
|
146
|
-
maybe_raise("Keyword stream must be followed by LF or CR/LF", pos: @tokenizer.pos,
|
147
|
-
force: tok1 != 32 || (tok2 != 10 && tok2 != 13)) # 32=space
|
149
|
+
raise_malformed("Keyword stream must be followed by LF or CR/LF", pos: @tokenizer.pos)
|
148
150
|
elsif tok1 == 13 && tok2 != 10
|
149
151
|
maybe_raise("Keyword stream must be followed by LF or CR/LF, not CR alone",
|
150
152
|
pos: @tokenizer.pos)
|
@@ -214,7 +216,12 @@ module HexaPDF
|
|
214
216
|
unless obj.respond_to?(:xref_section)
|
215
217
|
raise_malformed("Object is not a cross-reference stream", pos: pos)
|
216
218
|
end
|
217
|
-
|
219
|
+
begin
|
220
|
+
xref_section = obj.xref_section
|
221
|
+
rescue MalformedPDFError => e
|
222
|
+
e.pos = pos
|
223
|
+
raise
|
224
|
+
end
|
218
225
|
trailer = obj.trailer
|
219
226
|
unless xref_section.entry?(obj.oid, obj.gen)
|
220
227
|
maybe_raise("Cross-reference stream doesn't contain entry for itself", pos: pos)
|
@@ -401,6 +408,7 @@ module HexaPDF
|
|
401
408
|
|
402
409
|
xref = XRefSection.new
|
403
410
|
@tokenizer.pos = 0
|
411
|
+
linearized = nil
|
404
412
|
while true
|
405
413
|
@tokenizer.skip_whitespace
|
406
414
|
pos = @tokenizer.pos
|
@@ -416,13 +424,17 @@ module HexaPDF
|
|
416
424
|
@tokenizer.pos = next_new_line_pos
|
417
425
|
elsif gen.kind_of?(Integer) && tok.kind_of?(Tokenizer::Token) && tok == 'obj'
|
418
426
|
xref.add_in_use_entry(token, gen, pos)
|
427
|
+
if linearized.nil?
|
428
|
+
obj = @tokenizer.next_object rescue nil
|
429
|
+
linearized = obj.kind_of?(Hash) && obj.key?(:Linearized)
|
430
|
+
end
|
419
431
|
@tokenizer.scan_until(/(?:\n|\r\n?)endobj\b/)
|
420
432
|
end
|
421
433
|
elsif token.kind_of?(Tokenizer::Token) && token == 'trailer'
|
422
434
|
obj = @tokenizer.next_object rescue nil
|
423
435
|
# Use last trailer found in case of multiple revisions but use first trailer in case of
|
424
436
|
# linearized file.
|
425
|
-
trailer = obj if obj.kind_of?(Hash) && (
|
437
|
+
trailer = obj if obj.kind_of?(Hash) && (!linearized || trailer.nil?)
|
426
438
|
elsif token == Tokenizer::NO_MORE_TOKENS
|
427
439
|
break
|
428
440
|
else
|
data/lib/hexapdf/revision.rb
CHANGED
@@ -158,6 +158,22 @@ module HexaPDF
|
|
158
158
|
add_without_check(obj)
|
159
159
|
end
|
160
160
|
|
161
|
+
# :call-seq:
|
162
|
+
# revision.update(obj) -> obj or nil
|
163
|
+
#
|
164
|
+
# Updates the stored object to point to the given HexaPDF::Object wrapper, returning the object
|
165
|
+
# if successful or +nil+ otherwise.
|
166
|
+
#
|
167
|
+
# If +obj+ isn't stored in this revision or the stored object doesn't contain the same
|
168
|
+
# HexaPDF::PDFData object as the given object, nothing is done.
|
169
|
+
#
|
170
|
+
# This method should only be used if the wrong wrapper class is stored (e.g. because
|
171
|
+
# auto-detection didn't or couldn't work correctly) and thus needs correction.
|
172
|
+
def update(obj)
|
173
|
+
return nil if object(obj)&.data != obj.data
|
174
|
+
add_without_check(obj)
|
175
|
+
end
|
176
|
+
|
161
177
|
# :call-seq:
|
162
178
|
# revision.delete(ref, mark_as_free: true)
|
163
179
|
# revision.delete(oid, mark_as_free: true)
|
@@ -48,6 +48,7 @@ module HexaPDF
|
|
48
48
|
autoload(:TextField, 'hexapdf/type/acro_form/text_field')
|
49
49
|
autoload(:ButtonField, 'hexapdf/type/acro_form/button_field')
|
50
50
|
autoload(:ChoiceField, 'hexapdf/type/acro_form/choice_field')
|
51
|
+
autoload(:SignatureField, 'hexapdf/type/acro_form/signature_field')
|
51
52
|
|
52
53
|
autoload(:AppearanceGenerator, 'hexapdf/type/acro_form/appearance_generator')
|
53
54
|
|
@@ -120,7 +120,7 @@ module HexaPDF
|
|
120
120
|
# widget.marker_style(style: :cross)
|
121
121
|
# # => no visible rectangle, gray background, cross mark when checked
|
122
122
|
def create_check_box_appearances
|
123
|
-
unless @widget.
|
123
|
+
unless @widget.appearance_dict&.normal_appearance&.value&.size == 2
|
124
124
|
raise HexaPDF::Error, "Widget of check box doesn't define name for on state"
|
125
125
|
end
|
126
126
|
border_style = @widget.border_style
|
@@ -128,11 +128,11 @@ module HexaPDF
|
|
128
128
|
|
129
129
|
rect = update_widget(@field[:V], border_width)
|
130
130
|
|
131
|
-
off_form = @widget.
|
131
|
+
off_form = @widget.appearance_dict.normal_appearance[:Off] =
|
132
132
|
@document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
|
133
133
|
apply_background_and_border(border_style, off_form.canvas)
|
134
134
|
|
135
|
-
on_form = @widget.
|
135
|
+
on_form = @widget.appearance_dict.normal_appearance[@field.check_box_on_name] =
|
136
136
|
@document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
|
137
137
|
canvas = on_form.canvas
|
138
138
|
apply_background_and_border(border_style, canvas)
|
@@ -169,22 +169,22 @@ module HexaPDF
|
|
169
169
|
# widget.marker_style(style: :circle, size: 0, color: 0)
|
170
170
|
# # => default appearance
|
171
171
|
def create_radio_button_appearances
|
172
|
-
unless @widget.
|
172
|
+
unless @widget.appearance_dict&.normal_appearance&.value&.size == 2
|
173
173
|
raise HexaPDF::Error, "Widget of radio button doesn't define unique name for on state"
|
174
174
|
end
|
175
175
|
|
176
|
-
on_name = (@widget.
|
176
|
+
on_name = (@widget.appearance_dict.normal_appearance.value.keys - [:Off]).first
|
177
177
|
border_style = @widget.border_style
|
178
178
|
marker_style = @widget.marker_style
|
179
179
|
|
180
180
|
rect = update_widget(@field[:V] == on_name ? on_name : :Off, border_style.width)
|
181
181
|
|
182
|
-
off_form = @widget.
|
182
|
+
off_form = @widget.appearance_dict.normal_appearance[:Off] =
|
183
183
|
@document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
|
184
184
|
apply_background_and_border(border_style, off_form.canvas,
|
185
185
|
circular: marker_style.style == :circle)
|
186
186
|
|
187
|
-
on_form = @widget.
|
187
|
+
on_form = @widget.appearance_dict.normal_appearance[on_name] =
|
188
188
|
@document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, rect.width, rect.height]})
|
189
189
|
canvas = on_form.canvas
|
190
190
|
apply_background_and_border(border_style, canvas,
|
@@ -219,17 +219,8 @@ module HexaPDF
|
|
219
219
|
#
|
220
220
|
# Note: Multiline, comb and rich text fields are currently not supported!
|
221
221
|
def create_text_appearances
|
222
|
-
font_name, font_size = @field.parse_default_appearance_string
|
223
222
|
default_resources = @document.acro_form.default_resources
|
224
|
-
font = default_resources
|
225
|
-
unless font
|
226
|
-
fallback_font_name, fallback_font_options = @document.config['acro_form.fallback_font']
|
227
|
-
if fallback_font_name
|
228
|
-
font = @document.fonts.add(fallback_font_name, **(fallback_font_options || {}))
|
229
|
-
else
|
230
|
-
raise(HexaPDF::Error, "Font #{font_name} of the AcroForm's default resources not usable")
|
231
|
-
end
|
232
|
-
end
|
223
|
+
font, font_size = retrieve_font_information(default_resources)
|
233
224
|
style = HexaPDF::Layout::Style.new(font: font)
|
234
225
|
border_style = @widget.border_style
|
235
226
|
padding = [1, border_style.width].max
|
@@ -482,6 +473,27 @@ module HexaPDF
|
|
482
473
|
end
|
483
474
|
end
|
484
475
|
|
476
|
+
# Returns the font wrapper and font size to be used for a variable text field.
|
477
|
+
def retrieve_font_information(resources)
|
478
|
+
font_name, font_size = @field.parse_default_appearance_string
|
479
|
+
font_object = resources.font(font_name) rescue nil
|
480
|
+
font = font_object&.font_wrapper
|
481
|
+
unless font
|
482
|
+
fallback_font = @document.config['acro_form.fallback_font']
|
483
|
+
fallback_font_name, fallback_font_options = if fallback_font.respond_to?(:call)
|
484
|
+
fallback_font.call(@field, font_object)
|
485
|
+
else
|
486
|
+
fallback_font
|
487
|
+
end
|
488
|
+
if fallback_font_name
|
489
|
+
font = @document.fonts.add(fallback_font_name, **(fallback_font_options || {}))
|
490
|
+
else
|
491
|
+
raise(HexaPDF::Error, "Font #{font_name} of the AcroForm's default resources not usable")
|
492
|
+
end
|
493
|
+
end
|
494
|
+
[font, font_size]
|
495
|
+
end
|
496
|
+
|
485
497
|
# Calculates the font size for text fields based on the font and font size of the default
|
486
498
|
# appearance string, the annotation rectangle and the border style.
|
487
499
|
def calculate_font_size(font, font_size, rect, border_style)
|
@@ -184,7 +184,7 @@ module HexaPDF
|
|
184
184
|
#
|
185
185
|
# Defaults to :Yes if no other name could be determined.
|
186
186
|
def check_box_on_name
|
187
|
-
each_widget.to_a.first&.
|
187
|
+
each_widget.to_a.first&.appearance_dict&.normal_appearance&.value&.each_key&.
|
188
188
|
find {|key| key != :Off } || :Yes
|
189
189
|
end
|
190
190
|
|
@@ -192,7 +192,7 @@ module HexaPDF
|
|
192
192
|
# button.
|
193
193
|
def radio_button_values
|
194
194
|
each_widget.map do |widget|
|
195
|
-
widget.
|
195
|
+
widget.appearance_dict&.normal_appearance&.value&.each_key&.find {|key| key != :Off }
|
196
196
|
end.compact
|
197
197
|
end
|
198
198
|
|
@@ -233,7 +233,11 @@ module HexaPDF
|
|
233
233
|
def create_appearances(force: false)
|
234
234
|
appearance_generator_class = document.config.constantize('acro_form.appearance_generator')
|
235
235
|
each_widget do |widget|
|
236
|
-
|
236
|
+
normal_appearance = widget.appearance_dict&.normal_appearance
|
237
|
+
next if !force && normal_appearance &&
|
238
|
+
((!push_button? && normal_appearance.value.length == 2 &&
|
239
|
+
normal_appearance.value.each_value.all?(HexaPDF::Stream)) ||
|
240
|
+
(push_button? && normal_appearance.kind_of?(HexaPDF::Stream)))
|
237
241
|
if check_box?
|
238
242
|
appearance_generator_class.new(widget).create_check_box_appearances
|
239
243
|
elsif radio_button?
|
@@ -250,7 +254,7 @@ module HexaPDF
|
|
250
254
|
create_appearances
|
251
255
|
value = self[:V]
|
252
256
|
each_widget do |widget|
|
253
|
-
widget[:AS] = (widget.
|
257
|
+
widget[:AS] = (widget.appearance_dict&.normal_appearance&.key?(value) ? value : :Off)
|
254
258
|
end
|
255
259
|
end
|
256
260
|
|
@@ -331,6 +331,43 @@ module HexaPDF
|
|
331
331
|
end
|
332
332
|
end
|
333
333
|
|
334
|
+
# Flattens the whole interactive form or only the given fields, and returns the fields that
|
335
|
+
# couldn't be flattened.
|
336
|
+
#
|
337
|
+
# Flattening means making the appearance streams of the field widgets part of the respective
|
338
|
+
# page's content stream and removing the fields themselves.
|
339
|
+
#
|
340
|
+
# If the whole interactive form is flattened, the form object itself is also removed if all
|
341
|
+
# fields were flattened.
|
342
|
+
#
|
343
|
+
# The +create_appearances+ argument controls whether missing appearances should
|
344
|
+
# automatically be created.
|
345
|
+
#
|
346
|
+
# See: HexaPDF::Type::Page#flatten_annotations
|
347
|
+
def flatten(fields: nil, create_appearances: true)
|
348
|
+
remove_form = fields.nil?
|
349
|
+
fields ||= each_field.to_a
|
350
|
+
if create_appearances
|
351
|
+
fields.each {|field| field.create_appearances if field.respond_to?(:create_appearances) }
|
352
|
+
end
|
353
|
+
|
354
|
+
not_flattened = fields.map {|field| field.each_widget.to_a }.flatten
|
355
|
+
document.pages.each {|page| not_flattened = page.flatten_annotations(not_flattened) }
|
356
|
+
fields -= not_flattened.map(&:form_field)
|
357
|
+
|
358
|
+
fields.each do |field|
|
359
|
+
(field[:Parent]&.[](:Kids) || self[:Fields]).delete(field)
|
360
|
+
document.delete(field)
|
361
|
+
end
|
362
|
+
|
363
|
+
if remove_form && not_flattened.empty?
|
364
|
+
document.catalog.delete(:AcroForm)
|
365
|
+
document.delete(self)
|
366
|
+
end
|
367
|
+
|
368
|
+
not_flattened
|
369
|
+
end
|
370
|
+
|
334
371
|
private
|
335
372
|
|
336
373
|
# Helper method for bit field getter access.
|