prawn-core 0.7.2 → 0.8.4

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 (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
@@ -11,6 +11,7 @@ require "prawn/graphics/dash"
11
11
  require "prawn/graphics/cap_style"
12
12
  require "prawn/graphics/join_style"
13
13
  require "prawn/graphics/transparency"
14
+ require "prawn/graphics/transformation"
14
15
 
15
16
  module Prawn
16
17
 
@@ -27,9 +28,10 @@ module Prawn
27
28
  include CapStyle
28
29
  include JoinStyle
29
30
  include Transparency
31
+ include Transformation
30
32
 
31
33
  #######################################################################
32
- # Low level drawing operations must translate to absolute coords! #
34
+ # Low level drawing operations must map the point to absolute coords! #
33
35
  #######################################################################
34
36
 
35
37
  # Moves the drawing position to a given point. The point can be
@@ -39,7 +41,7 @@ module Prawn
39
41
  # pdf.move_to(100,50)
40
42
  #
41
43
  def move_to(*point)
42
- x,y = translate(point)
44
+ x,y = map_to_absolute(point)
43
45
  add_content("%.3f %.3f m" % [ x, y ])
44
46
  end
45
47
 
@@ -50,7 +52,7 @@ module Prawn
50
52
  # pdf.line_to(50,50)
51
53
  #
52
54
  def line_to(*point)
53
- x,y = translate(point)
55
+ x,y = map_to_absolute(point)
54
56
  add_content("%.3f %.3f l" % [ x, y ])
55
57
  end
56
58
 
@@ -64,7 +66,7 @@ module Prawn
64
66
  "Bounding points for bezier curve must be specified "+
65
67
  "as :bounds => [[x1,y1],[x2,y2]]"
66
68
 
67
- curve_points = (options[:bounds] << dest).map { |e| translate(e) }
69
+ curve_points = (options[:bounds] << dest).map { |e| map_to_absolute(e) }
68
70
  add_content("%.3f %.3f %.3f %.3f %.3f %.3f c" %
69
71
  curve_points.flatten )
70
72
  end
@@ -75,9 +77,21 @@ module Prawn
75
77
  # pdf.rectangle [300,300], 100, 200
76
78
  #
77
79
  def rectangle(point,width,height)
78
- x,y = translate(point)
80
+ x,y = map_to_absolute(point)
79
81
  add_content("%.3f %.3f %.3f %.3f re" % [ x, y - height, width, height ])
80
82
  end
83
+
84
+ # Draws a rounded rectangle given <tt>point</tt>, <tt>width</tt> and
85
+ # <tt>height</tt> and <tt>radius</tt> for the rounded corner. The rectangle
86
+ # is bounded by its upper-left corner.
87
+ #
88
+ # pdf.rounded_rectangle [300,300], 100, 200, 10
89
+ #
90
+ def rounded_rectangle(point,width,height,radius)
91
+ x, y = point
92
+ rounded_polygon(radius, point, [x + width, y], [x + width, y - height], [x, y - height])
93
+ end
94
+
81
95
 
82
96
  ###########################################################
83
97
  # Higher level functions: May use relative coords #
@@ -221,6 +235,34 @@ module Prawn
221
235
  line_to(*point)
222
236
  end
223
237
  end
238
+
239
+ # Draws a rounded polygon from specified points using the radius to define bezier curves
240
+ #
241
+ # # draws a rounded filled in polygon
242
+ # pdf.fill_and_stroke_rounded_polygon(10, [100, 250], [200, 300], [300, 250],
243
+ # [300, 150], [200, 100], [100, 150])
244
+ def rounded_polygon(radius, *points)
245
+ move_to point_on_line(radius, points[1], points[0])
246
+ sides = points.size
247
+ points << points[0] << points[1]
248
+ (sides).times do |i|
249
+ rounded_vertex(radius, points[i], points[i + 1], points[i + 2])
250
+ end
251
+ end
252
+
253
+
254
+ # Creates a rounded vertex for a line segment used for building a rounded polygon
255
+ # requires a radius to define bezier curve and three points. The first two points define
256
+ # the line segment and the third point helps define the curve for the vertex.
257
+ def rounded_vertex(radius, *points)
258
+ x0,y0,x1,y1,x2,y2 = points.flatten
259
+ radial_point_1 = point_on_line(radius, points[0], points[1])
260
+ bezier_point_1 = point_on_line((radius - radius*KAPPA), points[0], points[1] )
261
+ radial_point_2 = point_on_line(radius, points[2], points[1])
262
+ bezier_point_2 = point_on_line((radius - radius*KAPPA), points[2], points[1])
263
+ line_to(radial_point_1)
264
+ curve_to(radial_point_2, :bounds => [bezier_point_1, bezier_point_2])
265
+ end
224
266
 
225
267
  # Strokes and closes the current path. See Graphic::Color for color details
226
268
  #
@@ -248,17 +290,32 @@ module Prawn
248
290
  yield if block_given?
249
291
  add_content "b"
250
292
  end
251
-
293
+
252
294
  private
253
295
 
254
- def translate(*point)
296
+ def map_to_absolute(*point)
255
297
  x,y = point.flatten
256
298
  [@bounding_box.absolute_left + x, @bounding_box.absolute_bottom + y]
257
299
  end
258
300
 
259
- def translate!(point)
260
- point.replace(translate(point))
301
+ def map_to_absolute!(point)
302
+ point.replace(map_to_absolute(point))
261
303
  end
262
304
 
305
+ def degree_to_rad(angle)
306
+ angle * Math::PI / 180
307
+ end
308
+
309
+ # Returns the coordinates for a point on a line that is a given distance away from the second
310
+ # point defining the line segement
311
+ def point_on_line(distance_from_end, *points)
312
+ x0,y0,x1,y1 = points.flatten
313
+ length = Math.sqrt((x1 - x0)**2 + (y1 - y0)**2)
314
+ p = (length - distance_from_end) / length
315
+ xr = x0 + p*(x1 - x0)
316
+ yr = y0 + p*(y1 - y0)
317
+ [xr, yr]
318
+ end
319
+
263
320
  end
264
321
  end
@@ -5,7 +5,7 @@
5
5
  # Copyright June 2008, Gregory Brown. All Rights Reserved.
6
6
  #
7
7
  # This is free software. Please see the LICENSE and COPYING files for details.
8
- #
8
+
9
9
  module Prawn
10
10
  module Graphics
11
11
  module Color
@@ -0,0 +1,156 @@
1
+ # encoding: utf-8
2
+ #
3
+ # transformation.rb: Implements rotate, translate, skew, scale and a generic
4
+ # transformation_matrix
5
+ #
6
+ # Copyright January 2010, Michael Witrant. All Rights Reserved.
7
+ #
8
+ # This is free software. Please see the LICENSE and COPYING files for details.
9
+
10
+ module Prawn
11
+ module Graphics
12
+ module Transformation
13
+
14
+ # Rotate the user space. If a block is not provided, then you must save
15
+ # and restore the graphics state yourself.
16
+ #
17
+ # == Options
18
+ # <tt>:origin</tt>:: <tt>[number, number]</tt>. The point around which to
19
+ # rotate. A block must be provided if using the :origin
20
+ #
21
+ # raises <tt>Prawn::Errors::BlockRequired</tt> if an :origin option is
22
+ # provided, but no block is given
23
+ #
24
+ # Example without a block:
25
+ #
26
+ # save_graphics_state
27
+ # rotate 30
28
+ # text "rotated text"
29
+ # restore_graphics_state
30
+ #
31
+ # Example with a block: rotating a rectangle around its upper-left corner
32
+ #
33
+ # x = 300
34
+ # y = 300
35
+ # width = 150
36
+ # height = 200
37
+ # angle = 30
38
+ # pdf.rotate(angle, :origin => [x, y]) do
39
+ # pdf.stroke_rectangle([x, y], width, height)
40
+ # end
41
+ #
42
+ def rotate(angle, options={}, &block)
43
+ Prawn.verify_options(:origin, options)
44
+ rad = degree_to_rad(angle)
45
+ cos = Math.cos(rad)
46
+ sin = Math.sin(rad)
47
+ if options[:origin].nil?
48
+ transformation_matrix(cos, sin, -sin, cos, 0, 0, &block)
49
+ else
50
+ raise Prawn::Errors::BlockRequired unless block_given?
51
+ x = options[:origin][0] + bounds.absolute_left
52
+ y = options[:origin][1] + bounds.absolute_bottom
53
+ x_prime = x * cos - y * sin
54
+ y_prime = x * sin + y * cos
55
+ translate(x - x_prime, y - y_prime) do
56
+ transformation_matrix(cos, sin, -sin, cos, 0, 0, &block)
57
+ end
58
+ end
59
+ end
60
+
61
+ # Translate the user space. If a block is not provided, then you must save
62
+ # and restore the graphics state yourself.
63
+ #
64
+ # Example without a block: move the text up and over 10
65
+ #
66
+ # save_graphics_state
67
+ # translate(10, 10)
68
+ # text "scaled text"
69
+ # restore_graphics_state
70
+ #
71
+ # Example with a block: draw a rectangle with its upper-left corner at
72
+ # x + 10, y + 10
73
+ #
74
+ # x = 300
75
+ # y = 300
76
+ # width = 150
77
+ # height = 200
78
+ # pdf.translate(10, 10) do
79
+ # pdf.stroke_rectangle([x, y], width, height)
80
+ # end
81
+ #
82
+ def translate(x, y, &block)
83
+ transformation_matrix(1, 0, 0, 1, x, y, &block)
84
+ end
85
+
86
+ # Scale the user space. If a block is not provided, then you must save
87
+ # and restore the graphics state yourself.
88
+ #
89
+ # == Options
90
+ # <tt>:origin</tt>:: <tt>[number, number]</tt>. The point from which to
91
+ # scale. A block must be provided if using the :origin
92
+ #
93
+ # raises <tt>Prawn::Errors::BlockRequired</tt> if an :origin option is
94
+ # provided, but no block is given
95
+ #
96
+ # Example without a block:
97
+ #
98
+ # save_graphics_state
99
+ # scale 1.5
100
+ # text "scaled text"
101
+ # restore_graphics_state
102
+ #
103
+ # Example with a block: scale a rectangle from its upper-left corner
104
+ #
105
+ # x = 300
106
+ # y = 300
107
+ # width = 150
108
+ # height = 200
109
+ # factor = 1.5
110
+ # pdf.scale(angle, :origin => [x, y]) do
111
+ # pdf.stroke_rectangle([x, y], width, height)
112
+ # end
113
+ #
114
+ def scale(factor, options={}, &block)
115
+ Prawn.verify_options(:origin, options)
116
+ if options[:origin].nil?
117
+ transformation_matrix(factor, 0, 0, factor, 0, 0, &block)
118
+ else
119
+ raise Prawn::Errors::BlockRequired unless block_given?
120
+ x = options[:origin][0] + bounds.absolute_left
121
+ y = options[:origin][1] + bounds.absolute_bottom
122
+ x_prime = factor * x
123
+ y_prime = factor * y
124
+ translate(x - x_prime, y - y_prime) do
125
+ transformation_matrix(factor, 0, 0, factor, 0, 0, &block)
126
+ end
127
+ end
128
+ end
129
+
130
+ # The following definition of skew would only work in a clearly
131
+ # predicatable manner when if the document had no margin. don't provide
132
+ # this shortcut until it behaves in a clearly understood manner
133
+ #
134
+ # def skew(a, b, &block)
135
+ # transformation_matrix(1,
136
+ # Math.tan(degree_to_rad(a)),
137
+ # Math.tan(degree_to_rad(b)),
138
+ # 1, 0, 0, &block)
139
+ # end
140
+
141
+ # Transform the user space (see notes for rotate regarding graphics state)
142
+ # Generally, one would use the rotate, scale, translate, and skew
143
+ # convenience methods instead of calling transformation_matrix directly
144
+ def transformation_matrix(a, b, c, d, e, f)
145
+ values = [a, b, c, d, e, f].map { |x| "%.5f" % x }.join(" ")
146
+ save_graphics_state if block_given?
147
+ add_content "#{values} cm"
148
+ if block_given?
149
+ yield
150
+ restore_graphics_state
151
+ end
152
+ end
153
+
154
+ end
155
+ end
156
+ end
@@ -57,14 +57,10 @@ module Prawn
57
57
  opacity = [[opacity, 0.0].max, 1.0].min
58
58
  stroke_opacity = [[stroke_opacity, 0.0].max, 1.0].min
59
59
 
60
- # push a new graphics context onto the graphics context stack
61
- add_content "q"
60
+ save_graphics_state
62
61
  add_content "/#{opacity_dictionary_name(opacity, stroke_opacity)} gs"
63
-
64
62
  yield if block_given?
65
-
66
- # restore the previous graphics context
67
- add_content "Q"
63
+ restore_graphics_state
68
64
  end
69
65
 
70
66
  private
@@ -94,7 +90,7 @@ module Prawn
94
90
  :obj => dictionary }
