hexapdf 0.22.0 → 0.23.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/lib/hexapdf/cli/form.rb +26 -3
  4. data/lib/hexapdf/cli/inspect.rb +12 -3
  5. data/lib/hexapdf/cli/modify.rb +23 -3
  6. data/lib/hexapdf/composer.rb +24 -2
  7. data/lib/hexapdf/document/destinations.rb +396 -0
  8. data/lib/hexapdf/document.rb +38 -89
  9. data/lib/hexapdf/layout/frame.rb +8 -9
  10. data/lib/hexapdf/layout/style.rb +280 -7
  11. data/lib/hexapdf/layout/text_box.rb +10 -2
  12. data/lib/hexapdf/layout/text_layouter.rb +6 -1
  13. data/lib/hexapdf/revision.rb +8 -1
  14. data/lib/hexapdf/revisions.rb +151 -50
  15. data/lib/hexapdf/task/optimize.rb +21 -11
  16. data/lib/hexapdf/type/acro_form/text_field.rb +8 -0
  17. data/lib/hexapdf/type/catalog.rb +9 -1
  18. data/lib/hexapdf/type/names.rb +13 -0
  19. data/lib/hexapdf/type/xref_stream.rb +2 -1
  20. data/lib/hexapdf/utils/sorted_tree_node.rb +3 -1
  21. data/lib/hexapdf/version.rb +1 -1
  22. data/lib/hexapdf/writer.rb +15 -2
  23. data/test/hexapdf/document/test_destinations.rb +338 -0
  24. data/test/hexapdf/encryption/test_security_handler.rb +2 -2
  25. data/test/hexapdf/layout/test_frame.rb +15 -1
  26. data/test/hexapdf/layout/test_text_box.rb +16 -0
  27. data/test/hexapdf/layout/test_text_layouter.rb +7 -0
  28. data/test/hexapdf/task/test_optimize.rb +17 -4
  29. data/test/hexapdf/test_composer.rb +24 -1
  30. data/test/hexapdf/test_document.rb +30 -133
  31. data/test/hexapdf/test_parser.rb +1 -1
  32. data/test/hexapdf/test_revision.rb +14 -0
  33. data/test/hexapdf/test_revisions.rb +137 -29
  34. data/test/hexapdf/test_writer.rb +43 -14
  35. data/test/hexapdf/type/acro_form/test_text_field.rb +17 -0
  36. data/test/hexapdf/type/test_catalog.rb +8 -0
  37. data/test/hexapdf/type/test_names.rb +20 -0
  38. data/test/hexapdf/type/test_xref_stream.rb +2 -1
  39. data/test/hexapdf/utils/test_sorted_tree_node.rb +11 -1
  40. metadata +5 -2
