hexapdf 1.1.1 → 1.2.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 +34 -0
- data/lib/hexapdf/cli/command.rb +63 -63
- data/lib/hexapdf/cli/inspect.rb +1 -1
- data/lib/hexapdf/cli/modify.rb +0 -1
- data/lib/hexapdf/cli/optimize.rb +5 -5
- data/lib/hexapdf/configuration.rb +21 -0
- data/lib/hexapdf/content/graphics_state.rb +1 -1
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +1 -1
- data/lib/hexapdf/document/annotations.rb +115 -0
- data/lib/hexapdf/document.rb +28 -7
- data/lib/hexapdf/font/true_type_wrapper.rb +1 -0
- data/lib/hexapdf/font/type1_wrapper.rb +1 -0
- data/lib/hexapdf/type/acro_form/java_script_actions.rb +9 -2
- data/lib/hexapdf/type/acro_form/text_field.rb +9 -2
- data/lib/hexapdf/type/annotation.rb +59 -1
- data/lib/hexapdf/type/annotations/appearance_generator.rb +273 -0
- data/lib/hexapdf/type/annotations/border_styling.rb +160 -0
- data/lib/hexapdf/type/annotations/line.rb +521 -0
- data/lib/hexapdf/type/annotations/widget.rb +2 -96
- data/lib/hexapdf/type/annotations.rb +3 -0
- data/lib/hexapdf/type/form.rb +2 -2
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +0 -1
- data/lib/hexapdf/xref_section.rb +7 -4
- data/test/hexapdf/content/test_graphics_state.rb +2 -3
- data/test/hexapdf/content/test_operator.rb +4 -5
- data/test/hexapdf/digital_signature/test_cms_handler.rb +7 -8
- data/test/hexapdf/digital_signature/test_handler.rb +2 -3
- data/test/hexapdf/digital_signature/test_pkcs1_handler.rb +1 -2
- data/test/hexapdf/document/test_annotations.rb +33 -0
- data/test/hexapdf/font/test_true_type_wrapper.rb +7 -0
- data/test/hexapdf/font/test_type1_wrapper.rb +7 -0
- data/test/hexapdf/task/test_optimize.rb +1 -1
- data/test/hexapdf/test_document.rb +11 -3
- data/test/hexapdf/test_stream.rb +1 -2
- data/test/hexapdf/test_xref_section.rb +1 -1
- data/test/hexapdf/type/acro_form/test_java_script_actions.rb +21 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +7 -1
- data/test/hexapdf/type/annotations/test_appearance_generator.rb +398 -0
- data/test/hexapdf/type/annotations/test_border_styling.rb +114 -0
- data/test/hexapdf/type/annotations/test_line.rb +189 -0
- data/test/hexapdf/type/annotations/test_widget.rb +0 -81
- data/test/hexapdf/type/test_annotation.rb +55 -0
- data/test/hexapdf/type/test_form.rb +6 -0
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 041330c9846091186ddeecd7900a7919bb96da26b3513b047f03c87888efedcd
|
4
|
+
data.tar.gz: ff89d47f1eeac1d2dda4982224403318528401918ccfe7f1bb775d05d8f0e6f3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 508fa118a26d5825f9ae2a105cdf4717b02901fd3095c49cf5855caae7648bf60b9cea414bb6670dd3bd0c82f99b076a8df7ffb79efa60990003468d7f3a9db9
|
7
|
+
data.tar.gz: 613323b8b2a93a01e31ec9e35664e6dc2939b210bbe7d7c055cb861eac6cd530978b14256a55a8e51f2e3804be06808440d5816c73e808df4af3f111f325b9c9
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,37 @@
|
|
1
|
+
## 1.2.0 - 2025-02-10
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* **Breaking change**: Argument `compact` to [HexaPDF::Document#write] to
|
6
|
+
automatically run the 'compact' optimization task
|
7
|
+
* [HexaPDF::Document::Annotations], accessible via
|
8
|
+
[HexaPDF::Document#annotations], as convenience interface for working with
|
9
|
+
annotations
|
10
|
+
* [HexaPDF::Type::Annotations::AppearanceGenerator] as central class for
|
11
|
+
generating appearance streams
|
12
|
+
* [HexaPDF::Type::Annotations::Line] for line annotations
|
13
|
+
* [HexaPDF::Type::Annotation#opacity] for setting the opacity values when
|
14
|
+
regenerating the appearance stream
|
15
|
+
* [HexaPDF::Type::Annotation#contents] for setting the text of the annotation
|
16
|
+
* Configuration option 'acro_form.text_field.on_max_len_exceeded' to allow
|
17
|
+
custom handling of too long values
|
18
|
+
|
19
|
+
### Changed
|
20
|
+
|
21
|
+
* **Breaking change**: Extracted `#border_style` and associated data class from
|
22
|
+
[HexaPDF::Type::Annotations::Widget] into
|
23
|
+
[HexaPDF::Type::Annotations::BorderStyling]
|
24
|
+
* [HexaPDF::Type::Form#canvas] to allow getting the canvas without the initial
|
25
|
+
translation
|
26
|
+
|
27
|
+
### Fixed
|
28
|
+
|
29
|
+
* AcroForm Javascript actions to gracefully handle the special values infinity
|
30
|
+
and NaN
|
31
|
+
* Type1 and TrueType font wrappers to handle the case where fonts are first
|
32
|
+
added and later deleted
|
33
|
+
|
34
|
+
|
1
35
|
## 1.1.1 - 2025-01-08
|
2
36
|
|
3
37
|
### Fixed
|
data/lib/hexapdf/cli/command.rb
CHANGED
@@ -35,7 +35,6 @@
|
|
35
35
|
#++
|
36
36
|
|
37
37
|
require 'io/console'
|
38
|
-
require 'ostruct'
|
39
38
|
require 'cmdparse'
|
40
39
|
require 'hexapdf/document'
|
41
40
|
require 'hexapdf/font/true_type'
|
@@ -68,21 +67,22 @@ module HexaPDF
|
|
68
67
|
|
69
68
|
def initialize(*args, **kwargs, &block) #:nodoc:
|
70
69
|
super
|
71
|
-
@out_options =
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
70
|
+
@out_options = {
|
71
|
+
compact: true,
|
72
|
+
compress_pages: false,
|
73
|
+
object_streams: :preserve,
|
74
|
+
xref_streams: :preserve,
|
75
|
+
streams: :preserve,
|
76
|
+
optimize_fonts: false,
|
77
|
+
prune_page_resources: false,
|
78
|
+
encryption: :preserve,
|
79
|
+
enc_user_pwd: nil,
|
80
|
+
enc_owner_pwd: nil,
|
81
|
+
enc_key_length: 128,
|
82
|
+
enc_algorithm: :aes,
|
83
|
+
enc_force_v4: false,
|
84
|
+
enc_permissions: [],
|
85
|
+
}
|
86
86
|
end
|
87
87
|
|
88
88
|
protected
|
@@ -163,7 +163,7 @@ module HexaPDF
|
|
163
163
|
if command_parser.verbosity_info?
|
164
164
|
puts "Creating output document #{out_file}"
|
165
165
|
end
|
166
|
-
doc.write(out_file, validate: false, incremental: incremental)
|
166
|
+
doc.write(out_file, validate: false, compact: false, incremental: incremental)
|
167
167
|
end
|
168
168
|
end
|
169
169
|
|
@@ -183,35 +183,35 @@ module HexaPDF
|
|
183
183
|
options.separator("")
|
184
184
|
options.separator("Optimization options:")
|
185
185
|
options.on("--[no-]compact", "Delete unnecessary PDF objects (default: " \
|
186
|
-
"#{@out_options
|
187
|
-
@out_options
|
186
|
+
"#{@out_options[:compact]})") do |c|
|
187
|
+
@out_options[:compact] = c
|
188
188
|
end
|
189
189
|
options.on("--object-streams MODE", [:generate, :preserve, :delete],
|
190
190
|
"Handling of object streams (either generate, preserve or delete; " \
|
191
|
-
"default: #{@out_options
|
192
|
-
@out_options
|
191
|
+
"default: #{@out_options[:object_streams]})") do |os|
|
192
|
+
@out_options[:object_streams] = os
|
193
193
|
end
|
194
194
|
options.on("--xref-streams MODE", [:generate, :preserve, :delete],
|
195
195
|
"Handling of cross-reference streams (either generate, preserve or delete; " \
|
196
|
-
"default: #{@out_options
|
197
|
-
@out_options
|
196
|
+
"default: #{@out_options[:xref_streams]})") do |x|
|
197
|
+
@out_options[:xref_streams] = x
|
198
198
|
end
|
199
199
|
options.on("--streams MODE", [:compress, :preserve, :uncompress],
|
200
200
|
"Handling of stream data (either compress, preserve or uncompress; default: " \
|
201
|
-
"#{@out_options
|
202
|
-
@out_options
|
201
|
+
"#{@out_options[:streams]})") do |streams|
|
202
|
+
@out_options[:streams] = streams
|
203
203
|
end
|
204
204
|
options.on("--[no-]compress-pages", "Recompress page content streams (may take a long " \
|
205
|
-
"time; default: #{@out_options
|
206
|
-
@out_options
|
205
|
+
"time; default: #{@out_options[:compress_pages]})") do |c|
|
206
|
+
@out_options[:compress_pages] = c
|
207
207
|
end
|
208
208
|
options.on("--[no-]prune-page-resources", "Prunes unused objects from the page resources " \
|
209
|
-
"(may take a long time; default: #{@out_options
|
210
|
-
@out_options
|
209
|
+
"(may take a long time; default: #{@out_options[:prune_page_resources]})") do |c|
|
210
|
+
@out_options[:prune_page_resources] = c
|
211
211
|
end
|
212
212
|
options.on("--[no-]optimize-fonts", "Optimize embedded font files; " \
|
213
|
-
"default: #{@out_options
|
214
|
-
@out_options
|
213
|
+
"default: #{@out_options[:optimize_fonts]})") do |o|
|
214
|
+
@out_options[:optimize_fonts] = o
|
215
215
|
end
|
216
216
|
end
|
217
217
|
|
@@ -222,37 +222,37 @@ module HexaPDF
|
|
222
222
|
options.separator("")
|
223
223
|
options.separator("Encryption options:")
|
224
224
|
options.on("--decrypt", "Remove any encryption") do
|
225
|
-
@out_options
|
225
|
+
@out_options[:encryption] = :remove
|
226
226
|
end
|
227
227
|
options.on("--encrypt", "Encrypt the output file") do
|
228
|
-
@out_options
|
228
|
+
@out_options[:encryption] = :add
|
229
229
|
end
|
230
230
|
options.on("--owner-password PASSWORD", String, "The owner password to be set on the " \
|
231
231
|
"output file (use - for reading from standard input)") do |pwd|
|
232
|
-
@out_options
|
233
|
-
@out_options
|
232
|
+
@out_options[:encryption] = :add
|
233
|
+
@out_options[:enc_owner_pwd] = (pwd == '-' ? read_password("Owner password") : pwd)
|
234
234
|
end
|
235
235
|
options.on("--user-password PASSWORD", String, "The user password to be set on the " \
|
236
236
|
"output file (use - for reading from standard input)") do |pwd|
|
237
|
-
@out_options
|
238
|
-
@out_options
|
237
|
+
@out_options[:encryption] = :add
|
238
|
+
@out_options[:enc_user_pwd] = (pwd == '-' ? read_password("User password") : pwd)
|
239
239
|
end
|
240
240
|
options.on("--algorithm ALGORITHM", [:aes, :arc4],
|
241
241
|
"The encryption algorithm: aes or arc4 (default: " \
|
242
|
-
"#{@out_options
|
243
|
-
@out_options
|
244
|
-
@out_options
|
242
|
+
"#{@out_options[:enc_algorithm]})") do |a|
|
243
|
+
@out_options[:encryption] = :add
|
244
|
+
@out_options[:enc_algorithm] = a
|
245
245
|
end
|
246
246
|
options.on("--key-length BITS", Integer,
|
247
247
|
"The encryption key length in bits (default: " \
|
248
|
-
"#{@out_options
|
249
|
-
@out_options
|
250
|
-
@out_options
|
248
|
+
"#{@out_options[:enc_key_length]})") do |i|
|
249
|
+
@out_options[:encryption] = :add
|
250
|
+
@out_options[:enc_key_length] = i
|
251
251
|
end
|
252
252
|
options.on("--force-V4",
|
253
253
|
"Force use of encryption version 4 if key length=128 and algorithm=arc4") do
|
254
|
-
@out_options
|
255
|
-
@out_options
|
254
|
+
@out_options[:encryption] = :add
|
255
|
+
@out_options[:enc_force_v4] = true
|
256
256
|
end
|
257
257
|
syms = HexaPDF::Encryption::StandardSecurityHandler::Permissions::SYMBOL_TO_PERMISSION.keys
|
258
258
|
options.on("--permissions PERMS", Array,
|
@@ -264,8 +264,8 @@ module HexaPDF
|
|
264
264
|
end
|
265
265
|
perm.to_sym
|
266
266
|
end
|
267
|
-
@out_options
|
268
|
-
@out_options
|
267
|
+
@out_options[:encryption] = :add
|
268
|
+
@out_options[:enc_permissions] = perms
|
269
269
|
end
|
270
270
|
end
|
271
271
|
|
@@ -273,12 +273,12 @@ module HexaPDF
|
|
273
273
|
#
|
274
274
|
# See: #define_optimization_options
|
275
275
|
def apply_optimization_options(doc)
|
276
|
-
doc.task(:optimize, compact: @out_options
|
277
|
-
object_streams: @out_options
|
278
|
-
xref_streams: @out_options
|
279
|
-
compress_pages: @out_options
|
280
|
-
prune_page_resources: @out_options
|
281
|
-
if @out_options
|
276
|
+
doc.task(:optimize, compact: @out_options[:compact],
|
277
|
+
object_streams: @out_options[:object_streams],
|
278
|
+
xref_streams: @out_options[:xref_streams],
|
279
|
+
compress_pages: @out_options[:compress_pages],
|
280
|
+
prune_page_resources: @out_options[:prune_page_resources])
|
281
|
+
if @out_options[:streams] != :preserve || @out_options[:optimize_fonts]
|
282
282
|
doc.each do |obj|
|
283
283
|
optimize_stream(obj)
|
284
284
|
optimize_font(obj)
|
@@ -292,15 +292,15 @@ module HexaPDF
|
|
292
292
|
|
293
293
|
# Applies the chosen stream mode to the given object.
|
294
294
|
def optimize_stream(obj)
|
295
|
-
return if @out_options
|
295
|
+
return if @out_options[:streams] == :preserve || !obj.respond_to?(:set_filter) ||
|
296
296
|
Array(obj[:Filter]).any? {|f| IGNORED_FILTERS[f] }
|
297
297
|
|
298
|
-
obj.set_filter(@out_options
|
298
|
+
obj.set_filter(@out_options[:streams] == :compress ? :FlateDecode : nil)
|
299
299
|
end
|
300
300
|
|
301
301
|
# Optimize the object if it is a font object.
|
302
302
|
def optimize_font(obj)
|
303
|
-
return unless @out_options
|
303
|
+
return unless @out_options[:optimize_fonts] && obj.kind_of?(HexaPDF::Type::Font) &&
|
304
304
|
(obj[:Subtype] == :TrueType ||
|
305
305
|
(obj[:Subtype] == :Type0 && obj.descendant_font[:Subtype] == :CIDFontType2)) &&
|
306
306
|
obj.embedded?
|
@@ -319,14 +319,14 @@ module HexaPDF
|
|
319
319
|
#
|
320
320
|
# See: #define_encryption_options
|
321
321
|
def apply_encryption_options(doc)
|
322
|
-
case @out_options
|
322
|
+
case @out_options[:encryption]
|
323
323
|
when :add
|
324
|
-
doc.encrypt(algorithm: @out_options
|
325
|
-
key_length: @out_options
|
326
|
-
force_v4: @out_options
|
327
|
-
permissions: @out_options
|
328
|
-
owner_password: @out_options
|
329
|
-
user_password: @out_options
|
324
|
+
doc.encrypt(algorithm: @out_options[:enc_algorithm],
|
325
|
+
key_length: @out_options[:enc_key_length],
|
326
|
+
force_v4: @out_options[:enc_force_v4],
|
327
|
+
permissions: @out_options[:enc_permissions],
|
328
|
+
owner_password: @out_options[:enc_owner_pwd],
|
329
|
+
user_password: @out_options[:enc_user_pwd])
|
330
330
|
when :remove
|
331
331
|
doc.encrypt(name: nil)
|
332
332
|
end
|
data/lib/hexapdf/cli/inspect.rb
CHANGED
@@ -396,7 +396,7 @@ module HexaPDF
|
|
396
396
|
io = @doc.revisions.parser.io
|
397
397
|
|
398
398
|
io.seek(0, IO::SEEK_END)
|
399
|
-
startxrefs = @doc.revisions.map {|rev| rev.trailer[:Prev] } <<
|
399
|
+
startxrefs = @doc.revisions.map {|rev| rev.trailer[:Prev].to_i } <<
|
400
400
|
@doc.revisions.parser.startxref_offset <<
|
401
401
|
io.pos
|
402
402
|
startxrefs.sort!
|
data/lib/hexapdf/cli/modify.rb
CHANGED
data/lib/hexapdf/cli/optimize.rb
CHANGED
@@ -53,11 +53,11 @@ module HexaPDF
|
|
53
53
|
EOF
|
54
54
|
|
55
55
|
@password = nil
|
56
|
-
@out_options
|
57
|
-
@out_options
|
58
|
-
@out_options
|
59
|
-
@out_options
|
60
|
-
@out_options
|
56
|
+
@out_options[:compact] = true
|
57
|
+
@out_options[:xref_streams] = :generate
|
58
|
+
@out_options[:object_streams] = :generate
|
59
|
+
@out_options[:streams] = :compress
|
60
|
+
@out_options[:optimize_fonts] = true
|
61
61
|
|
62
62
|
options.on("--password PASSWORD", "-p", String,
|
63
63
|
"The password for decryption. Use - for reading from standard input.") do |pwd|
|
@@ -224,6 +224,21 @@ module HexaPDF
|
|
224
224
|
# acro_form.text_field.default_width::
|
225
225
|
# A number specifying the default width of AcroForm text fields which should be auto-sized.
|
226
226
|
#
|
227
|
+
# acro_form.text_field.on_max_len_exceeded::
|
228
|
+
# Callback hook when the value of a text field exceeds the set maximum length.
|
229
|
+
#
|
230
|
+
# The value needs to be an object that responds to \#call(field, value) where +field+ is the
|
231
|
+
# AcroForm text field on which the value is set and +value+ is the invalid value. The returned
|
232
|
+
# value is used instead of the invalid value.
|
233
|
+
#
|
234
|
+
# The default implementation raises an error.
|
235
|
+
#
|
236
|
+
# annotation.appearance_generator::
|
237
|
+
# The class that should be used for generating appearances for annotations. If the value is a
|
238
|
+
# String, it should contain the name of a constant to such a class.
|
239
|
+
#
|
240
|
+
# See HexaPDF::Type::Annotations::AppearanceGenerator
|
241
|
+
#
|
227
242
|
# debug::
|
228
243
|
# If set to +true+, enables debug output.
|
229
244
|
#
|
@@ -502,6 +517,10 @@ module HexaPDF
|
|
502
517
|
"#{field.concrete_field_type} field named '#{field.full_field_name}'"
|
503
518
|
end,
|
504
519
|
'acro_form.text_field.default_width' => 100,
|
520
|
+
'acro_form.text_field.on_max_len_exceeded' => proc do |field, value|
|
521
|
+
raise HexaPDF::Error, "Value exceeds maximum allowed length of #{field[:MaxLen]}"
|
522
|
+
end,
|
523
|
+
'annotation.appearance_generator' => 'HexaPDF::Type::Annotations::AppearanceGenerator',
|
505
524
|
'debug' => false,
|
506
525
|
'document.auto_decrypt' => true,
|
507
526
|
'document.on_invalid_string' => proc do |str|
|
@@ -746,6 +765,7 @@ module HexaPDF
|
|
746
765
|
Text: 'HexaPDF::Type::Annotations::Text',
|
747
766
|
Link: 'HexaPDF::Type::Annotations::Link',
|
748
767
|
Widget: 'HexaPDF::Type::Annotations::Widget',
|
768
|
+
Line: 'HexaPDF::Type::Annotations::Line',
|
749
769
|
XML: 'HexaPDF::Type::Metadata',
|
750
770
|
GTS_PDFX: 'HexaPDF::Type::OutputIntent',
|
751
771
|
GTS_PDFA1: 'HexaPDF::Type::OutputIntent',
|
@@ -774,6 +794,7 @@ module HexaPDF
|
|
774
794
|
Text: 'HexaPDF::Type::Annotations::Text',
|
775
795
|
Link: 'HexaPDF::Type::Annotations::Link',
|
776
796
|
Widget: 'HexaPDF::Type::Annotations::Widget',
|
797
|
+
Line: 'HexaPDF::Type::Annotations::Line',
|
777
798
|
},
|
778
799
|
XXAcroFormField: {
|
779
800
|
Tx: 'HexaPDF::Type::AcroForm::TextField',
|
@@ -255,7 +255,7 @@ module HexaPDF
|
|
255
255
|
array.inject(0) {|m, n| m < 0 ? m : (n < 0 ? -1 : m + n) } <= 0)
|
256
256
|
raise ArgumentError, "Invalid line dash pattern: #{array.inspect} #{phase.inspect}"
|
257
257
|
end
|
258
|
-
@array = array
|
258
|
+
@array = array
|
259
259
|
@phase = phase
|
260
260
|
end
|
261
261
|
|
@@ -108,7 +108,7 @@ module HexaPDF
|
|
108
108
|
# +type+::
|
109
109
|
# The type can either be :cms when creating standard PDF CMS signatures or :pades when
|
110
110
|
# creating PAdES compatible signatures. PAdES signatures are part of PDF 2.0.
|
111
|
-
def create(data, type: :cms, &block) # :yield:
|
111
|
+
def create(data, type: :cms, &block) # :yield: digest_algorithm, hashed_data
|
112
112
|
signed_attrs = create_signed_attrs(data, signing_time: (type == :cms))
|
113
113
|
signature = digest_and_sign_data(set(*signed_attrs.value).to_der, &block)
|
114
114
|
unsigned_attrs = create_unsigned_attrs(signature)
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# -*- encoding: utf-8; frozen_string_literal: true -*-
|
2
|
+
#
|
3
|
+
#--
|
4
|
+
# This file is part of HexaPDF.
|
5
|
+
#
|
6
|
+
# HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
|
7
|
+
# Copyright (C) 2014-2025 Thomas Leitner
|
8
|
+
#
|
9
|
+
# HexaPDF is free software: you can redistribute it and/or modify it
|
10
|
+
# under the terms of the GNU Affero General Public License version 3 as
|
11
|
+
# published by the Free Software Foundation with the addition of the
|
12
|
+
# following permission added to Section 15 as permitted in Section 7(a):
|
13
|
+
# FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
|
14
|
+
# THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
|
15
|
+
# INFRINGEMENT OF THIRD PARTY RIGHTS.
|
16
|
+
#
|
17
|
+
# HexaPDF is distributed in the hope that it will be useful, but WITHOUT
|
18
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
20
|
+
# License for more details.
|
21
|
+
#
|
22
|
+
# You should have received a copy of the GNU Affero General Public License
|
23
|
+
# along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
|
24
|
+
#
|
25
|
+
# The interactive user interfaces in modified source and object code
|
26
|
+
# versions of HexaPDF must display Appropriate Legal Notices, as required
|
27
|
+
# under Section 5 of the GNU Affero General Public License version 3.
|
28
|
+
#
|
29
|
+
# In accordance with Section 7(b) of the GNU Affero General Public
|
30
|
+
# License, a covered work must retain the producer line in every PDF that
|
31
|
+
# is created or manipulated using HexaPDF.
|
32
|
+
#
|
33
|
+
# If the GNU Affero General Public License doesn't fit your need,
|
34
|
+
# commercial licenses are available at <https://gettalong.at/hexapdf/>.
|
35
|
+
#++
|
36
|
+
|
37
|
+
require 'hexapdf/dictionary'
|
38
|
+
require 'hexapdf/error'
|
39
|
+
|
40
|
+
module HexaPDF
|
41
|
+
class Document
|
42
|
+
|
43
|
+
# This class provides methods for creating and managing the annotations of a PDF file.
|
44
|
+
#
|
45
|
+
# An annotation is an object that can be added to a certain location on a page, provides a
|
46
|
+
# visual appearance and allows for interaction with the user via keyboard and mouse.
|
47
|
+
#
|
48
|
+
# == Usage
|
49
|
+
#
|
50
|
+
# To create an annotation either call the general #create method or a specific creation method
|
51
|
+
# for an annotation type. After the annotation has been created customize it using the
|
52
|
+
# convenience methods on the annotation object. The last step should be the call to
|
53
|
+
# +regenerate_appearance+ so that the appearance is generated.
|
54
|
+
#
|
55
|
+
# See: PDF2.0 s12.5
|
56
|
+
class Annotations
|
57
|
+
|
58
|
+
include Enumerable
|
59
|
+
|
60
|
+
# Creates a new Annotations object for the given PDF document.
|
61
|
+
def initialize(document)
|
62
|
+
@document = document
|
63
|
+
end
|
64
|
+
|
65
|
+
# :call-seq:
|
66
|
+
# annotations.create(type, page, **options) -> annotation
|
67
|
+
#
|
68
|
+
# Creates a new annotation object with the given +type+ and +page+ by calling the respective
|
69
|
+
# +create_type+ method.
|
70
|
+
#
|
71
|
+
# The +options+ are passed on the specific annotation creation method.
|
72
|
+
def create(type, page, **options)
|
73
|
+
method_name = "create_#{type}"
|
74
|
+
unless respond_to?(method_name)
|
75
|
+
raise ArgumentError, "Invalid type specified"
|
76
|
+
end
|
77
|
+
send("create_#{type}", page, **options)
|
78
|
+
end
|
79
|
+
|
80
|
+
# :call-seq:
|
81
|
+
# annotations.create_line(page, start_point:, end_point:) -> annotation
|
82
|
+
#
|
83
|
+
# Creates a line annotation from +start_point+ to +end_point+ on the given page and returns
|
84
|
+
# it.
|
85
|
+
#
|
86
|
+
# The line uses a black color and a width of 1pt. It can be further styled using the
|
87
|
+
# convenience methods on the returned annotation object.
|
88
|
+
#
|
89
|
+
# Example:
|
90
|
+
#
|
91
|
+
# doc.annotations.create_line(doc.pages[0], start_point: [100, 100], end_point: [130, 180]).
|
92
|
+
# border_style(color: "blue", width: 2).
|
93
|
+
# leader_line_length(10).
|
94
|
+
# regenerate_appearance
|
95
|
+
#
|
96
|
+
# See: Type::Annotations::Line
|
97
|
+
def create_line(page, start_point:, end_point:)
|
98
|
+
create_and_add_to_page(:Line, page).
|
99
|
+
line(*start_point, *end_point).
|
100
|
+
border_style(color: 0, width: 1)
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
# Returns the root of the destinations name tree.
|
106
|
+
def create_and_add_to_page(subtype, page)
|
107
|
+
annot = @document.add({Type: :Annot, Subtype: subtype})
|
108
|
+
(page[:Annots] ||= []) << annot
|
109
|
+
annot
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
data/lib/hexapdf/document.rb
CHANGED
@@ -123,6 +123,7 @@ module HexaPDF
|
|
123
123
|
autoload(:Destinations, 'hexapdf/document/destinations')
|
124
124
|
autoload(:Layout, 'hexapdf/document/layout')
|
125
125
|
autoload(:Metadata, 'hexapdf/document/metadata')
|
126
|
+
autoload(:Annotations, 'hexapdf/document/annotations')
|
126
127
|
|
127
128
|
# :call-seq:
|
128
129
|
# Document.open(filename, **docargs) -> doc
|
@@ -539,6 +540,12 @@ module HexaPDF
|
|
539
540
|
@destinations ||= Destinations.new(self)
|
540
541
|
end
|
541
542
|
|
543
|
+
# Returns the Annotations object that provides convenience methods for working with annotation
|
544
|
+
# objects.
|
545
|
+
def annotations
|
546
|
+
@annotations ||= Annotations.new(self)
|
547
|
+
end
|
548
|
+
|
542
549
|
# Returns the Layout object that provides convenience methods for working with the
|
543
550
|
# HexaPDF::Layout classes for document layout.
|
544
551
|
def layout
|
@@ -726,8 +733,8 @@ module HexaPDF
|
|
726
733
|
end
|
727
734
|
|
728
735
|
# :call-seq:
|
729
|
-
# doc.write(filename, incremental: false, validate: true, update_fields: true, optimize: false) -> [start_xref, section]
|
730
|
-
# doc.write(io, incremental: false, validate: true, update_fields: true, optimize: false) -> [start_xref, section]
|
736
|
+
# doc.write(filename, incremental: false, validate: true, update_fields: true, optimize: false, compact: true) -> [start_xref, section]
|
737
|
+
# doc.write(io, incremental: false, validate: true, update_fields: true, optimize: false, compact: true) -> [start_xref, section]
|
731
738
|
#
|
732
739
|
# Writes the document to the given file (in case +io+ is a String) or IO stream. Returns the
|
733
740
|
# file position of the start of the last cross-reference section and the last XRefSection object
|
@@ -755,7 +762,20 @@ module HexaPDF
|
|
755
762
|
# optimize::
|
756
763
|
# Optimize the file size by using object and cross-reference streams. This will raise the PDF
|
757
764
|
# version to at least 1.5.
|
758
|
-
|
765
|
+
#
|
766
|
+
# compact::
|
767
|
+
# Compact the document by reducing it to a single revision and removing null and unused
|
768
|
+
# objects.
|
769
|
+
#
|
770
|
+
# The initial revision of a document has to contain objects with continuous numbering. If some
|
771
|
+
# object numbers refer to free entries, other PDF libraries/viewers might not work
|
772
|
+
# correctly. So continuous object numbers are assigned to stay compliant with the
|
773
|
+
# specification.
|
774
|
+
#
|
775
|
+
# Only change this argument to +false+ if you run the optimization task with 'compact: true'
|
776
|
+
# beforehand or if you know exactly what you do and what not compacting implies.
|
777
|
+
def write(file_or_io, incremental: false, validate: true, update_fields: true, optimize: false,
|
778
|
+
compact: true)
|
759
779
|
if update_fields
|
760
780
|
trailer.update_id
|
761
781
|
if @metadata
|
@@ -774,10 +794,11 @@ module HexaPDF
|
|
774
794
|
end
|
775
795
|
end
|
776
796
|
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
797
|
+
optimize_opts = {}
|
798
|
+
optimize_opts[:object_streams] = :generate if optimize
|
799
|
+
optimize_opts[:compact] = true if compact && !incremental
|
800
|
+
task(:optimize, **optimize_opts) unless optimize_opts.empty?
|
801
|
+
self.version = '1.5' if version < '1.5' if optimize
|
781
802
|
|
782
803
|
dispatch_message(:before_write)
|
783
804
|
|
@@ -496,7 +496,7 @@ module HexaPDF
|
|
496
496
|
else
|
497
497
|
nil
|
498
498
|
end
|
499
|
-
result && (result == result.truncate ? result.to_i.to_s : result.to_s)
|
499
|
+
result && (result.finite? && result == result.truncate ? result.to_i.to_s : result.to_s)
|
500
500
|
end
|
501
501
|
|
502
502
|
AF_SIMPLE_CALCULATE_MAPPING = { #:nodoc:
|
@@ -613,7 +613,14 @@ module HexaPDF
|
|
613
613
|
|
614
614
|
# Returns the numeric value of the string, interpreting comma as point.
|
615
615
|
def af_make_number(value)
|
616
|
-
value.to_s
|
616
|
+
value = value.to_s
|
617
|
+
if value.match?(/(?:[+-])?Inf(?:inity)?/i)
|
618
|
+
value.start_with?('-') ? -Float::INFINITY : Float::INFINITY
|
619
|
+
elsif value.match?(/NaN/i)
|
620
|
+
Float::NAN
|
621
|
+
else
|
622
|
+
value.tr(',', '.').to_f
|
623
|
+
end
|
617
624
|
end
|
618
625
|
|
619
626
|
# Formats the numeric value according to the format string and separator style.
|
@@ -176,7 +176,7 @@ module HexaPDF
|
|
176
176
|
end
|
177
177
|
str = str.gsub(/[[:space:]]/, ' ') if str && concrete_field_type == :single_line_text_field
|
178
178
|
if key?(:MaxLen) && str && str.length > self[:MaxLen]
|
179
|
-
|
179
|
+
str = @document.config['acro_form.text_field.on_max_len_exceeded'].call(self, str)
|
180
180
|
end
|
181
181
|
self[:V] = str
|
182
182
|
update_widgets
|
@@ -348,7 +348,14 @@ module HexaPDF
|
|
348
348
|
return
|
349
349
|
end
|
350
350
|
if (max_len = self[:MaxLen]) && field_value && field_value.length > max_len
|
351
|
-
|
351
|
+
correctable = true
|
352
|
+
begin
|
353
|
+
str = @document.config['acro_form.text_field.on_max_len_exceeded'].call(self, field_value)
|
354
|
+
rescue HexaPDF::Error
|
355
|
+
correctable = false
|
356
|
+
end
|
357
|
+
yield("Text contents of field '#{full_field_name}' is too long", correctable)
|
358
|
+
self.field_value = str if correctable
|
352
359
|
end
|
353
360
|
if comb_text_field? && !max_len
|
354
361
|
yield("Comb text field needs a value for /MaxLen")
|