hexapdf 0.34.1 → 0.35.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -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 +4 -3
  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/graphic_object/endpoint_arc.rb +2 -8
  23. data/lib/hexapdf/content.rb +1 -0
  24. data/lib/hexapdf/dictionary.rb +14 -3
  25. data/lib/hexapdf/document/layout.rb +35 -13
  26. data/lib/hexapdf/document.rb +1 -0
  27. data/lib/hexapdf/encryption/standard_security_handler.rb +15 -0
  28. data/lib/hexapdf/error.rb +2 -1
  29. data/lib/hexapdf/font/invalid_glyph.rb +22 -6
  30. data/lib/hexapdf/font/true_type_wrapper.rb +48 -20
  31. data/lib/hexapdf/font/type1_wrapper.rb +48 -24
  32. data/lib/hexapdf/layout/box.rb +14 -10
  33. data/lib/hexapdf/layout/column_box.rb +5 -3
  34. data/lib/hexapdf/layout/frame.rb +78 -40
  35. data/lib/hexapdf/layout/image_box.rb +3 -3
  36. data/lib/hexapdf/layout/list_box.rb +20 -19
  37. data/lib/hexapdf/layout/style.rb +173 -68
  38. data/lib/hexapdf/layout/table_box.rb +3 -3
  39. data/lib/hexapdf/layout/text_box.rb +5 -5
  40. data/lib/hexapdf/layout/text_fragment.rb +50 -0
  41. data/lib/hexapdf/layout/text_layouter.rb +7 -6
  42. data/lib/hexapdf/layout/width_from_polygon.rb +1 -1
  43. data/lib/hexapdf/object.rb +5 -2
  44. data/lib/hexapdf/pdf_array.rb +5 -0
  45. data/lib/hexapdf/type/acro_form/appearance_generator.rb +16 -11
  46. data/lib/hexapdf/type/icon_fit.rb +2 -2
  47. data/lib/hexapdf/type/page.rb +35 -35
  48. data/lib/hexapdf/utils/sorted_tree_node.rb +0 -10
  49. data/lib/hexapdf/utils.rb +66 -0
  50. data/lib/hexapdf/version.rb +1 -1
  51. data/test/hexapdf/content/test_canvas.rb +37 -0
  52. data/test/hexapdf/content/test_canvas_composer.rb +112 -0
  53. data/test/hexapdf/document/test_layout.rb +40 -12
  54. data/test/hexapdf/document/test_pages.rb +5 -5
  55. data/test/hexapdf/encryption/test_standard_security_handler.rb +43 -0
  56. data/test/hexapdf/font/test_invalid_glyph.rb +13 -1
  57. data/test/hexapdf/font/test_true_type_wrapper.rb +15 -2
  58. data/test/hexapdf/font/test_type1_wrapper.rb +21 -2
  59. data/test/hexapdf/layout/test_box.rb +7 -0
  60. data/test/hexapdf/layout/test_column_box.rb +18 -4
  61. data/test/hexapdf/layout/test_frame.rb +181 -95
  62. data/test/hexapdf/layout/test_list_box.rb +7 -7
  63. data/test/hexapdf/layout/test_page_style.rb +5 -5
  64. data/test/hexapdf/layout/test_style.rb +14 -10
  65. data/test/hexapdf/layout/test_table_box.rb +3 -3
  66. data/test/hexapdf/layout/test_text_box.rb +2 -2
  67. data/test/hexapdf/layout/test_text_fragment.rb +37 -0
  68. data/test/hexapdf/layout/test_text_layouter.rb +10 -10
  69. data/test/hexapdf/test_composer.rb +13 -13
  70. data/test/hexapdf/test_configuration.rb +49 -0
  71. data/test/hexapdf/test_dictionary.rb +1 -1
  72. data/test/hexapdf/test_object.rb +13 -12
  73. data/test/hexapdf/test_pdf_array.rb +9 -0
  74. data/test/hexapdf/test_utils.rb +20 -0
  75. data/test/hexapdf/test_writer.rb +3 -3
  76. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +41 -13
  77. data/test/hexapdf/type/test_page.rb +3 -3
  78. data/test/hexapdf/type/test_page_tree_node.rb +1 -1
  79. data/test/hexapdf/utils/test_sorted_tree_node.rb +1 -1
  80. metadata +9 -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
@@ -62,8 +62,7 @@ module HexaPDF
62
62
  # https://web.archive.org/web/20160310153722/https://www.w3.org/TR/SVG/implnote.html).
63
63
  class EndpointArc
64
64
 
65
- EPSILON = 1e-10 # :nodoc:
66
-
65
+ include Utils
67
66
  include Utils::MathHelpers
68
67
 
69
68
  # Creates and configures a new endpoint arc object.
@@ -283,7 +282,7 @@ module HexaPDF
283
282
 
284
283
  # F.6.5.2
285
284
  sqrt = (rxs * rys - rxs * y1ps - rys * x1ps) / (rxs * y1ps + rys * x1ps)
286
- sqrt = 0 if sqrt.abs < EPSILON
285
+ sqrt = 0 if sqrt.abs < Utils::EPSILON
287
286
  sqrt = Math.sqrt(sqrt)
288
287
  sqrt *= -1 unless @large_arc == @clockwise
289
288
  cxp = sqrt * rx * y1p / ry
@@ -303,11 +302,6 @@ module HexaPDF
303
302
  inclination: @inclination, clockwise: @clockwise, max_curves: @max_curves}
304
303
  end
305
304
 
306
- # Compares two float numbers if they are within a certain delta.
307
- def float_equal(a, b)
308
- (a - b).abs < EPSILON
309
- end
310
-
311
305
  # Computes the angle in degrees between the x-axis and the vector.
312
306
  def compute_angle_to_x_axis(vx, vy)
313
307
  (vy < 0 ? -1 : 1) * rad_to_deg(Math.acos(vx / Math.sqrt(vx**2 + vy**2)))
@@ -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
@@ -53,6 +53,7 @@ require 'hexapdf/image_loader'
53
53
  require 'hexapdf/font_loader'
54
54
  require 'hexapdf/layout'
55
55
  require 'hexapdf/digital_signature'
56
+ require 'hexapdf/utils'
56
57
 
57
58
  begin
58
59
  require 'hexapdf/cext'
@@ -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.