prawn-core 0.7.2 → 0.8.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/Rakefile +1 -1
  2. data/examples/general/background.rb +1 -1
  3. data/examples/general/measurement_units.rb +2 -2
  4. data/examples/general/outlines.rb +50 -0
  5. data/examples/general/repeaters.rb +11 -7
  6. data/examples/general/stamp.rb +6 -6
  7. data/examples/graphics/basic_images.rb +1 -1
  8. data/examples/graphics/curves.rb +1 -1
  9. data/examples/graphics/rounded_polygons.rb +19 -0
  10. data/examples/graphics/rounded_rectangle.rb +20 -0
  11. data/examples/graphics/transformations.rb +52 -0
  12. data/examples/m17n/win_ansi_charset.rb +1 -1
  13. data/examples/text/font_calculations.rb +3 -3
  14. data/examples/text/indent_paragraphs.rb +18 -0
  15. data/examples/text/kerning.rb +4 -4
  16. data/examples/text/rotated.rb +98 -0
  17. data/examples/text/simple_text.rb +3 -3
  18. data/examples/text/simple_text_ttf.rb +1 -1
  19. data/lib/prawn/byte_string.rb +1 -0
  20. data/lib/prawn/core.rb +12 -5
  21. data/lib/prawn/core/object_store.rb +99 -0
  22. data/lib/prawn/core/page.rb +96 -0
  23. data/lib/prawn/core/text.rb +75 -0
  24. data/lib/prawn/document.rb +71 -78
  25. data/lib/prawn/document/annotations.rb +2 -2
  26. data/lib/prawn/document/bounding_box.rb +19 -9
  27. data/lib/prawn/document/column_box.rb +13 -12
  28. data/lib/prawn/document/graphics_state.rb +49 -0
  29. data/lib/prawn/document/internals.rb +5 -40
  30. data/lib/prawn/document/page_geometry.rb +1 -18
  31. data/lib/prawn/document/snapshot.rb +12 -7
  32. data/lib/prawn/errors.rb +18 -0
  33. data/lib/prawn/font.rb +4 -2
  34. data/lib/prawn/font/afm.rb +8 -0
  35. data/lib/prawn/font/dfont.rb +12 -4
  36. data/lib/prawn/font/ttf.rb +9 -0
  37. data/lib/prawn/graphics.rb +66 -9
  38. data/lib/prawn/graphics/color.rb +1 -1
  39. data/lib/prawn/graphics/transformation.rb +156 -0
  40. data/lib/prawn/graphics/transparency.rb +3 -7
  41. data/lib/prawn/images.rb +4 -3
  42. data/lib/prawn/images/png.rb +2 -2
  43. data/lib/prawn/outline.rb +278 -0
  44. data/lib/prawn/pdf_object.rb +5 -3
  45. data/lib/prawn/repeater.rb +25 -13
  46. data/lib/prawn/stamp.rb +6 -29
  47. data/lib/prawn/text.rb +139 -121
  48. data/lib/prawn/text/box.rb +168 -102
  49. data/spec/bounding_box_spec.rb +7 -2
  50. data/spec/document_spec.rb +7 -5
  51. data/spec/font_spec.rb +9 -1
  52. data/spec/graphics_spec.rb +229 -0
  53. data/spec/object_store_spec.rb +5 -5
  54. data/spec/outline_spec.rb +229 -0
  55. data/spec/repeater_spec.rb +18 -1
  56. data/spec/snapshot_spec.rb +7 -7
  57. data/spec/span_spec.rb +6 -2
  58. data/spec/spec_helper.rb +7 -3
  59. data/spec/stamp_spec.rb +13 -0
  60. data/spec/text_at_spec.rb +119 -0
  61. data/spec/text_box_spec.rb +257 -4
  62. data/spec/text_spec.rb +278 -180
  63. data/vendor/pdf-inspector/lib/pdf/inspector/graphics.rb +12 -0
  64. metadata +16 -3
  65. data/lib/prawn/object_store.rb +0 -92
@@ -12,6 +12,14 @@ describe "Font behavior" do
12
12
 
13
13
  end
14
14
 
15
+ describe "#font_size" do
16
+ it "should allow setting font size in DSL style" do
17
+ create_pdf
18
+ @pdf.font_size 20
19
+ @pdf.font_size.should == 20
20
+ end
21
+ end
22
+
15
23
  describe "font style support" do
