hexapdf 0.40.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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'"],