hexapdf 0.34.1 → 0.35.1

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 (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
@@ -62,41 +62,41 @@ module HexaPDF
62
62
  # * ISO sizes: A0x4, A0x2, A0-A10, B0-B10, C0-C10
63
63
  # * Letter, Legal, Ledger, Tabloid, Executive
64
64
  PAPER_SIZE = {
65
- A0x4: [0, 0, 4768, 6741].freeze,
66
- A0x2: [0, 0, 3370, 4768].freeze,
67
- A0: [0, 0, 2384, 3370].freeze,
68
- A1: [0, 0, 1684, 2384].freeze,
69
- A2: [0, 0, 1191, 1684].freeze,
70
- A3: [0, 0, 842, 1191].freeze,
71
- A4: [0, 0, 595, 842].freeze,
72
- A5: [0, 0, 420, 595].freeze,
73
- A6: [0, 0, 298, 420].freeze,
74
- A7: [0, 0, 210, 298].freeze,
75
- A8: [0, 0, 147, 210].freeze,
76
- A9: [0, 0, 105, 147].freeze,
77
- A10: [0, 0, 74, 105].freeze,
78
- B0: [0, 0, 2835, 4008].freeze,
79
- B1: [0, 0, 2004, 2835].freeze,
80
- B2: [0, 0, 1417, 2004].freeze,
81
- B3: [0, 0, 1001, 1417].freeze,
82
- B4: [0, 0, 709, 1001].freeze,
83
- B5: [0, 0, 499, 709].freeze,
84
- B6: [0, 0, 354, 499].freeze,
85
- B7: [0, 0, 249, 354].freeze,
86
- B8: [0, 0, 176, 249].freeze,
87
- B9: [0, 0, 125, 176].freeze,
88
- B10: [0, 0, 88, 125].freeze,
89
- C0: [0, 0, 2599, 3677].freeze,
90
- C1: [0, 0, 1837, 2599].freeze,
91
- C2: [0, 0, 1298, 1837].freeze,
92
- C3: [0, 0, 918, 1298].freeze,
93
- C4: [0, 0, 649, 918].freeze,
94
- C5: [0, 0, 459, 649].freeze,
95
- C6: [0, 0, 323, 459].freeze,
96
- C7: [0, 0, 230, 323].freeze,
97
- C8: [0, 0, 162, 230].freeze,
98
- C9: [0, 0, 113, 162].freeze,
99
- C10: [0, 0, 79, 113].freeze,
65
+ A0x4: [0, 0, 4767.874016, 6740.787402].freeze,
66
+ A0x2: [0, 0, 3370.393701, 4767.874016].freeze,
67
+ A0: [0, 0, 2383.937008, 3370.393701].freeze,
68
+ A1: [0, 0, 1683.779528, 2383.937008].freeze,
69
+ A2: [0, 0, 1190.551181, 1683.779528].freeze,
70
+ A3: [0, 0, 841.889764, 1190.551181].freeze,
71
+ A4: [0, 0, 595.275591, 841.889764].freeze,
72
+ A5: [0, 0, 419.527559, 595.275591].freeze,
73
+ A6: [0, 0, 297.637795, 419.527559].freeze,
74
+ A7: [0, 0, 209.76378, 297.637795].freeze,
75
+ A8: [0, 0, 147.401575, 209.76378].freeze,
76
+ A9: [0, 0, 104.88189, 147.401575].freeze,
77
+ A10: [0, 0, 73.700787, 104.88189].freeze,
78
+ B0: [0, 0, 2834.645669, 4008.188976].freeze,
79
+ B1: [0, 0, 2004.094488, 2834.645669].freeze,
80
+ B2: [0, 0, 1417.322835, 2004.094488].freeze,
81
+ B3: [0, 0, 1000.629921, 1417.322835].freeze,
82
+ B4: [0, 0, 708.661417, 1000.629921].freeze,
83
+ B5: [0, 0, 498.897638, 708.661417].freeze,
84
+ B6: [0, 0, 354.330709, 498.897638].freeze,
85
+ B7: [0, 0, 249.448819, 354.330709].freeze,
86
+ B8: [0, 0, 175.748031, 249.448819].freeze,
87
+ B9: [0, 0, 124.724409, 175.748031].freeze,
88
+ B10: [0, 0, 87.874016, 124.724409].freeze,
89
+ C0: [0, 0, 2599.370079, 3676.535433].freeze,
90
+ C1: [0, 0, 1836.850394, 2599.370079].freeze,
91
+ C2: [0, 0, 1298.267717, 1836.850394].freeze,
92
+ C3: [0, 0, 918.425197, 1298.267717].freeze,
93
+ C4: [0, 0, 649.133858, 918.425197].freeze,
94
+ C5: [0, 0, 459.212598, 649.133858].freeze,
95
+ C6: [0, 0, 323.149606, 459.212598].freeze,
96
+ C7: [0, 0, 229.606299, 323.149606].freeze,
97
+ C8: [0, 0, 161.574803, 229.606299].freeze,
98
+ C9: [0, 0, 113.385827, 161.574803].freeze,
99
+ C10: [0, 0, 79.370079, 113.385827].freeze,
100
100
  Letter: [0, 0, 612, 792].freeze,
101
101
  Legal: [0, 0, 612, 1008].freeze,
102
102
  Ledger: [0, 0, 792, 1224].freeze,
@@ -307,16 +307,6 @@ module HexaPDF
307
307
  super
308
308
  container_name = leaf_node_container_name
309
309
 
310
- # All kids entries must be indirect objects
311
- if key?(:Kids)
312
- self[:Kids].each_with_index do |kid, index|
313
- unless kid.kind_of?(HexaPDF::Object) && kid.indirect?
314
- yield("Child entries of sorted tree nodes must be indirect objects", true)
315
- value[:Kids][index] = document.add(kid)
316
- end
317
- end
318
- end
319
-
320
310
  # All keys of the container must be lexically ordered strings and the container must be
321
311
  # correctly formatted
322
312
  if key?(container_name)
@@ -0,0 +1,66 @@
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 'geom2d/utils'
38
+
39
+ module HexaPDF
40
+ module Utils
41
+
42
+ # The precision with which to compare floating point numbers.
43
+ #
44
+ # This is chosen with respect to precision that is used for serializing floating point numbers.
45
+ EPSILON = 1e-6
46
+
47
+ # Best effort of setting Geom2D's precision to the one used by HexaPDF.
48
+ ::Geom2D::Utils.precision = EPSILON
49
+
50
+ module_function
51
+
52
+ private
53
+
54
+ # Compares two floats whether they are equal using the FLOAT_EPSILON precision.
55
+ def float_equal(a, b)
56
+ (a - b).abs < EPSILON
57
+ end
58
+
59
+ # Compares two floats like the <=> operator but using the FLOAT_EPSILON precision for detecting
60
+ # whether they are equal.
61
+ def float_compare(a, b)
62
+ (a - b).abs < EPSILON ? 0 : a <=> b
63
+ end
64
+
65
+ end
66
+ end
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.34.1'
40
+ VERSION = '0.35.1'
41
41
 
42
42
  end
@@ -756,6 +756,28 @@ describe HexaPDF::Content::Canvas do
756
756
  end
757
757
  end
758
758
 
759
+ describe "form" do
760
+ it "uses the context dimensions if none are given" do
761
+ form = @canvas.form
762
+ assert_equal(@canvas.context.box.value, form.box.value)
763
+ end
764
+
765
+ it "uses the provided dimensions" do
766
+ form = @canvas.form(300, 200)
767
+ assert_equal([0, 0, 300, 200], form.box.value)
768
+ end
769
+
770
+ it "yields the canvas for defining the form's content" do
771
+ yielded_canvas = nil
772
+ form = @canvas.form {|canvas| yielded_canvas = canvas }
773
+ assert_equal(form.canvas, yielded_canvas)
774
+ end
775
+
776
+ it "raises an ArgumentError if only one of width/height is provided" do
777
+ assert_raises(ArgumentError) { @canvas.form(20) }
778
+ end
779
+ end
780
+
759
781
  describe "graphic_object" do
760
782
  it "returns a new graphic object given a name" do
761
783
  arc = @canvas.graphic_object(:arc)
@@ -1329,6 +1351,21 @@ describe HexaPDF::Content::Canvas do
1329
1351
  end
1330
1352
  end
1331
1353
 
1354
+ describe "composer" do
1355
+ it "creates a CanvasComposer, yields it and returns it" do
1356
+ comp1 = nil
1357
+ comp2 = @canvas.composer {|composer| comp1 = composer }
1358
+ assert_kind_of(HexaPDF::Content::CanvasComposer, comp1)
1359
+ assert_same(comp1, comp2)
1360
+ assert_same(@canvas, comp1.canvas)
1361
+ end
1362
+
1363
+ it "passes on the margin argument" do
1364
+ comp = @canvas.composer(margin: 20)
1365
+ assert_equal(20, comp.frame.x)
1366
+ end
1367
+ end
1368
+
1332
1369
  describe "color_from_specification "do
1333
1370
  it "accepts a color string" do
1334
1371
  assert_equal([1, 0, 0], @canvas.color_from_specification("red").components)
@@ -0,0 +1,112 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/content/canvas_composer'
5
+ require 'hexapdf/document'
6
+
7
+ describe HexaPDF::Content::CanvasComposer do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @page = @doc.pages.add
11
+ @canvas = @page.canvas
12
+ @composer = @canvas.composer
13
+ end
14
+
15
+ describe "initialize" do
16
+ it "creates the necessary objects like frame for doing the work" do
17
+ assert_equal(@page.box.width, @composer.frame.width)
18
+ assert_equal(@page.box.height, @composer.frame.height)
19
+ end
20
+
21
+ it 'allows specifying a value for the margin' do
22
+ composer = @canvas.composer(margin: [10, 30])
23
+ assert_equal(@page.box.width - 60, composer.frame.width)
24
+ assert_equal(@page.box.height - 20, composer.frame.height)
25
+ end
26
+ end
27
+
28
+ it "provides easy access to the global styles" do
29
+ assert_same(@doc.layout.style(:base), @composer.style(:base))
30
+ end
31
+
32
+ describe "draw_box" do
33
+ def create_box(**kwargs)
34
+ HexaPDF::Layout::Box.new(**kwargs) {}
35
+ end
36
+
37
+ it "draws the box if it completely fits" do
38
+ @composer.draw_box(create_box(height: 100))
39
+ @composer.draw_box(create_box)
40
+ assert_operators(@composer.canvas.contents,
41
+ [[:save_graphics_state],
42
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 741.889764]],
43
+ [:restore_graphics_state],
44
+ [:save_graphics_state],
45
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 0]],
46
+ [:restore_graphics_state]])
47
+ end
48
+
49
+ it "splits the box if possible" do
50
+ @composer.draw_box(create_box(width: 400, style: {position: :float}))
51
+ box = create_box(width: 400, height: 100)
52
+ box.define_singleton_method(:split) do |*|
53
+ [box, HexaPDF::Layout::Box.new(height: 100) {}]
54
+ end
55
+ @composer.draw_box(box)
56
+ assert_operators(@composer.canvas.contents,
57
+ [[:save_graphics_state],
58
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 0]],
59
+ [:restore_graphics_state],
60
+ [:save_graphics_state],
61
+ [:concatenate_matrix, [1, 0, 0, 1, 400, 741.889764]],
62
+ [:restore_graphics_state],
63
+ [:save_graphics_state],
64
+ [:concatenate_matrix, [1, 0, 0, 1, 400, 641.889764]],
65
+ [:restore_graphics_state]])
66
+ end
67
+
68
+ it "finds a new region if splitting doesn't work" do
69
+ @composer.draw_box(create_box(width: 400, height: 100, style: {position: :float}))
70
+ @composer.draw_box(create_box(width: 400, height: 100))
71
+ assert_operators(@composer.canvas.contents,
72
+ [[:save_graphics_state],
73
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 741.889764]],
74
+ [:restore_graphics_state],
75
+ [:save_graphics_state],
76
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 641.889764]],
77
+ [:restore_graphics_state]])
78
+ end
79
+
80
+ it "returns the last drawn box" do
81
+ box = create_box(height: 400)
82
+ assert_same(box, @composer.draw_box(box))
83
+ end
84
+
85
+ it "raises an error if the frame is full" do
86
+ @composer.draw_box(create_box)
87
+ exception = assert_raises(HexaPDF::Error) { @composer.draw_box(create_box(height: 10)) }
88
+ assert_match(/Frame.*full/, exception.message)
89
+ end
90
+
91
+ it "raises an error if a new region cannot be found after splitting" do
92
+ @composer.draw_box(create_box(height: 400))
93
+ exception = assert_raises(HexaPDF::Error) { @composer.draw_box(create_box(height: 500)) }
94
+ assert_match(/Frame.*full/, exception.message)
95
+ end
96
+ end
97
+
98
+ describe "method_missing" do
99
+ it "delegates box methods to @document.layout" do
100
+ box = @composer.column(width: 100)
101
+ assert_equal(100, box.width)
102
+ end
103
+
104
+ it "fails for missing methods that can't be delegated to @document.layout" do
105
+ assert_raises(NameError) { @composer.unknown_box }
106
+ end
107
+ end
108
+
109
+ it "can be asked whether a missing method is supported" do
110
+ assert(@composer.respond_to?(:column))
111
+ end
112
+ end
@@ -110,6 +110,9 @@ end
110
110
  describe HexaPDF::Document::Layout do