16
24
  before(:each) { create_pdf }
17
25
 
@@ -115,7 +123,7 @@ describe "Document#page_fonts" do
115
123
  end
116
124
 
117
125
  def page_includes_font?(font)
118
- @pdf.page_fonts.values.map { |e| e.data[:BaseFont] }.include?(font.to_sym)
126
+ @pdf.page.fonts.values.map { |e| e.data[:BaseFont] }.include?(font.to_sym)
119
127
  end
120
128
 
121
129
  def page_should_include_font(font)
@@ -113,7 +113,39 @@ describe "When drawing a curve" do
113
113
  curve = PDF::Inspector::Graphics::Curve.analyze(@pdf.render)
114
114
  curve.coords.should == [100.0, 100.0, 20.0, 90.0, 90.0, 75.0, 50.0, 50.0]
115
115
  end
116
+
116
117
 
118
+ end
119
+
120
+ describe "When drawing a rounded rectangle" do
121
+ before(:each) do
122
+ create_pdf
123
+ @pdf.rounded_rectangle([50, 550], 50, 100, 10)
124
+ curve = PDF::Inspector::Graphics::Curve.analyze(@pdf.render)
125
+ curve_points = []
126
+ curve.coords.each_slice(2) {|p| curve_points << p}
127
+ @original_point = curve_points.shift
128
+ curves = []
129
+ curve_points.each_slice(3) {|c| curves << c}
130
+ line_points = PDF::Inspector::Graphics::Line.analyze(@pdf.render).points
131
+ line_points.shift
132
+ @all_coords = []
133
+ line_points.zip(curves).flatten.each_slice(2) {|p| @all_coords << p }
134
+ @all_coords.unshift @original_point
135
+ end
136
+
137
+ it "should draw a rectangle by connecting lines with rounded bezier curves" do
138
+ @all_coords.should == [[60.0, 550.0],[90.0, 550.0], [95.523, 550.0], [100.0, 545.523], [100.0, 540.0],
139
+ [100.0, 460.0], [100.0, 454.477], [95.523, 450.0], [90.0, 450.0],
140
+ [60.0, 450.0], [54.477, 450.0], [50.0, 454.477], [50.0, 460.0],
141
+ [50.0, 540.0], [50.0, 545.523], [54.477, 550.0], [60.0, 550.0]]
142
+ end
143
+
144
+ it "should start and end with the same point" do
145
+ @original_point.should == @all_coords.last
146
+ end
147
+
148
+
117
149
  end
118
150
 
119
151
  describe "When drawing an ellipse" do
@@ -207,3 +239,200 @@ describe "When using painting shortcuts" do
207
239
  should.raise(NoMethodError)
208
240
  end
209
241
  end
