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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/lib/hexapdf/cli/command.rb +63 -63
  4. data/lib/hexapdf/cli/inspect.rb +1 -1
  5. data/lib/hexapdf/cli/modify.rb +0 -1
  6. data/lib/hexapdf/cli/optimize.rb +5 -5
  7. data/lib/hexapdf/configuration.rb +21 -0
  8. data/lib/hexapdf/content/graphics_state.rb +1 -1
  9. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +1 -1
  10. data/lib/hexapdf/document/annotations.rb +115 -0
  11. data/lib/hexapdf/document.rb +30 -7
  12. data/lib/hexapdf/font/true_type_wrapper.rb +1 -0
  13. data/lib/hexapdf/font/type1_wrapper.rb +1 -0
  14. data/lib/hexapdf/type/acro_form/java_script_actions.rb +9 -2
  15. data/lib/hexapdf/type/acro_form/text_field.rb +9 -2
  16. data/lib/hexapdf/type/annotation.rb +59 -1
  17. data/lib/hexapdf/type/annotations/appearance_generator.rb +273 -0
  18. data/lib/hexapdf/type/annotations/border_styling.rb +160 -0
  19. data/lib/hexapdf/type/annotations/line.rb +521 -0
  20. data/lib/hexapdf/type/annotations/widget.rb +2 -96
  21. data/lib/hexapdf/type/annotations.rb +3 -0
  22. data/lib/hexapdf/type/form.rb +2 -2
  23. data/lib/hexapdf/version.rb +1 -1
  24. data/lib/hexapdf/writer.rb +0 -1
  25. data/lib/hexapdf/xref_section.rb +7 -4
  26. data/test/hexapdf/content/test_graphics_state.rb +2 -3
  27. data/test/hexapdf/content/test_operator.rb +4 -5
  28. data/test/hexapdf/digital_signature/test_cms_handler.rb +7 -8
  29. data/test/hexapdf/digital_signature/test_handler.rb +2 -3
  30. data/test/hexapdf/digital_signature/test_pkcs1_handler.rb +1 -2
  31. data/test/hexapdf/document/test_annotations.rb +33 -0
  32. data/test/hexapdf/font/test_true_type_wrapper.rb +7 -0
  33. data/test/hexapdf/font/test_type1_wrapper.rb +7 -0
  34. data/test/hexapdf/task/test_optimize.rb +1 -1
  35. data/test/hexapdf/test_document.rb +11 -3
  36. data/test/hexapdf/test_stream.rb +1 -2
  37. data/test/hexapdf/test_xref_section.rb +1 -1
  38. data/test/hexapdf/type/acro_form/test_java_script_actions.rb +21 -0
  39. data/test/hexapdf/type/acro_form/test_text_field.rb +7 -1
  40. data/test/hexapdf/type/annotations/test_appearance_generator.rb +398 -0
  41. data/test/hexapdf/type/annotations/test_border_styling.rb +114 -0
  42. data/test/hexapdf/type/annotations/test_line.rb +189 -0
  43. data/test/hexapdf/type/annotations/test_widget.rb +0 -81
  44. data/test/hexapdf/type/test_annotation.rb +55 -0
  45. data/test/hexapdf/type/test_form.rb +6 -0
  46. metadata +10 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f05785a632fc75d280395078734af23925d219183182d8660bf08ba352cf74c
4
- data.tar.gz: ee143096b0e71bbccfc0397e0cc1d01fac908f6fb4969e015b2b7c7952088af0
3
+ metadata.gz: 041330c9846091186ddeecd7900a7919bb96da26b3513b047f03c87888efedcd
4
+ data.tar.gz: ff89d47f1eeac1d2dda4982224403318528401918ccfe7f1bb775d05d8f0e6f3
5
5
  SHA512:
6
- metadata.gz: e555fcfde66afe9cbff99c0643c53c79659b5a82f9abe15ac042b449b4a2258be0cea5b8f0cea28e11cd3c429211a6972e3bc45909e3cfd5f9effd8f15553c4f
7
- data.tar.gz: ee7f9f8ffbf80f9171343edb49ced27ca6e89767c54cc23db7c26c51ccb34b17976005cded879132b319bea8b7e890b3840e2359c01eb12f32b69d61a65e24a1
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
@@ -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 = OpenStruct.new
72
- @out_options.compact = true
73
- @out_options.compress_pages = false
74
- @out_options.object_streams = :preserve
75
- @out_options.xref_streams = :preserve
76
- @out_options.streams = :preserve
77
- @out_options.optimize_fonts = false
78
- @out_options.prune_page_resources = false
79
-
80
- @out_options.encryption = :preserve
81
- @out_options.enc_user_pwd = @out_options.enc_owner_pwd = nil
82
- @out_options.enc_key_length = 128
83
- @out_options.enc_algorithm = :aes
84
- @out_options.enc_force_v4 = false
85
- @out_options.enc_permissions = []
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.compact})") do |c|
187
- @out_options.compact = c
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.object_streams})") do |os|
192
- @out_options.object_streams = os
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.xref_streams})") do |x|
197
- @out_options.xref_streams = x
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.streams})") do |streams|
202
- @out_options.streams = streams
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.compress_pages})") do |c|
206
- @out_options.compress_pages = c
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.prune_page_resources})") do |c|
210
- @out_options.prune_page_resources = c
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.optimize_fonts})") do |o|
214
- @out_options.optimize_fonts = o
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.encryption = :remove
225
+ @out_options[:encryption] = :remove
226
226
  end
227
227
  options.on("--encrypt", "Encrypt the output file") do
228
- @out_options.encryption = :add
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.encryption = :add
233
- @out_options.enc_owner_pwd = (pwd == '-' ? read_password("Owner password") : pwd)
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.encryption = :add
238
- @out_options.enc_user_pwd = (pwd == '-' ? read_password("User password") : pwd)
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.enc_algorithm})") do |a|
243
- @out_options.encryption = :add
244
- @out_options.enc_algorithm = a
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.enc_key_length})") do |i|
249
- @out_options.encryption = :add
250
- @out_options.enc_key_length = i
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.encryption = :add
255
- @out_options.enc_force_v4 = true
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.encryption = :add
268
- @out_options.enc_permissions = perms
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.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
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.streams == :preserve || !obj.respond_to?(:set_filter) ||
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.streams == :compress ? :FlateDecode : nil)
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.optimize_fonts && obj.kind_of?(HexaPDF::Type::Font) &&
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.encryption
322
+ case @out_options[:encryption]
323
323
  when :add
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)
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
@@ -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!
@@ -34,7 +34,6 @@
34
34
  # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
35
  #++
36
36
 
37
- require 'ostruct'
38
37
  require 'hexapdf/cli/command'
39
38
 
40
39
  module HexaPDF
@@ -53,11 +53,11 @@ module HexaPDF
53
53
  EOF
54
54
 
55
55
  @password = nil
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
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.freeze
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: digested_data
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
@@ -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
- def write(file_or_io, incremental: false, validate: true, update_fields: true, optimize: false)
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
- if optimize
776
- task(:optimize, object_streams: :generate)
777
- self.version = '1.5' if version < '1.5'
778
- end
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
 
@@ -299,6 +299,7 @@ module HexaPDF
299
299
  dict.font_wrapper = self
300
300
 
301
301
  document.register_listener(:complete_objects) do
302
+ next if dict.null?
302
303
  update_font_name(dict)
303
304
  embed_font(dict, document)
304
305
  complete_width_information(dict)
@@ -270,6 +270,7 @@ module HexaPDF
270
270
  dict.font_wrapper = self
271
271
 
272
272
  document.register_listener(:complete_objects) do
273
+ next if dict.null?
273
274
  min, max = @encoding.code_to_name.keys.minmax
274
275
  dict[:FirstChar] = min
275
276
  dict[:LastChar] = max
@@ -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.tr(',', '.').to_f
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
- raise HexaPDF::Error, "Value exceeds maximum allowed length of #{self[:MaxLen]}"
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
- yield("Text contents of field '#{full_field_name}' is too long")
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")