111
111
  before do
112
112
  @doc = HexaPDF::Document.new
113
+ @doc.config['font.on_invalid_glyph'] = lambda do |codepoint, invalid_glyph|
114
+ [@doc.fonts.add('ZapfDingbats').decode_codepoint(codepoint)]
115
+ end
113
116
  @layout = @doc.layout
114
117
  end
115
118
 
@@ -177,14 +180,37 @@ describe HexaPDF::Document::Layout do
177
180
  end
178
181
  end
179
182
 
183
+ describe "text_fragments" do
184
+ it "creates an array of text fragments with fallback glyph support" do
185
+ result = @layout.text_fragments("Tomāœ‚")
186
+ assert_equal(2, result.size)
187
+ assert_equal(@doc.fonts.add('ZapfDingbats'), result[1].style.font)
188
+
189
+ @doc.config['font.on_invalid_glyph'] = nil
190
+ assert_equal(1, @layout.text_fragments("Tomāœ‚").size)
191
+ end
192
+
193
+ it "uses the standard rules for creating the style object" do
194
+ @layout.style(:named, font_size: 20)
195
+ result = @layout.text_fragments("Test", style: :named)
196
+ assert_equal(20, result[0].style.font_size)
197
+ end
198
+
199
+ it "optionally assigns the properties to all fragments" do
200
+ result = @layout.text_fragments("Tomāœ‚", properties: {key: :value})
201
+ assert_equal(:value, result[0].properties[:key])
202
+ assert_equal(:value, result[1].properties[:key])
203
+ end
204
+ end
205
+
180
206
  describe "text_box" do
