preflight 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/CHANGELOG +8 -1
  2. data/README.rdoc +21 -4
  3. data/lib/preflight.rb +1 -0
  4. data/lib/preflight/issue.rb +35 -0
  5. data/lib/preflight/profile.rb +38 -16
  6. data/lib/preflight/profiles/pdfa1a.rb +0 -1
  7. data/lib/preflight/profiles/pdfx1a.rb +4 -3
  8. data/lib/preflight/rules.rb +17 -2
  9. data/lib/preflight/rules/box_nesting.rb +35 -23
  10. data/lib/preflight/rules/compression_algorithms.rb +14 -4
  11. data/lib/preflight/rules/consistent_boxes.rb +63 -0
  12. data/lib/preflight/rules/cropbox_matches_mediabox.rb +38 -0
  13. data/lib/preflight/rules/document_id.rb +14 -2
  14. data/lib/preflight/rules/info_has_keys.rb +15 -3
  15. data/lib/preflight/rules/info_specifies_trapping.rb +16 -3
  16. data/lib/preflight/rules/match_info_entries.rb +17 -3
  17. data/lib/preflight/rules/max_ink_density.rb +69 -0
  18. data/lib/preflight/rules/max_version.rb +14 -2
  19. data/lib/preflight/rules/mediabox_at_origin.rb +42 -0
  20. data/lib/preflight/rules/min_bleed.rb +171 -0
  21. data/lib/preflight/rules/min_ppi.rb +54 -116
  22. data/lib/preflight/rules/no_cmyk.rb +113 -0
  23. data/lib/preflight/rules/no_filespecs.rb +15 -5
  24. data/lib/preflight/rules/no_font_subsets.rb +15 -6
  25. data/lib/preflight/rules/no_gray.rb +105 -0
  26. data/lib/preflight/rules/no_page_rotation.rb +36 -0
  27. data/lib/preflight/rules/no_private_data.rb +37 -0
  28. data/lib/preflight/rules/no_registration_black.rb +102 -0
  29. data/lib/preflight/rules/no_rgb.rb +112 -0
  30. data/lib/preflight/rules/no_separation.rb +85 -0
  31. data/lib/preflight/rules/no_transparency.rb +90 -0
  32. data/lib/preflight/rules/only_embedded_fonts.rb +28 -14
  33. data/lib/preflight/rules/output_intent_for_pdfx.rb +14 -2
  34. data/lib/preflight/rules/page_box_height.rb +88 -0
  35. data/lib/preflight/rules/page_box_size.rb +106 -0
  36. data/lib/preflight/rules/page_box_width.rb +88 -0
  37. data/lib/preflight/rules/page_count.rb +87 -0
  38. data/lib/preflight/rules/pdfx_output_intent_has_keys.rb +12 -2
  39. data/lib/preflight/rules/print_boxes.rb +21 -19
  40. data/lib/preflight/rules/root_has_keys.rb +15 -3
  41. metadata +97 -113
  42. data/lib/preflight/rules/no_encryption.rb +0 -16
  43. data/lib/preflight/rules/no_proprietary_fonts.rb +0 -50
@@ -2,11 +2,23 @@
2
2
 
3
3
  module Preflight
4
4
  module Rules
5
+ # check the file has a document ID
6
+ #
7
+ # Arguments: none
8
+ #
9
+ # Usage:
10
+ #
11
+ # class MyPreflight
12
+ # include Preflight::Profile
13
+ #
14
+ # rule Preflight::Rules::DocumentId
15
+ # end
16
+ #
5
17
  class DocumentId
6
18
 
7
- def messages(ohash)
19
+ def check_hash(ohash)
8
20
  if ohash.trailer[:ID].nil?
9
- ["Document ID missing"]
21
+ [Issue.new("Document ID missing", self)]
10
22
  else
11
23
  []
12
24
  end
@@ -3,18 +3,30 @@
3
3
  module Preflight
4
4
  module Rules
5
5
 
6
- # ensure the info dict has the specified keys
6
+ # Every PDF has an optional 'Info' dictionary. Check that the target file
7
+ # has certain keys
8
+ #
9
+ # Arguments: the required keys
10
+ #
11
+ # Usage:
12
+ #
13
+ # class MyPreflight
14
+ # include Preflight::Profile
15
+ #
16
+ # rule Preflight::Rules::InfoHasKeys, :Title, :CreationDate, :ModDate
17
+ # end
18
+ #
7
19
  class InfoHasKeys
