hexapdf 0.40.0 → 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 +45 -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/style.rb +5 -4
- 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/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
|
|
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
|
@@ -1413,7 +1414,7 @@ module HexaPDF
|
|
1413
1414
|
# #>pdf-composer100
|
1414
1415
|
# composer.text("This is some longer text that does appear in two lines.")
|
1415
1416
|
# composer.text("This is some longer text that does not appear in two lines.",
|
1416
|
-
# height: 15,
|
1417
|
+
# height: 15, overflow: :truncate)
|
1417
1418
|
|
1418
1419
|
[
|
1419
1420
|
[:font, "raise HexaPDF::Error, 'No font set'"],
|
@@ -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)
|