hexapdf 1.1.0 → 1.2.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 +41 -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 +30 -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,44 @@
|
|
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
|
+
|
35
|
+
## 1.1.1 - 2025-01-08
|
36
|
+
|
37
|
+
### Fixed
|
38
|
+
|
39
|
+
* Missing require statements leading to problems loading type classes
|
40
|
+
|
41
|
+
|
1
42
|
## 1.1.0 - 2025-01-08
|
2
43
|
|
3
44
|
### Added
|
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
@@ -43,6 +43,8 @@ require 'hexapdf/reference'
|
|
43
43
|
require 'hexapdf/object'
|
44
44
|
require 'hexapdf/pdf_array'
|
45
45
|
require 'hexapdf/stream'
|
46
|
+
require 'hexapdf/name_tree_node'
|
47
|
+
require 'hexapdf/number_tree_node'
|
46
48
|
require 'hexapdf/revisions'
|
47
49
|
require 'hexapdf/type'
|
48
50
|
require 'hexapdf/task'
|
@@ -121,6 +123,7 @@ module HexaPDF
|
|
121
123
|
autoload(:Destinations, 'hexapdf/document/destinations')
|
122
124
|
autoload(:Layout, 'hexapdf/document/layout')
|
123
125
|
autoload(:Metadata, 'hexapdf/document/metadata')
|
126
|
+
autoload(:Annotations, 'hexapdf/document/annotations')
|
124
127
|
|
125
128
|
# :call-seq:
|
126
129
|
# Document.open(filename, **docargs) -> doc
|
@@ -537,6 +540,12 @@ module HexaPDF
|
|
537
540
|
@destinations ||= Destinations.new(self)
|
538
541
|
end
|
539
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
|
+
|
540
549
|
# Returns the Layout object that provides convenience methods for working with the
|
541
550
|
# HexaPDF::Layout classes for document layout.
|
542
551
|
def layout
|
@@ -724,8 +733,8 @@ module HexaPDF
|
|
724
733
|
end
|
725
734
|
|
726
735
|
# :call-seq:
|
727
|
-
# doc.write(filename, incremental: false, validate: true, update_fields: true, optimize: false) -> [start_xref, section]
|
728
|
-
# 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]
|
729
738
|
#
|
730
739
|
# Writes the document to the given file (in case +io+ is a String) or IO stream. Returns the
|
731
740
|
# file position of the start of the last cross-reference section and the last XRefSection object
|
@@ -753,7 +762,20 @@ module HexaPDF
|
|
753
762
|
# optimize::
|
754
763
|
# Optimize the file size by using object and cross-reference streams. This will raise the PDF
|
755
764
|
# version to at least 1.5.
|
756
|
-
|
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)
|
757
779
|
if update_fields
|
758
780
|
trailer.update_id
|
759
781
|
if @metadata
|
@@ -772,10 +794,11 @@ module HexaPDF
|
|
772
794
|
end
|
773
795
|
end
|
774
796
|
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
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
|
779
802
|
|
780
803
|
dispatch_message(:before_write)
|
781
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")
|