95
91
  end
96
92
 
97
- page_ext_gstates.merge!(dictionary_name => dictionary)
93
+ page.ext_gstates.merge!(dictionary_name => dictionary)
98
94
  dictionary_name
99
95
  end
100
96
 
@@ -91,7 +91,7 @@ module Prawn
91
91
  w,h = calc_image_dimensions(info, options)
92
92
 
93
93
  if options[:at]
94
- x,y = translate(options[:at])
94
+ x,y = map_to_absolute(options[:at])
95
95
  else
96
96
  x,y = image_position(w,h,options)
97
97
  move_text_position h
@@ -100,7 +100,7 @@ module Prawn
100
100
  # add a reference to the image object to the current page
101
101
  # resource list and give it a label
102
102
  label = "I#{next_image_id}"
103
- page_xobjects.merge!( label => image_obj )
103
+ page.xobjects.merge!( label => image_obj )
104
104
 
105
105
  # add the image to the current page
106
106
  instruct = "\nq\n%.3f 0 0 %.3f %.3f %.3f cm\n/%s Do\nQ"
@@ -251,7 +251,7 @@ module Prawn
251
251
  # - An array with N elements, where N is two times the number of color
252
252
  # components.
253
253
  rgb = png.transparency[:rgb]
254
- obj.data[:Mask] = rgb.collect { |val| [val,val] }.flatten
254
+ obj.data[:Mask] = rgb.collect { |x| [x,x] }.flatten
255
255
  elsif png.transparency[:indexed]
