preflight 0.1.1 → 0.2.0

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/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