hexapdf 0.22.0 → 0.23.0

Sign up to get free protection for your applications and to get access to all the features.
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