hexapdf 0.34.1 → 0.35.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -0
  3. data/examples/009-text_layouter_alignment.rb +7 -7
  4. data/examples/010-text_layouter_inline_boxes.rb +1 -1
  5. data/examples/011-text_layouter_line_wrapping.rb +2 -4
  6. data/examples/013-text_layouter_shapes.rb +9 -11
  7. data/examples/014-text_in_polygon.rb +2 -2
  8. data/examples/016-frame_automatic_box_placement.rb +6 -7
  9. data/examples/017-frame_text_flow.rb +2 -2
  10. data/examples/018-composer.rb +5 -6
  11. data/examples/020-column_box.rb +2 -2
  12. data/examples/021-list_box.rb +1 -1
  13. data/examples/027-composer_optional_content.rb +5 -5
  14. data/examples/028-frame_mask_mode.rb +23 -0
  15. data/examples/029-composer_fallback_fonts.rb +22 -0
  16. data/lib/hexapdf/cli/info.rb +1 -0
  17. data/lib/hexapdf/cli/inspect.rb +55 -2
  18. data/lib/hexapdf/composer.rb +2 -2
  19. data/lib/hexapdf/configuration.rb +61 -1
  20. data/lib/hexapdf/content/canvas.rb +63 -0
  21. data/lib/hexapdf/content/canvas_composer.rb +142 -0
  22. data/lib/hexapdf/content.rb +1 -0
  23. data/lib/hexapdf/dictionary.rb +14 -3
  24. data/lib/hexapdf/document/layout.rb +35 -13
  25. data/lib/hexapdf/encryption/standard_security_handler.rb +15 -0
  26. data/lib/hexapdf/error.rb +2 -1
  27. data/lib/hexapdf/font/invalid_glyph.rb +22 -6
  28. data/lib/hexapdf/font/true_type_wrapper.rb +48 -20
  29. data/lib/hexapdf/font/type1_wrapper.rb +48 -24
  30. data/lib/hexapdf/layout/box.rb +11 -8
  31. data/lib/hexapdf/layout/column_box.rb +5 -3
  32. data/lib/hexapdf/layout/frame.rb +77 -39
  33. data/lib/hexapdf/layout/image_box.rb +3 -3
  34. data/lib/hexapdf/layout/list_box.rb +20 -19
  35. data/lib/hexapdf/layout/style.rb +173 -68
  36. data/lib/hexapdf/layout/table_box.rb +3 -3
  37. data/lib/hexapdf/layout/text_box.rb +5 -5
  38. data/lib/hexapdf/layout/text_fragment.rb +50 -0
  39. data/lib/hexapdf/layout/text_layouter.rb +7 -6
  40. data/lib/hexapdf/object.rb +5 -2
  41. data/lib/hexapdf/pdf_array.rb +5 -0
  42. data/lib/hexapdf/type/acro_form/appearance_generator.rb +16 -11
  43. data/lib/hexapdf/utils/sorted_tree_node.rb +0 -10
  44. data/lib/hexapdf/version.rb +1 -1
  45. data/test/hexapdf/content/test_canvas.rb +37 -0
  46. data/test/hexapdf/content/test_canvas_composer.rb +112 -0
  47. data/test/hexapdf/document/test_layout.rb +40 -12
  48. data/test/hexapdf/encryption/test_standard_security_handler.rb +43 -0
  49. data/test/hexapdf/font/test_invalid_glyph.rb +13 -1
  50. data/test/hexapdf/font/test_true_type_wrapper.rb +15 -2
  51. data/test/hexapdf/font/test_type1_wrapper.rb +21 -2
  52. data/test/hexapdf/layout/test_column_box.rb +14 -0
  53. data/test/hexapdf/layout/test_frame.rb +181 -95
  54. data/test/hexapdf/layout/test_list_box.rb +7 -7
  55. data/test/hexapdf/layout/test_style.rb +14 -10
  56. data/test/hexapdf/layout/test_table_box.rb +3 -3
  57. data/test/hexapdf/layout/test_text_box.rb +2 -2
  58. data/test/hexapdf/layout/test_text_fragment.rb +37 -0
  59. data/test/hexapdf/layout/test_text_layouter.rb +10 -10
  60. data/test/hexapdf/test_configuration.rb +49 -0
  61. data/test/hexapdf/test_dictionary.rb +1 -1
  62. data/test/hexapdf/test_object.rb +13 -12
  63. data/test/hexapdf/test_pdf_array.rb +9 -0
  64. data/test/hexapdf/test_writer.rb +3 -3
  65. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +41 -13
  66. data/test/hexapdf/utils/test_sorted_tree_node.rb +1 -1
  67. metadata +7 -3