8
20
 
9
21
  def initialize(*keys)
10
22
  @keys = keys.flatten
11
23
  end
12
24
 
13
- def messages(ohash)
25
+ def check_hash(ohash)
14
26
  info = ohash.object(ohash.trailer[:Info])
15
27
  missing = @keys - info.keys
16
28
  missing.map { |key|
17
- "Info dict missing required key #{key}"
29
+ Issue.new("Info dict missing required key", self, :key => key)
18
30
  }
19
31
  end
20
32
  end
@@ -2,15 +2,28 @@
2
2
 
3
3
  module Preflight
4
4
  module Rules
5
+ # Every PDF has an optional 'Info' dictionary. Check that the dictionary
6
+ # has a 'Trapped' entry that is set to True or False
7
+ #
8
+ # Arguments: none
9
+ #
10
+ # Usage:
11
+ #
12
+ # class MyPreflight
13
+ # include Preflight::Profile
14
+ #
15
+ # rule Preflight::Rules::InfoSpecifiesTrapping
16
+ # end
17
+ #
5
18
  class InfoSpecifiesTrapping
6
19
 
7
- def messages(ohash)
20
+ def check_hash(ohash)
8
21
  info = ohash.object(ohash.trailer[:Info])
9
22
 
10
23
  if !info.has_key?(:Trapped)
11
- [ "Info dict does not specify Trapped" ]
24
+ [ Issue.new("Info dict does not specify Trapped", self) ]
12
25
  elsif info[:Trapped] != :True && info[:Trapped] != :False
13
- [ "Trapped value of Info dict must be True or False" ]
26
+ [ Issue.new("Trapped value of Info dict must be True or False", self) ]
14
27
  else
15
28
  []
16
29
  end
@@ -2,20 +2,34 @@
2
2
 
3
3
  module Preflight
4
4
  module Rules
5
+ # Every PDF has an optional 'Info' dictionary. Check that the target file
6
+ # has certain keys and that the keys match a given regexp
7
+ #
8
+ # Arguments: the required keys
9
+ #
10
+ # Usage:
11
+ #
12
+ # class MyPreflight
13
+ # include Preflight::Profile
14
+ #
15
+ # rule Preflight::Rules::MatchInfoEntries, {:GTS_PDFXVersion => /\APDF\/X/}
16
+ # end
17
+ #
5
18
  class MatchInfoEntries
6
19
 
7
20
  def initialize(matches = {})
8
21
  @matches = matches
9
22
  end
10
23
 
11
- def messages(ohash)
24
+ def check_hash(ohash)
12
25
  array = []
13
26
  info = ohash.object(ohash.trailer[:Info])
14
27
  @matches.each do |key, regexp|
15
28
  if !info.has_key?(key)
16
- array << "Info dict missing required key #{key}"
29
+ array << Issue.new("Info dict missing required key", self, :key => key)
17
30
  elsif !info[key].to_s.match(regexp)
18
- array << "value of Info entry #{key} doesn't match (#{info[key]} != #{regexp})"
31
+ array << Issue.new("value of Info entry #{key} doesn't match #{regexp}", self, :key => key,
32
+ :regexp => regexp)
19
33
  end
20
34
  end
21
35
  array