242
+
243
+ describe "When using graphics states" do
244
+ before(:each) { create_pdf }
245
+
246
+ it "should add the right content on save_graphics_state" do
247
+ @pdf.expects(:add_content).with('q')
248
+
249
+ @pdf.save_graphics_state
250
+ end
251
+
252
+ it "should add the right content on restore_graphics_state" do
253
+ @pdf.expects(:add_content).with('Q')
254
+
255
+ @pdf.restore_graphics_state
256
+ end
257
+
258
+ it "should save and restore when save_graphics_state is used with a block" do
259
+ state = sequence "state"
260
+ @pdf.expects(:add_content).with('q').in_sequence(state)
261
+ @pdf.expects(:foo).in_sequence(state)
262
+ @pdf.expects(:add_content).with('Q').in_sequence(state)
263
+
264
+ @pdf.save_graphics_state do
265
+ @pdf.foo
266
+ end
267
+ end
268
+ end
269
+
270
+ describe "When using transformation matrix" do
271
+ before(:each) { create_pdf }
272
+
273
+ # Note: The (approximate) number of significant decimal digits of precision in fractional
274
+ # part is 5 (PDF Reference, Third Edition, p. 706)
275
+
276
+ it "should send the right content on transformation_matrix" do
277
+ @pdf.expects(:add_content).with('1.00000 0.00000 0.12346 -1.00000 5.50000 20.00000 cm')
278
+ @pdf.transformation_matrix 1, 0, 0.123456789, -1.0, 5.5, 20
279
+ end
280
+
281
+ it "should use fixed digits with very small number" do
282
+ values = Array.new(6, 0.000000000001)
283
+ string = Array.new(6, "0.00000").join " "
284
+ @pdf.expects(:add_content).with("#{string} cm")
285
+ @pdf.transformation_matrix *values
286
+ end
287
+
288
+ it "should be received by the inspector" do
289
+ @pdf.transformation_matrix 1, 0, 0, -1, 5.5, 20
290
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(@pdf.render)
291
+ matrices.matrices.should == [[1, 0, 0, -1, 5.5, 20]]
292
+ end
293
+
294
+ it "should save the graphics state inside the given block" do
295
+ values = Array.new(6, 0.000000000001)
296
+ string = Array.new(6, "0.00000").join " "
297
+ process = sequence "process"
298
+
299
+ @pdf.expects(:save_graphics_state).with().in_sequence(process)
300
+ @pdf.expects(:add_content).with("#{string} cm").in_sequence(process)
301
+ @pdf.expects(:do_something).with().in_sequence(process)
302
+ @pdf.expects(:restore_graphics_state).with().in_sequence(process)
303
+ @pdf.transformation_matrix(*values) do
304
+ @pdf.do_something
305
+ end
306
+ end
307
+
308
+ end
309
+
310
+ describe "When using transformations shortcuts" do
311
+ before(:each) do
312
+ create_pdf
313
+ @x, @y = 12, 54.32
314
+ @angle = 12.32
315
+ @cos = Math.cos(@angle * Math::PI / 180)
316
+ @sin = Math.sin(@angle * Math::PI / 180)
317
+ @factor = 0.12
318
+ end
319
+
320
+ describe "#rotate" do
321
+ it "should rotate" do
322
+ @pdf.expects(:transformation_matrix).with(@cos, @sin, -@sin, @cos, 0, 0)
323
+ @pdf.rotate(@angle)
324
+ end
325
+ end
326
+
327
+ describe "#rotate with :origin option" do
328
+ it "should rotate around the origin" do
329
+ x_prime = @x * @cos - @y * @sin
330
+ y_prime = @x * @sin + @y * @cos
331
+
332
+ @pdf.rotate(@angle, :origin => [@x, @y]) { @pdf.text('hello world') }
333
+
334
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(@pdf.render)
335
+ matrices.matrices[0].should == [1, 0, 0, 1,
336
+ reduce_precision(@x - x_prime),
337
+ reduce_precision(@y - y_prime)]
338
+ matrices.matrices[1].should == [reduce_precision(@cos),
339
+ reduce_precision(@sin),
340
+ reduce_precision(-@sin),
341
+ reduce_precision(@cos), 0, 0]
342
+ end
343
+
344
+ it "should rotate around the origin in a document with a margin" do
345
+ @pdf = Prawn::Document.new
346
+
347
+ @pdf.rotate(@angle, :origin => [@x, @y]) { @pdf.text('hello world') }
348
+
349
+ x = @x + @pdf.bounds.absolute_left
350
+ y = @y + @pdf.bounds.absolute_bottom
351
+ x_prime = x * @cos - y * @sin
352
+ y_prime = x * @sin + y * @cos
353
+
354
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(@pdf.render)
355
+ matrices.matrices[0].should == [1, 0, 0, 1,
356
+ reduce_precision(x - x_prime),
357
+ reduce_precision(y - y_prime)]
358
+ matrices.matrices[1].should == [reduce_precision(@cos),
359
+ reduce_precision(@sin),
360
+ reduce_precision(-@sin),
361
+ reduce_precision(@cos), 0, 0]
362
+ end
363
+
364
+ it "should raise BlockRequired if no block is given" do
365
+ lambda {
366
+ @pdf.rotate(@angle, :origin => [@x, @y])
367
+ }.should.raise(Prawn::Errors::BlockRequired)
368
+ end
369
+
370
+ def reduce_precision(float)
371
+ ("%.5f" % float).to_f
372
+ end
373
+ end
374
+
375
+ describe "#translate" do
376
+ it "should translate" do
377
+ x, y = 12, 54.32
378
+ @pdf.expects(:transformation_matrix).with(1, 0, 0, 1, x, y)
379
+ @pdf.translate(x, y)
380
+ end
381
+ end
382
+
383
+ describe "#scale" do
384
+ it "should scale" do
385
+ @pdf.expects(:transformation_matrix).with(@factor, 0, 0, @factor, 0, 0)
386
+ @pdf.scale(@factor)
387
+ end
388
+ end
389
+
390
+ describe "#scale with :origin option" do
391
+ it "should scale from the origin" do
392
+ x_prime = @factor * @x
393
+ y_prime = @factor * @y
394
+
395
+ @pdf.scale(@factor, :origin => [@x, @y]) { @pdf.text('hello world') }
396
+
397
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(@pdf.render)
398
+ matrices.matrices[0].should == [1, 0, 0, 1,
399
+ reduce_precision(@x - x_prime),
400
+ reduce_precision(@y - y_prime)]
401
+ matrices.matrices[1].should == [@factor, 0, 0, @factor, 0, 0]
402
+ end
403
+
404
+ it "should scale from the origin in a document with a margin" do
405
+ @pdf = Prawn::Document.new
406
+ x = @x + @pdf.bounds.absolute_left
407
+ y = @y + @pdf.bounds.absolute_bottom
408
+ x_prime = @factor * x
409
+ y_prime = @factor * y
410
+
411
+ @pdf.scale(@factor, :origin => [@x, @y]) { @pdf.text('hello world') }
412
+
413
+ matrices = PDF::Inspector::Graphics::Matrix.analyze(@pdf.render)
414
+ matrices.matrices[0].should == [1, 0, 0, 1,
415
+ reduce_precision(x - x_prime),
416
+ reduce_precision(y - y_prime)]
417
+ matrices.matrices[1].should == [@factor, 0, 0, @factor, 0, 0]
418
+ end
419
+
420
+ it "should raise BlockRequired if no block is given" do
421
+ lambda {
422
+ @pdf.scale(@factor, :origin => [@x, @y])
423
+ }.should.raise(Prawn::Errors::BlockRequired)
424
+ end
425
+
426
+ def reduce_precision(float)
427
+ ("%.5f" % float).to_f
428
+ end
429
+ end
430
+
431
+ # describe "skew" do
432
+ # it "should skew" do
433
+ # a, b = 30, 50.2
434
+ # @pdf.expects(:transformation_matrix).with(1, Math.tan(a * Math::PI / 180), Math.tan(b * Math::PI / 180), 1, 0, 0)
435
+ # @pdf.skew(a, b)
436
+ # end
437
+ # end
438
+ end
@@ -4,11 +4,11 @@ require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")
4
4
 