@@ -36,6 +36,7 @@
36
36
 
37
37
  require 'hexapdf/content/graphics_state'
38
38
  require 'hexapdf/content/operator'
39
+ require 'hexapdf/content/canvas_composer'
39
40
  require 'hexapdf/serializer'
40
41
  require 'hexapdf/utils/math_helpers'
41
42
  require 'hexapdf/utils/graphics_helpers'
@@ -1450,6 +1451,43 @@ module HexaPDF
1450
1451
  self
1451
1452
  end
1452
1453
 
1454
+ # :call-seq:
1455
+ # canvas.form {|form_canvas| block } => form
1456
+ # canvas.form(width, height) {|form_canvas| block } => form
1457
+ #
1458
+ # Creates a reusable Form XObject, yields its canvas and then returns it.
1459
+ #
1460
+ # If no arguments are provided, the bounding box of the form is the same as that of the
1461
+ # context object of this canvas. Otherwise you need to provide the +width+ and +height+ for
1462
+ # the form.
1463
+ #
1464
+ # Once the form has been created, it can be used like an image and drawn mulitple times with
1465
+ # the #xobject method. Note that the created form object is independent of this canvas and its
1466
+ # context object. This means it can also be used with other canvases.
1467
+ #
1468
+ # Examples:
1469
+ #
1470
+ # #>pdf
1471
+ # form = canvas.form do |form_canvas|
1472
+ # form_canvas.fill_color("hp-blue").line_width(5).
1473
+ # rectangle(10, 10, 80, 80).fill_stroke
1474
+ # end
1475
+ # canvas.xobject(form, at: [0, 0])
1476
+ # canvas.xobject(form, width: 50, at: [100, 100])
1477
+ #
1478
+ # See: HexaPDF::Type::Form
1479
+ def form(width = nil, height = nil) # :yield: canvas
1480
+ obj = if width && height
1481
+ context.document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height]})
1482
+ elsif width || height
1483
+ raise ArgumentError, "Both arguments width and height need to be provided"
1484
+ else
1485
+ context.document.add({Type: :XObject, Subtype: :Form, BBox: context.box.value.dup})
1486
+ end
1487
+ yield(obj.canvas) if block_given?
1488
+ obj
1489
+ end
1490
+
1453
1491
  # :call-seq:
1454
1492
  # canvas.graphic_object(obj, **options) => obj
1455
1493
  # canvas.graphic_object(name, **options) => graphic_object
@@ -2535,6 +2573,31 @@ module HexaPDF
2535
2573
  # See: PDF2.0 s8.11
2536
2574
  alias :end_optional_content :end_marked_content_sequence
2537
2575
 
2576
+ # :call-seq:
2577
+ # canvas.composer(margin: 0) {|composer| block } -> composer
2578
+ #
2579
+ # Creates a CanvasComposer object for composing content using high-level document layout
2580
+ # features, yields it, if a block is given, and returns it.
2581
+ #
2582
+ # The +margin+ can be any value allowed by HexaPDF::Layout::Style::Quad#set and defines the
2583
+ # margin that should not be used during composition. For the remaining area of the canvas a
2584
+ # frame object will be created.
2585
+ #
2586
+ # Examples:
2587
+ #
2588
+ # #>pdf
2589
+ # canvas.composer(margin: [10, 30]) do |composer|
2590
+ # composer.image(machu_picchu, height: 30, position: :float)
2591
+ # composer.lorem_ipsum(position: :flow)
2592
+ # end
2593
+ #
2594
+ # See: CanvasComposer, HexaPDF::Document::Layout
2595
+ def composer(margin: 0)
2596
+ composer = CanvasComposer.new(self, margin: margin)
2597
+ yield(composer) if block_given?
2598
+ composer
2599
+ end
2600
+
2538
2601
  # Creates and returns a color object from the given color specification. See #stroke_color for
2539
2602
  # details on the possible color specifications.
2540
2603
  #