@@ -0,0 +1,69 @@
1
+ # coding: utf-8
2
+
3
+ require 'yaml'
4
+ require 'matrix'
5
+
6
+ module Preflight
7
+ module Rules
8
+
9
+ # Most CMYK printers will have a stated upper tolerance for ink density. If
10
+ # the total percentage of the 4 components (C, M, Y and K) is over that
11
+ # tolerance then the result can be unpredictable and often ugly.
12
+ #
13
+ # Use this rule to detect CMYK ink densities over a certain threshold.
14
+ #
15
+ # Arguments: the highest density that is Ok
16
+ #
17
+ # Usage:
18
+ #
19
+ # class MyPreflight
20
+ # include Preflight::Profile
21
+ #
22
+ # rule Preflight::Rules::MaxInkDensity, 300
23
+ # end
24
+ #
25
+ # TODO:
26
+ #
27
+ # * check CMYK colours used as alternates in a separation color
28
+ # * check CMYK raster images
29
+ #
30
+ class MaxInkDensity
31
+
32
+ attr_reader :issues
33
+
34
+ def initialize(max_ink)
35
+ @max_ink = max_ink.to_i
36
+ end
37
+
38
+ # we're about to start a new page, reset state
39
+ #
40
+ def page=(page)
41
+ @issues = []
42
+ @page = page
43
+ @objects = page.objects
44
+ end
45
+
46
+ def set_cmyk_color_for_nonstroking(c, m, y, k)
47
+ check_ink(c, m, y, k)
48
+ end
49
+
50
+ def set_cmyk_color_for_stroking(c, m, y, k)
51
+ check_ink(c, m, y, k)
52
+ end
53
+
54
+ private
55
+
56
+ def check_ink(c, m, y, k)
57
+ ink = (c + m + y + k) * 100.0
58
+ if ink > @max_ink && @issues.empty?
59
+ @issues << Issue.new("Ink density too high", self, :page => @page.number,
60
+ :cyan => c,
61
+ :magenta => m,
62
+ :yellow => y,
63
+ :k => k)
64
+ end
65
+ end
66
+ end
67
+
68
+ end
69
+ end
@@ -4,15 +4,27 @@ module Preflight
4
4
  module Rules
5
5
  # ensure the PDF version of the file under review is not more recent
6
6
  # than desired
7
+ #
8
+ # Arguments: the maximum version
9
+ #
10
+ # Usage:
11
+ #
12
+ # class MyPreflight
13
+ # include Preflight::Profile
14
+ #
15
+ # rule Preflight::Rules::MaxVersion, 1.4
16
+ # end
17
+ #
7
18
  class MaxVersion
8
19
 
9
20
  def initialize(max_version)
10
21
  @max_version = max_version.to_f
11
22
  end
12
23
 
13
- def messages(ohash)
24
+ def check_hash(ohash)
14
25
  if ohash.pdf_version > @max_version
15
- ["PDF version should be #{@max_version} or lower (value: #{ohash.pdf_version})"]
26
+ [Issue.new("PDF version should be #{@max_version} or lower", self, :max_version => @max_version,
27
+ :current_version => ohash.pdf_version)]
16
28
  else
17
29
  []
18
30
  end
