hexapdf 0.40.0 → 0.42.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/examples/019-acro_form.rb +12 -23
  4. data/examples/027-composer_optional_content.rb +1 -1
  5. data/examples/030-pdfa.rb +6 -6
  6. data/examples/031-acro_form_java_script.rb +113 -0
  7. data/lib/hexapdf/cli/command.rb +25 -11
  8. data/lib/hexapdf/cli/files.rb +31 -7
  9. data/lib/hexapdf/cli/form.rb +46 -38
  10. data/lib/hexapdf/cli/info.rb +4 -0
  11. data/lib/hexapdf/cli/inspect.rb +1 -1
  12. data/lib/hexapdf/cli/usage.rb +215 -0
  13. data/lib/hexapdf/cli.rb +2 -0
  14. data/lib/hexapdf/configuration.rb +11 -1
  15. data/lib/hexapdf/content/canvas.rb +2 -0
  16. data/lib/hexapdf/document/layout.rb +8 -1
  17. data/lib/hexapdf/encryption/aes.rb +13 -6
  18. data/lib/hexapdf/encryption/security_handler.rb +6 -4
  19. data/lib/hexapdf/font/cmap/parser.rb +1 -5
  20. data/lib/hexapdf/font/cmap.rb +22 -3
  21. data/lib/hexapdf/font_loader/from_configuration.rb +1 -1
  22. data/lib/hexapdf/font_loader/variant_from_name.rb +72 -0
  23. data/lib/hexapdf/font_loader.rb +1 -0
  24. data/lib/hexapdf/layout/style.rb +5 -4
  25. data/lib/hexapdf/type/acro_form/appearance_generator.rb +11 -76
  26. data/lib/hexapdf/type/acro_form/button_field.rb +7 -5
  27. data/lib/hexapdf/type/acro_form/field.rb +14 -0
  28. data/lib/hexapdf/type/acro_form/form.rb +70 -8
  29. data/lib/hexapdf/type/acro_form/java_script_actions.rb +649 -0
  30. data/lib/hexapdf/type/acro_form/text_field.rb +90 -0
  31. data/lib/hexapdf/type/acro_form.rb +1 -0
  32. data/lib/hexapdf/type/annotations/widget.rb +1 -1
  33. data/lib/hexapdf/type/resources.rb +2 -1
  34. data/lib/hexapdf/utils.rb +19 -0
  35. data/lib/hexapdf/version.rb +1 -1
  36. data/test/hexapdf/encryption/test_aes.rb +18 -8
  37. data/test/hexapdf/encryption/test_security_handler.rb +17 -0
  38. data/test/hexapdf/encryption/test_standard_security_handler.rb +1 -1
  39. data/test/hexapdf/font/cmap/test_parser.rb +5 -3
  40. data/test/hexapdf/font/test_cmap.rb +8 -0
  41. data/test/hexapdf/font_loader/test_from_configuration.rb +4 -0
  42. data/test/hexapdf/font_loader/test_variant_from_name.rb +34 -0
  43. data/test/hexapdf/test_utils.rb +16 -0
  44. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +31 -47
  45. data/test/hexapdf/type/acro_form/test_button_field.rb +5 -0
  46. data/test/hexapdf/type/acro_form/test_field.rb +11 -0
  47. data/test/hexapdf/type/acro_form/test_form.rb +80 -0
  48. data/test/hexapdf/type/acro_form/test_java_script_actions.rb +327 -0
  49. data/test/hexapdf/type/acro_form/test_text_field.rb +62 -0
  50. data/test/hexapdf/type/test_resources.rb +5 -0
  51. metadata +8 -2
@@ -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
@@ -270,7 +270,7 @@ module HexaPDF
270
270
  if (rev_index = data.shift)
271
271
  rev_index = rev_index.to_i - 1
272
272
  if rev_index < 0 || rev_index >= @doc.revisions.count
273
- $stderr.puts("Error: Invalid revision numer specified")
273
+ $stderr.puts("Error: Invalid revision number specified")
274
274
  next
275
275
  end
276
276
  length = 0