@@ -0,0 +1,338 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+
6
+ describe HexaPDF::Document::Destinations::Destination do
7
+ def destination(dest)
8
+ HexaPDF::Document::Destinations::Destination.new(dest)
9
+ end
10
+
11
+ it "can be asked whether the referenced page is in a remote document" do
12
+ assert(destination([5, :Fit]).remote?)
13
+ refute(destination([HexaPDF::Dictionary.new({}), :Fit]).remote?)
14
+ end
15
+
16
+ it "returns the page object" do
17
+ assert_equal(:page, destination([:page, :Fit]).page)
18
+ end
19
+
20
+ describe "type :xyz" do
21
+ before do
22
+ @dest = destination([:page, :XYZ, :left, :top, :zoom])
23
+ end
24
+
25
+ it "returns the type of the destination" do
26
+ assert_equal(:xyz, @dest.type)
27
+ end
28
+
29
+ it "returns the argument left" do
30
+ assert_equal(:left, @dest.left)
31
+ end
32
+
33
+ it "returns the argument top" do
34
+ assert_equal(:top, @dest.top)
35
+ end
36
+
37
+ it "returns the argument zoom" do
38
+ assert_equal(:zoom, @dest.zoom)
39
+ end
40
+
41
+ it "raises an error if the bottom and right properties are accessed" do
42
+ assert_raises(HexaPDF::Error) { @dest.bottom }
43
+ assert_raises(HexaPDF::Error) { @dest.right }
44
+ end
45
+ end
46
+
47
+ describe "type :fit_page" do
48
+ before do
49
+ @dest = destination([:page, :Fit])
50
+ end
51
+
52
+ it "returns the type of the destination" do
53
+ assert_equal(:fit_page, @dest.type)
54
+ end
55
+
56
+ it "raises an error if the top, left, bottom, right, zoom properties are accessed" do
57
+ assert_raises(HexaPDF::Error) { @dest.top }
58
+ assert_raises(HexaPDF::Error) { @dest.left }
59
+ assert_raises(HexaPDF::Error) { @dest.bottom }
60
+ assert_raises(HexaPDF::Error) { @dest.right }
61
+ assert_raises(HexaPDF::Error) { @dest.zoom }
62
+ end
63
+ end
64
+
65
+ describe "type :fit_page_horizontal" do
66
+ before do
67
+ @dest = destination([:page, :FitH, :top])
68
+ end
69
+
70
+ it "returns the type of the destination" do
71
+ assert_equal(:fit_page_horizontal, @dest.type)
72
+ end
73
+
74
+ it "returns the argument top" do
75
+ assert_equal(:top, @dest.top)
76
+ end
77
+
78
+ it "raises an error if the left, bottom, right, zoom properties are accessed" do
79
+ assert_raises(HexaPDF::Error) { @dest.left }
80
+ assert_raises(HexaPDF::Error) { @dest.bottom }
81
+ assert_raises(HexaPDF::Error) { @dest.right }
82
+ assert_raises(HexaPDF::Error) { @dest.zoom }
83
+ end
84
+ end
85
+
86
+ describe "type :fit_page_vertical" do
87
+ before do
88
+ @dest = destination([:page, :FitV, :left])
89
+ end
90
+
91
+ it "returns the type of the destination" do
92
+ assert_equal(:fit_page_vertical, @dest.type)
93
+ end
94
+
95
+ it "returns the argument left" do
96
+ assert_equal(:left, @dest.left)
97
+ end
98
+
99
+ it "raises an error if the top, bottom, right, zoom properties are accessed" do
100
+ assert_raises(HexaPDF::Error) { @dest.top }
101
+ assert_raises(HexaPDF::Error) { @dest.bottom }
102
+ assert_raises(HexaPDF::Error) { @dest.right }
103
+ assert_raises(HexaPDF::Error) { @dest.zoom }
104
+ end
105
+ end
106
+
107
+ describe "type :fit_rectangle" do
108
+ before do
109
+ @dest = destination([:page, :FitR, :left, :bottom, :right, :top])
110
+ end
111
+
112
+ it "returns the type of the destination" do
113
+ assert_equal(:fit_rectangle, @dest.type)
114
+ end
115
+
116
+ it "returns the argument left" do
117
+ assert_equal(:left, @dest.left)
118
+ end
119
+
120
+ it "returns the argument top" do
121
+ assert_equal(:top, @dest.top)
122
+ end
123
+
124
+ it "returns the argument right" do
125
+ assert_equal(:right, @dest.right)
126
+ end
127
+
128
+ it "returns the argument bottom" do
129
+ assert_equal(:bottom, @dest.bottom)
130
+ end
131
+
132
+ it "raises an error if the zoom property is accessed" do
133
+ assert_raises(HexaPDF::Error) { @dest.zoom }
134
+ end
135
+ end
136
+
137
+ describe "type :fit_bounding_box" do
138
+ before do
139
+ @dest = destination([:page, :FitB])
140
+ end
141
+
142
+ it "returns the type of the destination" do
143
+ assert_equal(:fit_bounding_box, @dest.type)
144
+ end
145
+
146
+ it "raises an error if the bottom and right properties are accessed" do
147
+ assert_raises(HexaPDF::Error) { @dest.left }
148
+ assert_raises(HexaPDF::Error) { @dest.bottom }
149
+ assert_raises(HexaPDF::Error) { @dest.right }
150
+ assert_raises(HexaPDF::Error) { @dest.top }
151
+ assert_raises(HexaPDF::Error) { @dest.zoom }
152
+ end
153
+ end
154
+
155
+ describe "type :fit_bounding_box_horizontal" do
156
+ before do
157
+ @dest = destination([:page, :FitBH, :top])
158
+ end
159
+
160
+ it "returns the type of the destination" do
161
+ assert_equal(:fit_bounding_box_horizontal, @dest.type)
162
+ end
163
+
164
+ it "returns the argument top" do
165
+ assert_equal(:top, @dest.top)
166
+ end
167
+
168
+ it "raises an error if the left, bottom, right, zoom properties are accessed" do
169
+ assert_raises(HexaPDF::Error) { @dest.left }
170
+ assert_raises(HexaPDF::Error) { @dest.bottom }
171
+ assert_raises(HexaPDF::Error) { @dest.right }
172
+ assert_raises(HexaPDF::Error) { @dest.zoom }
173
+ end
174
+ end
175
+
176
+ describe "type :fit_bounding_box_vertical::" do
177
+ before do
178
+ @dest = destination([:page, :FitBV, :left])
179
+ end
180
+
181
+ it "returns the type of the destination" do
182
+ assert_equal(:fit_bounding_box_vertical, @dest.type)
183
+ end
184
+
185
+ it "returns the argument left" do
186
+ assert_equal(:left, @dest.left)
187
+ end
188
+
189
+ it "raises an error if the left, bottom, right, zoom properties are accessed" do
190
+ assert_raises(HexaPDF::Error) { @dest.top }
191
+ assert_raises(HexaPDF::Error) { @dest.bottom }
192
+ assert_raises(HexaPDF::Error) { @dest.right }
193
+ assert_raises(HexaPDF::Error) { @dest.zoom }
194
+ end
195
+ end
196
+ end
197
+
198
+ describe HexaPDF::Document::Destinations do
199
+ before do
200
+ @doc = HexaPDF::Document.new
201
+ @page = @doc.pages.add
202
+ end
203
+
204
+ describe "create_xyz" do
205
+ it "creates the destination" do
206
+ dest = @doc.destinations.create_xyz(@page, left: 1, top: 2, zoom: 3)
207
+ assert_equal([@page, :XYZ, 1, 2, 3], dest)
208
+ end
209
+
210
+ it "creates the destination and registers it under the given name" do
211
+ dest = @doc.destinations.create_xyz(@page, name: 'xyz')
212
+ assert_equal([@page, :XYZ, nil, nil, nil], @doc.destinations[dest])
213
+ end
214
+ end
215
+
216
+ describe "create_fit_page" do
217
+ it "creates the destination" do
218
+ dest = @doc.destinations.create_fit_page( @page)
219
+ assert_equal([@page, :Fit], dest)
220
+ end
221
+
222
+ it "creates the destination and registers it under the given name" do
223
+ dest = @doc.destinations.create_fit_page(@page, name: 'xyz')
224
+ assert_equal([@page, :Fit], @doc.destinations[dest])
225
+ end
226
+ end
227
+
228
+ describe "create_fit_page_horizontal" do
229
+ it "creates the destination" do
230
+ dest = @doc.destinations.create_fit_page_horizontal(@page, top: 2)
231
+ assert_equal([@page, :FitH, 2], dest)
232
+ end
233
+
234
+ it "creates the destination and registers it under the given name" do
235
+ dest = @doc.destinations.create_fit_page_horizontal(@page, name: 'xyz')
236
+ assert_equal([@page, :FitH, nil], @doc.destinations[dest])
237
+ end
238
+ end
239
+
240
+ describe "create_fit_page_vertical" do
241
+ it "creates the destination" do
242
+ dest = @doc.destinations.create_fit_page_vertical(@page, left: 2)
243
+ assert_equal([@page, :FitV, 2], dest)
244
+ end
245
+
246
+ it "creates the destination and registers it under the given name" do
247
+ dest = @doc.destinations.create_fit_page_vertical(@page, name: 'xyz')
248
+ assert_equal([@page, :FitV, nil], @doc.destinations[dest])
249
+ end
250
+ end
251
+
252
+ describe "create_fit_rectangle" do
253
+ it "creates the destination" do
254
+ dest = @doc.destinations.create_fit_rectangle(@page, left: 1, bottom: 2, right: 3, top: 4)
255
+ assert_equal([@page, :FitR, 1, 2, 3, 4], dest)
256
+ end
257
+
258
+ it "creates the destination and registers it under the given name" do
259
+ dest = @doc.destinations.create_fit_rectangle(@page, name: 'xyz', left: 1, bottom: 2, right: 3, top: 4)
260
+ assert_equal([@page, :FitR, 1, 2, 3, 4], @doc.destinations[dest])
261
+ end
262
+ end
263
+
264
+ describe "create_fit_bounding_box" do
265
+ it "creates the destination" do
266
+ dest = @doc.destinations.create_fit_bounding_box(@page)
267
+ assert_equal([@page, :FitB], dest)
268
+ end
269
+
270
+ it "creates the destination and registers it under the given name" do
271
+ dest = @doc.destinations.create_fit_bounding_box(@page, name: 'xyz')
272
+ assert_equal([@page, :FitB], @doc.destinations[dest])
273
+ end
274
+ end
275
+
276
+ describe "create_fit_bounding_box_horizontal" do
277
+ it "creates the destination" do
278
+ dest = @doc.destinations.create_fit_bounding_box_horizontal(@page, top: 2)
279
+ assert_equal([@page, :FitBH, 2], dest)
280
+ end
281
+
282
+ it "creates the destination and registers it under the given name" do
283
+ dest = @doc.destinations.create_fit_bounding_box_horizontal(@page, name: 'xyz')
284
+ assert_equal([@page, :FitBH, nil], @doc.destinations[dest])
285
+ end
286
+ end
287
+
288
+ describe "create_fit_bounding_box_vertical" do
289
+ it "creates the destination" do
290
+ dest = @doc.destinations.create_fit_bounding_box_vertical(@page, left: 2)
291
+ assert_equal([@page, :FitBV, 2], dest)
292
+ end
293
+
294
+ it "creates the destination and registers it under the given name" do
295
+ dest = @doc.destinations.create_fit_bounding_box_vertical(@page, name: 'xyz')
296
+ assert_equal([@page, :FitBV, nil], @doc.destinations[dest])
297
+ end
298
+ end
299
+
300
+ it "adds a destination array to the destinations name tree and allows to retrieve it" do
301
+ @doc.destinations.add('abc', [:page, :Fit])
302
+ assert_equal([:page, :Fit], @doc.destinations['abc'])
303
+ end
304
+
305
+ it "deletes a named destination" do
306
+ @doc.destinations.add('abc', [:page, :Fit])
307
+ assert(@doc.destinations['abc'])
308
+ @doc.destinations.delete('abc')
309
+ refute(@doc.destinations['abc'])
310
+ end
311
+
312
+ describe "each" do
313
+ before do
314
+ 3.times {|i| @doc.destinations.add("abc#{i}", [:page, :Fit]) }
315
+ end
316
+
317
+ it "returns an enumerator if no block is given" do
318
+ enum = @doc.destinations.each
319
+ assert_equal('abc0', enum.next.first)
320
+ assert_equal('abc1', enum.next.first)
321
+ assert_equal('abc2', enum.next.first)
322
+ assert_raises(StopIteration) { enum.next }
323
+ end
324
+
325
+ it "iterates over all name-destination pairs in order" do
326
+ result = [
327
+ ['abc0', :fit_page],
328
+ ['abc1', :fit_page],
329
+ ['abc2', :fit_page],
330
+ ]
331
+ @doc.destinations.each do |name, dest|
332
+ exp_name, exp_type = result.shift
333
+ assert_equal(exp_name, name)
334
+ assert_equal(exp_type, dest.type)
335
+ end
336
+ end
337
+ end
338
+ end
@@ -302,9 +302,9 @@ describe HexaPDF::Encryption::SecurityHandler do
302
302
  @handler = TestHandler.new(@document)