@@ -0,0 +1,142 @@
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-2023 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/layout'
38
+
39
+ module HexaPDF
40
+ module Content
41
+
42
+ # The CanvasComposer class allows using the document layout functionality for a single canvas.
43
+ # It works in a similar manner as the HexaPDF::Composer class.
44
+ #
45
+ # See: HexaPDF::Composer, HexaPDF::Document::Layout
46
+ class CanvasComposer
47
+
48
+ # The associated canvas.
49
+ attr_reader :canvas
50
+
51
+ # The associated HexaPDF::Document instance.
52
+ attr_reader :document
53
+
54
+ # The HexaPDF::Layout::Frame instance into which the boxes are laid out.
55
+ attr_reader :frame
56
+
57
+ # Creates a new CanvasComposer instance for the given +canvas+.
58
+ #
59
+ # The +margin+ can be any value allowed by HexaPDF::Layout::Style::Quad#set and defines the
60
+ # margin that should not be used during composition. For the remaining area of the canvas a
61
+ # frame object will be created.
62
+ def initialize(canvas, margin: 0)
63
+ @canvas = canvas
64
+ @document = @canvas.context.document
65
+
66
+ box = @canvas.context.box
67
+ margin = Layout::Style::Quad.new(margin)
68
+ @frame = Layout::Frame.new(box.left + margin.left,
69
+ box.bottom + margin.bottom,
70
+ box.width - margin.left - margin.right,
71
+ box.height - margin.bottom - margin.top,
72
+ context: @canvas.context)
73
+ end
74
+
75
+ # Invokes HexaPDF::Document::Layout#style with the given arguments to create/update and return
76
+ # a style object.
77
+ def style(name, base: :base, **properties)
78
+ @document.layout.style(name, base: base, **properties)
79
+ end
80
+
81
+ # Draws the given HexaPDF::Layout::Box and returns the last drawn box.
82
+ #
83
+ # The box is drawn into the frame. If it doesn't fit, the box is split. If it still doesn't
84
+ # fit, a new region of the frame is determined and then the process starts again.
85
+ #
86
+ # If none or only some parts of the box fit into the frame, an exception is thrown.
87
+ def draw_box(box)
88
+ while true
89
+ result = @frame.fit(box)
90
+ if result.success?
91
+ @frame.draw(@canvas, result)
92
+ break
93
+ elsif @frame.full?
94
+ raise HexaPDF::Error, "Frame for canvas composer is full and box doesn't fit anymore"
95
+ else
96
+ draw_box, box = @frame.split(result)
97
+ if draw_box
98
+ @frame.draw(@canvas, result)
99
+ elsif !@frame.find_next_region
100
+ raise HexaPDF::Error, "Frame for canvas composer is full and box doesn't fit anymore"
101
+ end
102
+ end
103
+ end
104
+ box
105
+ end
106
+
107
+ # Draws any box that can be created using HexaPDF::Document::Layout.
108
+ #
109
+ # This includes all named boxes defined in the 'layout.boxes.map' configuration option.
110
+ #
111
+ # Examples:
112
+ #
113
+ # #>pdf
114
+ # canvas.composer(margin: 10) do |composer|
115
+ # composer.text("Some text", position: :float)
116
+ # composer.image(machu_picchu, height: 30, align: :right)
117
+ # composer.lorem_ipsum(sentences: 1, margin: [0, 0, 5])
118
+ # composer.list(item_spacing: 2) do |list|
119
+ # composer.document.config['layout.boxes.map'].each do |name, klass|
120
+ # list.formatted_text([{text: name.to_s, fill_color: "hp-blue-dark"},
121
+ # {text: "\n#{klass}", font_size: 7}])
122
+ # end
123
+ # end
124
+ # end
125
+ #
126
+ # See: HexaPDF::Document::Layout#box
127
+ def method_missing(name, *args, **kwargs, &block)
128
+ if @document.layout.box_creation_method?(name)
129
+ draw_box(@document.layout.send(name, *args, **kwargs, &block))
130
+ else
131
+ super
132
+ end
133
+ end
134
+
135
+ def respond_to_missing?(name, _private) # :nodoc:
136
+ @document.layout.box_creation_method?(name) || super
137
+ end
138
+
139
+ end
140
+
141
+ end
142
+ end
@@ -51,6 +51,7 @@ module HexaPDF
51
51
  autoload(:Processor, 'hexapdf/content/processor')