@@ -0,0 +1,215 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2024 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ require 'hexapdf/cli/command'
38
+
39
+ module HexaPDF
40
+ module CLI
41
+
42
+ # Shows the space usage of various parts of a PDF file.
43
+ class Usage < Command
44
+
45
+ # Modifies the HexaPDF::PDFData class to store the size information
46
+ module PDFDataExtension
47
+
48
+ # Used to store the size of the indirect object.
49
+ attr_accessor :size
50
+
51
+ # Used to store the size of the object inside the object stream.
52
+ attr_accessor :size_in_object_stream
53
+
54
+ end
55
+
56
+ # Modifies HexaPDF::Parser to retrieve space used by indirect objects.
57
+ module ParserExtension
58
+
59
+ # :nodoc:
60
+ def initialize(*)
61
+ super
62
+ @last_size = nil
63
+ end
64
+
65
+ # :nodoc:
66
+ def load_object(xref_entry)
67
+ super.tap do |obj|
68
+ if xref_entry.type == :compressed
69
+ obj.data.size_in_object_stream = @last_size
70
+ elsif xref_entry.type == :in_use
71
+ obj.data.size = @last_size
72
+ end
73
+ @last_size = nil
74
+ end
75
+ end
76
+
77
+ # :nodoc:
78
+ def parse_indirect_object(offset = nil)
79
+ real_offset = (offset ? @header_offset + offset : @tokenizer.pos)
80
+ result = super
81
+ @last_size = @tokenizer.pos - real_offset
82
+ result
83
+ end
84
+
85
+ # :nodoc:
86
+ def load_compressed_object(xref_entry)
87
+ result = super
88
+ offsets = @object_stream_data[xref_entry.objstm].instance_variable_get(:@offsets)
89
+ @last_size = if xref_entry.pos == offsets.size - 1
90
+ @object_stream_data[xref_entry.objstm].instance_variable_get(:@tokenizer).
91
+ io.size - offsets[xref_entry.pos]
92
+ else
93
+ offsets[xref_entry.pos + 1] - offsets[xref_entry.pos]
94
+ end
95
+ result
96
+ end
97
+
98
+ end
99
+
100
+ def initialize #:nodoc:
101
+ super('usage', takes_commands: false)
102
+ short_desc("Show space usage of various parts of a PDF file")
103
+ long_desc(<<~EOF)
104
+ This command displays some usage statistics of the PDF file, i.e. which parts take which
105
+ approximate space in the file.
106
+
107
+ Each statistic line shows the space used followed by the number of indirect objects in
108
+ parentheses. If some of those objects are in object streams, that number is displayed
109
+ after a slash.
110
+ EOF
111
+
112
+ options.on("--password PASSWORD", "-p", String,
113
+ "The password for decryption. Use - for reading from standard input.") do |pwd|
114
+ @password = (pwd == '-' ? read_password : pwd)
115
+ end
116
+
117
+ @password = nil
118
+ end
119
+
120
+ def execute(file) #:nodoc:
121
+ HexaPDF::Parser.prepend(ParserExtension)
122
+ HexaPDF::PDFData.prepend(PDFDataExtension)
123
+
124
+ with_document(file, password: @password) do |doc|
125
+ # Prepare cache of outline items
126
+ outline_item_cache = {}
127
+ if doc.catalog.key?(:Outlines)
128
+ doc.outline.each_item {|item| outline_item_cache[item] = true }
129
+ outline_item_cache[doc.outline] = true
130
+ end
131
+
132
+ doc.revisions.each.with_index do |rev, index|
133
+ sum = count = 0
134
+ categories = {
135
+ Content: [],
136
+ Files: [],
137
+ Fonts: [],
138
+ Images: [],
139
+ Metadata: [],
140
+ ObjectStreams: [],
141
+ Outline: [],
142
+ XObjects: [],
143
+ }
144
+ puts if index > 0
145
+ puts "Usage information for revision #{index + 1}" if doc.revisions.count > 1
146
+ rev.each do |obj|
147
+ if command_parser.verbosity_info?
148
+ print "(#{obj.oid},#{obj.gen}): #{obj.data.size.to_i}"
149
+ print " (#{obj.data.size_in_object_stream})" if obj.data.size.nil?
150
+ puts
151
+ end
152
+ next unless obj.kind_of?(HexaPDF::Dictionary)
153
+
154
+ case obj.type
155
+ when :Page
156
+ Array(obj[:Contents]).each do |content|
157
+ categories[:Content] << content if object_in_rev?(content, rev)
158
+ end
159
+ when :Font
160
+ categories[:Fonts] << obj
161
+ when :FontDescriptor
162
+ categories[:Fonts] << obj
163
+ [:FontFile, :FontFile2, :FontFile3].each do |name|
164
+ categories[:Fonts] << obj[name] if object_in_rev?(obj[name], rev)
165
+ end
166
+ when :Metadata
167
+ categories[:Metadata] << obj
168
+ when :Filespec
169
+ categories[:Files] << obj
170
+ categories[:Files] << obj.embedded_file_stream if obj.embedded_file?
171
+ when :ObjStm
172
+ categories[:ObjectStreams] << obj
173
+ else
174
+ if obj[:Subtype] == :Image
175
+ categories[:Images] << obj
176
+ elsif obj[:Subtype] == :Form
177
+ categories[:XObjects] << obj
178
+ end
179
+ end
180
+ sum += obj.data.size if obj.data.size
181
+ count += 1
182
+ end
183
+
184
+ # Populate Outline category
185
+ outline_item_cache.reject! do |obj, _val|
186
+ object_in_rev?(obj, rev) && categories[:Outline] << obj
187
+ end
188
+
189
+ categories.each do |name, data|
190
+ next if data.empty?
191
+ object_stream_count = 0
192
+ category_sum = data.sum do |o|
193
+ object_stream_count += 1 unless o.data.size
194
+ o.data.size.to_i
195
+ end
196
+ object_stream_count = object_stream_count > 0 ? "/#{object_stream_count}" : ''
197
+ size = human_readable_file_size(category_sum)
198
+ puts "#{name.to_s.ljust(15)} #{size.rjust(8)} (#{data.count}#{object_stream_count})"
199
+ end
200
+ puts "#{'Total'.ljust(15)} #{human_readable_file_size(sum).rjust(8)} (#{count})"
201
+ end
202
+ end
203
+ end
204
+
205
+ private
206
+
207
+ # Returns +true+ if the +obj+ is in the given +rev+.
208
+ def object_in_rev?(obj, rev)
209
+ obj && rev.object(obj) == obj
210
+ end
211
+
212
+ end
213
+
214
+ end
215
+ end
data/lib/hexapdf/cli.rb CHANGED
@@ -48,6 +48,7 @@ require 'hexapdf/cli/watermark'
48
48
  require 'hexapdf/cli/image2pdf'
