hexapdf 0.41.0 → 0.42.0

Sign up to get free protection for your applications and to get access to all the features.
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