hexapdf 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -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 +28 -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: 902a2c520a3fa1a6c0128edc938ab82f604faca6f08548d68e0226a7f9426422
4
- data.tar.gz: 942551d188d12f5a841f4a1631982e813a85823570b4ba3ec085cf846cf164dd
3
+ metadata.gz: 041330c9846091186ddeecd7900a7919bb96da26b3513b047f03c87888efedcd
4
+ data.tar.gz: ff89d47f1eeac1d2dda4982224403318528401918ccfe7f1bb775d05d8f0e6f3
5
5
  SHA512:
6
- metadata.gz: 26be23186793f7d704831969652e2392135eb5e0d8460ba7505b27fb546ac5043c10513cf358f16890723978ee0cbe28129f97185cba8a6f9c69f279f446cdd4
7
- data.tar.gz: c094967af75096e58d6493d459a46ac345d109ef1feb58de0bd5ca746ce0dc39880467c7208928e881c325bbb32c9c5c5ac55738e3b5a0570ad330c2a0fd39ba
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
@@ -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
@@ -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
- 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)
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
- if optimize
778
- task(:optimize, object_streams: :generate)
779
- self.version = '1.5' if version < '1.5'
780
- 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
781
802
 
782
803
  dispatch_message(:before_write)
783
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")