181
207
  it "creates a text box" do
182
- box = @layout.text_box("Test", width: 10, height: 15, properties: {key: :value})
208
+ box = @layout.text_box("Testāœ‚", width: 10, height: 15, properties: {key: :value})
183
209
  assert_equal(10, box.width)
184
210
  assert_equal(15, box.height)
185
211
  assert_same(@doc.fonts.add("Times"), box.style.font)
186
212
  items = box.instance_variable_get(:@items)
187
- assert_equal(1, items.length)
213
+ assert_equal(2, items.length)
188
214
  assert_same(box.style, items.first.style)
189
215
  assert_equal({key: :value}, box.properties)
190
216
  end
@@ -227,20 +253,22 @@ describe HexaPDF::Document::Layout do
227
253
 
228
254
  describe "formatted_text" do
229
255
  it "creates a text box with the given text" do
230
- box = @layout.formatted_text_box(["Test"], width: 10, height: 15)
256
+ box = @layout.formatted_text_box(["Testāœ‚"], width: 10, height: 15)
231
257
  assert_equal(10, box.width)
232
258
  assert_equal(15, box.height)
233
- assert_equal(1, box.instance_variable_get(:@items).length)
259
+ assert_equal(2, box.instance_variable_get(:@items).length)
234
260
  end