256
256
  # TODO: broken. I was attempting to us Color Key Masking, but I think
257
257
  # we need to construct an SMask i think. Maybe do it inside
@@ -263,6 +263,7 @@ module Prawn
263
263
  # channel mixed in with the main image data. The PNG class seperates
264
264
  # it out for us and makes it available via the alpha_channel attribute
265
265
  if png.alpha_channel
266
+ min_version 1.4
266
267
  smask_obj = ref!(:Type => :XObject,
267
268
  :Subtype => :Image,
268
269
  :Height => png.height,
@@ -203,8 +203,8 @@ module Prawn
203
203
  # convert the pixel data to seperate strings for colours and alpha
204
204
  color_byte_size = self.colors * self.bits / 8
205
205
  alpha_byte_size = self.bits / 8
206
- pixels.each do |row|
207
- row.each do |pixel|
206
+ pixels.each do |this_row|
207
+ this_row.each do |pixel|
208
208
  @img_data << pixel[0, color_byte_size].pack("C*")
209
209
  @alpha_channel << pixel[color_byte_size, alpha_byte_size].pack("C*")
210
210
  end
@@ -0,0 +1,278 @@
1
+ # encoding: utf-8
2
+ #
3
+ # generates outline dictionary and items for document
4
+ #
5
+ # Author Jonathan Greenberg
6
+
7
+ require 'forwardable'
8
+
9
+ module Prawn
10
+
11
+ class Document
12
+
13
+ # See Outline#define below for documentation
14
+ def define_outline(&block)
15
+ outline.define(&block)
16
+ end
17
+
18
+ # The Outline dictionary (12.3.3) for this document. It is
19
+ # lazily initialized, so that documents that do not have an outline
20
+ # do not incur the additional overhead.
21
+ def outline_root(outline_root)
22
+ @store.root.data[:Outlines] ||= ref!(outline_root)
23
+ end
24
+
25
+ # Lazily instantiates an Outline object for document. This is used as point of entry
26
+ # to methods to build the outline tree.
27
+ def outline
28
+ @outline ||= Outline.new(self)
29
+ end
30
+
31
+ end
32
+
33
+ # The Outline class organizes the outline tree items for the document.
34
+ # Note that the prev and parent instance variables are adjusted while navigating
35
+ # through the nested blocks. These variables along with the presence or absense
36
+ # of blocks are the primary means by which the relations for the various
37
+ # OutlineItems and the OutlineRoot are set. Unfortunately, the best way to
38
+ # understand how this works is to follow the method calls through a real example.
39
+ #
40
+ # Some ideas for the organization of this class were gleaned from name_tree. In
41
+ # particular the way in which the OutlineItems are finally rendered into document
42
+ # objects in PdfObject through a hash.
43
+ #
44
+ class Outline
45
+
46
+ extend Forwardable
47
+ def_delegator :@document, :page_number
48
+
49
+ attr_accessor :parent
50
+ attr_accessor :prev
51
+ attr_accessor :document
52
+ attr_accessor :outline_root
53
+ attr_accessor :items
54
+
55
+ def initialize(document)
56
+ @document = document
57
+ @outline_root = document.outline_root(OutlineRoot.new)
58
+ @parent = outline_root
59
+ @prev = nil
60
+ @items = {}
61
+ end
62
+
63
+ # Defines an outline for the document.
64
+ # The outline is an optional nested index that appears on the side of a PDF
65
+ # document usually with direct links to pages. The outline DSL is defined by nested
66
+ # blocks involving two methods: section and page.
67
+ #
68
+ # section(title, options{}, &block)
69
+ # title: the outline text that appears for the section.
70
+ # options: page - optional integer defining the page number for a destination link.
71
+ # - currently only :FIT destination supported with link to top of page.
72
+ # closed - whether the section should show its nested outline elements.
73
+ # - defaults to false.
74
+ # page(page, options{})
75
+ # page: integer defining the page number for the destination link.
76
+ # currently only :FIT destination supported with link to top of page.
77
+ # set to nil if destination link is not desired.
78
+ # options: title - the outline text that appears for the section.
79
+ # closed - whether the section should show its nested outline elements.
80
+ # - defaults to false.
81
+ #
82
+ # The syntax is best illustrated with an example:
83
+ #
84
+ # Prawn::Document.generate(outlined document) do
85
+ # text "Page 1. This is the first Chapter. "
86
+ # start_new_page
87
+ # text "Page 2. More in the first Chapter. "
88
+ # start_new_page
89
+ # define_outline do
90
+ # section 'Chapter 1', :page => 1, :closed => true do
91
+ # page 1, :title => 'Page 1'
92
+ # page 2, :title => 'Page 2'
93
+ # end
94
+ # end
95
+ # end
96
+ #
97
+ # It should be noted that not defining a title for a page element will raise
98
+ # a RequiredOption error
99
+ #
100
+ def define(&block)
101
+ if block
102
+ block.arity < 1 ? instance_eval(&block) : block[self]
103
+ end
104
+ end
105
+
106
+ # Adds an outine section to the outline tree (see define_outline).
107
+ # Although you will probably choose to exclusively use define_outline so
108
+ # that your outline tree is contained and easy to manage, this method
109
+ # gives you the option to add sections to the outline tree at any point
110
+ # during document generation. Note that the section will be added at the
111
+ # top level at the end of the outline. For more a more flexible API try
112
+ # using outline.insert_section_after.
113
+ #
114
+ # block uses the same DSL syntax as define_outline, for example:
115
+ #
116
+ # outline.add_section do
117
+ # section 'Added Section', :page => 3 do
118
+ # page 3, :title => 'Page 3'
119
+ # end
120
+ # end
121
+ def add_section(&block)
122
+ @parent = outline_root
123
+ @prev = outline_root.data.last
124
+ if block
125
+ block.arity < 1 ? instance_eval(&block) : block[self]
126
+ end
127
+ end
128
+
129
+ # Inserts an outline section to the outline tree (see define_outline).
130
+ # Although you will probably choose to exclusively use define_outline so
131
+ # that your outline tree is contained and easy to manage, this method
132
+ # gives you the option to insert sections to the outline tree at any point
133
+ # during document generation. Unlike outline.add_section, this method allows
134
+ # you to enter a section after any other item at any level in the outline tree.
135
+ # Currently the only way to locate the place of entry is with the title for the
136
+ # item. If your titles names are not unique consider using define_outline.
137
+ #
138
+ # block uses the same DSL syntax as define_outline, for example:
139
+ #
140
+ # go_to_page 2
141
+ # start_new_page
142
+ # text "Inserted Page"
143
+ # outline.insert_section_after :title => 'Page 2' do
144
+ # page page_number, :title => "Inserted Page"
145
+ # end
146
+ #
147
+ def insert_section_after(title, &block)
148
+ @prev = items[title]
149
+ if @prev
150
+ @parent = @prev.data.parent
151
+ nxt = @prev.data.next
152
+ if block
153
+ block.arity < 1 ? instance_eval(&block) : block[self]
154
+ end
155
+ adjust_relations(nxt)
156
+ else
157
+ raise Prawn::Errors::UnknownOutlineTitle,
158
+ "\n No outline item with title: '#{title}' exists in the outline tree"
159
+ end
160
+ end
161
+
162
+ private
163
+
164
+ def section(title, options = {}, &block)
165
+ add_outline_item(title, options, &block)
166
+ end
167
+
168
+ def page(page = nil, options = {})
169
+ if options[:title]
170
+ title = options[:title]
171
+ options[:page] = page
172
+ else
173
+ raise Prawn::Errors::RequiredOption,
174
+ "\nTitle is a required option for page"
175
+ end
176
+ add_outline_item(title, options)
177
+ end
178
+
179
+ def add_outline_item(title, options, &block)
180
+ outline_item = create_outline_item(title, options)
181
+ set_relations(outline_item)
182
+ increase_count
183
+ set_variables_for_block(outline_item, block)
184
+ block.call if block
185
+ reset_parent(outline_item)
186
+ end
187
+
188
+ def create_outline_item(title, options)
189
+ outline_item = OutlineItem.new(title, parent, options)
190
+
191
+ if options[:page]
192
+ page_index = options[:page] - 1
193
+ outline_item.dest = [document.pages[page_index].dictionary, :Fit]
194
+ end
195
+
196
+ outline_item.prev = prev if @prev
197
+ items[title] = document.ref!(outline_item)
198
+ end
199
+
200
+ def set_relations(outline_item)
201
+ prev.data.next = outline_item if prev
202
+ parent.data.first = outline_item unless prev
203
+ parent.data.last = outline_item
204
+ end
205
+
206
+ def increase_count
207
+ counting_parent = parent
208
+ while counting_parent
209
+ counting_parent.data.count += 1
210
+ if counting_parent == outline_root
211
+ counting_parent = nil
212
+ else
213
+ counting_parent = counting_parent.data.parent
214
+ end
215
+ end
216
+ end
217
+
218
+ def set_variables_for_block(outline_item, block)
219
+ self.prev = block ? nil : outline_item
220
+ self.parent = outline_item if block
221
+ end
222
+
223
+ def reset_parent(outline_item)
224
+ if parent == outline_item
225
+ self.prev = outline_item
226
+ self.parent = outline_item.data.parent
227
+ end
228
+ end
229
+
230
+ def adjust_relations(nxt)
231
+ if nxt
232
+ nxt.data.prev = @prev
233
+ @prev.data.next = nxt
234
+ @parent.data.last = nxt
235
+ else
236
+ @parent.data.last = @prev
237
+ end
238
+ end
239
+
240
+ end
241
+
242
+ class OutlineRoot #:nodoc:
243
+ attr_accessor :count, :first, :last
244
+
245
+ def initialize
246
+ @count = 0
247
+ end
248
+
249
+ def to_hash
250
+ {:Type => :Outlines, :Count => count, :First => first, :Last => last}
251
+ end
252
+ end
253
+
254
+ class OutlineItem #:nodoc:
255
+ attr_accessor :count, :first, :last, :next, :prev, :parent, :title, :dest, :closed
256
+
257
+ def initialize(title, parent, options)
258
+ @closed = options[:closed]
259
+ @title = title
260
+ @parent = parent
261
+ @count = 0
262
+ end
263
+
264
+ def to_hash
265
+ hash = { :Title => Prawn::LiteralString.new(title),
266
+ :Parent => parent,
267
+ :Count => closed ? -count : count }
268
+ [{:First => first}, {:Last => last}, {:Next => @next},
269
+ {:Prev => prev}, {:Dest => dest}].each do |h|
270
+ unless h.values.first.nil?
271
+ hash.merge!(h)
272
+ end
273
+ end
274
+ hash
275
+ end
276
+ end
277
+ end
278
+