52
52
  autoload(:ColorSpace, 'hexapdf/content/color_space')
53
53
  autoload(:Operator, 'hexapdf/content/operator')
54
+ autoload(:CanvasComposer, 'hexapdf/content/canvas_composer')
54
55
 
55
56
  end
56
57
 
@@ -257,14 +257,25 @@ module HexaPDF
257
257
  end
258
258
  end
259
259
 
260
+ # Iterates over all currently set entries and all fields that are required.
261
+ def each_set_key_or_required_field #:yields: name, field
262
+ value.keys.each {|name| yield(name, self.class.field(name)) }
263
+ self.class.each_field do |name, field|
264
+ yield(name, field) if field.required? && !value.key?(name)
265
+ end
266
+ end
267
+
260
268
  # Performs validation tasks based on the currently set keys and defined fields.
261
269
  def perform_validation(&block)
262
270
  super
263
- self.class.each_field do |name, field|
264
- next unless field.required? || value.key?(name)
265
-
271
+ each_set_key_or_required_field do |name, field|
266
272
  obj = key?(name) ? self[name] : nil
267
273
 
274
+ validate_nested(obj, &block)
275
+
276
+ # The checks below need associated field information
277
+ next unless field
278
+
268
279
  # Check that required fields are set
269
280
  if field.required? && obj.nil?
270
281
  yield("Required field #{name} is not set", field.default?)
@@ -260,6 +260,28 @@ module HexaPDF
260
260
  style: retrieve_style(style), **box_options)
261
261
  end
262
262
 
263
+ # Creates an array of HexaPDF::Layout::TextFragment objects for the given +text+.
264
+ #
265
+ # This method uses the configuration option 'font.on_invalid_glyph' to map Unicode characters
266
+ # without a valid glyph in the given font to zero, one or more glyphs in a fallback font.
267
+ #
268
+ # +style+, +style_properties+::
269
+ # The text is styled using the given +style+. This can either be a style name set via
270
+ # #style or anything Layout::Style::create accepts. If any additional +style_properties+
271
+ # are specified, the style is duplicated and the additional styles are applied.
272
+ #
273
+ # +properties+::
274
+ # This can be used to set custom properties on the created text fragments. See
275
+ # Layout::Box#properties for details and usage.
276
+ def text_fragments(text, style: nil, properties: nil, **style_properties)
277
+ style = retrieve_style(style, style_properties)
278
+ fragments = HexaPDF::Layout::TextFragment.create_with_fallback_glyphs(
279
+ text, style, &@document.config['font.on_invalid_glyph']
280
+ )
281
+ fragments.each {|f| f.properties.update(properties) } if properties
282
+ fragments
283
+ end
284
+
263
285
  # Creates a HexaPDF::Layout::TextBox for the given text.
264
286
  #
265
287
  # This method is of the two main methods for creating text boxes, the other being
@@ -300,7 +322,7 @@ module HexaPDF
300
322
  **style_properties)
301
323
  style = retrieve_style(style, style_properties)
302
324
  box_style = (box_style ? retrieve_style(box_style) : style)
303
- box_class_for_name(:text).new(items: [HexaPDF::Layout::TextFragment.create(text, style)],
325
+ box_class_for_name(:text).new(items: text_fragments(text, style: style),
304
326
  width: width, height: height, properties: properties,
305
327
  style: box_style)
306
328
  end
@@ -319,11 +341,11 @@ module HexaPDF
319
341
  # * Hashes can contain any style properties and the following special keys:
320
342
  #
321
343
  # text:: The text to be formatted. If this is set and :box is not, the hash will be
322
- # transformed into a text fragment.
344
+ # transformed into text fragments.
323
345
  #
324
346
  # link:: A URL that should be linked to. If no text is provided but a link, the link is used
325
- # for the text. If this is set and :box is not, the hash will be transformed into a
326
- # text fragment with an appropriate link overlay.
347
+ # for the text. If this is set and :box is not, the hash will be transformed into
348
+ # text fragments with an appropriate link overlay.
327
349
  #
328
350
  # style:: The style to use as base style instead of the style created from the +style+ and
329
351
  # +style_properties+ arguments. This can either be a style name set via #style or
@@ -334,6 +356,8 @@ module HexaPDF
334
356
  #
335
357
  # The final style is used for a created text fragment.
336
358
  #
359
+ # properties:: The custom properties that should be set on the created text fragments.
360
+ #
337
361
  # box:: An inline box to be used. If this is set, the hash will be transformed into an
338
362
  # inline box.
339
363
  #
@@ -379,26 +403,24 @@ module HexaPDF
379
403
  **style_properties)
