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 +4 -4
- data/CHANGELOG.md +26 -0
- data/examples/031-acro_form_java_script.rb +36 -24
- data/lib/hexapdf/cli/command.rb +14 -11
- data/lib/hexapdf/cli/files.rb +31 -7
- data/lib/hexapdf/cli/form.rb +10 -31
- data/lib/hexapdf/cli/inspect.rb +1 -1
- data/lib/hexapdf/cli/usage.rb +215 -0
- data/lib/hexapdf/cli.rb +2 -0
- data/lib/hexapdf/configuration.rb +1 -1
- data/lib/hexapdf/type/acro_form/button_field.rb +7 -5
- data/lib/hexapdf/type/acro_form/form.rb +45 -0
- data/lib/hexapdf/type/acro_form/java_script_actions.rb +165 -14
- data/lib/hexapdf/type/acro_form/text_field.rb +13 -1
- data/lib/hexapdf/type/resources.rb +2 -1
- data/lib/hexapdf/utils.rb +19 -0
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/test_utils.rb +16 -0
- data/test/hexapdf/type/acro_form/test_button_field.rb +5 -0
- data/test/hexapdf/type/acro_form/test_form.rb +47 -0
- data/test/hexapdf/type/acro_form/test_java_script_actions.rb +102 -1
- data/test/hexapdf/type/acro_form/test_text_field.rb +22 -4
- data/test/hexapdf/type/test_resources.rb +5 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dd69db2c04dc802784438f9038c9a6ab32dac0806edca43b6c59406bba9d7ea6
|
4
|
+
data.tar.gz: a51a510a6a98b547b2b11fb07804ca9039a666adf95b52f65db9b070c3ffe9c3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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("
|
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("
|
37
|
-
|
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,
|
52
|
+
widget = tx.create_widget(page, Rect: [220, 475, 280, 495])
|
41
53
|
tx.field_value = "10,50"
|
42
|
-
canvas.text("b:", at: [310,
|
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,
|
57
|
+
widget = tx.create_widget(page, Rect: [330, 475, 390, 495])
|
46
58
|
tx.field_value = "20,60"
|
47
|
-
canvas.text("c:", at: [420,
|
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,
|
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,
|
54
|
-
canvas.text("Sum", at: [90,
|
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,
|
60
|
-
canvas.text("Average", at: [90,
|
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,
|
66
|
-
canvas.text("Product", at: [90,
|
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,
|
72
|
-
canvas.text("Minimum", at: [90,
|
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,
|
78
|
-
canvas.text("Maximum", at: [90,
|
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,
|
95
|
+
widget = tx.create_widget(page, Rect: [310, 295, 500, 315])
|
84
96
|
|
85
|
-
canvas.text("Simplified Field Notation", at: [70,
|
86
|
-
canvas.text("a + b + c", at: [90,
|
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,
|
92
|
-
canvas.text("(a + b)*(c - a) / b + 3.14", at: [90,
|
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,
|
109
|
+
widget = tx.create_widget(page, Rect: [310, 205, 500, 225])
|
98
110
|
|
99
111
|
form.recalculate_fields
|
100
112
|
|
data/lib/hexapdf/cli/command.rb
CHANGED
@@ -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-
|
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
|
171
|
-
# HexaPDF::CLI#force is
|
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
|
-
|
175
|
-
|
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
|
-
|
413
|
-
|
414
|
-
|
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
|
data/lib/hexapdf/cli/files.rb
CHANGED
@@ -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
|
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
|
52
|
-
indices. The --extract option can then be
|
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
|
-
|
76
|
-
|
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)
|
data/lib/hexapdf/cli/form.rb
CHANGED
@@ -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
|
-
|
228
|
+
form.fill(field.full_field_name => value)
|
228
229
|
rescue HexaPDF::Error => e
|
229
|
-
puts " ⚠
|
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
|
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
|
245
|
-
|
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
|
data/lib/hexapdf/cli/inspect.rb
CHANGED
@@ -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
|
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
|
-
#
|
169
|
-
#
|
170
|
-
#
|
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
|
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
|
-
|
239
|
-
|
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
|
-
|
339
|
+
[af_format_number(value, format, match[:sep_style]), text_color]
|
340
|
+
end
|
327
341
|
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
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
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
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
|
-
|
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.
|
data/lib/hexapdf/version.rb
CHANGED
data/test/hexapdf/test_utils.rb
CHANGED
@@ -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
|
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.
|
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-
|
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
|