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
@@ -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
+