prawn-core 0.6.3 → 0.7.1

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 (43) hide show
  1. data/Rakefile +1 -1
  2. data/examples/general/context_sensitive_headers.rb +37 -0
  3. data/examples/general/float.rb +11 -0
  4. data/examples/general/repeaters.rb +43 -0
  5. data/examples/m17n/chinese_text_wrapping.rb +1 -3
  6. data/examples/text/font_calculations.rb +6 -6
  7. data/examples/text/text_box.rb +80 -17
  8. data/lib/prawn/core.rb +3 -1
  9. data/lib/prawn/document/bounding_box.rb +9 -0
  10. data/lib/prawn/document/column_box.rb +13 -2
  11. data/lib/prawn/document/internals.rb +21 -3
  12. data/lib/prawn/document/snapshot.rb +7 -2
  13. data/lib/prawn/document/span.rb +3 -3
  14. data/lib/prawn/document.rb +78 -19
  15. data/lib/prawn/font/afm.rb +10 -7
  16. data/lib/prawn/font/ttf.rb +6 -4
  17. data/lib/prawn/font.rb +34 -24
  18. data/lib/prawn/graphics/cap_style.rb +5 -2
  19. data/lib/prawn/graphics/color.rb +117 -57
  20. data/lib/prawn/graphics/dash.rb +4 -2
  21. data/lib/prawn/graphics/join_style.rb +6 -3
  22. data/lib/prawn/graphics/transparency.rb +65 -18
  23. data/lib/prawn/images/jpg.rb +1 -1
  24. data/lib/prawn/images/png.rb +1 -1
  25. data/lib/prawn/object_store.rb +30 -1
  26. data/lib/prawn/reference.rb +25 -3
  27. data/lib/prawn/repeater.rb +117 -0
  28. data/lib/prawn/stamp.rb +102 -40
  29. data/lib/prawn/text/box.rb +344 -0
  30. data/lib/prawn/text.rb +255 -0
  31. data/spec/document_spec.rb +125 -4
  32. data/spec/object_store_spec.rb +33 -0
  33. data/spec/repeater_spec.rb +79 -0
  34. data/spec/stamp_spec.rb +8 -0
  35. data/spec/text_box_spec.rb +282 -69
  36. data/spec/text_spec.rb +49 -29
  37. data/spec/transparency_spec.rb +14 -0
  38. data/vendor/pdf-inspector/lib/pdf/inspector/graphics.rb +2 -2
  39. metadata +158 -155
  40. data/examples/general/measurement_units.pdf +0 -4667
  41. data/lib/prawn/document/text/box.rb +0 -90
  42. data/lib/prawn/document/text/wrapping.rb +0 -62
  43. data/lib/prawn/document/text.rb +0 -184
@@ -20,9 +20,9 @@ module Prawn
20
20
  end
21
21
  end
22
22
 
23
- attr_reader :attributes
23
+ attr_reader :attributes #:nodoc:
24
24
 
25
- def initialize(document, name, options={})
25
+ def initialize(document, name, options={}) #:nodoc:
26
26
  unless BUILT_INS.include?(name)
27
27
  raise Prawn::Errors::UnknownFont, "#{name} is not a known font."
28
28
  end
@@ -45,15 +45,14 @@ module Prawn
45
45
  @line_gap = Float(bbox[3] - bbox[1]) - (@ascender - @descender)
46
46
  end
47
47
 
48
+ # The font bbox, as an array of integers
49
+ #
48
50
  def bbox
49
51
  @bbox ||= @attributes['fontbbox'].split(/\s+/).map { |e| Integer(e) }
50
52
  end
51
53
 
52
- # calculates the width of the supplied string.
53
- #
54
- # String *must* be encoded as WinAnsi
55
- #
56
- def compute_width_of(string, options={})
54
+ # NOTE: String *must* be encoded as WinAnsi
55
+ def compute_width_of(string, options={}) #:nodoc:
57
56
  scale = (options[:size] || size) / 1000.0
58
57
 
59
58
  if options[:kerning]
@@ -65,6 +64,8 @@ module Prawn
65
64
  end
66
65
  end
67
66
 
67
+ # Returns true if the font has kerning data, false otherwise
68
+ #
68
69
  def has_kerning_data?
69
70
  @kern_pairs.any?
70
71
  end
@@ -72,6 +73,7 @@ module Prawn
72
73
  # built-in fonts only work with winansi encoding, so translate the
73
74
  # string. Changes the encoding in-place, so the argument itself