380
404
  style = retrieve_style(style, style_properties)
381
405
  box_style = (box_style ? retrieve_style(box_style) : style)
382
- data.map! do |item|
406
+ data = data.inject([]) do |result, item|
383
407
  case item
384
408
  when String
385
- HexaPDF::Layout::TextFragment.create(item, style)
409
+ result.concat(text_fragments(item, style: style))
386
410
  when Hash
387
411
  if (args = item.delete(:box))
388
412
  block = item.delete(:block)
389
- inline_box(*args, **item, &block)
413
+ result << inline_box(*args, **item, &block)
390
414
  else
391
415
  link = item.delete(:link)
392
416
  (item[:overlays] ||= []) << [:link, {uri: link}] if link
393
417
  text = item.delete(:text) || link || ""
394
- properties = item.delete(:properties)
418
+ item_properties = item.delete(:properties)
395
419
  frag_style = retrieve_style(item.delete(:style) || style, item)
396
- fragment = HexaPDF::Layout::TextFragment.create(text, frag_style)
397
- fragment.properties.update(properties) if properties
398
- fragment
420
+ result.concat(text_fragments(text, style: frag_style, properties: item_properties))
399
421
  end
400
422
  when HexaPDF::Layout::InlineBox
401
- item
423
+ result << item
402
424
  else
403
425
  raise ArgumentError, "Invalid item of class #{item.class} in data array"
404
426
  end
@@ -506,7 +528,7 @@ module HexaPDF
506
528
  # # row 0 has a grey background and bold text
507
529
  # args[0] = {font: ['Helvetica', variant: :bold], cell: {background_color: 'eee'}}
508
530
  # # text in last column is right aligned
509
- # args[0..-1, -1] = {align: :right}
531
+ # args[0..-1, -1] = {text_align: :right}
510
532
  # end
511
533
  #
512
534
  # See: HexaPDF::Layout::TableBox
@@ -250,6 +250,18 @@ module HexaPDF
250
250
  end
251
251
  end
252
252
 
253
+ # Returns the type of password used for decrypting the PDF document.
254
+ #
255
+ # The return value is one of the following:
256
+ #
257
+ # :none:: No password was needed for decryption.
258
+ # :user:: The provided user password was used for decryption.
259
+ # :owner:: The provided owner password was used for decryption.
260
+ # :unknown:: The document was not decrypted, only encrypted.
261
+ def decryption_password_type
262
+ @decryption_password_type || :unknown
263
+ end
264
+
253
265
  def decrypt(obj) #:nodoc:
254
266
  if dict[:V] >= 4 && obj.type == :Metadata && obj[:Subtype] == :XML && !dict[:EncryptMetadata]
255
267
  obj
@@ -345,10 +357,13 @@ module HexaPDF
345
357
  password = prepare_password(password)
346
358
 
347
359
  if user_password_valid?(prepare_password(''))
360
+ @decryption_password_type = :none
348
361
  encryption_key = compute_user_encryption_key(prepare_password(''))
349
362
  elsif user_password_valid?(password)
363
+ @decryption_password_type = :user
350
364
  encryption_key = compute_user_encryption_key(password)
351
365
  elsif owner_password_valid?(password)
366
+ @decryption_password_type = :owner
352
367
  encryption_key = compute_owner_encryption_key(password)
353
368
  else
354
369
  raise HexaPDF::EncryptionError, "Invalid password specified"
data/lib/hexapdf/error.rb CHANGED
@@ -94,7 +94,8 @@ module HexaPDF
94
94
  end
95
95
 
96
96
  def message # :nodoc:
97
- "No glyph for #{glyph.str.inspect} in font '#{glyph.font.full_name}' found. \n\n" \
97
+ "No glyph for #{glyph.str.inspect} in font '#{glyph.font_wrapper.wrapped_font.full_name}' " \
98
+ "found. \n\n" \
98
99
  "Use the configuration option 'font.on_missing_glyph' to customize missing glyph handling."
99
100
  end
100
101
 
@@ -34,6 +34,8 @@
34
34
  # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
35
  #++
36
36
 
