hexapdf 0.39.1 → 0.41.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -0
- data/examples/019-acro_form.rb +12 -23
- data/examples/027-composer_optional_content.rb +1 -1
- data/examples/030-pdfa.rb +6 -6
- data/examples/031-acro_form_java_script.rb +101 -0
- data/lib/hexapdf/cli/command.rb +11 -0
- data/lib/hexapdf/cli/form.rb +38 -9
- data/lib/hexapdf/cli/info.rb +4 -0
- data/lib/hexapdf/configuration.rb +10 -0
- data/lib/hexapdf/content/canvas.rb +2 -0
- data/lib/hexapdf/document/layout.rb +8 -1
- data/lib/hexapdf/encryption/aes.rb +13 -6
- data/lib/hexapdf/encryption/security_handler.rb +6 -4
- data/lib/hexapdf/font/cmap/parser.rb +1 -5
- data/lib/hexapdf/font/cmap.rb +22 -3
- data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
- data/lib/hexapdf/font_loader/variant_from_name.rb +72 -0
- data/lib/hexapdf/font_loader.rb +1 -0
- data/lib/hexapdf/layout/list_box.rb +40 -33
- data/lib/hexapdf/layout/style.rb +25 -23
- data/lib/hexapdf/layout/text_box.rb +3 -3
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +11 -76
- data/lib/hexapdf/type/acro_form/field.rb +14 -0
- data/lib/hexapdf/type/acro_form/form.rb +25 -8
- data/lib/hexapdf/type/acro_form/java_script_actions.rb +498 -0
- data/lib/hexapdf/type/acro_form/text_field.rb +78 -0
- data/lib/hexapdf/type/acro_form.rb +1 -0
- data/lib/hexapdf/type/annotations/widget.rb +1 -1
- data/lib/hexapdf/version.rb +1 -1
- data/test/hexapdf/encryption/test_aes.rb +18 -8
- data/test/hexapdf/encryption/test_security_handler.rb +17 -0
- data/test/hexapdf/encryption/test_standard_security_handler.rb +1 -1
- data/test/hexapdf/font/cmap/test_parser.rb +5 -3
- data/test/hexapdf/font/test_cmap.rb +8 -0
- data/test/hexapdf/font_loader/test_from_configuration.rb +4 -0
- data/test/hexapdf/font_loader/test_variant_from_name.rb +34 -0
- data/test/hexapdf/layout/test_list_box.rb +30 -1
- data/test/hexapdf/layout/test_style.rb +1 -1
- data/test/hexapdf/layout/test_text_box.rb +4 -4
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +31 -47
- data/test/hexapdf/type/acro_form/test_field.rb +11 -0
- data/test/hexapdf/type/acro_form/test_form.rb +33 -0
- data/test/hexapdf/type/acro_form/test_java_script_actions.rb +226 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +44 -0
- metadata +7 -2
data/lib/hexapdf/font/cmap.rb
CHANGED
@@ -100,8 +100,10 @@ module HexaPDF
|
|
100
100
|
# The writing mode of the CMap: 0 for horizontal, 1 for vertical writing.
|
101
101
|
attr_accessor :wmode
|
102
102
|
|
103
|
-
attr_reader :codespace_ranges, :cid_mapping, :cid_range_mappings, :unicode_mapping
|
104
|
-
|
103
|
+
attr_reader :codespace_ranges, :cid_mapping, :cid_range_mappings, :unicode_mapping,
|
104
|
+
:unicode_range_mappings # :nodoc:
|
105
|
+
protected :codespace_ranges, :cid_mapping, :cid_range_mappings, :unicode_mapping,
|
106
|
+
:unicode_range_mappings
|
105
107
|
|
106
108
|
# Creates a new CMap object.
|
107
109
|
def initialize
|
@@ -109,6 +111,7 @@ module HexaPDF
|
|
109
111
|
@cid_mapping = {}
|
110
112
|
@cid_range_mappings = []
|
111
113
|
@unicode_mapping = {}
|
114
|
+
@unicode_range_mappings = []
|
112
115
|
end
|
113
116
|
|
114
117
|
# Add all mappings from the given CMap to this CMap.
|
@@ -117,6 +120,7 @@ module HexaPDF
|
|
117
120
|
@cid_mapping.merge!(cmap.cid_mapping)
|
118
121
|
@cid_range_mappings.concat(cmap.cid_range_mappings)
|
119
122
|
@unicode_mapping.merge!(cmap.unicode_mapping)
|
123
|
+
@unicode_range_mappings.concat(cmap.unicode_range_mappings)
|
120
124
|
end
|
121
125
|
|
122
126
|
# Add a codespace range using an array of ranges for the individual bytes.
|
@@ -193,10 +197,25 @@ module HexaPDF
|
|
193
197
|
@unicode_mapping[code] = string
|
194
198
|
end
|
195
199
|
|
200
|
+
# Adds a mapping from a range of character codes to strings starting with the given 16-bit
|
201
|
+
# integer values (representing the raw UTF-16BE characters).
|
202
|
+
def add_unicode_range_mapping(start_code, end_code, start_values)
|
203
|
+
@unicode_range_mappings << [start_code..end_code, start_values]
|
204
|
+
end
|
205
|
+
|
196
206
|
# Returns the Unicode string in UTF-8 encoding for the given character code, or +nil+ if no
|
197
207
|
# mapping was found.
|
198
208
|
def to_unicode(code)
|
199
|
-
unicode_mapping
|
209
|
+
@unicode_mapping.fetch(code) do
|
210
|
+
@unicode_range_mappings.reverse_each do |range, start_values|
|
211
|
+
if range.cover?(code)
|
212
|
+
str = start_values[0..-2].append(start_values[-1] + code - range.first).
|
213
|
+
pack('n*').encode(::Encoding::UTF_8, ::Encoding::UTF_16BE)
|
214
|
+
return @unicode_mapping[code] = str
|
215
|
+
end
|
216
|
+
end
|
217
|
+
nil
|
218
|
+
end
|
200
219
|
end
|
201
220
|
|
202
221
|
end
|
@@ -62,7 +62,7 @@ module HexaPDF
|
|
62
62
|
# Specifies whether the font should be subset if possible.
|
63
63
|
#
|
64
64
|
# This method uses the FromFile font loader behind the scenes.
|
65
|
-
def self.call(document, name, variant: :none, subset: true)
|
65
|
+
def self.call(document, name, variant: :none, subset: true, **)
|
66
66
|
file = document.config['font.map'].dig(name, variant)
|
67
67
|
return nil if file.nil?
|
68
68
|
|
@@ -0,0 +1,72 @@
|
|
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/font/true_type_wrapper'
|
38
|
+
require 'hexapdf/font_loader/from_file'
|
39
|
+
|
40
|
+
module HexaPDF
|
41
|
+
module FontLoader
|
42
|
+
|
43
|
+
# This module translates font names like 'Helvetica bold' into the arguments 'Helvetica' and
|
44
|
+
# {variant: :bold}.
|
45
|
+
#
|
46
|
+
# This eases the usage of font names where specifying a font variant is not straight-forward.
|
47
|
+
# The actual loading of the font is deferred to Document::Fonts#add.
|
48
|
+
#
|
49
|
+
# Note that this should be the last entry in the list of font loaders to ensure correct
|
50
|
+
# operation.
|
51
|
+
module VariantFromName
|
52
|
+
|
53
|
+
# Returns a font wrapper for the given font by splitting the font name into the font name part
|
54
|
+
# and variant selector part. If the the resulting font cannot be resolved, +nil+ is returned.
|
55
|
+
#
|
56
|
+
# A font name should have the form 'Fontname selector' where selector can be 'bold', 'italic'
|
57
|
+
# or 'bold_italic', for example 'Helvetica bold'.
|
58
|
+
#
|
59
|
+
# Note that a supplied :variant keyword argument is ignored!
|
60
|
+
def self.call(document, name, recursive_invocation: false, **options)
|
61
|
+
return if recursive_invocation
|
62
|
+
name, variant = name.split(/ (?=(?:bold|italic|bold_italic)\z)/, 2)
|
63
|
+
return if variant.nil?
|
64
|
+
|
65
|
+
options[:variant] = variant.to_sym
|
66
|
+
document.fonts.add(name, **options, recursive_invocation: true) rescue nil
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
data/lib/hexapdf/font_loader.rb
CHANGED
@@ -88,6 +88,7 @@ module HexaPDF
|
|
88
88
|
autoload(:Standard14, 'hexapdf/font_loader/standard14')
|
89
89
|
autoload(:FromConfiguration, 'hexapdf/font_loader/from_configuration')
|
90
90
|
autoload(:FromFile, 'hexapdf/font_loader/from_file')
|
91
|
+
autoload(:VariantFromName, 'hexapdf/font_loader/variant_from_name')
|
91
92
|
|
92
93
|
end
|
93
94
|
|
@@ -255,7 +255,9 @@ module HexaPDF
|
|
255
255
|
|
256
256
|
@draw_pos_x = frame.x + reserved_width_left
|
257
257
|
@draw_pos_y = frame.y - @height + reserved_height_bottom
|
258
|
-
@
|
258
|
+
@all_items_fitted = @results.all? {|r| r.box_fitter.fit_successful? } &&
|
259
|
+
@results.size == @children.size
|
260
|
+
@fit_successful = @all_items_fitted || (@initial_height > 0 && style.overflow == :truncate)
|
259
261
|
end
|
260
262
|
|
261
263
|
private
|
@@ -307,7 +309,7 @@ module HexaPDF
|
|
307
309
|
# Splits the content of the list box. This method is called from Box#split.
|
308
310
|
def split_content(_available_width, _available_height, _frame)
|
309
311
|
remaining_boxes = @results[-1].box_fitter.remaining_boxes
|
310
|
-
first_is_split_box = remaining_boxes.
|
312
|
+
first_is_split_box = !remaining_boxes.empty?
|
311
313
|
children = (remaining_boxes.empty? ? [] : [remaining_boxes]) + @children[@results.size..-1]
|
312
314
|
|
313
315
|
box = create_split_box(split_box_value: first_is_split_box ? :hide_first_marker : :show_first_marker)
|
@@ -323,42 +325,47 @@ module HexaPDF
|
|
323
325
|
# its contents.
|
324
326
|
def item_marker_box(document, index)
|
325
327
|
return @marker_type.call(document, self, index) if @marker_type.kind_of?(Proc)
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
328
|
+
|
329
|
+
unless (fragment = @item_marker_fragment)
|
330
|
+
marker_style = {
|
331
|
+
font: style.font? ? style.font : document.fonts.add("Times"),
|
332
|
+
font_size: style.font_size || 10, fill_color: style.fill_color
|
333
|
+
}
|
334
|
+
fragment = case @marker_type
|
335
|
+
when :disc
|
336
|
+
TextFragment.create("•", marker_style)
|
337
|
+
when :circle
|
338
|
+
unless marker_style[:font].decode_codepoint("❍".ord).valid?
|
339
|
+
marker_style[:font] = document.fonts.add("ZapfDingbats")
|
340
|
+
end
|
341
|
+
TextFragment.create("❍", **marker_style,
|
342
|
+
font_size: style.font_size / 2.0,
|
343
|
+
text_rise: -style.font_size / 1.8)
|
344
|
+
when :square
|
345
|
+
unless marker_style[:font].decode_codepoint("■".ord).valid?
|
346
|
+
marker_style[:font] = document.fonts.add("ZapfDingbats")
|
347
|
+
end
|
348
|
+
TextFragment.create("■", **marker_style,
|
349
|
+
font_size: style.font_size / 2.0,
|
350
|
+
text_rise: -style.font_size / 1.8)
|
351
|
+
when :decimal
|
352
|
+
text = (@start_number + index).to_s << "."
|
353
|
+
TextFragment.create(text, marker_style)
|
354
|
+
else
|
355
|
+
raise HexaPDF::Error, "Unknown list marker type #{@marker_type.inspect}"
|
345
356
|
end
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
when :decimal
|
350
|
-
text = (@start_number + index).to_s << "."
|
351
|
-
TextFragment.create(text, marker_style)
|
352
|
-
else
|
353
|
-
raise HexaPDF::Error, "Unknown list marker type #{@marker_type.inspect}"
|
354
|
-
end
|
355
|
-
box = TextBox.new(items: [fragment], style: {text_align: :right, padding: [0, 5, 0, 0]})
|
356
|
-
@item_marker_box = box unless @marker_type == :decimal
|
357
|
-
box
|
357
|
+
@item_marker_fragment = fragment unless @marker_type == :decimal
|
358
|
+
end
|
359
|
+
TextBox.new(items: [fragment], style: {text_align: :right, padding: [0, 5, 0, 0]})
|
358
360
|
end
|
359
361
|
|
360
362
|
# Draws the list items onto the canvas at position [x, y].
|
361
363
|
def draw_content(canvas, x, y)
|
364
|
+
if !@all_items_fitted && (@initial_height > 0 && style.overflow == :error)
|
365
|
+
raise HexaPDF::Error, "Some items don't fit into box with limited height and " \
|
366
|
+
"style property overflow is set to :error"
|
367
|
+
end
|
368
|
+
|
362
369
|
translate = style.position != :flow && (x != @draw_pos_x || y != @draw_pos_y)
|
363
370
|
|
364
371
|
if translate
|
data/lib/hexapdf/layout/style.rb
CHANGED
@@ -614,7 +614,7 @@ module HexaPDF
|
|
614
614
|
# The font to be used, must be set to a valid font wrapper object before it can be used.
|
615
615
|
#
|
616
616
|
# HexaPDF::Composer handles this property specially in that it resolves a set string or array
|
617
|
-
# to a font wrapper object before doing else with the style object.
|
617
|
+
# to a font wrapper object before doing anything else with the style object.
|
618
618
|
#
|
619
619
|
# This is the only style property without a default value!
|
620
620
|
#
|
@@ -624,11 +624,12 @@ module HexaPDF
|
|
624
624
|
#
|
625
625
|
# #>pdf-composer100
|
626
626
|
# composer.text("Helvetica", font: composer.document.fonts.add("Helvetica"))
|
627
|
-
# composer.text("Courier", font: "Courier")
|
627
|
+
# composer.text("Courier", font: "Courier")
|
628
628
|
#
|
629
629
|
# helvetica_bold = composer.document.fonts.add("Helvetica", variant: :bold)
|
630
630
|
# composer.text("Helvetica Bold", font: helvetica_bold)
|
631
|
-
# composer.text("Courier Bold", font:
|
631
|
+
# composer.text("Courier Bold", font: "Courier bold")
|
632
|
+
# composer.text("Courier Bold also", font: ["Courier", variant: :bold])
|
632
633
|
|
633
634
|
##
|
634
635
|
# :method: font_size
|
@@ -1084,25 +1085,6 @@ module HexaPDF
|
|
1084
1085
|
# 'Centered',
|
1085
1086
|
# {text: "\u{00a0}", fill_horizontal: 1, overlays: overlays}])
|
1086
1087
|
|
1087
|
-
##
|
1088
|
-
# :method: text_overflow
|
1089
|
-
# :call-seq:
|
1090
|
-
# text_overflow(mode = nil)
|
1091
|
-
#
|
1092
|
-
# Specifies how text overflowing a box with a given initial height should be handled:
|
1093
|
-
#
|
1094
|
-
# Possible values:
|
1095
|
-
#
|
1096
|
-
# :error:: An error is raised (default).
|
1097
|
-
# :truncate:: Truncates the overflowing text.
|
1098
|
-
#
|
1099
|
-
# Examples:
|
1100
|
-
#
|
1101
|
-
# #>pdf-composer100
|
1102
|
-
# composer.text("This is some longer text that does appear in two lines.")
|
1103
|
-
# composer.text("This is some longer text that does not appear in two lines.",
|
1104
|
-
# height: 15, text_overflow: :truncate)
|
1105
|
-
|
1106
1088
|
##
|
1107
1089
|
# :method: background_color
|
1108
1090
|
# :call-seq:
|
@@ -1414,6 +1396,26 @@ module HexaPDF
|
|
1414
1396
|
# composer.text('Mask covers everything', mask_mode: :fill)
|
1415
1397
|
# composer.text('On the next page')
|
1416
1398
|
|
1399
|
+
##
|
1400
|
+
# :method: overflow
|
1401
|
+
# :call-seq:
|
1402
|
+
# overflow(mode = nil)
|
1403
|
+
#
|
1404
|
+
# Specifies how overflowing boxes (e.g. the text of a box or the children of a container) with
|
1405
|
+
# a given initial height should be handled:
|
1406
|
+
#
|
1407
|
+
# Possible values:
|
1408
|
+
#
|
1409
|
+
# :error:: An error is raised (default).
|
1410
|
+
# :truncate:: Truncates the overflowing parts.
|
1411
|
+
#
|
1412
|
+
# Examples:
|
1413
|
+
#
|
1414
|
+
# #>pdf-composer100
|
1415
|
+
# composer.text("This is some longer text that does appear in two lines.")
|
1416
|
+
# composer.text("This is some longer text that does not appear in two lines.",
|
1417
|
+
# height: 15, overflow: :truncate)
|
1418
|
+
|
1417
1419
|
[
|
1418
1420
|
[:font, "raise HexaPDF::Error, 'No font set'"],
|
1419
1421
|
[:font_size, 10],
|
@@ -1454,7 +1456,6 @@ module HexaPDF
|
|
1454
1456
|
extra_args: ", extra_arg = nil"}],
|
1455
1457
|
[:last_line_gap, false, {valid_values: [true, false]}],
|
1456
1458
|
[:fill_horizontal, nil],
|
1457
|
-
[:text_overflow, :error],
|
1458
1459
|
[:background_color, nil],
|
1459
1460
|
[:background_alpha, 1],
|
1460
1461
|
[:padding, "Quad.new(0)", {setter: "Quad.new(value)"}],
|
@@ -1467,6 +1468,7 @@ module HexaPDF
|
|
1467
1468
|
[:valign, :top, {valid_values: [:top, :center, :bottom]}],
|
1468
1469
|
[:mask_mode, :default, {valid_values: [:default, :none, :box, :fill_horizontal,
|
1469
1470
|
:fill_frame_horizontal, :fill_vertical, :fill]}],
|
1471
|
+
[:overflow, :error],
|
1470
1472
|
].each do |name, default, options = {}|
|
1471
1473
|
default = default.inspect unless default.kind_of?(String)
|
1472
1474
|
setter = options.delete(:setter) || "value"
|
@@ -106,7 +106,7 @@ module HexaPDF
|
|
106
106
|
end
|
107
107
|
|
108
108
|
@result.status == :success ||
|
109
|
-
(@result.status == :height && @initial_height > 0 && style.
|
109
|
+
(@result.status == :height && @initial_height > 0 && style.overflow == :truncate)
|
110
110
|
end
|
111
111
|
|
112
112
|
# Splits the text box into two boxes if necessary and possible.
|
@@ -136,9 +136,9 @@ module HexaPDF
|
|
136
136
|
def draw_content(canvas, x, y)
|
137
137
|
return unless @result
|
138
138
|
|
139
|
-
if @result.status == :height && @initial_height > 0 && style.
|
139
|
+
if @result.status == :height && @initial_height > 0 && style.overflow == :error
|
140
140
|
raise HexaPDF::Error, "Text doesn't fit into box with limited height and " \
|
141
|
-
"style property
|
141
|
+
"style property overflow is set to :error"
|
142
142
|
end
|
143
143
|
|
144
144
|
return if @result.lines.empty?
|
@@ -39,6 +39,7 @@ require 'hexapdf/error'
|
|
39
39
|
require 'hexapdf/layout/style'
|
40
40
|
require 'hexapdf/layout/text_fragment'
|
41
41
|
require 'hexapdf/layout/text_layouter'
|
42
|
+
require 'hexapdf/type/acro_form/java_script_actions'
|
42
43
|
|
43
44
|
module HexaPDF
|
44
45
|
module Type
|
@@ -129,14 +130,20 @@ module HexaPDF
|
|
129
130
|
# widget.background_color(1)
|
130
131
|
# widget.marker_style(style: :circle, size: 0, color: 0)
|
131
132
|
def create_check_box_appearances
|
132
|
-
|
133
|
-
|
133
|
+
normal_appearance = @widget.appearance_dict&.normal_appearance
|
134
|
+
if !normal_appearance.kind_of?(HexaPDF::Dictionary) || normal_appearance.kind_of?(HexaPDF::Stream)
|
135
|
+
(@widget[:AP] ||= {})[:N] = {Off: nil}
|
136
|
+
normal_appearance = @widget[:AP][:N]
|
137
|
+
normal_appearance[@field[:V] == :Off ? :Yes : @field[:V]] = nil
|
138
|
+
end
|
139
|
+
on_name = (normal_appearance.value.keys - [:Off]).first
|
134
140
|
unless on_name
|
135
141
|
raise HexaPDF::Error, "Widget of button field doesn't define name for on state"
|
136
142
|
end
|
137
143
|
|
138
144
|
@widget[:AS] = (@field[:V] == on_name ? on_name : :Off)
|
139
145
|
@widget.flag(:print)
|
146
|
+
@widget.unflag(:hidden)
|
140
147
|
|
141
148
|
border_style = @widget.border_style
|
142
149
|
marker_style = @widget.marker_style
|
@@ -206,6 +213,7 @@ module HexaPDF
|
|
206
213
|
|
207
214
|
@widget[:AS] = :N
|
208
215
|
@widget.flag(:print)
|
216
|
+
@widget.unflag(:hidden)
|
209
217
|
rect = @widget[:Rect]
|
210
218
|
rect.width = @document.config['acro_form.text_field.default_width'] if rect.width == 0
|
211
219
|
if rect.height == 0
|
@@ -358,7 +366,7 @@ module HexaPDF
|
|
358
366
|
|
359
367
|
# Draws a single line of text inside the widget's rectangle.
|
360
368
|
def draw_single_line_text(canvas, width, height, style, padding)
|
361
|
-
value, text_color =
|
369
|
+
value, text_color = JavaScriptActions.apply_format(@field.field_value, @field[:AA]&.[](:F))
|
362
370
|
style.fill_color = text_color if text_color
|
363
371
|
calculate_and_apply_font_size(value, style, width, height, padding)
|
364
372
|
line = HexaPDF::Layout::Line.new(@document.layout.text_fragments(value, style: style))
|
@@ -500,79 +508,6 @@ module HexaPDF
|
|
500
508
|
style.clear_cache
|
501
509
|
end
|
502
510
|
|
503
|
-
# Handles Javascript formatting routines for single-line text fields.
|
504
|
-
#
|
505
|
-
# Returns [value, nil_or_text_color] where value is the new, potentially adjusted field
|
506
|
-
# value and the second argument is either +nil+ or the color that should be used for the
|
507
|
-
# text value.
|
508
|
-
def apply_javascript_formatting(value)
|
509
|
-
format_action = @widget[:AA]&.[](:F)
|
510
|
-
return [value, nil] unless format_action && format_action[:S] == :JavaScript
|
511
|
-
if (match = AF_NUMBER_FORMAT_RE.match(format_action[:JS]))
|
512
|
-
apply_af_number_format(value, match)
|
513
|
-
else
|
514
|
-
[value, nil]
|
515
|
-
end
|
516
|
-
end
|
517
|
-
|
518
|
-
# Regular expression for matching the AFNumber_Format Javascript method.
|
519
|
-
AF_NUMBER_FORMAT_RE = /
|
520
|
-
\AAFNumber_Format\(
|
521
|
-
\s*(?<ndec>\d+)\s*,
|
522
|
-
\s*(?<sep_style>[0-3])\s*,
|
523
|
-
\s*(?<neg_style>[0-3])\s*,
|
524
|
-
\s*0\s*,
|
525
|
-
\s*(?<currency_string>".*?")\s*,
|
526
|
-
\s*(?<prepend>false|true)\s*
|
527
|
-
\);\z
|
528
|
-
/x
|
529
|
-
|
530
|
-
# Implements the Javascript AFNumber_Format method.
|
531
|
-
#
|
532
|
-
# See:
|
533
|
-
# - https://experienceleague.adobe.com/docs/experience-manager-learn/assets/FormsAPIReference.pdf
|
534
|
-
# - https://opensource.adobe.com/dc-acrobat-sdk-docs/library/jsapiref/JS_API_AcroJS.html#printf
|
535
|
-
def apply_af_number_format(value, match)
|
536
|
-
value = value.to_f
|
537
|
-
format = "%.#{match[:ndec]}f"
|
538
|
-
text_color = 'black'
|
539
|
-
|
540
|
-
currency_string = JSON.parse(match[:currency_string])
|
541
|
-
format = (match[:prepend] == 'true' ? currency_string + format : format + currency_string)
|
542
|
-
|
543
|
-
if value < 0
|
544
|
-
value = value.abs
|
545
|
-
case match[:neg_style]
|
546
|
-
when '0' # MinusBlack
|
547
|
-
format = "-#{format}"
|
548
|
-
when '1' # Red
|
549
|
-
text_color = 'red'
|
550
|
-
when '2' # ParensBlack
|
551
|
-
format = "(#{format})"
|
552
|
-
when '3' # ParensRed
|
553
|
-
format = "(#{format})"
|
554
|
-
text_color = 'red'
|
555
|
-
end
|
556
|
-
end
|
557
|
-
|
558
|
-
result = sprintf(format, value)
|
559
|
-
|
560
|
-
# sep_style: 0=12,345.67, 1=12345.67, 2=12.345,67, 3=12345,67
|
561
|
-
before_decimal_point, after_decimal_point = result.split('.')
|
562
|
-
if match[:sep_style] == '0' || match[:sep_style] == '2'
|
563
|
-
separator = (match[:sep_style] == '0' ? ',' : '.')
|
564
|
-
before_decimal_point.gsub!(/\B(?=(\d\d\d)+(?:[^\d]|\z))/, separator)
|
565
|
-
end
|
566
|
-
result = if after_decimal_point
|
567
|
-
decimal_point = (match[:sep_style] =~ /[01]/ ? '.' : ',')
|
568
|
-
"#{before_decimal_point}#{decimal_point}#{after_decimal_point}"
|
569
|
-
else
|
570
|
-
before_decimal_point
|
571
|
-
end
|
572
|
-
|
573
|
-
[result, text_color]
|
574
|
-
end
|
575
|
-
|
576
511
|
end
|
577
512
|
|
578
513
|
end
|
@@ -155,6 +155,12 @@ module HexaPDF
|
|
155
155
|
field.value[name].nil? ? nil : field[name]
|
156
156
|
end
|
157
157
|
|
158
|
+
# Wraps the given +field+ object inside the correct field class and returns the wrapped
|
159
|
+
# object.
|
160
|
+
def self.wrap(document, field)
|
161
|
+
document.wrap(field, type: :XXAcroFormField, subtype: inherited_value(field, :FT))
|
162
|
+
end
|
163
|
+
|
158
164
|
# Form fields must always be indirect objects.
|
159
165
|
def must_be_indirect?
|
160
166
|
true
|
@@ -236,6 +242,14 @@ module HexaPDF
|
|
236
242
|
kids.nil? || kids.empty? || kids.none? {|kid| kid.key?(:T) }
|
237
243
|
end
|
238
244
|
|
245
|
+
# Returns self.
|
246
|
+
#
|
247
|
+
# This method is only here to make it easier to get the form field when the object may
|
248
|
+
# either be a form field or a field widget.
|
249
|
+
def form_field
|
250
|
+
self
|
251
|
+
end
|
252
|
+
|
239
253
|
# Returns +true+ if the field contains an embedded widget.
|
240
254
|
def embedded_widget?
|
241
255
|
key?(:Subtype)
|
@@ -99,11 +99,11 @@ module HexaPDF
|
|
99
99
|
document.pages.each do |page|
|
100
100
|
page.each_annotation do |annot|
|
101
101
|
if !annot.key?(:Parent) && annot.key?(:FT)
|
102
|
-
result <<
|
102
|
+
result << Field.wrap(document, annot)
|
103
103
|
elsif annot.key?(:Parent)
|
104
104
|
field = annot[:Parent]
|
105
105
|
field = field[:Parent] while field[:Parent]
|
106
|
-
result <<
|
106
|
+
result << Field.wrap(document, field)
|
107
107
|
end
|
108
108
|
end
|
109
109
|
end
|
@@ -129,8 +129,7 @@ module HexaPDF
|
|
129
129
|
array.each_with_index do |field, index|
|
130
130
|
next if field.nil?
|
131
131
|
unless field.respond_to?(:type) && field.type == :XXAcroFormField
|
132
|
-
array[index] = field =
|
133
|
-
subtype: Field.inherited_value(field, :FT))
|
132
|
+
array[index] = field = Field.wrap(document, field)
|
134
133
|
end
|
135
134
|
if field.terminal_field?
|
136
135
|
yield(field)
|
@@ -153,8 +152,7 @@ module HexaPDF
|
|
153
152
|
name.split('.').each do |part|
|
154
153
|
field = nil
|
155
154
|
fields&.each do |f|
|
156
|
-
f =
|
157
|
-
subtype: Field.inherited_value(f, :FT))
|
155
|
+
f = Field.wrap(document, f)
|
158
156
|
next unless f[:T] == part
|
159
157
|
field = f
|
160
158
|
fields = field[:Kids] unless field.terminal_field?
|
@@ -420,6 +418,26 @@ module HexaPDF
|
|
420
418
|
not_flattened
|
421
419
|
end
|
422
420
|
|
421
|
+
# Recalculates all form fields that have a calculate action applied (which are all fields
|
422
|
+
# listed in the /CO entry).
|
423
|
+
#
|
424
|
+
# If HexaPDF doesn't support a calculation method or an error occurs during calculation, the
|
425
|
+
# field value is not updated.
|
426
|
+
#
|
427
|
+
# Note that calculations are *not* done automatically when a form field's value changes
|
428
|
+
# since it would lead to possibly many calls to this actions. So first fill in all field
|
429
|
+
# values and then call this method.
|
430
|
+
#
|
431
|
+
# See: JavaScriptActions
|
432
|
+
def recalculate_fields
|
433
|
+
self[:CO]&.each do |field|
|
434
|
+
field = Field.wrap(document, field)
|
435
|
+
next unless field && (calculation_action = field[:AA]&.[](:C))
|
436
|
+
result = JavaScriptActions.calculate(self, calculation_action)
|
437
|
+
field.form_field.field_value = result if result
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
423
441
|
private
|
424
442
|
|
425
443
|
# Creates a new field with the full name +name+ and the field type +type+.
|
@@ -468,8 +486,7 @@ module HexaPDF
|
|
468
486
|
end
|
469
487
|
next false unless field.key?(:T) # Skip widgets
|
470
488
|
|
471
|
-
field =
|
472
|
-
subtype: Field.inherited_value(field, :FT))
|
489
|
+
field = Field.wrap(document, field)
|
473
490
|
reject = false
|
474
491
|
if field[:Parent] != parent
|
475
492
|
yield("Parent entry of field (#{field.oid},#{field.gen}) invalid", true)
|