74
75
  # is replaced with a string in WinAnsi encoding.
76
+ #
75
77
  def normalize_encoding(text)
76
78
  enc = Prawn::Encoding::WinAnsi.new
77
79
  text.unpack("U*").collect { |i| enc[i] }.pack("C*")
@@ -88,6 +90,7 @@ module Prawn
88
90
  # the string itself (or an array, if kerning is performed).
89
91
  #
90
92
  # The +text+ parameter must be in WinAnsi encoding (cp1252).
93
+ #
91
94
  def encode_text(text, options={})
92
95
  [[0, options[:kerning] ? kern(text) : text]]
93
96
  end
@@ -22,9 +22,8 @@ module Prawn
22
22
  @line_gap = Integer(@ttf.line_gap * scale_factor)
23
23
  end
24
24
 
25
- # +string+ must be UTF8-encoded.
26
- #
27
- def compute_width_of(string, options={})
25
+ # NOTE: +string+ must be UTF8-encoded.
26
+ def compute_width_of(string, options={}) #:nodoc:
28
27
  scale = (options[:size] || size) / 1000.0
29
28
  if options[:kerning]
30
29
  kern(string).inject(0) do |s,r|
@@ -40,11 +39,14 @@ module Prawn
40
39
  end * scale
41
40
  end
42
41
  end
43
-
42
+
43
+ # The font bbox, as an array of integers
44
+ #
44
45
  def bbox
45
46
  @bbox ||= @ttf.bbox.map { |i| Integer(i * scale_factor) }
46
47
  end
47
48
 
49
+ # Returns true if the font has kerning data, false otherwise
48
50
  def has_kerning_data?
49
51
  @has_kerning_data
50
52
  end
data/lib/prawn/font.rb CHANGED
@@ -113,21 +113,18 @@ module Prawn
113
113
  # font will be embedded twice. Since we do font subsetting, this double
114
114
  # embedding won't be catastrophic, just annoying.
115
115
  # ++
116
- #
117
- def find_font(name, options={}) #:nodoc:
118
- if font_families.key?(name)
119
- family, name = name, font_families[name][options[:style] || :normal]
120
-
121
- if name.is_a?(Hash)
122
- options = options.merge(name)
123
- name = options[:file]
124
- end
125
- end
126
-
127
- key = "#{name}:#{options[:font] || 0}"
128
- font_registry[key] ||= Font.load(self, name, options.merge(:family => family))
129
- end
130
-
116
+ def find_font(name, options={}) #:nodoc:
117
+ if font_families.key?(name)
118
+ family, name = name, font_families[name][options[:style] || :normal]
119
+ if name.is_a?(Hash)
120
+ options = options.merge(name)
121
+ name = options[:file]
122
+ end
123
+ end
124
+ key = "#{name}:#{options[:font] || 0}"
125
+ font_registry[key] ||= Font.load(self, name, options.merge(:family => family))
126
+ end
127
+
131
128
  # Hash of Font objects keyed by names
132
129
  #
133
130
  def font_registry #:nodoc:
@@ -210,6 +207,10 @@ module Prawn
210
207
  # The options hash used to initialize the font
211
208
  attr_reader :options
212
209
 
210
+ # Shortcut interface for constructing a font object. Filenames of the form
211
+ # *.ttf will call Font::TTF.new, *.dfont Font::DFont.new, and anything else
212
+ # will be passed through to Font::AFM.new()
213
+ #
213
214
  def self.load(document,name,options={})
214
215
  case name
215
216
  when /\.ttf$/ then TTF.new(document, name, options)
@@ -231,26 +232,24 @@ module Prawn
231
232
  @references = {}
232
233
  end
233
234
 
235
+ # The size of the font ascender in PDF points
236
+ #
234
237
  def ascender
235
238
  @ascender / 1000.0 * size
236
239
  end
237
240
 
241
+ # The size of the font descender in PDF points
242
+ #
238
243
  def descender
239
- @descender / 1000.0 * size
244
+ -@descender / 1000.0 * size
240
245
  end
241
246
 
247
+ # The size of the recommended gap between lines of text in PDF points
248
+ #
242
249
  def line_gap
243
250
  @line_gap / 1000.0 * size
244
251
  end
245
252
 
246
- def identifier_for(subset)
247
- "#{@identifier}.#{subset}"
248
- end
249
-
250
- def inspect
251
- "#{self.class.name}< #{name}: #{size} >"
252
- end
253
-
254
253
  # Normalizes the encoding of the string to an encoding supported by the
