hexapdf 0.41.0 → 0.42.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae86345e0f2ed2dd27c9c58c550e7eaffb4d7c5d3ba388afb04318ee20491313
4
- data.tar.gz: cfd9f8575ce9f4324c594c2617cc7e1bcf1d735276ac92a275f26acf74566bc9
3
+ metadata.gz: dd69db2c04dc802784438f9038c9a6ab32dac0806edca43b6c59406bba9d7ea6
4
+ data.tar.gz: a51a510a6a98b547b2b11fb07804ca9039a666adf95b52f65db9b070c3ffe9c3
5
5
  SHA512:
6
- metadata.gz: d36715922fbbf5a93eeb5512ed0abf7ec78fbd099c49131500bd9f7db75c39248bb2bac12ad7e3df5c4a8a449351f63e0d83e0ed635f9a025e4bf25fdbe9f0e1
7
- data.tar.gz: fc7a89694614826c8d151b7dab2c3f45ec335fc94efff873065c3dc68c845a691311b91b42c3b791eb71be80e1f8fd623838eb0cf1b94d0d1b758441df1b1a7a
6
+ metadata.gz: 714cba0b758960066498413b545dc7cf2806e1e483f99b017958450bfe29fd12aac1eef40f670d189f7cd7dab5784dd83e7b6f95533b97cf71c883dd9f307485
7
+ data.tar.gz: c93ceed7393b57fa5c6ca83141a8307b6015a1fa6907886b52c57bf72384d4d23ac8356adede0dabc35578cad5e1cef074f75c94e01e5c9532cadd0a688bbd57
data/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
1
+ ## 0.42.0 - 2024-05-12
2
+
3
+ ### Added
4
+
5
+ * Support for the `AFPercent_Format` JavaScript method
6
+ * Support for the `AFTime_Format` JavaScript method
7
+ * [HexaPDF::Type::AcroForm::Form#fill] for easily filling out form fields
8
+ * CLI command `hexapdf usage` for showing space usage information
9
+ * Support for attaching files via `hexapdf files` CLI command
10
+ * Refinement on [HexaPDF::Utils] to support conversion of Numeric values to
11
+ points (e.g. `5.mm`, `5.cm`, `5.inch`)
12
+
13
+ ### Changed
14
+
15
+ * [HexaPDF::Type::AcroForm::ButtonField#field_value=] to always allow using
16
+ `true` for check boxes
17
+ * CLI commands to prompt whether an existing output file should be overwritten
18
+
19
+ ### Fixed
20
+
21
+ * [HexaPDF::Type::Resources#font] to always return a correctly wrapped font
22
+ object
23
+ * [HexaPDF::Type::AcroForm::TextField#field_value=] to actually use the value
24
+ returned by the call to the config option 'acro_form.on_invalid_value'
25
+
26
+
1
27
  ## 0.41.0 - 2024-05-05
2
28
 
3
29
  ### Added
@@ -31,70 +31,82 @@ tx.set_format_action(:number, decimals: 2, separator_style: :comma)
31
31
  widget = tx.create_widget(page, Rect: [200, 615, 500, 635])
32
32
  tx.field_value = "1234567.898"
33
33
 
34
- canvas.text("Calculate actions", at: [50, 570])
34
+ canvas.text("Percent format", at: [70, 590])
35
+ tx = form.create_text_field("Percent_Format", font_size: 16)
36
+ tx.set_format_action(:percent, decimals: 2, separator_style: :comma)
37
+ widget = tx.create_widget(page, Rect: [200, 585, 500, 605])
38
+ tx.field_value = "12,45678"
35
39
 
36
- canvas.text("Source fields", at: [70, 540])
37
- canvas.text("a:", at: [200, 540])
40
+ canvas.text("Time format", at: [70, 560])
41
+ tx = form.create_text_field("Time_Format", font_size: 16)
42
+ tx.set_format_action(:time, format: :hh_mm_ss)
43
+ widget = tx.create_widget(page, Rect: [200, 555, 500, 575])
44
+ tx.field_value = "3:15:20 pm"
45
+
46
+ canvas.text("Calculate actions", at: [50, 510])
47
+
48
+ canvas.text("Source fields", at: [70, 480])
49
+ canvas.text("a:", at: [200, 480])
38
50
  tx = form.create_text_field("a", font_size: 16)
39
51
  tx.set_format_action(:number, decimals: 2)
40
- widget = tx.create_widget(page, Rect: [220, 535, 280, 555])
52
+ widget = tx.create_widget(page, Rect: [220, 475, 280, 495])
41
53
  tx.field_value = "10,50"
42
- canvas.text("b:", at: [310, 540])
54
+ canvas.text("b:", at: [310, 480])
43
55
  tx = form.create_text_field("b", font_size: 16)
44
56
  tx.set_format_action(:number, decimals: 2)
45
- widget = tx.create_widget(page, Rect: [330, 535, 390, 555])
57
+ widget = tx.create_widget(page, Rect: [330, 475, 390, 495])
46
58
  tx.field_value = "20,60"
47
- canvas.text("c:", at: [420, 540])
59
+ canvas.text("c:", at: [420, 480])
48
60
  tx = form.create_text_field("c", font_size: 16)
49
61
  tx.set_format_action(:number, decimals: 2)
50
- widget = tx.create_widget(page, Rect: [440, 535, 500, 555])
62
+ widget = tx.create_widget(page, Rect: [440, 475, 500, 495])
51
63
  tx.field_value = "30,70"
52
64
 
53
- canvas.text("Predefined", at: [70, 510])
54
- canvas.text("Sum", at: [90, 480])
65
+ canvas.text("Predefined", at: [70, 450])
66
+ canvas.text("Sum", at: [90, 420])
55
67
  tx = form.create_text_field("sum", font_size: 16)
56
68
  tx.set_format_action(:number, decimals: 2)
57
69
  tx.set_calculate_action(:sum, fields: ['a', 'b', 'c'])
58
70
  tx.flag(:read_only)
59
- widget = tx.create_widget(page, Rect: [310, 475, 500, 495])
60
- canvas.text("Average", at: [90, 450])
71
+ widget = tx.create_widget(page, Rect: [310, 415, 500, 435])
72
+ canvas.text("Average", at: [90, 390])
61
73
  tx = form.create_text_field("average", font_size: 16)
62
74
  tx.set_format_action(:number, decimals: 2)
63
75
  tx.set_calculate_action(:average, fields: ['a', 'b', 'c'])
64
76
  tx.flag(:read_only)
65
- widget = tx.create_widget(page, Rect: [310, 445, 500, 465])
66
- canvas.text("Product", at: [90, 420])
77
+ widget = tx.create_widget(page, Rect: [310, 385, 500, 405])
78
+ canvas.text("Product", at: [90, 360])
67
79
  tx = form.create_text_field("product", font_size: 16)
68
80
  tx.set_format_action(:number, decimals: 2)
69
81
  tx.set_calculate_action(:product, fields: ['a', 'b', 'c'])
70
82
  tx.flag(:read_only)
71
- widget = tx.create_widget(page, Rect: [310, 415, 500, 435])
72
- canvas.text("Minimum", at: [90, 390])
83
+ widget = tx.create_widget(page, Rect: [310, 355, 500, 375])
84
+ canvas.text("Minimum", at: [90, 330])
73
85
  tx = form.create_text_field("min", font_size: 16)
74
86
  tx.set_format_action(:number, decimals: 2)
75
87
  tx.set_calculate_action(:min, fields: ['a', 'b', 'c'])
76
88
  tx.flag(:read_only)
77
- widget = tx.create_widget(page, Rect: [310, 385, 500, 405])
78
- canvas.text("Maximum", at: [90, 360])
89
+ widget = tx.create_widget(page, Rect: [310, 325, 500, 345])
90
+ canvas.text("Maximum", at: [90, 300])
79
91
  tx = form.create_text_field("max", font_size: 16)
80
92
  tx.set_format_action(:number, decimals: 2)
81
93
  tx.set_calculate_action(:max, fields: ['a', 'b', 'c'])
82
94
  tx.flag(:read_only)
83
- widget = tx.create_widget(page, Rect: [310, 355, 500, 375])
95
+ widget = tx.create_widget(page, Rect: [310, 295, 500, 315])
84
96
 
85
- canvas.text("Simplified Field Notation", at: [70, 330])
86
- canvas.text("a + b + c", at: [90, 300])
97
+ canvas.text("Simplified Field Notation", at: [70, 270])
98
+ canvas.text("a + b + c", at: [90, 240])
87
99
  tx = form.create_text_field("sfn1", font_size: 16)
88
100
  tx.set_format_action(:number, decimals: 2)
89
101
  tx.set_calculate_action(:sfn, fields: "a + b + c")
90
102
  tx.flag(:read_only)
91
- widget = tx.create_widget(page, Rect: [310, 295, 500, 315])
92
- canvas.text("(a + b)*(c - a) / b + 3.14", at: [90, 270])
103
+ widget = tx.create_widget(page, Rect: [310, 235, 500, 255])
104
+ canvas.text("(a + b)*(c - a) / b + 3.14", at: [90, 210])
93
105
  tx = form.create_text_field("sfn2", font_size: 16)
94
106
  tx.set_format_action(:number, decimals: 2)
95
107
  tx.set_calculate_action(:sfn, fields: "(a + b)*(c - a) / b + 3.14")
96
108
  tx.flag(:read_only)
97
- widget = tx.create_widget(page, Rect: [310, 265, 500, 285])
109
+ widget = tx.create_widget(page, Rect: [310, 205, 500, 225])
98
110
 
99
111
  form.recalculate_fields
100
112
 
@@ -53,7 +53,7 @@ module HexaPDF
53
53
  module Extensions #:nodoc:
54
54
  def help_banner #:nodoc:
55
55
  "hexapdf #{HexaPDF::VERSION} - Versatile PDF Manipulation Tool\n" \
56
- "Copyright (c) 2014-2023 Thomas Leitner; licensed under the AGPLv3\n\n" \
56
+ "Copyright (c) 2014-2024 Thomas Leitner; licensed under the AGPLv3\n\n" \
57
57
  "#{format(usage, indent: 7)}\n\n"
58
58
  end
59
59
 
@@ -167,12 +167,12 @@ module HexaPDF
167
167
  end
168
168
  end
169
169
 
170
- # Checks whether the given output file exists and raises an error if it does and
171
- # HexaPDF::CLI#force is not set.
170
+ # Checks whether the given output file exists and ask whether to overwrite the output file if
171
+ # it does. If HexaPDF::CLI#force is set, a possibly existing output file is always overwritten.
172
172
  def maybe_raise_on_existing_file(filename)
173
173
  if !command_parser.force && File.exist?(filename)
174
- raise Error, "Output file '#{filename}' already exists, not overwriting. Use --force to " \
175
- "force writing"
174
+ response = read_from_console("Output file '#{filename}' already exists - overwrite? (y/n)")
175
+ exit(1) unless response =~ /y/i
176
176
  end
177
177
  end
178
178
 
@@ -377,9 +377,9 @@ module HexaPDF
377
377
  # console.
378
378
  def read_password(prompt = "Password")
379
379
  if $stdin.tty?
380
- read_from_console(prompt)
380
+ read_from_console(prompt, noecho: true)
381
381
  else
382
- ($stdin.gets || read_from_console(prompt)).chomp
382
+ ($stdin.gets || read_from_console(prompt, noecho: true)).chomp
383
383
  end
384
384
  end
385
385
 
@@ -407,11 +407,14 @@ module HexaPDF
407
407
  private
408
408
 
409
409
  # Displays the given prompt, reads from the console without echo and returns the read string.
410
- def read_from_console(prompt)
410
+ def read_from_console(prompt, noecho: false)
411
411
  IO.console.write("#{prompt}: ")
412
- str = IO.console.noecho {|io| io.gets.chomp }
413
- puts
414
- str
412
+ if noecho
413
+ IO.console.noecho {|io| io.gets.chomp }
414
+ puts
415
+ else
416
+ IO.console.gets.chomp
417
+ end
415
418
  end
416
419
 
417
420
  end
@@ -39,19 +39,29 @@ require 'hexapdf/cli/command'
39
39
  module HexaPDF
40
40
  module CLI
41
41
 
42
- # Lists or extracts embedded files from a PDF file.
42
+ # Lists or extracts embedded files from a PDF file or attaches them.
43
43
  #
44
44
  # See: HexaPDF::Type::EmbeddedFile
45
45
  class Files < Command
46
46
 
47
47
  def initialize #:nodoc:
48
48
  super('files', takes_commands: false)
49
- short_desc("List or extract embedded files from a PDF file")
49
+ short_desc("List and extract embedded files from a PDF or attach files")
50
50
  long_desc(<<~EOF)
51
- If the option --extract is not given, the available files are listed with their names and
52
- indices. The --extract option can then be used to extract one or more files.
51
+ If neither the option --attach nor the option --extract is given, the available
52
+ files are listed with their names and indices. The --extract option can then be
53
+ used to extract one or more files. Or the --attach option can be used to attach
54
+ files to the PDF.
53
55
  EOF
54
56
 
57
+ options.on("--attach FILE", "-a FILE", String,
58
+ "The file that should be attached. Can be used multiple times.") do |file|
59
+ @attach_files << [file, nil]
60
+ end
61
+ options.on("--description DESC", "-d DESC", String,
62
+ "Adds a description to the last file to be attached.") do |description|
63
+ @attach_files[-1][1] = description
64
+ end
55
65
  options.on("--extract [a,b,c,...]", "-e [a,b,c,...]", Array,
56
66
  "The indices of the files that should be extracted. Use 0 or no argument to " \
57
67
  "extract all files.") do |indices|
@@ -66,15 +76,24 @@ module HexaPDF
66
76
  @password = (pwd == '-' ? read_password : pwd)
67
77
  end
68
78
 
79
+ @attach_files = []
69
80
  @indices = []
70
81
  @password = nil
71
82
  @search = false
72
83
  end
73
84
 
74
- def execute(pdf) #:nodoc:
75
- with_document(pdf, password: @password) do |doc|
76
- if @indices.empty?
85
+ def execute(pdf, output = nil) #:nodoc:
86
+ if @indices.empty? && !@attach_files.empty?
87
+ raise Error, "Missing output file" unless output
88
+ maybe_raise_on_existing_file(output)
89
+ end
90
+ with_document(pdf, password: @password, out_file: output) do |doc|
91
+ if @indices.empty? && @attach_files.empty?
77
92
  list_files(doc)
93
+ elsif !@indices.empty? && !@attach_files.empty?
94
+ raise Error, "Use either --attach or --extract but not both"
95
+ elsif !@attach_files.empty?
96
+ attach_files(doc)
78
97
  else
79
98
  extract_files(doc)
80
99
  end
@@ -116,6 +135,11 @@ module HexaPDF
116
135
  end
117
136
  end
118
137
 
138
+ # Attaches the files given on the CLI to the document.
139
+ def attach_files(doc)
140
+ @attach_files.each {|file, desc| doc.files.add(file, description: desc) }
141
+ end
142
+
119
143
  # Iterates over all embedded files.
120
144
  def each_file(doc, &block) # :yields: obj, index
121
145
  doc.files.each(search: @search).select(&:embedded_file?).each_with_index(&block)
@@ -195,6 +195,7 @@ module HexaPDF
195
195
  # Fills out the form by interactively asking the user for field values.
196
196
  def fill_form(doc)
197
197
  current_page_index = -1
198
+ form = doc.acro_form
198
199
  each_field(doc) do |_page, page_index, field, _widget|
199
200
  next if field.flagged?(:read_only) && !@fill_read_only_fields
200
201
  if current_page_index != page_index
@@ -224,9 +225,9 @@ module HexaPDF
224
225
  print " └─ New value: "
225
226
  value = $stdin.readline.chomp
226
227
  next if value.empty?
227
- apply_field_value(field, value)
228
+ form.fill(field.full_field_name => value)
228
229
  rescue HexaPDF::Error => e
229
- puts " ⚠ #{e.message}"
230
+ puts " ⚠ Error while setting '#{field.full_field_name}': #{e.message}"
230
231
  retry
231
232
  end
232
233
  end
@@ -234,18 +235,20 @@ module HexaPDF
234
235
 
235
236
  # Fills out the form using the data from the provided template file.
236
237
  def fill_form_with_template(doc)
237
- data = parse_template
238
238
  form = doc.acro_form
239
- data.each do |name, value|
239
+ data = parse_template
240
+ data.reject! do |name, _value|
240
241
  field = form.field_by_name(name)
241
242
  raise Error, "Field '#{name}' not found in input PDF" unless field
242
243
  if field.flagged?(:read_only) && !@fill_read_only_fields
243
244
  puts "Ignoring field '#{name}' because it is read only and --fill-read-only-fields " \
244
- "is no set"
245
- next
245
+ "is not set"
246
+ true
247
+ else
248
+ false
246
249
  end
247
- apply_field_value(field, value)
248
250
  end
251
+ form.fill(data)
249
252
  end
250
253
 
251
254
  # Parses the data from the given template file.
@@ -273,30 +276,6 @@ module HexaPDF
273
276
  data
274
277
  end
275
278
 
276
- # Applies the given value to the field.
277
- def apply_field_value(field, value)
278
- case field.concrete_field_type
279
- when :single_line_text_field, :multiline_text_field, :comb_text_field, :file_select_field,
280
- :combo_box, :list_box, :editable_combo_box
281
- field.field_value = value
282
- when :check_box
283
- field.field_value = case value
284
- when /y(es)?|t(rue)?/
285
- true
286
- when /n(o)?|f(alse)?/
287
- false
288
- else
289
- value
290
- end
291
- when :radio_button
292
- field.field_value = value.to_sym
293
- else
294
- raise Error, "Field type #{field.concrete_field_type} not yet supported"
295
- end
296
- rescue StandardError
297
- raise Error, "Error while setting '#{field.full_field_name}': #{$!.message}"
298
- end
299
-
300
279
  # Iterates over all non-push button fields in page order. If a field appears on multiple
301
280
  # pages, it is only yielded on the first page if +with_seen+ is +false.
302
281
  def each_field(doc, with_seen: false) # :yields: page, page_index, field
@@ -270,7 +270,7 @@ module HexaPDF
270
270
  if (rev_index = data.shift)
271
271
  rev_index = rev_index.to_i - 1
272
272
  if rev_index < 0 || rev_index >= @doc.revisions.count
273
- $stderr.puts("Error: Invalid revision numer specified")
273
+ $stderr.puts("Error: Invalid revision number specified")
274
274
  next
275
275
  end
276
276
  length = 0
@@ -0,0 +1,215 @@
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-2024 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/cli/command'
38
+
39
+ module HexaPDF
40
+ module CLI
41
+
42
+ # Shows the space usage of various parts of a PDF file.
43
+ class Usage < Command
44
+
45
+ # Modifies the HexaPDF::PDFData class to store the size information
46
+ module PDFDataExtension
47
+
48
+ # Used to store the size of the indirect object.
49
+ attr_accessor :size
50
+
51
+ # Used to store the size of the object inside the object stream.
52
+ attr_accessor :size_in_object_stream
53
+
54
+ end
55
+
56
+ # Modifies HexaPDF::Parser to retrieve space used by indirect objects.
57
+ module ParserExtension
58
+
59
+ # :nodoc:
60
+ def initialize(*)
61
+ super
62
+ @last_size = nil
63
+ end
64
+
65
+ # :nodoc:
66
+ def load_object(xref_entry)
67
+ super.tap do |obj|
68
+ if xref_entry.type == :compressed
69
+ obj.data.size_in_object_stream = @last_size
70
+ elsif xref_entry.type == :in_use
71
+ obj.data.size = @last_size
72
+ end
73
+ @last_size = nil
74
+ end
75
+ end
76
+
77
+ # :nodoc:
78
+ def parse_indirect_object(offset = nil)
79
+ real_offset = (offset ? @header_offset + offset : @tokenizer.pos)
80
+ result = super
81
+ @last_size = @tokenizer.pos - real_offset
82
+ result
83
+ end
84
+
85
+ # :nodoc:
86
+ def load_compressed_object(xref_entry)
87
+ result = super
88
+ offsets = @object_stream_data[xref_entry.objstm].instance_variable_get(:@offsets)
89
+ @last_size = if xref_entry.pos == offsets.size - 1
90
+ @object_stream_data[xref_entry.objstm].instance_variable_get(:@tokenizer).
91
+ io.size - offsets[xref_entry.pos]
92
+ else
93
+ offsets[xref_entry.pos + 1] - offsets[xref_entry.pos]
94
+ end
95
+ result
96
+ end
97
+
98
+ end
99
+
100
+ def initialize #:nodoc:
101
+ super('usage', takes_commands: false)
102
+ short_desc("Show space usage of various parts of a PDF file")
103
+ long_desc(<<~EOF)
104
+ This command displays some usage statistics of the PDF file, i.e. which parts take which
105
+ approximate space in the file.
106
+
107
+ Each statistic line shows the space used followed by the number of indirect objects in
108
+ parentheses. If some of those objects are in object streams, that number is displayed
109
+ after a slash.
110
+ EOF
111
+
112
+ options.on("--password PASSWORD", "-p", String,
113
+ "The password for decryption. Use - for reading from standard input.") do |pwd|
114
+ @password = (pwd == '-' ? read_password : pwd)
115
+ end
116
+
117
+ @password = nil
118
+ end
119
+
120
+ def execute(file) #:nodoc:
121
+ HexaPDF::Parser.prepend(ParserExtension)
122
+ HexaPDF::PDFData.prepend(PDFDataExtension)
123
+
124
+ with_document(file, password: @password) do |doc|
125
+ # Prepare cache of outline items
126
+ outline_item_cache = {}
127
+ if doc.catalog.key?(:Outlines)
128
+ doc.outline.each_item {|item| outline_item_cache[item] = true }
129
+ outline_item_cache[doc.outline] = true
130
+ end
131
+
132
+ doc.revisions.each.with_index do |rev, index|
133
+ sum = count = 0
134
+ categories = {
135
+ Content: [],
136
+ Files: [],
137
+ Fonts: [],
138
+ Images: [],
139
+ Metadata: [],
140
+ ObjectStreams: [],
141
+ Outline: [],
142
+ XObjects: [],
143
+ }
144
+ puts if index > 0
145
+ puts "Usage information for revision #{index + 1}" if doc.revisions.count > 1
146
+ rev.each do |obj|
147
+ if command_parser.verbosity_info?
148
+ print "(#{obj.oid},#{obj.gen}): #{obj.data.size.to_i}"
149
+ print " (#{obj.data.size_in_object_stream})" if obj.data.size.nil?
150
+ puts
151
+ end
152
+ next unless obj.kind_of?(HexaPDF::Dictionary)
153
+
154
+ case obj.type
155
+ when :Page
156
+ Array(obj[:Contents]).each do |content|
157
+ categories[:Content] << content if object_in_rev?(content, rev)
158
+ end
159
+ when :Font
160
+ categories[:Fonts] << obj
161
+ when :FontDescriptor
162
+ categories[:Fonts] << obj
163
+ [:FontFile, :FontFile2, :FontFile3].each do |name|
164
+ categories[:Fonts] << obj[name] if object_in_rev?(obj[name], rev)
165
+ end
166
+ when :Metadata
167
+ categories[:Metadata] << obj
168
+ when :Filespec
169
+ categories[:Files] << obj
170
+ categories[:Files] << obj.embedded_file_stream if obj.embedded_file?
171
+ when :ObjStm
172
+ categories[:ObjectStreams] << obj
173
+ else
174
+ if obj[:Subtype] == :Image
175
+ categories[:Images] << obj
176
+ elsif obj[:Subtype] == :Form
177
+ categories[:XObjects] << obj
178
+ end
179
+ end
180
+ sum += obj.data.size if obj.data.size
181
+ count += 1
182
+ end
183
+
184
+ # Populate Outline category
185
+ outline_item_cache.reject! do |obj, _val|
186
+ object_in_rev?(obj, rev) && categories[:Outline] << obj
187
+ end
188
+
189
+ categories.each do |name, data|
190
+ next if data.empty?
191
+ object_stream_count = 0
192
+ category_sum = data.sum do |o|
193
+ object_stream_count += 1 unless o.data.size
194
+ o.data.size.to_i
195
+ end
196
+ object_stream_count = object_stream_count > 0 ? "/#{object_stream_count}" : ''
197
+ size = human_readable_file_size(category_sum)
198
+ puts "#{name.to_s.ljust(15)} #{size.rjust(8)} (#{data.count}#{object_stream_count})"
199
+ end
200
+ puts "#{'Total'.ljust(15)} #{human_readable_file_size(sum).rjust(8)} (#{count})"
201
+ end
202
+ end
203
+ end
204
+
205
+ private
206
+
207
+ # Returns +true+ if the +obj+ is in the given +rev+.
208
+ def object_in_rev?(obj, rev)
209
+ obj && rev.object(obj) == obj
210
+ end
211
+
212
+ end
213
+
214
+ end
215
+ end
data/lib/hexapdf/cli.rb CHANGED
@@ -48,6 +48,7 @@ require 'hexapdf/cli/watermark'
48
48
  require 'hexapdf/cli/image2pdf'
49
49
  require 'hexapdf/cli/form'
50
50
  require 'hexapdf/cli/fonts'
51
+ require 'hexapdf/cli/usage'
51
52
  require 'hexapdf/version'
52
53
  require 'hexapdf/document'
53
54
 
@@ -107,6 +108,7 @@ module HexaPDF
107
108
  add_command(HexaPDF::CLI::Image2PDF.new)
108
109
  add_command(HexaPDF::CLI::Form.new)
109
110
  add_command(HexaPDF::CLI::Fonts.new)
111
+ add_command(HexaPDF::CLI::Usage.new)
110
112
  add_command(CmdParse::HelpCommand.new)
111
113
  version_command = CmdParse::VersionCommand.new(add_switches: false)
112
114
  add_command(version_command)
@@ -481,7 +481,7 @@ module HexaPDF
481
481
  'acro_form.fallback_font' => 'Helvetica',
482
482
  'acro_form.on_invalid_value' => proc do |field, value|
483
483
  raise HexaPDF::Error, "Invalid value #{value.inspect} for " \
484
- "#{field.concrete_field_type} field #{field.full_field_name}"
484
+ "#{field.concrete_field_type} field named '#{field.full_field_name}'"
485
485
  end,
486
486
  'acro_form.text_field.default_width' => 100,
487
487
  'debug' => false,
@@ -165,14 +165,16 @@ module HexaPDF
165
165
  # nothing is stored for them (e.g a no-op).
166
166
  #
167
167
  # Check boxes:: Provide +nil+ or +false+ as value to toggle all check box widgets off. If
168
- # there is only one possible value, +true+ may be used for checking the box,
169
- # i.e. toggling it to the on state. Otherwise provide the value (a Symbol or
170
- # an object responding to +#to_sym+) of the check box widget that should be
171
- # toggled on.
168
+ # +true+ is provided, all check box widgets with the same name as the first
169
+ # one are toggled on. Otherwise provide the value (a Symbol or an object
170
+ # responding to +#to_sym+) of the check box widget that should be toggled on.
172
171
  #
173
172
  # Radio buttons:: To turn all radio buttons off, provide +nil+ as value. Otherwise provide
174
173
  # the value (a Symbol or an object responding to +#to_sym+) of a radio
175
174
  # button that should be turned on.
175
+ #
176
+ # Note that in most cases the field needs to already have widgets because the value is
177
+ # checked against the possibly allowed values which depend on the existing widgets.
176
178
  def field_value=(value)
177
179
  normalized_field_value_set(:V, value)
178
180
  end
@@ -303,7 +305,7 @@ module HexaPDF
303
305
  elsif check_box?
304
306
  if value == false
305
307
  :Off
306
- elsif value == true && av.size == 1
308
+ elsif value == true && av.size >= 1
307
309
  av[0]
308
310
  elsif av.include?(value.to_sym)
309
311
  value.to_sym
@@ -272,6 +272,9 @@ module HexaPDF
272
272
  #
273
273
  # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
274
274
  # fields must already exist. If it doesn't contain dots, a top-level field is created.
275
+ #
276
+ # Before a field value other than +false+ can be assigned to the check box, a widget needs
277
+ # to be created.
275
278
  def create_check_box(name)
276
279
  create_field(name, :Btn, &:initialize_as_check_box)
277
280
  end
@@ -280,6 +283,9 @@ module HexaPDF
280
283
  #
281
284
  # The +name+ may contain dots to signify a field hierarchy. If so, the referenced parent
282
285
  # fields must already exist. If it doesn't contain dots, a top-level field is created.
286
+ #
287
+ # Before a field value other than +nil+ can be assigned to the radio button, at least one
288
+ # widget needs to be created.
283
289
  def create_radio_button(name)
284
290
  create_field(name, :Btn, &:initialize_as_radio_button)
285
291
  end
@@ -345,6 +351,45 @@ module HexaPDF
345
351
  create_field(name, :Sig) {}
346
352
  end
347
353
 
354
+ # Fills form fields with the values from the given +data+ hash.
355
+ #
356
+ # The keys of the +data+ hash need to be full field names and the values are the respective
357
+ # values, usually in string form. It is possible to specify only some of the fields of the
358
+ # form.
359
+ #
360
+ # What kind of values are supported for a field depends on the field type:
361
+ #
362
+ # * For fields containing text (single/multiline/comb text fields, file select fields, combo
363
+ # boxes and list boxes) the value needs to be a string and it is assigned as is.
364
+ #
365
+ # * For check boxes, the values "y"/"yes"/"t"/"true" are handled as assigning +true+ to the
366
+ # field, the values "n"/"no"/"f"/"false" are handled as assigning +false+ to the field,
367
+ # and every other string value is assigned as is. See ButtonField#field_value= for
368
+ # details.
369
+ #
370
+ # * For radio buttons the value needs to be a String or a Symbol representing the name of
371
+ # the radio button widget to select.
372
+ def fill(data)
373
+ data.each do |field_name, value|
374
+ field = field_by_name(field_name)
375
+ raise HexaPDF::Error, "AcroForm field named '#{field_name}' not found" unless field
376
+
377
+ case field.concrete_field_type
378
+ when :single_line_text_field, :multiline_text_field, :comb_text_field, :file_select_field,
379
+ :combo_box, :list_box, :editable_combo_box, :radio_button
380
+ field.field_value = value
381
+ when :check_box
382
+ field.field_value = case value
383
+ when /\A(?:y(es)?|t(rue)?)\z/ then true
384
+ when /\A(?:n(o)?|f(alse)?)\z/ then false
385
+ else value
386
+ end
387
+ else
388
+ raise HexaPDF::Error, "AcroForm field type #{field.concrete_field_type} not yet supported"
389
+ end
390
+ end
391
+ end
392
+
348
393
  # Returns the dictionary containing the default resources for form field appearance streams.
349
394
  def default_resources
350
395
  self[:DR] ||= document.wrap({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]},
@@ -35,6 +35,7 @@
35
35
  #++
36
36
 
37
37
  require 'json'
38
+ require 'time'
38
39
  require 'hexapdf/error'
39
40
  require 'hexapdf/layout/style'
40
41
  require 'hexapdf/layout/text_fragment'
@@ -56,6 +57,7 @@ module HexaPDF
56
57
  # JavaScript actions are:
57
58
  #
58
59
  # * +AFNumber_Format+: See #af_number_format_action and #apply_af_number_format
60
+ # * +AFPercent_Format+: See #af_percent_format_action and #apply_af_percent_format
59
61
  #
60
62
  # Calculating a field's value::
61
63
  #
@@ -184,6 +186,10 @@ module HexaPDF
184
186
  return [value, nil] unless (action_string = action_string(format_action))
185
187
  if action_string.start_with?('AFNumber_Format(')
186
188
  apply_af_number_format(value, action_string)
189
+ elsif action_string.start_with?('AFPercent_Format(')
190
+ apply_af_percent_format(value, action_string)
191
+ elsif action_string.start_with?('AFTime_Format(')
192
+ apply_af_time_format(value, action_string)
187
193
  else
188
194
  [value, nil]
189
195
  end
@@ -235,8 +241,15 @@ module HexaPDF
235
241
  # See: #apply_af_number_format
236
242
  def af_number_format_action(decimals: 2, separator_style: :point, negative_style: :minus_black,
237
243
  currency_string: "", prepend_currency: true)
238
- "AFNumber_Format(#{decimals}, #{AF_NUMBER_FORMAT_MAPPINGS[:separator][separator_style]}, " \
239
- "#{AF_NUMBER_FORMAT_MAPPINGS[:negative][negative_style]}, 0, \"#{currency_string}\", " \
244
+ separator_style = AF_NUMBER_FORMAT_MAPPINGS[:separator].fetch(separator_style) do
245
+ raise ArgumentError, "Unsupported value for separator_style argument: #{separator_style}"
246
+ end
247
+ negative_style = AF_NUMBER_FORMAT_MAPPINGS[:negative].fetch(negative_style) do
248
+ raise ArgumentError, "Unsupported value for negative_style argument: #{negative_style}"
249
+ end
250
+
251
+ "AFNumber_Format(#{decimals}, #{separator_style}, " \
252
+ "#{negative_style}, 0, \"#{currency_string}\", " \
240
253
  "#{prepend_currency});"
241
254
  end
242
255
 
@@ -323,21 +336,141 @@ module HexaPDF
323
336
  end
324
337
  end
325
338
 
326
- result = sprintf(format, value)
339
+ [af_format_number(value, format, match[:sep_style]), text_color]
340
+ end
327
341
 
328
- before_decimal_point, after_decimal_point = result.split('.')
329
- if match[:sep_style] == '0' || match[:sep_style] == '2'
330
- separator = (match[:sep_style] == '0' ? ',' : '.')
331
- before_decimal_point.gsub!(/\B(?=(\d\d\d)+(?:[^\d]|\z))/, separator)
342
+ # Returns the appropriate JavaScript action string for the AFPercent_Format function.
343
+ #
344
+ # +decimals+::
345
+ # The number of decimal digits to use. Default 2.
346
+ #
347
+ # +separator_style+::
348
+ # Specifies the character for the decimal and thousands separator, one of:
349
+ #
350
+ # :point:: (Default) Use point as decimal separator and comma as thousands separator.
351
+ # :point_no_thousands:: Use point as decimal separator and no thousands separator.
352
+ # :comma:: Use comma as decimal separator and point as thousands separator.
353
+ # :comma_no_thousands:: Use comma as decimal separator and no thousands separator.
354
+ #
355
+ # See: #apply_af_percent_format
356
+ def af_percent_format_action(decimals: 2, separator_style: :point)
357
+ separator_style = AF_NUMBER_FORMAT_MAPPINGS[:separator].fetch(separator_style) do
358
+ raise ArgumentError, "Unsupported value for separator_style argument: #{separator_style}"
332
359
  end
333
- result = if after_decimal_point
334
- decimal_point = (match[:sep_style] =~ /[01]/ ? '.' : ',')
335
- "#{before_decimal_point}#{decimal_point}#{after_decimal_point}"
336
- else
337
- before_decimal_point
338
- end
360
+ "AFPercent_Format(#{decimals}, #{separator_style});"
361
+ end
362
+
363
+ # Regular expression for matching the AFPercent_Format method.
364
+ #
365
+ # See: #apply_af_percent_format
366
+ AF_PERCENT_FORMAT_RE = /
367
+ \AAFPercent_Format\(
368
+ \s*(?<ndec>\d+)\s*,
369
+ \s*(?<sep_style>[0-3])\s*
370
+ \);?\z
371
+ /x
372
+
373
+ # Implements the JavaScript AFPercent_Format function and returns the formatted field value.
374
+ #
375
+ # The argument +value+ has to be the field's value (a String) and +action_string+ has to be
376
+ # the JavaScript action string.
377
+ #
378
+ # The AFPercent_Format function assumes that the text field's value contains a number (as a
379
+ # string) and formats it according to the instructions.
380
+ #
381
+ # It has the form <tt>AFPercent_Format(no_of_decimals, separator_style)</tt> where the
382
+ # arguments have the following meaning:
383
+ #
384
+ # +no_of_decimals+::
385
+ # The number of decimal places after the decimal point, e.g. for 3 it would result in
386
+ # 123.456.
387
+ #
388
+ # +separator_style+::
389
+ # Defines which decimal separator and whether a thousands separator should be used.
390
+ #
391
+ # Possible values are:
392
+ #
393
+ # +0+:: Comma for thousands separator, point for decimal separator: 12,345.67
394
+ # +1+:: No thousands separator, point for decimal separator: 12345.67
395
+ # +2+:: Point for thousands separator, comma for decimal separator: 12.345,67
396
+ # +3+:: No thousands separator, comma for decimal separator: 12345,67
397
+ def apply_af_percent_format(value, action_string)
398
+ return value unless (match = AF_PERCENT_FORMAT_RE.match(action_string))
399
+ af_format_number(af_make_number(value) * 100, "%.#{match[:ndec]}f%%", match[:sep_style])
400
+ end
339
401
 
340
- [result, text_color]
402
+ AF_TIME_FORMAT_MAPPINGS = { #:nodoc:
403
+ format_integers: {
404
+ hh_mm: 0,
405
+ 0 => 0,
406
+ hh12_mm: 1,
407
+ 1 => 1,
408
+ hh_mm_ss: 2,
409
+ 2 => 2,
410
+ hh12_mm_ss: 3,
411
+ 3 => 3,
412
+ },
413
+ strftime_format: {
414
+ '0' => '%H:%M',
415
+ '1' => '%l:%M %p',
416
+ '2' => '%H:%M:%S',
417
+ '3' => '%l:%M:%S %p',
418
+ },
419
+ }
420
+
421
+ # Returns the appropriate JavaScript action string for the AFTime_Format function.
422
+ #
423
+ # +format+::
424
+ # Specifies the time format, one of:
425
+ #
426
+ # :hh_mm:: (Default) Use 24h time format %H:%M (e.g. 15:25)
427
+ # :hh12_mm:: (Default) Use 12h time format %l:%M %p (e.g. 3:25 PM)
428
+ # :hh_mm_ss:: Use 24h time format with seconds %H:%M:%S (e.g. 15:25:37)
429
+ # :hh12_mm_ss:: Use 24h time format with seconds %l:%M:%S %p (e.g. 3:25:37 PM)
430
+ #
431
+ # See: #apply_af_time_format
432
+ def af_time_format_action(format: :hh_mm)
433
+ format = AF_TIME_FORMAT_MAPPINGS[:format_integers].fetch(format) do
434
+ raise ArgumentError, "Unsupported value for time_format argument: #{format}"
435
+ end
436
+ "AFTime_Format(#{format});"
437
+ end
438
+
439
+ # Regular expression for matching the AFTime_Format method.
440
+ #
441
+ # See: #apply_af_time_format
442
+ AF_TIME_FORMAT_RE = /
443
+ \AAFTime_Format\(
444
+ \s*(?<time_format>[0-3])\s*
445
+ \);?\z
446
+ /x
447
+
448
+ # Implements the JavaScript AFTime_Format function and returns the formatted field value.
449
+ #
450
+ # The argument +value+ has to be the field's value (a String) and +action_string+ has to be
451
+ # the JavaScript action string.
452
+ #
453
+ # The AFTime_Format function assumes that the text field's value contains a valid time
454
+ # string (for HexaPDF that is anything Time.parse can work with) and formats it according to
455
+ # the instructions.
456
+ #
457
+ # It has the form <tt>AFTime_Format(time_format)</tt> where the argument has the following
458
+ # meaning:
459
+ #
460
+ # +time_format+::
461
+ # Defines the time format which should be applied.
462
+ #
463
+ # Possible values are:
464
+ #
465
+ # +0+:: Use 24h time format, e.g. 15:25
466
+ # +1+:: Use 12h time format, e.g. 3:25 PM
467
+ # +2+:: Use 24h time format with seconds, e.g. 15:25:37
468
+ # +3+:: Use 12h time format with seconds, e.g. 3:25:37 PM
469
+ def apply_af_time_format(value, action_string)
470
+ return value unless (match = AF_TIME_FORMAT_RE.match(action_string))
471
+ value = Time.parse(value) rescue nil
472
+ return "" unless value
473
+ value.strftime(AF_TIME_FORMAT_MAPPINGS[:strftime_format][match[:time_format]]).strip
341
474
  end
342
475
 
343
476
  # Handles JavaScript calculate actions for single-line text fields.
@@ -483,6 +616,24 @@ module HexaPDF
483
616
  value.to_s.tr(',', '.').to_f
484
617
  end
485
618
 
619
+ # Formats the numeric value according to the format string and separator style.
620
+ def af_format_number(value, format, sep_style)
621
+ result = sprintf(format, value)
622
+
623
+ before_decimal_point, after_decimal_point = result.split('.')
624
+ if sep_style == '0' || sep_style == '2'
625
+ separator = (sep_style == '0' ? ',' : '.')
626
+ before_decimal_point.gsub!(/\B(?=(\d\d\d)+(?:[^\d]|\z))/, separator)
627
+ end
628
+
629
+ if after_decimal_point
630
+ decimal_point = (sep_style <= "1" ? '.' : ',')
631
+ "#{before_decimal_point}#{decimal_point}#{after_decimal_point}"
632
+ else
633
+ before_decimal_point
634
+ end
635
+ end
636
+
486
637
  # Returns the JavaScript action string for the given action.
487
638
  def action_string(action)
488
639
  return nil unless action && action[:S] == :JavaScript
@@ -170,7 +170,7 @@ module HexaPDF
170
170
  elsif comb_text_field? && !key?(:MaxLen)
171
171
  raise HexaPDF::Error, "A comb text field need a valid /MaxLen value"
172
172
  elsif str && !str.kind_of?(String)
173
- @document.config['acro_form.on_invalid_value'].call(self, str)
173
+ str = @document.config['acro_form.on_invalid_value'].call(self, str)
174
174
  end
175
175
  str = str.gsub(/[[:space:]]/, ' ') if str && concrete_field_type == :single_line_text_field
176
176
  if key?(:MaxLen) && str && str.length > self[:MaxLen]
@@ -254,9 +254,21 @@ module HexaPDF
254
254
  # :number::
255
255
  # Assumes that the field value is a number and formats it according to the given
256
256
  # arguments. See JavaScriptActions.af_number_format_action for details on the arguments.
257
+ #
258
+ # :percent::
259
+ # Assumes that the field value is a number and formats it as percentage (where 1=100%
260
+ # and 0=0%). See JavaScriptActions.af_percent_format_action for details on the
261
+ # arguments.
262
+ #
263
+ # :time::
264
+ # Assumes that the field value is a string with a time value and formats it according to
265
+ # the given argument. See JavaScriptActions.af_time_format_action for details on the
266
+ # arguments.
257
267
  def set_format_action(type, **arguments)
258
268
  action_string = case type
259
269
  when :number then JavaScriptActions.af_number_format_action(**arguments)
270
+ when :percent then JavaScriptActions.af_percent_format_action(**arguments)
271
+ when :time then JavaScriptActions.af_time_format_action(**arguments)
260
272
  else
261
273
  raise ArgumentError, "Invalid value for type argument: #{type.inspect}"
262
274
  end
@@ -143,7 +143,8 @@ module HexaPDF
143
143
  #
144
144
  # If the dictionary is not found, an error is raised.
145
145
  def font(name)
146
- object_getter(:Font, name)
146
+ font = object_getter(:Font, name)
147
+ font.kind_of?(Hash) ? document.wrap(font) : font
147
148
  end
148
149
 
149
150
  # Adds the font dictionary to the resources and returns the name under which it is stored.
data/lib/hexapdf/utils.rb CHANGED
@@ -39,8 +39,27 @@ require 'geom2d/utils'
39
39
  module HexaPDF
40
40
 
41
41
  # This module contains helper methods for the whole library.
42
+ #
43
+ # Furthermore, it refines Numeric to provide #mm, #cm, and #inch methods.
42
44
  module Utils
43
45
 
46
+ refine Numeric do
47
+ # Intrepeting self as millimeters returns the equivalent number of points.
48
+ def mm
49
+ self * 72 / 25.4
50
+ end
51
+
52
+ # Intrepeting self as centimeters returns the equivalent number of points.
53
+ def cm
54
+ self * 72 / 2.54
55
+ end
56
+
57
+ # Intrepeting self as inches returns the equivalent number of points.
58
+ def inch
59
+ self * 72
60
+ end
61
+ end
62
+
44
63
  # The precision with which to compare floating point numbers.
45
64
  #
46
65
  # This is chosen with respect to precision that is used for serializing floating point numbers.
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.41.0'
40
+ VERSION = '0.42.0'
41
41
 
42
42
  end
@@ -6,6 +6,22 @@ require 'hexapdf/utils'
6
6
  describe HexaPDF::Utils do
7
7
  include HexaPDF::Utils
8
8
 
9
+ describe "Numeric refinement" do
10
+ using HexaPDF::Utils
11
+
12
+ it "converts mm to points" do
13
+ assert_equal(72, 25.4.mm)
14
+ end
15
+
16
+ it "converts cm to points" do
17
+ assert_equal(72, 2.54.cm)
18
+ end
19
+
20
+ it "converts inch to points" do
21
+ assert_equal(144, 2.inch)
22
+ end
23
+ end
24
+
9
25
  it "checks floats for equality with a certain precision" do
10
26
  assert(float_equal(1.0, 1))
11
27
  assert(float_equal(1.0, 1.0000003))
@@ -93,6 +93,11 @@ describe HexaPDF::Type::AcroForm::ButtonField do
93
93
  @field.field_value = "check"
94
94
  assert_equal(:check, @field[:V])
95
95
  assert_raises(HexaPDF::Error) { @field.field_value = :unknown }
96
+
97
+ @field.field_value = :Off
98
+ @field.create_widget(@doc.pages[0], value: :other)
99
+ @field.field_value = true
100
+ assert_equal(:check, @field[:V])
96
101
  end
97
102
 
98
103
  it "returns the correct concrete field type" do
@@ -241,6 +241,53 @@ describe HexaPDF::Type::AcroForm::Form do
241
241
  end
242
242
  end
243
243
 
244
+ describe "fill" do
245
+ it "works for text field types" do
246
+ field = @acro_form.create_text_field('test')
247
+ @acro_form.fill("test" => "value")
248
+ assert_equal("value", field.field_value)
249
+ end
250
+
251
+ it "works for radio buttons" do
252
+ field = @acro_form.create_radio_button("test")
253
+ field.create_widget(@doc.pages.add, value: :name)
254
+ @acro_form.fill("test" => "name")
255
+ assert_equal(:name, field.field_value)
256
+ end
257
+
258
+ it "works for check boxes" do
259
+ field = @acro_form.create_check_box('test')
260
+ field.create_widget(@doc.pages.add)
261
+
262
+ ["t", "true", "y", "yes"].each do |value|
263
+ @acro_form.fill("test" => value)
264
+ assert_equal(:Yes, field.field_value)
265
+ field.field_value = :Off
266
+ end
267
+
268
+ ["f", "false", "n", "no"].each do |value|
269
+ @acro_form.fill("test" => value)
270
+ assert_nil(field.field_value)
271
+ field.field_value = :Yes
272
+ end
273
+
274
+ field.create_widget(@doc.pages.add, value: :Other)
275
+ @acro_form.fill("test" => "Other")
276
+ assert_equal(:Other, field.field_value)
277
+ end
278
+
279
+ it "raises an error if a field is not found" do
280
+ error = assert_raises(HexaPDF::Error) { @acro_form.fill("unknown" => "test") }
281
+ assert_match(/named 'unknown' not found/, error.message)
282
+ end
283
+
284
+ it "raises an error if a field type is not supported for filling in" do
285
+ @acro_form.create_check_box('test').initialize_as_push_button
286
+ error = assert_raises(HexaPDF::Error) { @acro_form.fill("test" => "test") }
287
+ assert_match(/push_button not yet supported/, error.message)
288
+ end
289
+ end
290
+
244
291
  it "returns the default resources" do
245
292
  assert_kind_of(HexaPDF::Type::Resources, @acro_form.default_resources)
246
293
  end
@@ -31,6 +31,11 @@ describe HexaPDF::Type::AcroForm::JavaScriptActions do
31
31
  prepend_currency: false))
32
32
  end
33
33
 
34
+ it "raise an error for invalid arguments" do
35
+ assert_raises(ArgumentError) { @klass.af_number_format_action(separator_style: :unknown) }
36
+ assert_raises(ArgumentError) { @klass.af_number_format_action(negative_style: :unknown) }
37
+ end
38
+
34
39
  def assert_format(arg_string, result_value, result_color)
35
40
  @action[:JS] = "AFNumber_Format(#{arg_string});"
36
41
  value, text_color = @klass.apply_format(@value, @action)
@@ -77,10 +82,106 @@ describe HexaPDF::Type::AcroForm::JavaScriptActions do
77
82
  assert_equal('1.234,57', value)
78
83
  end
79
84
 
80
- it "does nothing to the value if the JavasSript method could not be determined " do
85
+ it "does nothing to the value if the JavaScript method could not be determined " do
81
86
  assert_format('2, 3, 0, 0, " E", false, a', "1234567.898765", nil)
82
87
  end
83
88
  end
89
+
90
+ describe "AFPercent_Format" do
91
+ before do
92
+ @value = '123.456789'
93
+ @action[:JS] = ''
94
+ end
95
+
96
+ it "returns a correct JavaScript string" do
97
+ assert_equal('AFPercent_Format(2, 0);',
98
+ @klass.af_percent_format_action)
99
+ assert_equal('AFPercent_Format(1, 1);',
100
+ @klass.af_percent_format_action(decimals: 1, separator_style: :point_no_thousands))
101
+ end
102
+
103
+ it "raise an error for invalid arguments" do
104
+ assert_raises(ArgumentError) { @klass.af_percent_format_action(separator_style: :unknown) }
105
+ end
106
+
107
+ def assert_format(arg_string, result_value)
108
+ @action[:JS] = "AFPercent_Format(#{arg_string});"
109
+ value, text_color = @klass.apply_format(@value, @action)
110
+ assert_equal(result_value, value)
111
+ assert_nil(text_color)
112
+ end
113
+
114
+ it "works with both commas and points as decimal separator" do
115
+ @value = '123.456789'
116
+ assert_format('2, 2', "12.345,68%")
117
+ @value = '123,456789'
118
+ assert_format('2, 2', "12.345,68%")
119
+ @value = '123,4567,89'
120
+ assert_format('2, 2', "12.345,67%")
121
+ end
122
+
123
+ it "respects the set number of decimals" do
124
+ assert_format('0, 2', "12.346%")
125
+ assert_format('2, 2', "12.345,68%")
126
+ end
127
+
128
+ it "respects the digit separator style" do
129
+ ["12,345.68%", "12345.68%", "12.345,68%", "12345,68%"].each_with_index do |result, style|
130
+ assert_format("2, #{style}", result)
131
+ end
132
+ end
133
+
134
+ it "allows omitting the trailing semicolon" do
135
+ @action[:JS] = "AFPercent_Format(2,2 )"
136
+ value, = @klass.apply_format('1.234', @action)
137
+ assert_equal('123,40%', value)
138
+ end
139
+
140
+ it "does nothing to the value if the JavaScript method could not be determined " do
141
+ assert_format('2, "df"', "123.456789")
142
+ end
143
+ end
144
+
145
+ describe "AFTime_Format" do
146
+ before do
147
+ @value = '15:25:37'
148
+ @action[:JS] = ''
149
+ end
150
+
151
+ it "returns a correct JavaScript string" do
152
+ assert_equal('AFTime_Format(0);',
153
+ @klass.af_time_format_action)
154
+ assert_equal('AFTime_Format(1);',
155
+ @klass.af_time_format_action(format: :hh12_mm))
156
+ end
157
+
158
+ it "raise an error for invalid arguments" do
159
+ assert_raises(ArgumentError) { @klass.af_time_format_action(format: :unknown) }
160
+ end
161
+
162
+ def assert_format(arg_string, result_value)
163
+ @action[:JS] = "AFTime_Format(#{arg_string});"
164
+ value, text_color = @klass.apply_format(@value, @action)
165
+ assert_equal(result_value, value)
166
+ assert_nil(text_color)
167
+ end
168
+
169
+ it "respects the time format" do
170
+ ["15:25", "3:25 PM", "15:25:37", "3:25:37 PM"].each_with_index do |result, style|
171
+ assert_format(style, result)
172
+ end
173
+ end
174
+
175
+ it "allows omitting the trailing semicolon" do
176
+ @action[:JS] = "AFTime_Format(2 )"
177
+ value, = @klass.apply_format('15:34', @action)
178
+ assert_equal('15:34:00', value)
179
+ end
180
+
181
+ it "does nothing to the value if the JavaScript method could not be determined " do
182
+ assert_format('1, "df"', "15:25:37")
183
+ end
184
+ end
84
185
  end
85
186
 
86
187
  describe "calculate" do
@@ -114,6 +114,12 @@ describe HexaPDF::Type::AcroForm::TextField do
114
114
  assert(widget[:AP][:N])
115
115
  end
116
116
 
117
+ it "calls acro_form.on_invalid_value if the provided value is not a string" do
118
+ @doc.config['acro_form.on_invalid_value'] = proc {|_field, value| value.to_s }
119
+ @field.field_value = 10
120
+ assert_equal("10", @field.field_value)
121
+ end
122
+
117
123
  it "fails if the :password flag is set" do
118
124
  @field.flag(:password)
119
125
  assert_raises(HexaPDF::Error) { @field.field_value = 'test' }
@@ -124,10 +130,6 @@ describe HexaPDF::Type::AcroForm::TextField do
124
130
  assert_raises(HexaPDF::Error) { @field.field_value = 'test' }
125
131
  end
126
132
 
127
- it "fails if the provided value is not a string" do
128
- assert_raises(HexaPDF::Error) { @field.field_value = 10 }
129
- end
130
-
131
133
  it "fails if the value exceeds the length set by /MaxLen" do
132
134
  @field[:MaxLen] = 5
133
135
  assert_raises(HexaPDF::Error) { @field.field_value = 'testdf' }
@@ -210,6 +212,22 @@ describe HexaPDF::Type::AcroForm::TextField do
210
212
  assert_equal('AFNumber_Format(0, 0, 0, 0, "", true);', @field[:AA][:F][:JS])
211
213
  end
212
214
 
215
+ it "applies the percent format" do
216
+ @doc.acro_form(create: true)
217
+ @field.set_format_action(:percent, decimals: 0)
218
+ assert(@field.key?(:AA))
219
+ assert(@field[:AA].key?(:F))
220
+ assert_equal('AFPercent_Format(0, 0);', @field[:AA][:F][:JS])
221
+ end
222
+
223
+ it "applies the time format" do
224
+ @doc.acro_form(create: true)
225
+ @field.set_format_action(:time, format: :hh_mm_ss)
226
+ assert(@field.key?(:AA))
227
+ assert(@field[:AA].key?(:F))
228
+ assert_equal('AFTime_Format(2);', @field[:AA][:F][:JS])
229
+ end
230
+
213
231
  it "fails if an unknown format action is specified" do
214
232
  assert_raises(ArgumentError) { @field.set_format_action(:unknown) }
215
233
  end
@@ -146,6 +146,11 @@ describe HexaPDF::Type::Resources do
146
146
  @res.font(:test)
147
147
  end
148
148
  end
149
+
150
+ it "wraps the resulting object in the appropriate font class" do
151
+ @res[:Font] = {test: {Type: :Font, Subtype: :Type1}}
152
+ assert_kind_of(HexaPDF::Type::FontType1, @res.font(:test))
153
+ end
149
154
  end
150
155
 
151
156
  describe "add_font" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.41.0
4
+ version: 0.42.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-05 00:00:00.000000000 Z
11
+ date: 2024-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse
@@ -325,6 +325,7 @@ files:
325
325
  - lib/hexapdf/cli/modify.rb
326
326
  - lib/hexapdf/cli/optimize.rb
327
327
  - lib/hexapdf/cli/split.rb
328
+ - lib/hexapdf/cli/usage.rb
328
329
  - lib/hexapdf/cli/watermark.rb
329
330
  - lib/hexapdf/composer.rb
330
331
  - lib/hexapdf/configuration.rb