235
261
 
236
262
  it "allows setting custom properties on the whole box" do
237
- box = @layout.formatted_text_box(["Test"], properties: {key: :value})
263
+ box = @layout.formatted_text_box([{text: "Test", properties: {key: :novalue}}],
264
+ properties: {key: :value})
238
265
  assert_equal({key: :value}, box.properties)
239
266
  end
240
267
 
241
268
  it "allows using a hash with :text key instead of a simple string" do
242
- box = @layout.formatted_text_box([{text: "Test"}])
269
+ box = @layout.formatted_text_box([{text: "Testāœ‚"}])
243
270
  items = box.instance_variable_get(:@items)
271
+ assert_equal(2, items.length)
244
272
  assert_equal(4, items[0].items.length)
245
273
  end
246
274
 
@@ -259,29 +287,29 @@ describe HexaPDF::Document::Layout do
259
287
  end
260
288
 
261
289
  it "allows using custom style properties for a single part" do
262
- box = @layout.formatted_text_box([{text: "Test", font_size: 20}, "test"], align: :center)
290
+ box = @layout.formatted_text_box([{text: "Test", font_size: 20}, "test"], text_align: :center)
263
291
  items = box.instance_variable_get(:@items)
264
292
  assert_equal(10, box.style.font_size)
265
293
 