255
254
  # font. The string is expected to be UTF-8 going in. It will be re-encoded
256
255
  # and the new string will be returned. For an in-place (destructive)
@@ -261,10 +260,13 @@ module Prawn
261
260
 
262
261
  # Destructive version of normalize_encoding; normalizes the encoding of a
263
262
  # string in place.
263
+ #
264
264
  def normalize_encoding!(str)
265
265
  str.replace(normalize_encoding(str))
266
266
  end
267
267
 
268
+ # Gets height of current font in PDF points at the given font size
269
+ #
268
270
  def height_at(size)
269
271
  @normalized_height ||= (@ascender - @descender + @line_gap) / 1000.0
270
272
  @normalized_height * size
@@ -285,6 +287,14 @@ module Prawn
285
287
  @document.page_fonts.merge!(identifier_for(subset) => @references[subset])
286
288
  end
287
289
 
290
+ def identifier_for(subset) #:nodoc:
291
+ "#{@identifier}.#{subset}"
292
+ end
293
+
294
+ def inspect #:nodoc:
295
+ "#{self.class.name}< #{name}: #{size} >"
296
+ end
297
+
288
298
  private
289
299
 
290
300
  def size
@@ -9,12 +9,15 @@
9
9
  module Prawn
10
10
  module Graphics
11
11
  module CapStyle
12
- # Sets the cap_style for stroked lines and curves
13
- #
14
12
 
15
13
  CAP_STYLES = { :butt => 0, :round => 1, :projecting_square => 2 }
16
14
 
15
+ # Sets the cap style for stroked lines and curves
16
+ #
17
17
  # style is one of :butt, :round, or :projecting_square
18
+ #
19
+ # NOTE: If this method is never called, :butt will be used by default.
20
+ #
18
21
  def cap_style(style=nil)
19
22
  return @cap_style || :butt if style.nil?
20
23
 
@@ -1,4 +1,4 @@
1
- # encoding: utf-8
1
+ # encoding: utf-8
2
2
 
3
3
  # color.rb : Implements color handling
4
4
  #
@@ -8,41 +8,41 @@
8
8
  #
9
9
  module Prawn
10
10
  module Graphics
11
- module Color
12
-
13
- # Sets the fill color.
11
+ module Color
12
+
13
+ # Sets the fill color.
14
14
  #
15
- # If a single argument is provided, it should be a 6 digit HTML color
15
+ # If a single argument is provided, it should be a 6 digit HTML color
16
16
  # code.
17
- #
17
+ #
18
18
  # pdf.fill_color "f0ffc1"
19
19
  #
20
20
  # If 4 arguments are provided, the color is assumed to be a CMYK value
21
21
  # Values range from 0 - 100.
22
- #
22
+ #
23
23
  # pdf.fill_color 0, 99, 95, 0
24
24
  #
25
- def fill_color(*color)
26
- return @fill_color if color.empty?
27
- @fill_color = process_color(*color)
25
+ def fill_color(*color)
26
+ return @fill_color if color.empty?
27
+ @fill_color = process_color(*color)
28
28
  set_fill_color
29
- end
29
+ end
30
30
 
31
- alias_method :fill_color=, :fill_color
31
+ alias_method :fill_color=, :fill_color
32
32
 
33
33
  # Sets the line stroking color. 6 digit HTML color codes are used.
34
34
  #
35
- # If a single argument is provided, it should be a 6 digit HTML color
35
+ # If a single argument is provided, it should be a 6 digit HTML color
36
36
  # code.
37
- #
37
+ #
38
38
  # pdf.stroke_color "f0ffc1"
39
39
  #
40
40
  # If 4 arguments are provided, the color is assumed to be a CMYK value
41
41
  # Values range from 0 - 100.
42
- #
42
+ #
43
43
  # pdf.stroke_color 0, 99, 95, 0
44
44
  #
45
- def stroke_color(*color)
45
+ def stroke_color(*color)
46
46
  return @stroke_color if color.empty?
47
47
  @stroke_color = process_color(*color)
48
48
  set_stroke_color
@@ -57,30 +57,30 @@ module Prawn
57
57
  # fill_and_stroke_some_method(*args) #=> some_method(*args); fill_and_stroke
58
58
  #
59
59
  def method_missing(id,*args,&block)
60
- case(id.to_s)
60
+ case(id.to_s)
61
61
  when /^fill_and_stroke_(.*)/
62
62
  send($1,*args,&block); fill_and_stroke