@@ -0,0 +1,42 @@
1
+ # coding: utf-8
2
+
3
+ require 'bigdecimal'
4
+
5
+ module Preflight
6
+ module Rules
7
+
8
+ # Checks the MediaBox for every page is at 0,0. This isn't required by
9
+ # any standards but is good practice to ensure correct rendering with
10
+ # some applications.
11
+ #
12
+ # Arguments: none
13
+ #
14
+ # Usage:
15
+ #
16
+ # class MyPreflight
17
+ # include Preflight::Profile
18
+ #
19
+ # rule Preflight::Rules::MediaboxAtOrigin
20
+ # end
21
+ #
22
+ class MediaboxAtOrigin
23
+
24
+ attr_reader :issues
25
+
26
+ def page=(page)
27
+ @issues = []
28
+ dict = page.attributes
29
+
30
+ if round_off(dict[:MediaBox][0,2]) != [0,0]
31
+ @issues << Issue.new("MediaBox must begin at 0,0", self, :page => page.number)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def round_off(*arr)
38
+ arr.flatten.compact.map { |n| BigDecimal.new(n.to_s).round(2) }
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,171 @@
1
+ # coding: utf-8
2
+
3
+ require 'forwardable'
4
+ require 'matrix'
5
+
6
+ module Preflight
7
+ module Rules
8
+
9
+ # To print colour to the edge of the page you must print past the intended
10
+ # page edge and then trim the printed sheet. The printed area that will be
11
+ # trimmed is called bleed. Generally you will probably want 3-4mm of bleed.
12
+ #
13
+ # Arguments: the distance from the TrimBox within which objects MUST include bleed
14
+ # the distance past the TrimBox that objects MUST bleed
15
+ # the units (:pt, :mm, :in)
16
+ #
17
+ # Usage:
18
+ #
19
+ # class MyPreflight
20
+ # include Preflight::Profile
21
+ #
22
+ # rule Preflight::Rules::MinBleed, 1, 4, :mm
23
+ # rule Preflight::Rules::MinBleed, 12, 50, :pt
24
+ # rule Preflight::Rules::MinBleed, 0.1, 0.25, :in
25
+ # end
26
+ #
27
+ class MinBleed
28
+ include Preflight::Measurements
29
+ extend Forwardable
30
+
31
+ attr_reader :issues
32
+
33
+ # Graphics State Operators
34
+ def_delegators :@state, :save_graphics_state, :restore_graphics_state
35
+
36
+ # Matrix Operators
37
+ def_delegators :@state, :concatenate_matrix
38
+
39
+ def initialize(range, bleed, units)
40
+ @range, @bleed, @units = range, bleed, units
41
+ end
42
+
43
+ # we're about to start a new page, reset state
44
+ #
45
+ def page=(page)
46
+ @issues = []
47
+ @page = page
48
+ @state = PDF::Reader::PageState.new(page)
49
+ @objects = page.objects
50
+
51
+ attrs = @page.attributes
52
+ box = attrs[:TrimBox] || attrs[:ArtBox] || attrs[:MediaBox]
53
+ @warning_min_x = box[0] + to_points(@range, @units)
54
+ @warning_min_y = box[1] + to_points(@range, @units)
55
+ @warning_max_x = box[2] - to_points(@range, @units)
56
+ @warning_max_y = box[3] - to_points(@range, @units)
57
+ @error_min_x = box[0] - to_points(@bleed, @units)
58
+ @error_min_y = box[1] - to_points(@bleed, @units)
59
+ @error_max_x = box[2] + to_points(@bleed, @units)
60
+ @error_max_y = box[3] + to_points(@bleed, @units)
61
+ end
62
+
63
+ # As each image is drawn on the canvas, determine the amount of device
64
+ # space it's being crammed into and therefore the PPI.
65
+ #
66
+ def invoke_xobject(label)
67
+ @state.invoke_xobject(label) do |xobj|
68
+ case xobj
69
+ when PDF::Reader::FormXObject then
70
+ xobj.walk(self)
71
+ when PDF::Reader::Stream
72
+ invoke_image_xobject(xobj) if xobj.hash[:Subtype] == :Image
73
+ else
74
+ raise xobj.inspect
75
+ end
76
+ end
77
+ end
78
+
79
+ def append_rectangle(x1, y1, x2, y2)
80
+ @path ||= []
81
+ @path << @state.ctm_transform(x1, y1)
82
+ @path << @state.ctm_transform(x1, y2)
83
+ @path << @state.ctm_transform(x2, y1)
84
+ @path << @state.ctm_transform(x2, y2)
85
+ end
86
+
87
+ def fill_path_with_nonzero
88
+ @path ||= []
89
+ points = select_points_in_danger_zone(@path)
90
+
91
+ if points.size > 0
92
+ @issues << Issue.new("Filled object with insufficient bleed", self, :page => @page.number,
93
+ :object_type => :filled_object,
94
+ :bleed => @bleed,
95
+ :units => @units)
96
+ end
97
+
98
+ @path = []
99
+ end
100
+ alias :fill_path_with_even_odd :fill_path_with_nonzero
101
+
102
+ def close_and_stroke_path
103
+ @path = []
104
+ end
105
+
106
+ def stroke_path
107
+ @path = []
108
+ end
109
+
110
+ def end_path
111
+ @path = []
112
+ end
113
+
114
+ private
115
+
116
+ def invoke_image_xobject(xobject)
117
+ return unless @page.attributes[:TrimBox] || @page.attributes[:ArtBox]
118
+
119
+ points = select_points_in_danger_zone(image_points)
120
+
121
+ if points.size > 0
122
+ @issues << Issue.new("Image with insufficient bleed", self, :page => @page.number,
123
+ :object_type => :image,
124
+ :bleed => @bleed,
125
+ :units => @units)
126
+ end
127
+ end
128
+
129
+ def deref(obj)
130
+ @objects ? @objects.deref(obj) : obj
131
+ end
132
+
133
+ # convert value units to PDF points. units should be :mm, :in or :pt
134
+ #
135
+ def to_points(value, units)
136
+ case units
137
+ when :mm then mm2pt(value)
138
+ when :in then in2pt(value)
139
+ else
140
+ value
141
+ end
142
+ end
143
+
144
+ # return the co-ordinates for the 4 corners of an image according
145
+ # to the current CTM
146
+ #
147
+ def image_points
148
+ [
149
+ @state.ctm_transform(0, 0),
150
+ @state.ctm_transform(0, 1),
151
+ @state.ctm_transform(1, 0),
152
+ @state.ctm_transform(1, 1)
153
+ ]
154
+ end
155
+
156
+ # given an array of points, returns the subset of points (if any) that
157
+ # fall within the danger zone indicating they're close to the TrimBox
158
+ # without enough bleed
159
+ #
160
+ def select_points_in_danger_zone(points)
161
+ points.select { |p|
162
+ (p.first < @warning_min_x && p.first > @error_min_x) ||
163
+ (p.last < @warning_min_y && p.last > @error_min_y) ||
164
+ (p.first > @warning_max_x && p.first < @error_max_x) ||
165
+ (p.last > @warning_max_x && p.last < @error_max_y)
166
+ }
167
+ end
168
+
169
+ end
170
+ end
171
+ end
@@ -1,7 +1,7 @@
1
1
  # coding: utf-8
