hexapdf 0.40.0 → 0.42.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 +71 -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 +113 -0
- data/lib/hexapdf/cli/command.rb +25 -11
- data/lib/hexapdf/cli/files.rb +31 -7
- data/lib/hexapdf/cli/form.rb +46 -38
- data/lib/hexapdf/cli/info.rb +4 -0
- 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 +11 -1
- 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/button_field.rb +7 -5
- data/lib/hexapdf/type/acro_form/field.rb +14 -0
- data/lib/hexapdf/type/acro_form/form.rb +70 -8
- data/lib/hexapdf/type/acro_form/java_script_actions.rb +649 -0
- data/lib/hexapdf/type/acro_form/text_field.rb +90 -0
- data/lib/hexapdf/type/acro_form.rb +1 -0
- data/lib/hexapdf/type/annotations/widget.rb +1 -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/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/test_utils.rb +16 -0
- data/test/hexapdf/type/acro_form/test_appearance_generator.rb +31 -47
- data/test/hexapdf/type/acro_form/test_button_field.rb +5 -0
- data/test/hexapdf/type/acro_form/test_field.rb +11 -0
- data/test/hexapdf/type/acro_form/test_form.rb +80 -0
- data/test/hexapdf/type/acro_form/test_java_script_actions.rb +327 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +62 -0
- data/test/hexapdf/type/test_resources.rb +5 -0
- metadata +8 -2
data/lib/hexapdf/cli/info.rb
CHANGED
@@ -192,6 +192,10 @@ module HexaPDF
|
|
192
192
|
puts "WARNING: Parse error at position #{pos}: #{msg}"
|
193
193
|
false
|
194
194
|
end
|
195
|
+
options[:config]['encryption.on_decryption_error'] = lambda do |obj, msg|
|
196
|
+
puts "WARNING: Decryption problem for object (#{obj.oid},#{obj.gen}): #{msg}"
|
197
|
+
false
|
198
|
+
end
|
195
199
|
options
|
196
200
|
else
|
197
201
|
super
|
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)
|
@@ -255,6 +255,12 @@ module HexaPDF
|
|
255
255
|
# PDF defines a standard security handler that is implemented
|
256
256
|
# (HexaPDF::Encryption::StandardSecurityHandler) and assigned the :Standard name.
|
257
257
|
#
|
258
|
+
# encryption.on_decryption_error::
|
259
|
+
# Callback hook when HexaPDF encounters a decryption error that can potentially be ignored.
|
260
|
+
#
|
261
|
+
# The value needs to be an object that responds to \#call(obj, message) and returns +true+ if
|
262
|
+
# an error should be raised.
|
263
|
+
#
|
258
264
|
# encryption.sub_filter_map::
|
259
265
|
# A mapping from a PDF name (a Symbol) to a security handler class (see
|
260
266
|
# HexaPDF::Encryption::SecurityHandler). If the value is a String, it should contain the name
|
@@ -475,7 +481,7 @@ module HexaPDF
|
|
475
481
|
'acro_form.fallback_font' => 'Helvetica',
|
476
482
|
'acro_form.on_invalid_value' => proc do |field, value|
|
477
483
|
raise HexaPDF::Error, "Invalid value #{value.inspect} for " \
|
478
|
-
"#{field.concrete_field_type} field #{field.full_field_name}"
|
484
|
+
"#{field.concrete_field_type} field named '#{field.full_field_name}'"
|
479
485
|
end,
|
480
486
|
'acro_form.text_field.default_width' => 100,
|
481
487
|
'debug' => false,
|
@@ -488,6 +494,9 @@ module HexaPDF
|
|
488
494
|
'encryption.filter_map' => {
|
489
495
|
Standard: 'HexaPDF::Encryption::StandardSecurityHandler',
|
490
496
|
},
|
497
|
+
'encryption.on_decryption_error' => proc do |_obj, _error|
|
498
|
+
false
|
499
|
+
end,
|
491
500
|
'encryption.sub_filter_map' => {},
|
492
501
|
'filter.map' => {
|
493
502
|
ASCIIHexDecode: 'HexaPDF::Filter::ASCIIHexDecode',
|
@@ -523,6 +532,7 @@ module HexaPDF
|
|
523
532
|
'HexaPDF::FontLoader::Standard14',
|
524
533
|
'HexaPDF::FontLoader::FromConfiguration',
|
525
534
|
'HexaPDF::FontLoader::FromFile',
|
535
|
+
'HexaPDF::FontLoader::VariantFromName',
|
526
536
|
],
|
527
537
|
'graphic_object.arc.max_curves' => 6,
|
528
538
|
'graphic_object.map' => {
|
@@ -2245,6 +2245,8 @@ module HexaPDF
|
|
2245
2245
|
# canvas.text("Times at size 10", at: [10, 150])
|
2246
2246
|
# canvas.font("Times", variant: :bold_italic, size: 15)
|
2247
2247
|
# canvas.text("Times bold+italic at size 15", at: [10, 100])
|
2248
|
+
# canvas.font("Times bold")
|
2249
|
+
# canvas.text("Times bold using the variant-from-name method", at: [10, 50])
|
2248
2250
|
#
|
2249
2251
|
# See: PDF2.0 s9.2.2, #font_size, #text
|
2250
2252
|
def font(name = nil, size: nil, **options)
|
@@ -92,6 +92,13 @@ module HexaPDF
|
|
92
92
|
#
|
93
93
|
# style.font = ['Helvetica', variant: :bold]
|
94
94
|
#
|
95
|
+
# Helvetica in bold could also be set the conventional way:
|
96
|
+
#
|
97
|
+
# style.font = 'Helvetica bold'
|
98
|
+
#
|
99
|
+
# However, using an array it is also possible to specify other options when setting a font,
|
100
|
+
# like the :subset option.
|
101
|
+
#
|
95
102
|
class Layout
|
96
103
|
|
97
104
|
# This class is used when a box can contain child boxes and the creation of such boxes should
|
@@ -539,7 +546,7 @@ module HexaPDF
|
|
539
546
|
# # assign the predefined style :cell_text to all texts
|
540
547
|
# args[] = {style: :cell_text}
|
541
548
|
# # row 0 has a grey background and bold text
|
542
|
-
# args[0] = {font:
|
549
|
+
# args[0] = {font: 'Helvetica bold', cell: {background_color: 'eee'}}
|
543
550
|
# # text in last column is right aligned
|
544
551
|
# args[0..-1, -1] = {text_align: :right}
|
545
552
|
# end
|
@@ -112,11 +112,15 @@ module HexaPDF
|
|
112
112
|
# It is assumed that the initialization vector is included in the first BLOCK_SIZE bytes
|
113
113
|
# of the data. After the decryption the PKCS#5 padding is removed.
|
114
114
|
#
|
115
|
+
# If a problem is encountered, an error message is yielded. If no block is given or if the
|
116
|
+
# supplied block returns +true+, an error is raised.
|
117
|
+
#
|
115
118
|
# See: PDF2.0 s7.6.3
|
116
|
-
def decrypt(key, data)
|
119
|
+
def decrypt(key, data) # :yields: error_message
|
117
120
|
return data if data.empty? # Handle invalid files with empty strings
|
118
121
|
if data.length % BLOCK_SIZE != 0 || data.length < BLOCK_SIZE
|
119
|
-
|
122
|
+
msg = "Invalid data for decryption, need 32 + 16*n bytes"
|
123
|
+
(!block_given? || yield(msg)) && raise(HexaPDF::EncryptionError, msg)
|
120
124
|
end
|
121
125
|
iv = data.slice!(0, BLOCK_SIZE)
|
122
126
|
# Handle invalid files with missing padding
|
@@ -126,8 +130,9 @@ module HexaPDF
|
|
126
130
|
# Returns a Fiber object that decrypts the data from the given source fiber with the
|
127
131
|
# +key+.
|
128
132
|
#
|
129
|
-
# Padding
|
130
|
-
|
133
|
+
# Padding, the initialization vector and an optionally given block are handled like in
|
134
|
+
# #decrypt.
|
135
|
+
def decryption_fiber(key, source) # :yields: error_message
|
131
136
|
Fiber.new do
|
132
137
|
data = ''.b
|
133
138
|
while data.length < BLOCK_SIZE && source.alive? && (new_data = source.resume)
|
@@ -145,8 +150,10 @@ module HexaPDF
|
|
145
150
|
end
|
146
151
|
|
147
152
|
if data.length % BLOCK_SIZE != 0
|
148
|
-
|
149
|
-
|
153
|
+
msg = "Invalid data for decryption, need 32 + 16*n bytes"
|
154
|
+
(!block_given? || yield(msg)) && raise(HexaPDF::EncryptionError, msg)
|
155
|
+
end
|
156
|
+
if data.empty?
|
150
157
|
data # Handle invalid files with missing padding
|
151
158
|
else
|
152
159
|
unpad(algorithm.process(data))
|
@@ -153,17 +153,18 @@ module HexaPDF
|
|
153
153
|
|
154
154
|
# Creates a new encrypted stream data object by utilizing the given stream data object +obj+
|
155
155
|
# as template. The arguments +key+ and +algorithm+ are used for decrypting purposes.
|
156
|
-
def initialize(obj, key, algorithm)
|
156
|
+
def initialize(obj, key, algorithm, &error_block)
|
157
157
|
obj.instance_variables.each {|v| instance_variable_set(v, obj.instance_variable_get(v)) }
|
158
158
|
@key = key
|
159
159
|
@algorithm = algorithm
|
160
|
+
@error_block = error_block
|
160
161
|
end
|
161
162
|
|
162
163
|
alias undecrypted_fiber fiber
|
163
164
|
|
164
165
|
# Returns a fiber like HexaPDF::StreamData#fiber, but one wrapped in a decrypting fiber.
|
165
166
|
def fiber(*args)
|
166
|
-
@algorithm.decryption_fiber(@key, super(*args))
|
167
|
+
@algorithm.decryption_fiber(@key, super(*args), &@error_block)
|
167
168
|
end
|
168
169
|
|
169
170
|
end
|
@@ -268,17 +269,18 @@ module HexaPDF
|
|
268
269
|
def decrypt(obj)
|
269
270
|
return obj if @is_encrypt_dict[obj] || obj.type == :XRef
|
270
271
|
|
272
|
+
error_proc = proc {|msg| document.config['encryption.on_decryption_error'].call(obj, msg) }
|
271
273
|
key = object_key(obj.oid, obj.gen, string_algorithm)
|
272
274
|
each_string_in_object(obj.value) do |str|
|
273
275
|
next if str.empty? || (obj.type == :Sig && obj[:Contents].equal?(str))
|
274
|
-
str.replace(string_algorithm.decrypt(key, str))
|
276
|
+
str.replace(string_algorithm.decrypt(key, str, &error_proc))
|
275
277
|
end
|
276
278
|
|
277
279
|
if obj.kind_of?(HexaPDF::Stream) && obj.raw_stream.filter[0] != :Crypt
|
278
280
|
unless string_algorithm == stream_algorithm
|
279
281
|
key = object_key(obj.oid, obj.gen, stream_algorithm)
|
280
282
|
end
|
281
|
-
obj.data.stream = EncryptedStreamData.new(obj.raw_stream, key, stream_algorithm)
|
283
|
+
obj.data.stream = EncryptedStreamData.new(obj.raw_stream, key, stream_algorithm, &error_proc)
|
282
284
|
end
|
283
285
|
|
284
286
|
obj
|
@@ -163,11 +163,7 @@ module HexaPDF
|
|
163
163
|
dest = tokenizer.next_object
|
164
164
|
|
165
165
|
if dest.kind_of?(String)
|
166
|
-
|
167
|
-
code1.upto(code2) do |code|
|
168
|
-
cmap.add_unicode_mapping(code, +'' << codepoint)
|
169
|
-
codepoint += 1
|
170
|
-
end
|
166
|
+
cmap.add_unicode_range_mapping(code1, code2, dest.unpack("n*"))
|
171
167
|
elsif dest.kind_of?(Array)
|
172
168
|
code1.upto(code2) do |code|
|
173
169
|
str = dest[code - code1].encode!(::Encoding::UTF_8, ::Encoding::UTF_16BE)
|
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'"],
|