63
63
  when /^stroke_(.*)/
64
- send($1,*args,&block); stroke
64
+ send($1,*args,&block); stroke
65
65
  when /^fill_(.*)/
66
66
  send($1,*args,&block); fill
67
67
  else
68
68
  super
69
69
  end
70
- end
71
-
70
+ end
71
+
72
72
  module_function
73
-
74
- # Converts RGB value array to hex string suitable for use with fill_color
75
- # and stroke_color
73
+
74
+ # Converts RGB value array to hex string suitable for use with fill_color
75
+ # and stroke_color
76
76
  #
77
77
  # >> Prawn::Graphics::Color.rgb2hex([255,120,8])
78
- # => "ff7808"
78
+ # => "ff7808"
79
79
  #
80
80
  def rgb2hex(rgb)
81
81
  rgb.map { |e| "%02x" % e }.join
82
- end
83
-
82
+ end
83
+
84
84
  # Converts hex string into RGB value array:
85
85
  #
86
86
  # >> Prawn::Graphics::Color.hex2rgb("ff7808")
@@ -89,53 +89,113 @@ module Prawn
89
89
  def hex2rgb(hex)
90
90
  r,g,b = hex[0..1], hex[2..3], hex[4..5]
91
91
  [r,g,b].map { |e| e.to_i(16) }
92
- end
93
-
92
+ end
93
+
94
94
  private
95
-
96
- def process_color(*color)
95
+
96
+ def process_color(*color)
97
97
  case(color.size)
98
- when 1
99
- color[0]
98
+ when 1
99
+ color[0]
100
100
  when 4
101
101
  color
102
102
  else
103
- raise ArgumentError, 'wrong number of arguments supplied'
104
- end
105
- end
106
-
107
- def set_fill_color
108
- case @fill_color
109
- when String
110
- r,g,b = hex2rgb(@fill_color)
111
- add_content "%.3f %.3f %.3f rg" %
112
- [r / 255.0, g / 255.0, b / 255.0]
113
- when Array
114
- c,m,y,k = *@fill_color
115
- add_content "%.3f %.3f %.3f %.3f k" %
116
- [c / 100.0, m / 100.0, y / 100.0, k / 100.0]
103
+ raise ArgumentError, 'wrong number of arguments supplied'
117
104
  end
118
105
  end
119
106
 
120
- def set_stroke_color
121
- case @stroke_color
107
+ def color_type(color)
108
+ case color
122
109
  when String
123
- r,g,b = hex2rgb(@stroke_color)
124
- add_content "%.3f %.3f %.3f RG" %
125
- [r / 255.0, g / 255.0, b / 255.0]
110
+ :RGB
126
111
  when Array
127
- c,m,y,k = *@stroke_color
128
- add_content "%.3f %.3f %.3f %.3f K" %
129
- [c / 100.0, m / 100.0, y / 100.0, k / 100.0]
112
+ :CMYK
113
+ end
114
+ end
115
+
116
+ def normalize_color(color)
117
+ case color_type(color)
118
+ when :RGB
119
+ r,g,b = hex2rgb(color)
120
+ [r / 255.0, g / 255.0, b / 255.0]
121
+ when :CMYK
122
+ c,m,y,k = *color
123
+ [c / 100.0, m / 100.0, y / 100.0, k / 100.0]
130
124
  end
131
- end
125
+ end
126
+
127
+ def color_to_s(color)
128
+ normalize_color(color).map { |c| '%.3f' % c }.join(' ')
129
+ end
132
130
 
133
- def update_colors
131
+ def color_space(color)
132
+ case color_type(color)
133
+ when :RGB
134
+ :DeviceRGB
135
+ when :CMYK
136
+ :DeviceCMYK
137
+ end
138
+ end
139
+
140
+ COLOR_SPACES = [:DeviceRGB, :DeviceCMYK, :Pattern]
141
+
142
+ def set_color_space(type, color_space)
143
+ # don't set the same color space again
144
+ return if @color_space[type] == color_space
145
+ @color_space[type] = color_space
146
+
147
+ unless COLOR_SPACES.include?(color_space)
148
+ raise ArgumentError, "unknown color space: '#{color_space}'"
149
+ end
150
+
151
+ operator = case type
152
+ when :fill
153
+ 'cs'
154
+ when :stroke
155
+ 'CS'
156
+ else
157
+ raise ArgumentError, "unknown type '#{type}'"
158
+ end
159
+
160
+ add_content "/#{color_space} #{operator}"
161
+ end
162
+
163
+ def set_color(type, color, options = {})
164
+ operator = case type
165
+ when :fill
166
+ 'scn'
167
+ when :stroke
168
+ 'SCN'
169
+ else
170
+ raise ArgumentError, "unknown type '#{type}'"
171
+ end
172
+
173
+ if options[:pattern]
174
+ set_color_space type, :Pattern
175
+ add_content "/#{color} #{operator}"
176
+ else
177
+ set_color_space type, color_space(color)
178
+ color = color_to_s(color)
179
+ add_content "#{color} #{operator}"
180
+ end
181
+ end
182
+
183
+ def set_fill_color
184
+ set_color :fill, @fill_color
185
+ end
186
+
187
+ def set_stroke_color
188
+ set_color :stroke, @stroke_color
189
+ end
190
+
191
+ def update_colors
192
+ @color_space = {}
134
193
  @fill_color ||= "000000"