2
2
 
3
- require 'yaml'
4
3
  require 'matrix'
4
+ require 'forwardable'
5
5
 
6
6
  module Preflight
7
7
  module Rules
@@ -9,160 +9,98 @@ module Preflight
9
9
  # For high quality prints, you generally want raster images to be
10
10
  # AT LEAST 300 points-per-inch (ppi). 600 is better, 1200 better again.
11
11
  #
12
+ # Arguments: the lowest PPI that is ok
13
+ #
14
+ # Usage:
15
+ #
16
+ # class MyPreflight
17
+ # include Preflight::Profile
18
+ #
19
+ # rule Preflight::Rules::MinPpi, 300
20
+ # end
21
+ #
12
22
  class MinPpi
13
23
  include Preflight::Measurements
24
+ extend Forwardable
25
+
26
+ attr_reader :issues
14
27
 
15
- DEFAULT_GRAPHICS_STATE = {
16
- :ctm => Matrix.identity(3)
17
- }
28
+ # Graphics State Operators
29
+ def_delegators :@state, :save_graphics_state, :restore_graphics_state
18
30
 
19
- attr_reader :messages
31
+ # Matrix Operators
32
+ def_delegators :@state, :concatenate_matrix
20
33
 
21
34
  def initialize(min_ppi)
22
35
  @min_ppi = min_ppi.to_i
23
- @messages = []
24
- @page_num = 0
25
- end
26
-
27
- def save_graphics_state
28
- @stack.push clone_state
29
- end
30
-
31
- def restore_graphics_state
32
- @stack.pop
33
- end
34
-
35
- def state
36
- @stack.last
37
36
  end
38
37
 
39
- # store sample width and height for each image on the current page
38
+ # we're about to start a new page, reset state
40
39
  #
41
- def resource_xobject(label, stream)
42
- return unless stream.hash[:Subtype] == :Image
43
-
44
- @images[label] = [
45
- stream.hash[:Width],
46
- stream.hash[:Height]
47
- ]
48
- end
49
-
50
- # update the current transformation matrix.
51
- #
52
- # If the CTM is currently undefined, just store the new values.
53
- #
54
- # If there's an existing CTM, then multiple the existing matrix
55
- # with the new matrix to form the updated matrix.
56
- #
57
- def concatenate_matrix(*args)
58
- transform = Matrix[
59
- [args[0], args[1], 0],
60
- [args[2], args[3], 0],
61
- [args[4], args[5], 1]
62
- ]
63
- if state[:ctm]
64
- state[:ctm] = transform * state[:ctm]
65
- else
66
- state[:ctm] = transform
67
- end
40
+ def page=(page)
41
+ @page = page
42
+ @state = PDF::Reader::PageState.new(page)
43
+ @issues = []
68
44
  end
69
45
 
70
46
  # As each image is drawn on the canvas, determine the amount of device
71
47
  # space it's being crammed into and therefore the PPI.
72
48
  #
73
49
  def invoke_xobject(label)
74
- return unless @images[label]
50
+ @state.invoke_xobject(label) do |xobj|
51
+ case xobj
52
+ when PDF::Reader::FormXObject then
53
+ xobj.walk(self)
54
+ when PDF::Reader::Stream
55
+ invoke_image_xobject(xobj) if xobj.hash[:Subtype] == :Image
56
+ end
57
+ end
58
+ end
59
+
60
+ private
75
61
 
76
- sample_w, sample_h = *@images[label]
62
+ def invoke_image_xobject(xobject)
63
+ sample_w = deref(xobject.hash[:Width]) || 0
64
+ sample_h = deref(xobject.hash[:Height]) || 0
77
65
  device_w = pt2in(image_width)