303
303
 
304
304
  assert_equal("Something",
305
- @handler.decrypt(@document.revisions[0].trailer[:Encrypt])[:Key])
305
+ @handler.decrypt(@document.revisions.all[0].trailer[:Encrypt])[:Key])
306
306
  assert_equal("Otherthing",
307
- @handler.decrypt(@document.revisions[1].trailer[:Encrypt])[:Key])
307
+ @handler.decrypt(@document.revisions.all[1].trailer[:Encrypt])[:Key])
308
308
  end
309
309
 
310
310
  it "defers handling encryption to a Crypt filter is specified" do
@@ -196,8 +196,15 @@ describe HexaPDF::Layout::Frame do
196
196
  [[[10, 10], [110, 10], [110, 60], [60, 60], [60, 110], [10, 110]]])
197
197
  end
198
198
 
199
+ it "draws the box in the center" do
200
+ check_box({width: 50, height: 50, position: :float, position_hint: :center},
201
+ [35, 60],
202
+ [[[10, 10], [110, 10], [110, 110], [85, 110], [85, 60], [35, 60],
203
+ [35, 110], [10, 110]]])
204
+ end
205
+
199
206
  describe "with margin" do
200
- [:left, :right].each do |hint|
207
+ [:left, :center, :right].each do |hint|
201
208
  it "ignores all margins if the box fills the whole frame, with position hint #{hint}" do