37
+ require 'set'
38
+
37
39
  module HexaPDF
38
40
  module Font
39
41
 
@@ -41,21 +43,21 @@ module HexaPDF
41
43
  # font.
42
44
  class InvalidGlyph
43
45
 
44
- # The associated font object.
45
- attr_reader :font
46
+ # The associated font wrapper object, either a Type1Wrapper or a TrueTypeWrapper.
47
+ attr_reader :font_wrapper
46
48
 
47
49
  # The string that could not be represented as a glyph.
48
50
  attr_reader :str
49
51
 
50
52
  # Creates a new Glyph object.
51
- def initialize(font, str)
52
- @font = font
53
+ def initialize(font_wrapper, str)
54
+ @font_wrapper = font_wrapper
53
55
  @str = str
54
56
  end
55
57
 
56
58
  # Returns the appropriate missing glyph id based on the used font.
57
59
  def id
58
- @font.missing_glyph_id
60
+ @font_wrapper.wrapped_font.missing_glyph_id
59
61
  end
60
62
  alias name id
61
63
 
@@ -73,9 +75,23 @@ module HexaPDF
73
75
  false
74
76
  end
75
77
 
78
+ # Returns +false+ since this is an invalid glyph.
79
+ def valid?
80
+ false
81
+ end
82
+
83
+ # Set of codepoints for text control characters, like tabulator, line separators, non-breaking
84
+ # space etc.
85
+ CONTROL_CHARS = Set.new([9, 10, 11, 12, 13, 133, 8232, 8233, 8203, 173, 160]) #:nodoc:
86
+
87
+ # Returns +true+ if this glyph represents a control character like tabulator or newline.
88
+ def control_char?
89
+ CONTROL_CHARS.include?(str.ord)
90
+ end
91
+
76
92
  #:nodoc:
77
93
  def inspect
78
- "#<#{self.class.name} font=#{@font.full_name.inspect} id=#{id} #{@str.inspect}>"
94
+ "#<#{self.class.name} font=#{@font_wrapper.wrapped_font.full_name.inspect} id=#{id} #{@str.inspect}>"
79
95
  end
80
96
 
81
97
  end
@@ -59,8 +59,8 @@ module HexaPDF
59
59
  # Represents a single glyph of the wrapped font.
60
60
  class Glyph
61
61
 
62
- # The associated font object.
63
- attr_reader :font
62
+ # The associated TrueTypeWrapper object.
63
+ attr_reader :font_wrapper
64
64
 
65
65
  # The glyph ID.
66
66
  attr_reader :id
@@ -69,35 +69,40 @@ module HexaPDF
69
69
  attr_reader :str
70
70
 
71
71
  # Creates a new Glyph object.
72
- def initialize(font, id, str)
73
- @font = font
72
+ def initialize(font_wrapper, id, str)
73
+ @font_wrapper = font_wrapper
74
74
  @id = id
75
75
  @str = str
76
76
  end
77
77
 
78
78
  # Returns the glyph's minimum x coordinate.
79
79
  def x_min
80
- @x_min ||= @font[:glyf][id].x_min * 1000.0 / @font[:head].units_per_em
80
+ @x_min ||= @font_wrapper.wrapped_font[:glyf][id].x_min * 1000.0 /
81
+ @font_wrapper.wrapped_font[:head].units_per_em
81
82
  end
82
83
 
83
84
  # Returns the glyph's maximum x coordinate.
84
85
  def x_max
85
- @x_max ||= @font[:glyf][id].x_max * 1000.0 / @font[:head].units_per_em
86
+ @x_max ||= @font_wrapper.wrapped_font[:glyf][id].x_max * 1000.0 /
87
+ @font_wrapper.wrapped_font[:head].units_per_em
86
88
  end
87
89
 
88
90
  # Returns the glyph's minimum y coordinate.
89
91
  def y_min
90
- @y_min ||= @font[:glyf][id].y_min * 1000.0 / @font[:head].units_per_em
92
+ @y_min ||= @font_wrapper.wrapped_font[:glyf][id].y_min * 1000.0 /
93
+ @font_wrapper.wrapped_font[:head].units_per_em
91
94
  end
92
95
 
93
96
  # Returns the glyph's maximum y coordinate.
94
97
  def y_max