5
5
  describe "Prawn::ObjectStore" do
6
6
  before(:each) do
7
- @store = Prawn::ObjectStore.new
7
+ @store = Prawn::Core::ObjectStore.new
8
8
  end
9
9
 
10
10
  it "should create required roots by default, including info passed to new" do
11
- store = Prawn::ObjectStore.new(:Test => 3)
11
+ store = Prawn::Core::ObjectStore.new(:Test => 3)
12
12
  store.size.should == 3 # 3 default roots
13
13
  store.info.data[:Test].should == 3
14
14
  store.pages.data[:Count].should == 0
@@ -43,7 +43,7 @@ end
43
43
 
44
44
  describe "Prawn::ObjectStore#compact" do
45
45
  it "should do nothing to an ObjectStore with all live refs" do
46
- store = Prawn::ObjectStore.new
46
+ store = Prawn::Core::ObjectStore.new
47
47
  store.info.data[:Blah] = store.ref(:some => "structure")
48
48
  old_size = store.size
49
49
  store.compact
@@ -52,7 +52,7 @@ describe "Prawn::ObjectStore#compact" do
52
52
  end
53
53
 
54
54
  it "should remove dead objects, renumbering live objects from 1" do
55
- store = Prawn::ObjectStore.new
55
+ store = Prawn::Core::ObjectStore.new
56
56
  store.ref(:some => "structure")
57
57
  old_size = store.size
58
58
  store.compact
@@ -62,7 +62,7 @@ describe "Prawn::ObjectStore#compact" do
62
62
  end
63
63
 