202
209
  check_box({margin: 10, position: :float, position_hint: hint},
203
210
  [10, 10], [])
@@ -219,6 +226,13 @@ describe HexaPDF::Layout::Frame do
219
226
  [[[20, 10], [110, 10], [110, 110], [90, 110], [90, 50], [20, 50]]])
220
227
  end
221
228
 
229
+ it "uses the left and the right margin if aligned center" do
230
+ check_box({width: 50, height: 50, margin: 10, position: :float, position_hint: :center},
231
+ [35, 60],
232
+ [[[10, 10], [110, 10], [110, 110], [95, 110], [95, 50], [25, 50],
233
+ [25, 110], [10, 110]]])
234
+ end
235
+
222
236
  it "ignores the right, but not the left margin if aligned right to the frame border" do
223
237
  check_box({width: 50, height: 50, margin: 10, position: :float, position_hint: :right},
224
238
  [60, 60],
@@ -53,6 +53,22 @@ describe HexaPDF::Layout::TextBox do
53
53
  assert_equal(20, box.height)
54
54
  end
55
55
 
56
+ it "uses the whole available width when aligning to the center or right" do
57
+ [:center, :right].each do |align|
58
+ box = create_box([@inline_box], style: {align: align})
59
+ assert(box.fit(100, 100, @frame))
60
+ assert_equal(100, box.width)
61
+ end
62
+ end
63
+
64
+ it "uses the whole available height when vertically aligning to the center or bottom" do
65
+ [:center, :bottom].each do |valign|
66
+ box = create_box([@inline_box], style: {valign: valign})
67
+ assert(box.fit(100, 100, @frame))
68
+ assert_equal(100, box.height)
69
+ end
70
+ end
71
+
56
72
  it "can't fit the text box if the set width is bigger than the available width" do
57
73
  box = create_box([@inline_box], width: 101)
58
74
  refute(box.fit(100, 100, @frame))
@@ -646,6 +646,13 @@ describe HexaPDF::Layout::TextLayouter do
646
646
  assert_equal(100 - 20 * 2 + 20, result.lines[0].y_offset)
647
647
  assert_equal(100, result.height)
648
648
  end
649
+
650
+ it "doesn't vertically align when layouting in variable-width mode" do
651
+ @style.valign = :bottom
652
+ result = @layouter.fit(@items, proc { 40 }, 100)
653
+ assert_equal(result.lines[0].y_max, result.lines[0].y_offset)
654
+ assert_equal(40, result.height)
655
+ end
649
656
  end
650
657
 
651
658
  it "post-processes lines for justification if needed" do
@@ -52,7 +52,7 @@ describe HexaPDF::Task::Optimize do
52
52
  describe "compact" do
53
53
  it "compacts the document" do
54
54
  @doc.task(:optimize, compact: true)
55
- assert_equal(1, @doc.revisions.size)
55
+ assert_equal(1, @doc.revisions.count)
56
56
  assert_equal(2, @doc.each(only_current: false).to_a.size)
57
57
  refute_equal(@obj2, @doc.object(@obj2))
58
58
  refute_equal(@obj3, @doc.object(@obj3))
@@ -81,8 +81,8 @@ describe HexaPDF::Task::Optimize do
81
81
  end
82
82
 
83
83
  it "compacts and deletes xref streams" do
84
- @doc.add({Type: :XRef}, revision: 0)
85
- @doc.add({Type: :XRef}, revision: 1)
84
+ @doc.revisions.all[0].add(@doc.wrap({Type: :XRef}, oid: @doc.revisions.next_oid))
85
+ @doc.revisions.all[1].add(@doc.wrap({Type: :XRef}, oid: @doc.revisions.next_oid))
86
86
  @doc.task(:optimize, compact: true, xref_streams: :delete)
87
87
  assert_no_xrefstms
88
88
  assert_default_deleted
@@ -108,7 +108,9 @@ describe HexaPDF::Task::Optimize do
108
108
  assert_objstms_generated
109
109
  assert_default_deleted
110
110
  assert_nil(@doc.object(objstm).value)
111
- assert_equal(2, @doc.revisions.current.find_all {|obj| obj.type == :ObjStm }.size)
111
+ objstms = @doc.revisions.current.find_all {|obj| obj.type == :ObjStm }
112
+ assert_equal(2, objstms.size)
113
+ assert_equal(400, objstms[0].instance_variable_get(:@objects).size)
112
114
  end
113
115
 
114
116
  it "deletes object and xref streams" do
@@ -158,6 +160,17 @@ describe HexaPDF::Task::Optimize do
158
160
  @doc.task(:optimize, compress_pages: true)
159
161
  assert_equal("10 10 m\nq\nQ\nBI\n/Name 5 ID\ndataEI\n", page.contents)
160
162
  end
163
+
164
+ it "uses parser.on_correctable_error to defer a decision regarding invalid operations" do
165
+ page = @doc.pages.add
166
+ page.contents = "10 20-30 m"
167
+ @doc.task(:optimize, compress_pages: true)
168
+ assert_equal("", page.contents)
169
+
170
+ @doc.config['parser.on_correctable_error'] = proc { true }
171
+ page.contents = "10 20-30 m"
172
+ assert_raises(HexaPDF::Error) { @doc.task(:optimize, compress_pages: true) }
173
+ end
161
174
  end
162
175
 
163
176
  describe "prune_page_resources" do
@@ -90,7 +90,7 @@ describe HexaPDF::Composer do
90
90
  it "creates a new style if it does not exist based on the base argument" do
91
91
  @composer.style(:base, font_size: 20)
92
92
  assert_equal(20, @composer.style(:newstyle, subscript: true).font_size)
93
- refute( @composer.style(:base).subscript)
93
+ refute(@composer.style(:base).subscript)
94
94
  assert_equal(10, @composer.style(:another_new, base: nil).font_size)
95
95
  assert(@composer.style(:yet_another_new, base: :newstyle).subscript)
96
96
  end
@@ -240,6 +240,13 @@ describe HexaPDF::Composer do
240
240
  assert(box.style.subscript)
241
241
  assert_same(@composer.document.images.add(image_path), box.image)
242
242
  end
243
+
244
+ it "allows using a form XObject" do
245
+ form = @composer.document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, 10, 10]})
246
+ @composer.image(form, width: 10)
247
+ assert_equal(796, @composer.y)
248
+ assert_equal(36, @composer.x)
249
+ end
243
250
  end
244
251
 
245
252
  describe "draw_box" do
@@ -318,4 +325,20 @@ describe HexaPDF::Composer do
318
325
  end
319
326
  end
320
327
  end
328
+
329
+ describe "create_stamp" do
330
+ it "creates and returns a form XObject" do
331
+ stamp = @composer.create_stamp(10, 5)
332
+ assert_kind_of(HexaPDF::Type::Form, stamp)
333
+ assert_equal(10, stamp.width)
334
+ assert_equal(5, stamp.height)
335
+ end
336
+
337
+ it "allows using a block to draw on the canvas of the form XObject" do
338
+ stamp = @composer.create_stamp(10, 10) do |canvas|
339
+ canvas.line_width(5)
340
+ end
341
+ assert_equal("5 w\n", stamp.canvas.contents)
342
+ end
343
+ end
321
344
  end