95
- @y_max ||= @font[:glyf][id].y_max * 1000.0 / @font[:head].units_per_em
98
+ @y_max ||= @font_wrapper.wrapped_font[:glyf][id].y_max * 1000.0 /
99
+ @font_wrapper.wrapped_font[:head].units_per_em
96
100
  end
97
101
 
98
102
  # Returns the width of the glyph.
99
103
  def width
100
- @width ||= @font[:hmtx][id].advance_width * 1000.0 / @font[:head].units_per_em
104
+ @width ||= @font_wrapper.wrapped_font[:hmtx][id].advance_width * 1000.0 /
105
+ @font_wrapper.wrapped_font[:head].units_per_em
101
106
  end
102
107
 
103
108
  # Returns +false+ since the word spacing parameter is never applied for multibyte font
@@ -106,9 +111,14 @@ module HexaPDF
106
111
  false
107
112
  end
108
113
 
114
+ # Returns +true+ since this is a valid glyph.
115
+ def valid?
116
+ true
117
+ end
118
+
109
119
  #:nodoc:
110
120
  def inspect
111
- "#<#{self.class.name} font=#{@font.full_name.inspect} id=#{id} #{str.inspect}>"
121
+ "#<#{self.class.name} font=#{@font_wrapper.wrapped_font.full_name.inspect} id=#{id} #{str.inspect}>"
112
122
  end
113
123
 
114
124
  end
@@ -154,6 +164,16 @@ module HexaPDF
154
164
  @scaling_factor ||= 1000.0 / @wrapped_font[:head].units_per_em
155
165
  end
156
166
 
167
+ # Returns +true+ if the font contains bold glyphs.
168
+ def bold?
169
+ @wrapped_font.weight > 500
170
+ end
171
+
172
+ # Returns +true+ if the font contains glyphs with an incline (italic or slant).
173
+ def italic?
174
+ @wrapped_font.italic_angle.to_i != 0
175
+ end
176
+
157
177
  # Returns +true+ if the wrapped TrueType font will be subset.
158
178
  def subset?
159
179
  !@subsetter.nil?
@@ -168,7 +188,7 @@ module HexaPDF
168
188
  def glyph(id, str = nil)
169
189
  @id_to_glyph[id] ||=
170
190
  if id >= 0 && id < @wrapped_font[:maxp].num_glyphs
171
- Glyph.new(@wrapped_font, id, str || (+'' << (@cmap.gid_to_code(id) || 0xFFFD)))
191
+ Glyph.new(self, id, str || (+'' << (@cmap.gid_to_code(id) || 0xFFFD)))
172
192
  else
173
193
  @pdf_object.document.config['font.on_missing_glyph'].call("\u{FFFD}", self)
174
194
  end
@@ -183,19 +203,27 @@ module HexaPDF
183
203
  if id < 0 || id >= @wrapped_font[:maxp].num_glyphs
184
204
  raise HexaPDF::Error, "Glyph ID #{id} is invalid for font '#{@wrapped_font.full_name}'"
185
205
  end
186
- Glyph.new(@wrapped_font, id, string)
206
+ Glyph.new(self, id, string)
187
207
  end
188
208
 
189
209
  # Returns an array of glyph objects representing the characters in the UTF-8 encoded string.
210
+ #
211
+ # See #decode_codepoint for details.
190
212
  def decode_utf8(str)
191
- str.codepoints.map! do |c|
192
- @codepoint_to_glyph[c] ||=
193
- if (gid = @cmap[c])
194
- glyph(gid, +'' << c)
195
- else
196
- @pdf_object.document.config['font.on_missing_glyph'].call(+'' << c, self)
197
- end
198
- end
213
+ str.codepoints.map! {|c| @codepoint_to_glyph[c] || decode_codepoint(c) }
214
+ end
215
+
216
+ # Returns a glyph object for the given Unicode codepoint.
217
+ #
218
+ # The configuration option 'font.on_missing_glyph' is invoked if no glyph for a given
219
+ # codepoint is available.
220
+ def decode_codepoint(codepoint)
221
+ @codepoint_to_glyph[codepoint] ||=
222
+ if (gid = @cmap[codepoint])
223
+ glyph(gid, +'' << codepoint)
224
+ else
225
+ @pdf_object.document.config['font.on_missing_glyph'].call(+'' << codepoint, self)
226
+ end
199
227
  end
200
228
 
201
229
  # Encodes the glyph and returns the code string.