64
64
  it "should detect and remove dead objects that were once live" do
65
- store = Prawn::ObjectStore.new
65
+ store = Prawn::Core::ObjectStore.new
66
66
  store.info.data[:Blah] = store.ref(:some => "structure")
67
67
  store.info.data[:Blah] = :overwritten
68
68
  old_size = store.size
@@ -0,0 +1,229 @@
1
+ require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")
2
+
3
+ describe "Outline" do
4
+ before(:each) do
5
+ @pdf = Prawn::Document.new() do
6
+ text "Page 1. This is the first Chapter. "
7
+ start_new_page
8
+ text "Page 2. More in the first Chapter. "
9
+ start_new_page
10
+ define_outline do
11
+ section 'Chapter 1', :page => 1, :closed => true do
12
+ page 1, :title => 'Page 1'
13
+ page 2, :title => 'Page 2'
14
+ end
15
+ end
16
+ end
17
+ end
18
+ describe "#generate_outline" do
19
+ before(:each) do
20
+ render_and_find_objects
21
+ end
22
+
23
+ it "should create a root outline dictionary item" do
24
+ assert_not_nil @outline_root
25
+ end
26
+
27
+ it "should set the first and last top items of the root outline dictionary item" do
28
+ referenced_object(@outline_root[:First]).should == @section_1
29
+ referenced_object(@outline_root[:Last]).should == @section_1
30
+ end
31
+
32
+ describe "#create_outline_item" do
33
+ it "should create outline items for each section and page" do
34
+ [@section_1, @page_1, @page_2].each {|item| assert_not_nil item}
35
+ end
36
+ end
37
+
38
+ describe "#set_relations, #set_variables_for_block, and #reset_parent" do
39
+ it "should link sibling items" do
40
+ referenced_object(@page_1[:Next]).should == @page_2
41
+ referenced_object(@page_2[:Prev]).should == @page_1
42
+ end
43
+
44
+ it "should link child items to parent item" do
45
+ [@page_1, @page_2].each {|page| referenced_object(page[:Parent]).should == @section_1 }
46
+ end
47
+
48
+ it "should set the first and last child items for parent item" do
49
+ referenced_object(@section_1[:First]).should == @page_1
50
+ referenced_object(@section_1[:Last]).should == @page_2
51
+ end
52
+ end
53
+
54
+ describe "#increase_count" do
55
+
56
+ it "should add the count of all descendant items" do
57
+ @outline_root[:Count].should == 3
58
+ @section_1[:Count].should.abs == 2
59
+ @page_1[:Count].should == 0
60
+ @page_2[:Count].should == 0
61
+ end
62
+
63
+ end
64
+
65
+ describe "closed option" do
66
+
67
+ it "should set the item's integer count to negative" do
68
+ @section_1[:Count].should == -2
69
+ end
70
+
71
+ end
72
+
73
+ end
74
+
75
+ describe "#outline.add_section" do
76
+ before(:each) do
77
+ @pdf.start_new_page
78
+ @pdf.text "Page 3. An added section "
79
+ @pdf.outline.add_section do
80
+ section 'Added Section', :page => 3 do
81
+ page 3, :title => 'Page 3'
82
+ end
83
+ end
84
+ render_and_find_objects
85
+ end
86
+
87
+ it "should add new outline items to document" do
88
+ [@section_2, @page_3].each { |item| assert_not_nil item}
89
+ end
90
+
91
+ it "should reset the last items for root outline dictionary" do
92
+ referenced_object(@outline_root[:First]).should == @section_1
93
+ referenced_object(@outline_root[:Last]).should == @section_2
94
+ end
95
+
96
+ it "should reset the next relation for the previous last top level item" do
97
+ referenced_object(@section_1[:Next]).should == @section_2
98
+ end
99
+
100
+ it "should set the previous relation of the addded to section" do
101
+ referenced_object(@section_2[:Prev]).should == @section_1
102
+ end
103
+
104
+ it "should increase the count of root outline dictionary" do
105
+ @outline_root[:Count].should == 5
106
+ end
107
+
108
+ end
109
+
110
+ describe "#outline.insert_section_after" do
111
+ describe "inserting in the middle of another section" do
112
+ before(:each) do
113
+ @pdf.go_to_page 1
114
+ @pdf.start_new_page
115
+ @pdf.text "Inserted Page"
116
+ @pdf.outline.insert_section_after 'Page 1' do
117
+ page page_number, :title => "Inserted Page"
118
+ end
119
+ render_and_find_objects
120
+ end
121
+
122
+ it "should insert new outline items to document" do
123
+ assert_not_nil @inserted_page
124
+ end
125
+
126
+ it "should adjust the count of all ancestors" do
127
+ @outline_root[:Count].should == 4
128
+ @section_1[:Count].should.abs == 3
129
+ end
130
+
131
+ describe "#adjust_relations" do
132
+
133
+ it "should reset the sibling relations of adjoining items to inserted item" do
134
+ referenced_object(@page_1[:Next]).should == @inserted_page
135
+ referenced_object(@page_2[:Prev]).should == @inserted_page
136
+ end
137
+
138
+ it "should set the sibling relation of added item to adjoining items" do
139
+ referenced_object(@inserted_page[:Next]).should == @page_2
140
+ referenced_object(@inserted_page[:Prev]).should == @page_1
141
+ end
142
+
143
+ it "should not affect the first and last relations of parent item" do
144
+ referenced_object(@section_1[:First]).should == @page_1
145
+ referenced_object(@section_1[:Last]).should == @page_2
146
+ end
147
+
148
+ end
149
+
150
+ end
151
+
152
+ describe "inserting at the end of another section" do
153
+ before(:each) do
154
+ @pdf.go_to_page 2
155
+ @pdf.start_new_page
156
+ @pdf.text "Inserted Page"
157
+ @pdf.outline.insert_section_after 'Page 2' do
158
+ page page_number, :title => "Inserted Page"
159
+ end
160
+ render_and_find_objects
161
+ end
162
+
163
+ describe "#adjust_relations" do
164
+
165
+ it "should reset the sibling relations of adjoining item to inserted item" do
166
+ referenced_object(@page_2[:Next]).should == @inserted_page
167
+ end
168
+
169
+ it "should set the sibling relation of added item to adjoining items" do
170
+ assert_nil referenced_object(@inserted_page[:Next])
171
+ referenced_object(@inserted_page[:Prev]).should == @page_2
172
+ end
173
+
174
+ it "should adjust the last relation of parent item" do
175
+ referenced_object(@section_1[:Last]).should == @inserted_page
176
+ end
177
+
178
+ end
179
+ end
180
+
181
+ it "should require an existing title" do
182
+ assert_raise Prawn::Errors::UnknownOutlineTitle do
183
+ @pdf.go_to_page 1
184
+ @pdf.start_new_page
185
+ @pdf.text "Inserted Page"
186
+ @pdf.outline.insert_section_after 'Wrong page' do
187
+ page page_number, :title => "Inserted Page"
188
+ end
189
+ render_and_find_objects
190
+ end
191
+ end
192
+
193
+ end
194
+
195
+ describe "#page" do
196
+ it "should require a title option to be set" do
197
+ assert_raise Prawn::Errors::RequiredOption do
198
+ @pdf = Prawn::Document.new() do
199
+ text "Page 1. This is the first Chapter. "
200
+ define_outline do
201
+ page 1, :title => nil
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
208
+
209
+ def render_and_find_objects
210
+ output = StringIO.new(@pdf.render, 'r+')
211
+ @hash = PDF::Hash.new(output)
212
+ @outline_root = @hash.values.find {|obj| obj.is_a?(Hash) && obj[:Type] == :Outlines}
213
+ @pages = @hash.values.find {|obj| obj.is_a?(Hash) && obj[:Type] == :Pages}[:Kids]
214
+ @section_1 = find_by_title('Chapter 1')
215
+ @page_1 = find_by_title('Page 1')
216
+ @page_2 = find_by_title('Page 2')
217
+ @section_2 = find_by_title('Added Section')
218
+ @page_3 = find_by_title('Page 3')
219
+ @inserted_page = find_by_title('Inserted Page')
220
+ end
221
+
222
+ def find_by_title(title)
223
+ @hash.values.find {|obj| obj.is_a?(Hash) && obj[:Title] == title}
224
+ end
225
+
226
+ def referenced_object(reference)
227
+ @hash[reference]
228
+ end
229
+