78
66
  device_h = pt2in(image_height)
79
67
 
80
- horizontal_ppi = (sample_w / device_w).round(3)
81
- vertical_ppi = (sample_h / device_h).round(3)
68
+ horizontal_ppi = BigDecimal.new((sample_w / device_w).to_s).round(3)
69
+ vertical_ppi = BigDecimal.new((sample_h / device_h).to_s).round(3)
82
70
 
83
71
  if horizontal_ppi < @min_ppi || vertical_ppi < @min_ppi
84
- @messages << "Image with low PPI/DPI on page #{@page_num} (h:#{horizontal_ppi} v:#{vertical_ppi})"
72
+ @issues << Issue.new("Image with low PPI/DPI", self, :page => @page.number,
73
+ :horizontal_ppi => horizontal_ppi,
74
+ :vertical_ppi => vertical_ppi,
75
+ :top_left => @state.ctm_transform(0, 1),
76
+ :bottom_left => @state.ctm_transform(0, 0),
77
+ :bottom_right => @state.ctm_transform(1, 0),
78
+ :top_right => @state.ctm_transform(1, 1))
85
79
  end
86
80
  end
87
81
 
88
- # start fresh on every page
89
- #
90
- def begin_page(hash = {})
91
- @images = {}
92
- @page_num += 1
93
- @stack = [DEFAULT_GRAPHICS_STATE]
94
- end
95
-
96
- private
97
-
98
- # return the current transformation matrix
99
- #
100
- def ctm
101
- state[:ctm]
102
- end
103
-
104
- # transform x and y co-ordinates from the current user space to the
105
- # underlying device space.
106
- #
107
- def transform(point, z = 1)
108
- Point.new(
109
- (ctm[0,0] * point.x) + (ctm[1,0] * point.y) + (ctm[2,0] * z),
110
- (ctm[0,1] * point.x) + (ctm[1,1] * point.y) + (ctm[2,1] * z)
111
- )
82
+ def deref(obj)
83
+ @objects ? @objects.deref(obj) : obj
112
84
  end
113
85
 
114
86
  # return a height of an image in the current device space. Auto
115
87
  # handles the translation from image space to device space.
116
88
  #
117
89
  def image_height
118
- bottom_left = transform(Point.new(0, 0))
119
- top_left = transform(Point.new(0, 1))
90
+ bottom_left = @state.ctm_transform(0, 0)
91
+ top_left = @state.ctm_transform(0, 1)
120
92
 
121
- bottom_left.distance(top_left)
93
+ Math.hypot(bottom_left.first-top_left.first, bottom_left.last-top_left.last)
122
94
  end
123
95
 
124
96
  # return a width of an image in the current device space. Auto
125
97
  # handles the translation from image space to device space.
126
98
  #
127
99
  def image_width
128
- bottom_left = transform(Point.new(0, 0))
129
- bottom_right = transform(Point.new(1, 0))
130
-
131
- bottom_left.distance(bottom_right)
132
- end
100
+ bottom_left = @state.ctm_transform(0, 0)
101
+ bottom_right = @state.ctm_transform(1, 0)
133
102
 
134
- # when save_graphics_state is called, we need to push a new copy of the
135
- # current state onto the stack. That way any modifications to the state
136
- # will be undone once restore_graphics_state is called.
137
- #
138
- # This returns a deep clone of the current state, ensuring changes are
139
- # keep separate from earlier states.
140
- #
141
- # YAML is used to round-trip the state through a string to easily perform
142
- # the deep clone. Kinda hacky, but effective.
143
- #
144
- def clone_state
145
- if @stack.empty?
146
- {}
147
- else
148
- yaml_state = YAML.dump(@stack.last)
149
- YAML.load(yaml_state)
150
- end
151
- end
152
-
153
- # private class for representing points on a cartesian plain. Used
154
- # to simplify maths in the MinPpi class.
155
- #
156
- class Point
157
- attr_reader :x, :y
158
-
159
- def initialize(x,y)
160
- @x, @y = x,y
161
- end
162
-
163
- def distance(point)
164
- Math.hypot(point.x - x, point.y - y)
165
- end
103
+ Math.hypot(bottom_left.first-bottom_right.first, bottom_left.last-bottom_right.last)
166
104
  end
167
105
 
168
106
  end