hexapdf 0.42.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +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
|