266
294
  assert_equal(20, items[0].style.font_size)
267
- assert_equal(:center, items[0].style.align)
295
+ assert_equal(:center, items[0].style.text_align)
268
296
 
269
297
  assert_equal(10, items[1].style.font_size)
270
- assert_equal(:center, items[1].style.align)
298
+ assert_equal(:center, items[1].style.text_align)
271
299
  end
272
300
 
273
301
  it "allows using a custom style as basis for a single part" do
274
302
  box = @layout.formatted_text_box([{text: "Test", style: {font_size: 20}, subscript: true},
275
- "test"], align: :center)
303
+ "test"], text_align: :center)
276
304
  items = box.instance_variable_get(:@items)
277
305
  assert_equal(10, box.style.font_size)
278
306
 
279
307
  assert_equal(20, items[0].style.font_size)
280
- assert_equal(:left, items[0].style.align)
308
+ assert_equal(:left, items[0].style.text_align)
281
309
  assert(items[0].style.subscript)
282
310
 
283
311
  assert_equal(10, items[1].style.font_size)
284
- assert_equal(:center, items[1].style.align)
312
+ assert_equal(:center, items[1].style.text_align)
285
313
  refute(items[1].style.subscript)
286
314
  end
287
315
 
@@ -17,20 +17,20 @@ describe HexaPDF::Document::Pages do
17
17
  describe "create" do
18
18
  it "uses the defaults from the configuration for missing arguments" do
19
19
  page = @doc.pages.create
20
- assert_equal([0, 0, 595, 842], page.box(:media).value)
20
+ assert_equal([0, 0, 595.275591, 841.889764], page.box(:media).value)
21
21
  end
22
22
 
23
23
  it "allows specifying a reference to a predefined page size" do
24
24
  page = @doc.pages.create(media_box: :A3)
25
- assert_equal([0, 0, 842, 1191], page.box(:media).value)
25
+ assert_equal([0, 0, 841.889764, 1190.551181], page.box(:media).value)
26
26
  end
27
27
 
28
28
  it "allows specifying the orientation for a predefined page size" do
29
29
  page = @doc.pages.create(media_box: :A4, orientation: :landscape)
30
- assert_equal([0, 0, 842, 595], page.box(:media).value)
30
+ assert_equal([0, 0, 841.889764, 595.275591], page.box(:media).value)
31
31
 
32
32
  page = @doc.pages.create(orientation: :landscape)
33
- assert_equal([0, 0, 842, 595], page.box(:media).value)
33
+ assert_equal([0, 0, 841.889764, 595.275591], page.box(:media).value)
34
34
  end
35
35
 
36
36
  it "allows using a media box array" do
@@ -54,7 +54,7 @@ describe HexaPDF::Document::Pages do
54
54
  it "adds a new empty page with the given page format" do
55
55
  page = @doc.pages.add(:A4, orientation: :landscape)
56
56
  assert_same(page, @doc.pages[0])
57
- assert_equal([0, 0, 842, 595], @doc.pages[0].box(:media).value)
57
+ assert_equal([0, 0, 841.889764, 595.275591], @doc.pages[0].box(:media).value)
58
58
  end
59
59
 
60
60
  it "adds the given page to the end" do
@@ -301,6 +301,49 @@ describe HexaPDF::Encryption::StandardSecurityHandler do
301
301
  assert_equal([:copy_content, :extract_content, :modify_content], @handler.permissions.sort)
302
302
  end
303
303
 