49
49
  require 'hexapdf/cli/form'
50
50
  require 'hexapdf/cli/fonts'
51
+ require 'hexapdf/cli/usage'
51
52
  require 'hexapdf/version'
52
53
  require 'hexapdf/document'
53
54
 
@@ -107,6 +108,7 @@ module HexaPDF
107
108
  add_command(HexaPDF::CLI::Image2PDF.new)
108
109
  add_command(HexaPDF::CLI::Form.new)
109
110
  add_command(HexaPDF::CLI::Fonts.new)
111
+ add_command(HexaPDF::CLI::Usage.new)
110
112
  add_command(CmdParse::HelpCommand.new)
111
113
  version_command = CmdParse::VersionCommand.new(add_switches: false)
112
114
  add_command(version_command)
@@ -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: ['Helvetica', variant: :bold], cell: {background_color: 'eee'}}
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
- raise HexaPDF::EncryptionError, "Invalid data for decryption, need 32 + 16*n bytes"
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 and the initialization vector are handled like in #decrypt.
130
- def decryption_fiber(key, source)
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
- raise HexaPDF::EncryptionError, "Invalid data for decryption, need 32 + 16*n bytes"
149
- elsif data.empty?
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
- codepoint = dest.force_encoding(::Encoding::UTF_16BE).ord
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)
@@ -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 # :nodoc:
104
- protected :codespace_ranges, :cid_mapping, :cid_range_mappings, :unicode_mapping
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[code]
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
@@ -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
 
@@ -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") # works only with composer
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: ["Courier", variant: :bold]) # only composer
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, text_overflow: :truncate)
1417
+ # height: 15, overflow: :truncate)
1417
1418
 
1418
1419
  [
1419
1420
  [:font, "raise HexaPDF::Error, 'No font set'"],