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
@@ -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, 742]],
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, 742]],
62
+ [:restore_graphics_state],
63
+ [:save_graphics_state],
64
+ [:concatenate_matrix, [1, 0, 0, 1, 400, 642]],
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, 742]],
74
+ [:restore_graphics_state],
75
+ [:save_graphics_state],
76
+ [:concatenate_matrix, [1, 0, 0, 1, 0, 642]],
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
 
@@ -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
@@ -69,12 +69,26 @@ describe HexaPDF::Layout::ColumnBox do
69
69
  it "respects the set initial width, position #{position}" do
70
70
  box = create_box(children: @text_boxes[0..1], width: 50, style: {position: position})
71
71
  check_box(box, 50, 80)
72
+
73
+ box = create_box(columns: 1, children: @fixed_size_boxes[0..0], width: 50,
74
+ style: {position: position})
75
+ check_box(box, 50, 10)
76
+
77
+ box = create_box(children: @fixed_size_boxes[0..0], width: 110)
78
+ refute(box.fit(@frame.available_width, @frame.available_height, @frame))
72
79
  end
73
80
 
74
81
  it "respects the set initial height, position #{position}" do
75
82
  box = create_box(children: @text_boxes[0..1], height: 50, equal_height: false,
76
83
  style: {position: position})
77
84
  check_box(box, 100, 50)
85
+
86
+ box = create_box(children: @text_boxes[0..1], height: 50, equal_height: true,
87
+ style: {position: position})
88
+ check_box(box, 100, 50)
89
+
90
+ box = create_box(children: @fixed_size_boxes[0..0], height: 110)
91
+ refute(box.fit(@frame.available_width, @frame.available_height, @frame))
78
92
  end
79
93
 
80
94
  it "respects the border and padding around all columns, position #{position}" do