304
+ test_files = Dir[File.join(TEST_DATA_DIR, 'standard-security-handler', '*.pdf')].sort
305
+ user_password = 'uhexapdf'
306
+ owner_password = 'ohexapdf'
307
+
308
+ describe "decryption_password_type" do
309
+ it "doesn't need a password for encrypted files without a password" do
310
+ file = test_files.find {|name| name =~ /nopwd-aes-256bit-V5.pdf/}
311
+ HexaPDF::Document.open(file) do |doc|
312
+ assert_equal(:none, doc.security_handler.decryption_password_type)
313
+ end
314
+ end
315
+
316
+ it "doesn't need a password for owner encrypted files" do
317
+ file = test_files.find {|name| name =~ /ownerpwd-aes-256bit-V5.pdf/}
318
+ HexaPDF::Document.open(file) do |doc|
319
+ assert_equal(:none, doc.security_handler.decryption_password_type)
320
+ end
321
+ end
322
+
323
+ it "needs the user password for user encrypted files" do
324
+ file = test_files.find {|name| name =~ /userpwd-aes-256bit-V5.pdf/}
325
+ HexaPDF::Document.open(file, decryption_opts: {password: user_password}) do |doc|
326
+ assert_equal(:user, doc.security_handler.decryption_password_type)
327
+ end
328
+ end
329
+
330
+ it "can user either the user or owner password for user+owner encrypted files" do
331
+ file = test_files.find {|name| name =~ /bothpwd-aes-256bit-V5.pdf/}
332
+ HexaPDF::Document.open(file, decryption_opts: {password: user_password}) do |doc|
333
+ assert_equal(:user, doc.security_handler.decryption_password_type)
334
+ end
335
+ HexaPDF::Document.open(file, decryption_opts: {password: owner_password}) do |doc|
336
+ assert_equal(:owner, doc.security_handler.decryption_password_type)
337
+ end
338
+ end
339
+
340
+ it "returns :unknown for loaded or created and then encrypted PDF documents" do
341
+ doc = HexaPDF::Document.new
342
+ doc.encrypt
343
+ assert_equal(:unknown, doc.security_handler.decryption_password_type)
344
+ end
345
+ end
346
+
304
347
  describe "handling of metadata streams" do
305
348
  before do
306
349
  @doc = HexaPDF::Document.new
@@ -8,7 +8,9 @@ describe HexaPDF::Font::InvalidGlyph do
8
8
  font = Object.new
9
9
  font.define_singleton_method(:missing_glyph_id) { 0 }
10
10
  font.define_singleton_method(:full_name) { "Test Roman" }
11
- @glyph = HexaPDF::Font::InvalidGlyph.new(font, "str")
11
+ font_wrapper = Object.new
12
+ font_wrapper.define_singleton_method(:wrapped_font) { font }
13
+ @glyph = HexaPDF::Font::InvalidGlyph.new(font_wrapper, "str")
12
14
  end
13
15
 
14
16
  it "returns the missing glyph id for id/name" do
@@ -27,6 +29,16 @@ describe HexaPDF::Font::InvalidGlyph do
27
29
  refute(@glyph.apply_word_spacing?)
28
30
  end
29
31
 
32
+ it "returns false when asked whether it is valid" do
33
+ refute(@glyph.valid?)
34
+ end
35
+
36
+ it "returns true if the glyph represents a control character" do
37
+ refute(@glyph.control_char?)
38
+ assert(HexaPDF::Font::InvalidGlyph.new(nil, "\n"))
39
+ assert(HexaPDF::Font::InvalidGlyph.new(nil, "\u{8203}"))
40
+ end
41
+
30
42
  it "can represent itself for debug purposes" do
31
43
  assert_equal('#<HexaPDF::Font::InvalidGlyph font="Test Roman" id=0 "str">',
32
44
  @glyph.inspect)
@@ -24,17 +24,29 @@ describe HexaPDF::Font::TrueTypeWrapper do
24
24
  end
25
25
  end
26
26
 
27
+ it "can be asked whether the font is a bold one" do
28
+ refute(@font_wrapper.bold?)
29
+ end
30
+
31
+ it "can be asked whether the font is an italic one" do
32
+ refute(@font_wrapper.italic?)
33
+ end
34
+
27
35
  it "can be asked whether font wil be subset" do
28
36
  assert(@font_wrapper.subset?)
29
37
  refute(HexaPDF::Font::TrueTypeWrapper.new(@doc, @font, subset: false).subset?)
