hexapdf 0.42.0 → 0.43.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 +29 -0
- data/Rakefile +1 -1
- data/lib/hexapdf/dictionary.rb +3 -3
- data/lib/hexapdf/document.rb +14 -1
- data/lib/hexapdf/encryption.rb +17 -0
- data/lib/hexapdf/layout/box.rb +1 -0
- data/lib/hexapdf/layout/box_fitter.rb +3 -3
- data/lib/hexapdf/layout/column_box.rb +2 -2
- data/lib/hexapdf/layout/container_box.rb +1 -1
- data/lib/hexapdf/layout/line.rb +4 -0
- data/lib/hexapdf/layout/list_box.rb +2 -2
- data/lib/hexapdf/layout/table_box.rb +1 -1
- data/lib/hexapdf/layout/text_box.rb +16 -2
- data/lib/hexapdf/parser.rb +20 -17
- data/lib/hexapdf/type/acro_form/form.rb +78 -27
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/layout/test_box_fitter.rb +3 -3
- data/test/hexapdf/layout/test_text_box.rb +27 -1
- data/test/hexapdf/test_dictionary.rb +6 -4
- data/test/hexapdf/test_parser.rb +12 -0
- data/test/hexapdf/type/acro_form/test_form.rb +63 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 24f6839e903fd945678915625b1e2ef6a12221f29a82cf1c7a6d8bcea38af288
|
4
|
+
data.tar.gz: 2f8309e2ef2406dd279e00643bf7fbf73ded63a1076c0067684a356fea8ee89e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c6ddd8ec6f19d39daa9a6422fdb3595d255b528fb93ca7907ef12dab0eca23c74de9fcfc27d02c15b3a9d3c24d47c7f7db6ea472f4c1ed34c2b7c06d4a8a351
|
7
|
+
data.tar.gz: 1d77c2da70d9048cbef6935afacde40fb6d4041414fce571a331a4bee33b0e3ee2ff59bb28eee02e515798c3c156a57f42f139356e946f64e878ea962756a1d8
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,32 @@
|
|
1
|
+
## 0.43.0 - 2024-05-26
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* [HexaPDF::Type::AcroForm::Form#create_namespace_field] for creating a pure
|
6
|
+
namespace field
|
7
|
+
* [HexaPDF::Type::AcroForm::Form#delete_field] for deleting fields
|
8
|
+
|
9
|
+
### Changed
|
10
|
+
|
11
|
+
* Minimum Ruby version to be 3.0
|
12
|
+
* **Breaking change**: Renamed `HexaPDF::Layout::BoxFitter#fit_successful?` to
|
13
|
+
[HexaPDF::Layout::BoxFitter#success?]
|
14
|
+
* **Breaking Change**: Removed HexaPDF::Dictionary#to_h
|
15
|
+
* Form field creation methods of [HexaPDF::Type::AcroForm::Form] to
|
16
|
+
automatically create parent fields as namespace fields
|
17
|
+
|
18
|
+
### Fixed
|
19
|
+
|
20
|
+
* [HexaPDF::Layout::TextBox#fit] to correctly calculate width in case of flowing
|
21
|
+
text around other boxes
|
22
|
+
* [HexaPDF::Layout::TextBox#draw] to correctly draw border, background... on
|
23
|
+
boxes using position 'flow'
|
24
|
+
* Comparison of Hash with [HexaPDF::Dictionary] objects by implementing
|
25
|
+
`#to_hash`
|
26
|
+
* Parsing of invalid files having multiple end-of-file markers with the last one
|
27
|
+
being invalid
|
28
|
+
|
29
|
+
|
1
30
|
## 0.42.0 - 2024-05-12
|
2
31
|
|
3
32
|
### Added
|
data/Rakefile
CHANGED
@@ -47,7 +47,7 @@ namespace :dev do
|
|
47
47
|
end
|
48
48
|
|
49
49
|
task :test_all do
|
50
|
-
versions = `rbenv versions --bare | grep -i ^
|
50
|
+
versions = `rbenv versions --bare | grep -i ^3.`.split("\n")
|
51
51
|
versions.each do |version|
|
52
52
|
sh "eval \"$(rbenv init -)\"; rbenv shell #{version} && ruby -v && rake test"
|
53
53
|
end
|
data/lib/hexapdf/dictionary.rb
CHANGED
@@ -228,9 +228,9 @@ module HexaPDF
|
|
228
228
|
value.empty?
|
229
229
|
end
|
230
230
|
|
231
|
-
# Returns a
|
232
|
-
def
|
233
|
-
value.
|
231
|
+
# Returns a hash containing the preprocessed values (like in #[]).
|
232
|
+
def to_hash
|
233
|
+
value.each_with_object({}) {|(k, _), h| h[k] = self[k] }
|
234
234
|
end
|
235
235
|
|
236
236
|
private
|
data/lib/hexapdf/document.rb
CHANGED
@@ -278,6 +278,14 @@ module HexaPDF
|
|
278
278
|
# If the same argument is provided in multiple invocations, the import is done only once and
|
279
279
|
# the previously imported object is returned.
|
280
280
|
#
|
281
|
+
# Note: If you first create a PDF document from scratch and then want to import objects from it
|
282
|
+
# into another PDF document, you need to run the following on the source document:
|
283
|
+
#
|
284
|
+
# doc.dispatch_message(:complete_objects)
|
285
|
+
# doc.validate
|
286
|
+
#
|
287
|
+
# This ensures that the source document has all the necessary PDF structures set-up correctly.
|
288
|
+
#
|
281
289
|
# See: Importer
|
282
290
|
def import(obj)
|
283
291
|
source = (obj.kind_of?(HexaPDF::Object) ? obj.document : nil)
|
@@ -617,13 +625,18 @@ module HexaPDF
|
|
617
625
|
# writing the document.
|
618
626
|
#
|
619
627
|
# The security handler used for encrypting is selected via the +name+ argument. All other
|
620
|
-
# arguments are passed on the security handler.
|
628
|
+
# arguments are passed on to the security handler.
|
621
629
|
#
|
622
630
|
# If the document should not be encrypted, the +name+ argument has to be set to +nil+. This
|
623
631
|
# removes the security handler and deletes the trailer's Encrypt dictionary.
|
624
632
|
#
|
625
633
|
# See: Encryption::SecurityHandler#set_up_encryption and
|
626
634
|
# Encryption::StandardSecurityHandler::EncryptionOptions for possible encryption options.
|
635
|
+
#
|
636
|
+
# Examples:
|
637
|
+
#
|
638
|
+
# document.encrypt(name: nil) # remove the existing encryption
|
639
|
+
# document.encrypt(algorithm: :aes, key_length: 256, permissions: [:print, :extract_content]
|
627
640
|
def encrypt(name: :Standard, **options)
|
628
641
|
if name.nil?
|
629
642
|
trailer.delete(:Encrypt)
|
data/lib/hexapdf/encryption.rb
CHANGED
@@ -46,6 +46,23 @@ module HexaPDF
|
|
46
46
|
#
|
47
47
|
# This module contains all encryption and security related code to facilitate PDF encryption.
|
48
48
|
#
|
49
|
+
# === Working With Encrypted Documents
|
50
|
+
#
|
51
|
+
# When a PDF document is opened, an encryption password can be specified. This is necessary if a
|
52
|
+
# user password is set on the file and optional otherwise (because the default password is
|
53
|
+
# automatically tried):
|
54
|
+
#
|
55
|
+
# HexaPDF::Document.open(filename, decryption_opts: {password: 'somepassword'}) do |doc|
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# To remove the encryption from a PDF document, use the following:
|
59
|
+
#
|
60
|
+
# document.encrypt(name: nil)
|
61
|
+
#
|
62
|
+
# To encrypt a PDF document, use the same method but specify the required encryption options:
|
63
|
+
#
|
64
|
+
# document.encrypt(algorithm: :aes, key_length: 256)
|
65
|
+
#
|
49
66
|
#
|
50
67
|
# === Security Handlers
|
51
68
|
#
|
data/lib/hexapdf/layout/box.rb
CHANGED
@@ -47,8 +47,8 @@ module HexaPDF
|
|
47
47
|
#
|
48
48
|
# * Then use the #fit method to fit boxes one after the other. No drawing is done.
|
49
49
|
#
|
50
|
-
# * Once all boxes have been fitted, the #fit_results, #remaining_boxes and #
|
51
|
-
#
|
50
|
+
# * Once all boxes have been fitted, the #fit_results, #remaining_boxes and #success? methods
|
51
|
+
# can be used to get the result:
|
52
52
|
#
|
53
53
|
# - If there are no remaining boxes, all boxes were successfully fitted into the frames.
|
54
54
|
# - If there are remaining boxes but no fit results, the first box could not be fitted.
|
@@ -126,7 +126,7 @@ module HexaPDF
|
|
126
126
|
end
|
127
127
|
|
128
128
|
# Returns +true+ if all boxes were successfully fitted.
|
129
|
-
def
|
129
|
+
def success?
|
130
130
|
@remaining_boxes.empty?
|
131
131
|
end
|
132
132
|
|
@@ -186,7 +186,7 @@ module HexaPDF
|
|
186
186
|
|
187
187
|
children.each {|box| @box_fitter.fit(box) }
|
188
188
|
|
189
|
-
fit_successful = @box_fitter.
|
189
|
+
fit_successful = @box_fitter.success?
|
190
190
|
initial_fit_successful = fit_successful if initial_fit_successful.nil?
|
191
191
|
|
192
192
|
if fit_successful
|
@@ -211,7 +211,7 @@ module HexaPDF
|
|
211
211
|
@draw_pos_x = frame.x + reserved_width_left
|
212
212
|
@draw_pos_y = frame.y - @height + reserved_height_bottom
|
213
213
|
|
214
|
-
@box_fitter.
|
214
|
+
@box_fitter.success?
|
215
215
|
end
|
216
216
|
|
217
217
|
private
|
@@ -137,7 +137,7 @@ module HexaPDF
|
|
137
137
|
@box_fitter = BoxFitter.new([my_frame])
|
138
138
|
children.each {|box| @box_fitter.fit(box) }
|
139
139
|
|
140
|
-
if @box_fitter.
|
140
|
+
if @box_fitter.success?
|
141
141
|
update_content_width do
|
142
142
|
result = @box_fitter.fit_results.max_by {|r| r.mask.x + r.mask.width }
|
143
143
|
children.empty? ? 0 : result.mask.x + result.mask.width - my_frame.left
|
data/lib/hexapdf/layout/line.rb
CHANGED
@@ -173,6 +173,10 @@ module HexaPDF
|
|
173
173
|
attr_accessor :items
|
174
174
|
|
175
175
|
# An optional horizontal offset that should be taken into account when positioning the line.
|
176
|
+
#
|
177
|
+
# This offset always describes the offset from the left side (and not, for example, the offset
|
178
|
+
# from the right side of another line even if those two lines are actually on the same
|
179
|
+
# horizontal level).
|
176
180
|
attr_accessor :x_offset
|
177
181
|
|
178
182
|
# An optional vertical offset that should be taken into account when positioning the line.
|
@@ -248,14 +248,14 @@ module HexaPDF
|
|
248
248
|
top -= item_result.height + item_spacing
|
249
249
|
height -= item_result.height + item_spacing
|
250
250
|
|
251
|
-
break if !box_fitter.
|
251
|
+
break if !box_fitter.success? || height <= 0
|
252
252
|
end
|
253
253
|
|
254
254
|
@height = @results.sum(&:height) + (@results.count - 1) * item_spacing + reserved_height
|
255
255
|
|
256
256
|
@draw_pos_x = frame.x + reserved_width_left
|
257
257
|
@draw_pos_y = frame.y - @height + reserved_height_bottom
|
258
|
-
@all_items_fitted = @results.all? {|r| r.box_fitter.
|
258
|
+
@all_items_fitted = @results.all? {|r| r.box_fitter.success? } &&
|
259
259
|
@results.size == @children.size
|
260
260
|
@fit_successful = @all_items_fitted || (@initial_height > 0 && style.overflow == :truncate)
|
261
261
|
end
|
@@ -233,7 +233,7 @@ module HexaPDF
|
|
233
233
|
@preferred_width = max_x_result.x + max_x_result.box.width + reserved_width
|
234
234
|
@height = @preferred_height = box_fitter.content_heights[0] + reserved_height
|
235
235
|
@fit_results = box_fitter.fit_results
|
236
|
-
@fit_successful = box_fitter.
|
236
|
+
@fit_successful = box_fitter.success?
|
237
237
|
else
|
238
238
|
@preferred_width = reserved_width
|
239
239
|
@height = @preferred_height = reserved_height
|
@@ -52,6 +52,7 @@ module HexaPDF
|
|
52
52
|
@tl = TextLayouter.new(style)
|
53
53
|
@items = items
|
54
54
|
@result = nil
|
55
|
+
@x_offset = 0
|
55
56
|
end
|
56
57
|
|
57
58
|
# Returns the text that will be drawn.
|
@@ -80,7 +81,7 @@ module HexaPDF
|
|
80
81
|
(@initial_height > 0 && @initial_height > available_height)
|
81
82
|
|
82
83
|
frame = frame.child_frame(box: self)
|
83
|
-
@width = @height = 0
|
84
|
+
@width = @x_offset = @height = 0
|
84
85
|
@result = if style.position == :flow
|
85
86
|
@tl.fit(@items, frame.width_specification, frame.shape.bbox.height,
|
86
87
|
apply_first_text_indent: !split_box?, frame: frame)
|
@@ -93,6 +94,14 @@ module HexaPDF
|
|
93
94
|
end
|
94
95
|
@width += if @initial_width > 0 || style.text_align == :center || style.text_align == :right
|
95
96
|
width
|
97
|
+
elsif style.position == :flow
|
98
|
+
min_x = +Float::INFINITY
|
99
|
+
max_x = -Float::INFINITY
|
100
|
+
@result.lines.each do |line|
|
101
|
+
min_x = [min_x, line.x_offset].min
|
102
|
+
max_x = [max_x, line.x_offset + line.width].max
|
103
|
+
end
|
104
|
+
min_x.finite? ? (@x_offset = min_x; max_x - min_x) : 0
|
96
105
|
else
|
97
106
|
@result.lines.max_by(&:width)&.width || 0
|
98
107
|
end
|
@@ -125,6 +134,11 @@ module HexaPDF
|
|
125
134
|
end
|
126
135
|
end
|
127
136
|
|
137
|
+
# :nodoc:
|
138
|
+
def draw(canvas, x, y)
|
139
|
+
super(canvas, x + @x_offset, y)
|
140
|
+
end
|
141
|
+
|
128
142
|
# :nodoc:
|
129
143
|
def empty?
|
130
144
|
super && (!@result || @result.lines.empty?)
|
@@ -142,7 +156,7 @@ module HexaPDF
|
|
142
156
|
end
|
143
157
|
|
144
158
|
return if @result.lines.empty?
|
145
|
-
@result.draw(canvas, x, y + content_height)
|
159
|
+
@result.draw(canvas, x - @x_offset, y + content_height)
|
146
160
|
end
|
147
161
|
|
148
162
|
# Creates a new TextBox instance for the items remaining after fitting the box.
|
data/lib/hexapdf/parser.rb
CHANGED
@@ -362,29 +362,32 @@ module HexaPDF
|
|
362
362
|
pos = @io.pos
|
363
363
|
lines = @io.read(step_size + 40).split(/[\r\n]+/)
|
364
364
|
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
365
|
+
# Need to iterate through the whole lines array in case there are multiple %%EOF to try
|
366
|
+
eof_index = 0
|
367
|
+
while (eof_index = lines[0..(eof_index - 1)].rindex {|l| l.strip == '%%EOF' })
|
368
|
+
if lines[eof_index - 1].strip =~ /\Astartxref\s(\d+)\z/
|
369
|
+
startxref_offset = $1.to_i
|
370
|
+
startxref_mangled = true
|
371
|
+
break # we found it even if it the syntax is not entirely correct
|
372
|
+
elsif eof_index < 2 || lines[eof_index - 2].strip != "startxref"
|
373
|
+
startxref_missing = true
|
374
|
+
else
|
375
|
+
startxref_offset = lines[eof_index - 1].to_i
|
376
|
+
break # we found it
|
377
|
+
end
|
377
378
|
end
|
379
|
+
eof_not_found ||= !eof_index
|
380
|
+
break if startxref_offset
|
378
381
|
end
|
379
382
|
|
380
|
-
if
|
381
|
-
maybe_raise("PDF file trailer with end-of-file marker not found", pos: pos,
|
382
|
-
force: !eof_index)
|
383
|
-
elsif startxref_mangled
|
383
|
+
if startxref_mangled
|
384
384
|
maybe_raise("PDF file trailer keyword startxref on same line as value", pos: pos)
|
385
385
|
elsif startxref_missing
|
386
386
|
maybe_raise("PDF file trailer is missing startxref keyword", pos: pos,
|
387
|
-
force:
|
387
|
+
force: !startxref_offset)
|
388
|
+
elsif eof_not_found
|
389
|
+
maybe_raise("PDF file trailer with end-of-file marker not found", pos: pos,
|
390
|
+
force: !startxref_offset)
|
388
391
|
end
|
389
392
|
|
390
393
|
@startxref_offset = startxref_offset
|
@@ -163,10 +163,21 @@ module HexaPDF
|
|
163
163
|
field
|
164
164
|
end
|
165
165
|
|
166
|
+
# Creates an untyped namespace field for creating hierarchies.
|
167
|
+
#
|
168
|
+
# Example:
|
169
|
+
#
|
170
|
+
# form.create_namespace_field('text')
|
171
|
+
# form.create_text_field('text.a1')
|
172
|
+
def create_namespace_field(name)
|
173
|
+
create_field(name)
|
174
|
+
end
|
175
|
+
|
166
176
|
# Creates a new text field with the given name and adds it to the form.
|
167
177
|
#
|
168
|
-
# The +name+ may contain dots to signify a field hierarchy. If
|
169
|
-
#
|
178
|
+
# The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
|
179
|
+
# already exist, they are created as pure namespace fields (see #create_namespace_field). If
|
180
|
+
# the +name+ doesn't contain dots, a top-level field is created.
|
170
181
|
#
|
171
182
|
# The optional keyword arguments allow setting often used properties of the field:
|
172
183
|
#
|
@@ -202,8 +213,9 @@ module HexaPDF
|
|
202
213
|
|
203
214
|
# Creates a new multiline text field with the given name and adds it to the form.
|
204
215
|
#
|
205
|
-
# The +name+ may contain dots to signify a field hierarchy. If
|
206
|
-
#
|
216
|
+
# The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
|
217
|
+
# already exist, they are created as pure namespace fields (see #create_namespace_field). If
|
218
|
+
# the +name+ doesn't contain dots, a top-level field is created.
|
207
219
|
#
|
208
220
|
# The optional keyword arguments allow setting often used properties of the field, see
|
209
221
|
# #create_text_field for details.
|
@@ -221,8 +233,9 @@ module HexaPDF
|
|
221
233
|
# The +max_chars+ argument defines the maximum number of characters the comb text field can
|
222
234
|
# accommodate.
|
223
235
|
#
|
224
|
-
# The +name+ may contain dots to signify a field hierarchy. If
|
225
|
-
#
|
236
|
+
# The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
|
237
|
+
# already exist, they are created as pure namespace fields (see #create_namespace_field). If
|
238
|
+
# the +name+ doesn't contain dots, a top-level field is created.
|
226
239
|
#
|
227
240
|
# The optional keyword arguments allow setting often used properties of the field, see
|
228
241
|
# #create_text_field for details.
|
@@ -238,8 +251,9 @@ module HexaPDF
|
|
238
251
|
|
239
252
|
# Creates a new file select field with the given name and adds it to the form.
|
240
253
|
#
|
241
|
-
# The +name+ may contain dots to signify a field hierarchy. If
|
242
|
-
#
|
254
|
+
# The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
|
255
|
+
# already exist, they are created as pure namespace fields (see #create_namespace_field). If
|
256
|
+
# the +name+ doesn't contain dots, a top-level field is created.
|
243
257
|
#
|
244
258
|
# The optional keyword arguments allow setting often used properties of the field, see
|
245
259
|
# #create_text_field for details.
|
@@ -254,8 +268,9 @@ module HexaPDF
|
|
254
268
|
|
255
269
|
# Creates a new password field with the given name and adds it to the form.
|
256
270
|
#
|
257
|
-
# The +name+ may contain dots to signify a field hierarchy. If
|
258
|
-
#
|
271
|
+
# The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
|
272
|
+
# already exist, they are created as pure namespace fields (see #create_namespace_field). If
|
273
|
+
# the +name+ doesn't contain dots, a top-level field is created.
|
259
274
|
#
|
260
275
|
# The optional keyword arguments allow setting often used properties of the field, see
|
261
276
|
# #create_text_field for details.
|
@@ -270,8 +285,9 @@ module HexaPDF
|
|
270
285
|
|
271
286
|
# Creates a new check box with the given name and adds it to the form.
|
272
287
|
#
|
273
|
-
# The +name+ may contain dots to signify a field hierarchy. If
|
274
|
-
#
|
288
|
+
# The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
|
289
|
+
# already exist, they are created as pure namespace fields (see #create_namespace_field). If
|
290
|
+
# the +name+ doesn't contain dots, a top-level field is created.
|
275
291
|
#
|
276
292
|
# Before a field value other than +false+ can be assigned to the check box, a widget needs
|
277
293
|
# to be created.
|
@@ -281,8 +297,9 @@ module HexaPDF
|
|
281
297
|
|
282
298
|
# Creates a radio button with the given name and adds it to the form.
|
283
299
|
#
|
284
|
-
# The +name+ may contain dots to signify a field hierarchy. If
|
285
|
-
#
|
300
|
+
# The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
|
301
|
+
# already exist, they are created as pure namespace fields (see #create_namespace_field). If
|
302
|
+
# the +name+ doesn't contain dots, a top-level field is created.
|
286
303
|
#
|
287
304
|
# Before a field value other than +nil+ can be assigned to the radio button, at least one
|
288
305
|
# widget needs to be created.
|
@@ -292,8 +309,9 @@ module HexaPDF
|
|
292
309
|
|
293
310
|
# Creates a combo box with the given name and adds it to the form.
|
294
311
|
#
|
295
|
-
# The +name+ may contain dots to signify a field hierarchy. If
|
296
|
-
#
|
312
|
+
# The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
|
313
|
+
# already exist, they are created as pure namespace fields (see #create_namespace_field). If
|
314
|
+
# the +name+ doesn't contain dots, a top-level field is created.
|
297
315
|
#
|
298
316
|
# The optional keyword arguments allow setting often used properties of the field:
|
299
317
|
#
|
@@ -319,8 +337,9 @@ module HexaPDF
|
|
319
337
|
|
320
338
|
# Creates a list box with the given name and adds it to the form.
|
321
339
|
#
|
322
|
-
# The +name+ may contain dots to signify a field hierarchy. If
|
323
|
-
#
|
340
|
+
# The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
|
341
|
+
# already exist, they are created as pure namespace fields (see #create_namespace_field). If
|
342
|
+
# the +name+ doesn't contain dots, a top-level field is created.
|
324
343
|
#
|
325
344
|
# The optional keyword arguments allow setting often used properties of the field:
|
326
345
|
#
|
@@ -345,10 +364,38 @@ module HexaPDF
|
|
345
364
|
|
346
365
|
# Creates a signature field with the given name and adds it to the form.
|
347
366
|
#
|
348
|
-
# The +name+ may contain dots to signify a field hierarchy. If
|
349
|
-
#
|
367
|
+
# The +name+ may contain dots to signify a field hierarchy. If the parent fields don't
|
368
|
+
# already exist, they are created as pure namespace fields (see #create_namespace_field). If
|
369
|
+
# the +name+ doesn't contain dots, a top-level field is created.
|
350
370
|
def create_signature_field(name)
|
351
|
-
create_field(name, :Sig)
|
371
|
+
create_field(name, :Sig)
|
372
|
+
end
|
373
|
+
|
374
|
+
# :call-seq:
|
375
|
+
# form.delete_field(name)
|
376
|
+
# form.delete_field(field)
|
377
|
+
#
|
378
|
+
# Deletes the field specified by the given name or via the given field object.
|
379
|
+
#
|
380
|
+
# If the field is a signature field, the associated signature dictionary is also deleted.
|
381
|
+
def delete_field(name_or_field)
|
382
|
+
field = (name_or_field.kind_of?(String) ? field_by_name(name_or_field) : name_or_field)
|
383
|
+
document.delete(field[:V]) if field.field_type == :Sig
|
384
|
+
|
385
|
+
to_delete = field.each_widget(direct_only: false).to_a
|
386
|
+
document.pages.each do |page|
|
387
|
+
next unless page.key?(:Annots)
|
388
|
+
page_annots = page[:Annots].to_a - to_delete
|
389
|
+
page[:Annots].value.replace(page_annots)
|
390
|
+
end
|
391
|
+
to_delete.each {|widget| document.delete(widget) }
|
392
|
+
|
393
|
+
if field[:Parent]
|
394
|
+
field[:Parent][:Kids].delete(field)
|
395
|
+
else
|
396
|
+
self[:Fields].delete(field)
|
397
|
+
end
|
398
|
+
document.delete(field)
|
352
399
|
end
|
353
400
|
|
354
401
|
# Fills form fields with the values from the given +data+ hash.
|
@@ -485,23 +532,27 @@ module HexaPDF
|
|
485
532
|
|
486
533
|
private
|
487
534
|
|
488
|
-
# Creates a new field with the full name +name+ and the field type +type+.
|
489
|
-
def create_field(name, type)
|
535
|
+
# Creates a new field with the full name +name+ and the optional field type +type+.
|
536
|
+
def create_field(name, type = nil)
|
490
537
|
parent_name, _, name = name.rpartition('.')
|
491
538
|
parent_field = parent_name.empty? ? nil : field_by_name(parent_name)
|
492
539
|
if !parent_name.empty? && !parent_field
|
493
|
-
|
540
|
+
parent_field = create_namespace_field(parent_name)
|
494
541
|
end
|
495
542
|
|
496
|
-
field =
|
497
|
-
|
543
|
+
field = if type
|
544
|
+
document.add({FT: type, T: name, Parent: parent_field},
|
545
|
+
type: :XXAcroFormField, subtype: type)
|
546
|
+
else
|
547
|
+
document.add({T: name, Parent: parent_field}, type: :XXAcroFormField)
|
548
|
+
end
|
498
549
|
if parent_field
|
499
550
|
(parent_field[:Kids] ||= []) << field
|
500
551
|
else
|
501
552
|
(self[:Fields] ||= []) << field
|
502
553
|
end
|
503
554
|
|
504
|
-
yield(field)
|
555
|
+
yield(field) if block_given?
|
505
556
|
|
506
557
|
field
|
507
558
|
end
|
data/lib/hexapdf/version.rb
CHANGED
@@ -20,13 +20,13 @@ describe HexaPDF::Layout::BoxFitter do
|
|
20
20
|
@box_fitter.fit(HexaPDF::Layout::TextBox.new(items: [ibox] * count))
|
21
21
|
end
|
22
22
|
|
23
|
-
def check_result(*pos, content_heights:,
|
23
|
+
def check_result(*pos, content_heights:, success: true, boxes_remain: false)
|
24
24
|
pos.each_slice(2).with_index do |(x, y), index|
|
25
25
|
assert_equal(x, @box_fitter.fit_results[index].x, "x #{index}")
|
26
26
|
assert_equal(y, @box_fitter.fit_results[index].y, "y #{index}")
|
27
27
|
end
|
28
28
|
assert_equal(content_heights, @box_fitter.content_heights)
|
29
|
-
|
29
|
+
success ? assert(@box_fitter.success?) : refute(@box_fitter.success?)
|
30
30
|
rboxes = @box_fitter.remaining_boxes.empty?
|
31
31
|
boxes_remain ? refute(rboxes) : assert(rboxes)
|
32
32
|
end
|
@@ -55,7 +55,7 @@ describe HexaPDF::Layout::BoxFitter do
|
|
55
55
|
fit_box(70)
|
56
56
|
fit_box(40)
|
57
57
|
fit_box(20)
|
58
|
-
check_result(10, 80, 0, 10, 0, 0, 100, 100,
|
58
|
+
check_result(10, 80, 0, 10, 0, 0, 100, 100, success: false, boxes_remain: true,
|
59
59
|
content_heights: [90, 50])
|
60
60
|
assert_equal(2, @box_fitter.remaining_boxes.size)
|
61
61
|
end
|
@@ -50,10 +50,12 @@ describe HexaPDF::Layout::TextBox do
|
|
50
50
|
end
|
51
51
|
|
52
52
|
it "fits into the frame's outline" do
|
53
|
+
@frame.remove_area(Geom2D::Rectangle(0, 80, 20, 20))
|
54
|
+
@frame.remove_area(Geom2D::Rectangle(80, 70, 20, 20))
|
53
55
|
box = create_box([@inline_box] * 20, style: {position: :flow})
|
54
56
|
assert(box.fit(100, 100, @frame))
|
55
57
|
assert_equal(100, box.width)
|
56
|
-
assert_equal(
|
58
|
+
assert_equal(30, box.height)
|
57
59
|
end
|
58
60
|
|
59
61
|
it "takes the style option last_line_gap into account" do
|
@@ -190,6 +192,30 @@ describe HexaPDF::Layout::TextBox do
|
|
190
192
|
[:restore_graphics_state]])
|
191
193
|
end
|
192
194
|
|
195
|
+
it "correctly draws borders, backgrounds... for position :flow" do
|
196
|
+
@frame.remove_area(Geom2D::Rectangle(0, 0, 40, 100))
|
197
|
+
box = create_box([@inline_box], style: {position: :flow, border: {width: 1}})
|
198
|
+
box.fit(60, 100, @frame)
|
199
|
+
box.draw(@canvas, 0, 90)
|
200
|
+
assert_operators(@canvas.contents, [[:save_graphics_state],
|
201
|
+
[:append_rectangle, [40, 90, 10, 10]],
|
202
|
+
[:clip_path_non_zero],
|
203
|
+
[:end_path],
|
204
|
+
[:append_rectangle, [40.5, 90.5, 9.0, 9.0]],
|
205
|
+
[:stroke_path],
|
206
|
+
[:restore_graphics_state],
|
207
|
+
[:save_graphics_state],
|
208
|
+
[:restore_graphics_state],
|
209
|
+
[:save_graphics_state],
|
210
|
+
[:concatenate_matrix, [1, 0, 0, 1, 41, 89]],
|
211
|
+
[:save_graphics_state],
|
212
|
+
[:concatenate_matrix, [1, 0, 0, 1, 0, 0]],
|
213
|
+
[:restore_graphics_state],
|
214
|
+
[:restore_graphics_state],
|
215
|
+
[:save_graphics_state],
|
216
|
+
[:restore_graphics_state]])
|
217
|
+
end
|
218
|
+
|
193
219
|
it "draws nothing onto the canvas if the box is empty" do
|
194
220
|
box = create_box([])
|
195
221
|
box.draw(@canvas, 5, 5)
|
@@ -323,11 +323,13 @@ describe HexaPDF::Dictionary do
|
|
323
323
|
end
|
324
324
|
end
|
325
325
|
|
326
|
-
describe "
|
327
|
-
it "returns a
|
328
|
-
|
326
|
+
describe "to_hash" do
|
327
|
+
it "returns a copy of the value where each entry is pre-processed" do
|
328
|
+
@dict[:value] = HexaPDF::Reference.new(1, 0)
|
329
|
+
obj = @dict.to_hash
|
329
330
|
refute_equal(obj.object_id, @dict.value.object_id)
|
330
|
-
assert_equal(obj,
|
331
|
+
assert_equal(:obj, obj[:Object])
|
332
|
+
assert_equal("deref", obj[:value])
|
331
333
|
end
|
332
334
|
end
|
333
335
|
|
data/test/hexapdf/test_parser.rb
CHANGED
@@ -367,6 +367,11 @@ describe HexaPDF::Parser do
|
|
367
367
|
assert_equal(5, @parser.startxref_offset)
|
368
368
|
end
|
369
369
|
|
370
|
+
it "handles the case of multiple %%EOF and the last one being invalid" do
|
371
|
+
create_parser("startxref\n5\n%%EOF\ntartxref\n3\n%%EOF")
|
372
|
+
assert_equal(5, @parser.startxref_offset)
|
373
|
+
end
|
374
|
+
|
370
375
|
it "fails even in big files when nothing is found" do
|
371
376
|
create_parser("\nhallo" * 5000)
|
372
377
|
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
|
@@ -402,6 +407,13 @@ describe HexaPDF::Parser do
|
|
402
407
|
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
|
403
408
|
assert_match(/startxref on same line/, exp.message)
|
404
409
|
end
|
410
|
+
|
411
|
+
it "fails on strict parsing if there are multiple %%EOF and the last one is invalid" do
|
412
|
+
@document.config['parser.on_correctable_error'] = proc { true }
|
413
|
+
create_parser("startxref\n5\n%%EOF\ntartxref\n3\n%%EOF")
|
414
|
+
exp = assert_raises(HexaPDF::MalformedPDFError) { @parser.startxref_offset }
|
415
|
+
assert_match(/missing startxref keyword/, exp.message)
|
416
|
+
end
|
405
417
|
end
|
406
418
|
|
407
419
|
describe "file_header_version" do
|
@@ -128,6 +128,12 @@ describe HexaPDF::Type::AcroForm::Form do
|
|
128
128
|
@acro_form = @doc.acro_form(create: true)
|
129
129
|
end
|
130
130
|
|
131
|
+
it "creates a pure namespace field" do
|
132
|
+
field = @acro_form.create_namespace_field('text')
|
133
|
+
assert_equal('text', field.full_field_name)
|
134
|
+
assert_nil(field.concrete_field_type)
|
135
|
+
end
|
136
|
+
|
131
137
|
describe "handles the general case" do
|
132
138
|
it "works for names with a dot" do
|
133
139
|
@acro_form[:Fields] = [{T: "root"}]
|
@@ -142,8 +148,13 @@ describe HexaPDF::Type::AcroForm::Form do
|
|
142
148
|
assert([field], @acro_form[:Fields])
|
143
149
|
end
|
144
150
|
|
145
|
-
it "
|
146
|
-
|
151
|
+
it "creates the parent fields as namespace fields if necessary" do
|
152
|
+
field = @acro_form.create_text_field("root.sub.field")
|
153
|
+
level1 = @acro_form.field_by_name('root')
|
154
|
+
assert_equal(1, level1[:Kids].size)
|
155
|
+
level2 = @acro_form.field_by_name('root.sub')
|
156
|
+
assert_equal(1, level2[:Kids].size)
|
157
|
+
assert_same(field, level2[:Kids][0])
|
147
158
|
end
|
148
159
|
end
|
149
160
|
|
@@ -241,6 +252,56 @@ describe HexaPDF::Type::AcroForm::Form do
|
|
241
252
|
end
|
242
253
|
end
|
243
254
|
|
255
|
+
describe "delete_field" do
|
256
|
+
before do
|
257
|
+
@field = @acro_form.create_signature_field("sig")
|
258
|
+
end
|
259
|
+
|
260
|
+
it "deletes a field via name" do
|
261
|
+
@acro_form.delete_field('sig')
|
262
|
+
assert_equal(0, @acro_form.root_fields.size)
|
263
|
+
end
|
264
|
+
|
265
|
+
it "deletes a field via field object" do
|
266
|
+
@acro_form.delete_field(@field)
|
267
|
+
assert_equal(0, @acro_form.root_fields.size)
|
268
|
+
end
|
269
|
+
|
270
|
+
it "deletes the set signature object" do
|
271
|
+
obj = @doc.add({})
|
272
|
+
@field.field_value = obj
|
273
|
+
@acro_form.delete_field(@field)
|
274
|
+
assert(obj.null?)
|
275
|
+
end
|
276
|
+
|
277
|
+
it "deletes all widget annotations from the document and the annotation array" do
|
278
|
+
widget1 = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
|
279
|
+
widget2 = @field.create_widget(@doc.pages.add, Rect: [0, 0, 0, 0])
|
280
|
+
refute(@doc.pages[1][:Annots].empty?)
|
281
|
+
@acro_form.delete_field(@field)
|
282
|
+
assert(@doc.pages[0][:Annots].empty?)
|
283
|
+
assert(@doc.pages[1][:Annots].empty?)
|
284
|
+
assert(@doc.object(widget1).null?)
|
285
|
+
assert(@doc.object(widget2).null?)
|
286
|
+
end
|
287
|
+
|
288
|
+
it "deletes the field from the field hierarchy" do
|
289
|
+
@acro_form.delete_field('sig')
|
290
|
+
refute(@acro_form.field_by_name('sig'))
|
291
|
+
assert(@acro_form[:Fields].empty?)
|
292
|
+
|
293
|
+
@acro_form.create_signature_field("sub.sub.sig")
|
294
|
+
@acro_form.delete_field("sub.sub.sig")
|
295
|
+
refute(@acro_form.field_by_name('sub.sub.sig'))
|
296
|
+
assert(@acro_form[:Fields][0][:Kids][0][:Kids].empty?)
|
297
|
+
end
|
298
|
+
|
299
|
+
it "deletes the field itself" do
|
300
|
+
@acro_form.delete_field('sig')
|
301
|
+
assert(@doc.object(@field).null?)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
244
305
|
describe "fill" do
|
245
306
|
it "works for text field types" do
|
246
307
|
field = @acro_form.create_text_field('test')
|
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.43.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-05-
|
11
|
+
date: 2024-05-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cmdparse
|