135
194
  @stroke_color ||= "000000"
136
195
  set_fill_color
137
196
  set_stroke_color
138
- end
197
+ end
139
198
  end
140
199
  end
141
200
  end
201
+
@@ -40,13 +40,15 @@ module Prawn
40
40
 
41
41
  alias_method :dash=, :dash
42
42
 
43
- # Restores solid stroking
43
+ # Stops dashing, restoring solid stroked lines and curves
44
+ #
44
45
  def undash
45
46
  @dash = undash_hash
46
47
  write_stroke_dash
47
48
  end
48
49
 
49
- # Returns true iff the stroke is dashed
50
+ # Returns when stroke is dashed, false otherwise
51
+ #
50
52
  def dashed?
51
53
  dash != undash_hash
52
54
  end
@@ -9,12 +9,15 @@
9
9
  module Prawn
10
10
  module Graphics
11
11
  module JoinStyle
12
- # Sets the join_style for stroked lines and curves
13
- #
14
-
15
12
  JOIN_STYLES = { :miter => 0, :round => 1, :bevel => 2 }
16
13
 
14
+ # Sets the join style for stroked lines and curves
15
+ #
17
16
  # style is one of :miter, :round, or :bevel
17
+ #
18
+ # NOTE: if this method is never called, :miter will be used for join style
19
+ # throughout the document
20
+ #
18
21
  def join_style(style=nil)
19
22
  return @join_style || :miter if style.nil?
20
23
 
@@ -9,35 +9,61 @@
9
9
 
10
10
  module Prawn
11
11
  module Graphics
12
+
13
+ # The Prawn::Transparency module is used to place transparent
14
+ # content on the page. It has the capacity for separate
15
+ # transparency values for stroked content and all other content.
16
+ #
17
+ # Example:
18
+ # # both the fill and stroke will be at 50% opacity
19
+ # pdf.transparent(0.5) do
20
+ # pdf.text("hello world")
21
+ # pdf.fill_and_stroke_circle_at([x, y], :radius => 25)
22
+ # end
23
+ #
24
+ # # the fill will be at 50% opacity, but the stroke will
25
+ # # be at 75% opacity
26
+ # pdf.transparent(0.5, 0.75) do
27
+ # pdf.text("hello world")
28
+ # pdf.fill_and_stroke_circle_at([x, y], :radius => 25)
29
+ # end
30
+ #
12
31
  module Transparency
13
32
 
33
+ # Sets the <tt>opacity</tt> and <tt>stroke_opacity</tt> for all
34
+ # the content within the <tt>block</tt>
35
+ # If <tt>stroke_opacity</tt> is not provided, then it takes on
36
+ # the same value as <tt>opacity</tt>
37
+ #
38
+ # Valid ranges for both paramters are 0.0 to 1.0
39
+ #
40
+ # Example:
41
+ # # both the fill and stroke will be at 50% opacity
42
+ # pdf.transparent(0.5) do
43
+ # pdf.text("hello world")
44
+ # pdf.fill_and_stroke_circle_at([x, y], :radius => 25)
45
+ # end
46
+ #
47
+ # # the fill will be at 50% opacity, but the stroke will
48
+ # # be at 75% opacity
49
+ # pdf.transparent(0.5, 0.75) do
50
+ # pdf.text("hello world")
51
+ # pdf.fill_and_stroke_circle_at([x, y], :radius => 25)
52
+ # end
53
+ #
14
54
  def transparent(opacity, stroke_opacity=opacity, &block)
15
55
  min_version(1.4)
16
56
 