30
38
  end
31
39
 
32
- describe "decode_utf8" do
33
- it "returns an array of glyph objects" do
40
+ describe "decode_*" do
41
+ it "decode_utf8 returns an array of glyph objects" do
34
42
  assert_equal("Test",
35
43
  @font_wrapper.decode_utf8("Test").map {|g| @cmap.gid_to_code(g.id) }.pack('U*'))
36
44
  end
37
45
 
46
+ it "decode_codepoint returns a single glyph object" do
47
+ assert_equal("A", @font_wrapper.decode_codepoint(65).str)
48
+ end
49
+
38
50
  it "invokes font.on_missing_glyph for UTF-8 characters for which no glyph exists" do
39
51
  glyphs = @font_wrapper.decode_utf8("😁")
40
52
  assert_equal(1, glyphs.length)
@@ -54,6 +66,7 @@ describe HexaPDF::Font::TrueTypeWrapper do
54
66
  assert_equal(584, glyph.x_max)
55
67
  assert_equal(696, glyph.y_max)
56
68
  refute(glyph.apply_word_spacing?)
69
+ assert(glyph.valid?)
57
70
  assert_equal('#<HexaPDF::Font::TrueTypeWrapper::Glyph font="Ubuntu-Title" id=17 "0">',
58
71
  glyph.inspect)
59
72
  end
@@ -21,15 +21,33 @@ describe HexaPDF::Font::Type1Wrapper do
21
21
  assert_equal("A", wrapper.encode(wrapper.glyph(:B)))
22
22
  end
23
23
 
24
+ it "can be asked whether the font is a bold one" do
25
+ refute(@times_wrapper.bold?)
26
+ refute(@symbol_wrapper.bold?)
27
+ assert(@doc.fonts.add("Times", variant: :bold).bold?)
28
+ refute(@doc.fonts.add("Helvetica").bold?)
29
+ end
30
+
31
+ it "can be asked whether the font is an italic one" do
32
+ refute(@times_wrapper.italic?)
33
+ refute(@symbol_wrapper.italic?)
34
+ assert(@doc.fonts.add("Times", variant: :italic).italic?)
35
+ assert(@doc.fonts.add("Helvetica", variant: :bold_italic).italic?)
36
+ end
37
+
24
38
  it "returns 1 for the scaling factor" do
25
39
  assert_equal(1, @times_wrapper.scaling_factor)
26
40
  end
27
41
 
28
- describe "decode_utf8" do
29
- it "returns an array of glyph objects" do
42
+ describe "decode_*" do
43
+ it "decode_utf8 returns an array of glyph objects" do
30
44
  assert_equal([:T, :e, :s, :t], @times_wrapper.decode_utf8("Test").map(&:name))
31
45
  end
32
46
 
47
+ it "decode_codepoint returns a single glyph object" do
48
+ assert_equal(:A, @times_wrapper.decode_codepoint(65).name)
49
+ end
50
+
33
51
  it "falls back to the internal font encoding if the Unicode codepoint is not mapped" do
34
52
  assert_equal([:Delta, :Delta], @symbol_wrapper.decode_utf8("Dāˆ†").map(&:name))
35
53
  end
@@ -53,6 +71,7 @@ describe HexaPDF::Font::Type1Wrapper do
53
71
  assert_equal(706, glyph.x_max)
54
72
  assert_equal(674, glyph.y_max)
55
73
  refute(glyph.apply_word_spacing?)
74
+ assert(glyph.valid?)
56
75
  assert_equal('#<HexaPDF::Font::Type1Wrapper::Glyph font="Times Roman" id=:A "A">',
57
76
  glyph.inspect)
58
77
  end
@@ -95,6 +95,13 @@ describe HexaPDF::Layout::Box do
95
95
  assert_equal(100, box.height)
96
96
  end
97
97
 
98
+ it "uses float comparison" do
99
+ box = create_box(width: 50.0000002, height: 49.9999996)
100
+ assert(box.fit(50, 50, @frame))
101
+ assert_equal(50.0000002, box.width)
102
+ assert_equal(49.9999996, box.height)
103
+ end
104
+
98
105
  it "returns false if the box doesn't fit" do
99
106
  box = create_box(width: 101)
100
107
  refute(box.fit(100, 100, @frame))