17
- key = "#{opacity}_#{stroke_opacity}"
18
-
19
- if opacity_dictionary_registry[key]
20
- opacity_dictionary = opacity_dictionary_registry[key][:obj]
21
- opacity_dictionary_name = opacity_dictionary_registry[key][:name]
22
- else
23
- opacity_dictionary = ref!(:Type => :ExtGState,
24
- :CA => stroke_opacity,
25
- :ca => opacity
26
- )
27
-
28
- opacity_dictionary_name = "Tr#{next_opacity_dictionary_id}"
29
- opacity_dictionary_registry[key] = { :name => opacity_dictionary_name,
30
- :obj => opacity_dictionary }
31
- end
32
-
33
- page_ext_gstates.merge!(opacity_dictionary_name => opacity_dictionary)
57
+ opacity = [[opacity, 0.0].max, 1.0].min
58
+ stroke_opacity = [[stroke_opacity, 0.0].max, 1.0].min
34
59
 
35
60
  # push a new graphics context onto the graphics context stack
36
61
  add_content "q"
37
- add_content "/#{opacity_dictionary_name} gs"
62
+ add_content "/#{opacity_dictionary_name(opacity, stroke_opacity)} gs"
38
63
 
39
64
  yield if block_given?
40
65
 
66
+ # restore the previous graphics context
41
67
  add_content "Q"
42
68
  end
43
69
 
@@ -51,6 +77,27 @@ module Prawn
51
77
  opacity_dictionary_registry.length + 1
52
78
  end
53
79
 
80
+ def opacity_dictionary_name(opacity, stroke_opacity)
81
+ key = "#{opacity}_#{stroke_opacity}"
82
+
83
+ if opacity_dictionary_registry[key]
84
+ dictionary = opacity_dictionary_registry[key][:obj]
85
+ dictionary_name = opacity_dictionary_registry[key][:name]
86
+ else
87
+ dictionary = ref!(:Type => :ExtGState,
88
+ :CA => stroke_opacity,
89
+ :ca => opacity
90
+ )
91
+
92
+ dictionary_name = "Tr#{next_opacity_dictionary_id}"
93
+ opacity_dictionary_registry[key] = { :name => dictionary_name,
94
+ :obj => dictionary }
95
+ end
96
+
97
+ page_ext_gstates.merge!(dictionary_name => dictionary)
98
+ dictionary_name
99
+ end
100
+
54
101
  end
55
102
  end
56
103
  end
@@ -22,7 +22,7 @@ module Prawn
22
22
 
23
23
  # Process a new JPG image
24
24
  #
25
- # <tt>:data</tt>:: A string containing a full PNG file
25
+ # <tt>:data</tt>:: A binary string of JPEG data
26
26
  #
27
27
  def initialize(data)
28
28
  data = StringIO.new(data.dup)
@@ -25,7 +25,7 @@ module Prawn
25
25
 
26
26
  # Process a new PNG image
27
27
  #
28
- # <tt>data</tt>:: A string containing a full PNG file
28
+ # <tt>data</tt>:: A binary string of PNG data
29
29
  #
30
30
  def initialize(data)
31
31
  data = StringIO.new(data.dup)
@@ -6,7 +6,7 @@
6
6
  #
7
7
  # This is free software. Please see the LICENSE and COPYING files for details.
8
8
  module Prawn
9
- class ObjectStore
9
+ class ObjectStore #:nodoc:
10
10
  include Enumerable
11
11
 
12
12
  def initialize(info={})
@@ -59,5 +59,34 @@ module Prawn
59
59
  end
60
60
  alias_method :length, :size
61
61
 
62
+ def compact
63
+ # Clear live markers
64
+ each { |o| o.live = false }
65
+
66
+ # Recursively mark reachable objects live, starting from the roots
67
+ # (the only objects referenced in the trailer)
68
+ root.mark_live
69
+ info.mark_live
70
+
71
+ # Renumber live objects to eliminate gaps (shrink the xref table)
72
+ if @objects.any?{ |_, o| !o.live }
73
+ new_id = 1
74
+ new_objects = {}
75
+ new_identifiers = []
76
+
77
+ each do |obj|
78
+ if obj.live
79
+ obj.identifier = new_id
80
+ new_objects[new_id] = obj
81
+ new_identifiers << new_id
82
+ new_id += 1
83
+ end
84
+ end
85
+
86
+ @objects = new_objects
87
+ @identifiers = new_identifiers
88
+ end
89
+ end
90
+
62
91
  end
63
92
  end