Class: Tabula::Ruling
- Inherits:
-
Object
- Object
- Tabula::Ruling
- Defined in:
- lib/tabula/core/ruling.rb
Overview
Represents a ruling line (horizontal or vertical line segment) in a PDF. Used for detecting table cell boundaries in lattice-mode extraction.
Constant Summary collapse
- ORIENTATION_TOLERANCE =
Tolerance for considering lines as horizontal/vertical
1.0- INTERSECTION_TOLERANCE =
Tolerance for near-intersection detection
1.0
Instance Attribute Summary collapse
-
#x1 ⇒ Object
Returns the value of attribute x1.
-
#x2 ⇒ Object
Returns the value of attribute x2.
-
#y1 ⇒ Object
Returns the value of attribute y1.
-
#y2 ⇒ Object
Returns the value of attribute y2.
Class Method Summary collapse
-
.collapse_oriented_rulings(rulings, tolerance = 1.0) ⇒ Object
Collapse colinear rulings that are close together.
-
.crop_to_area(rulings, rect) ⇒ Object
Crop rulings to a rectangular area.
-
.find_intersections(horizontal_rulings, vertical_rulings) ⇒ Object
Find all intersection points between horizontal and vertical rulings Uses sweep line algorithm for O(n log n) performance.
-
.from_bounds(top, left, width, height) ⇒ Object
Create from top, left, width, height (like Rectangle).
-
.from_points(p1, p2) ⇒ Object
Create from two points.
Instance Method Summary collapse
- #==(other) ⇒ Object (also: #eql?)
-
#angle ⇒ Object
Calculate angle in degrees (0 = horizontal, 90 = vertical).
- #bottom ⇒ Object
-
#bounds ⇒ Object
Get bounding rectangle.
-
#clip_to(rect) ⇒ Object
Clip ruling to a rectangular area.
-
#colinear_with?(other, tolerance = 1.0) ⇒ Boolean
Check if this ruling overlaps with another (for collapsing).
- #dup ⇒ Object
-
#end ⇒ Object
End point along the line direction.
-
#expand(amount) ⇒ Object
Expand the ruling by extending its endpoints.
- #hash ⇒ Object
- #height ⇒ Object
- #horizontal? ⇒ Boolean
-
#initialize(x1, y1, x2, y2) ⇒ Ruling
constructor
A new instance of Ruling.
- #inspect ⇒ Object
-
#intersection_point(other) ⇒ Object
Find intersection point with another ruling (only for orthogonal lines).
-
#intersects?(other, tolerance = INTERSECTION_TOLERANCE) ⇒ Boolean
Check if this ruling intersects another (with tolerance).
- #left ⇒ Object
- #length ⇒ Object
-
#nearly_intersects?(other, tolerance = INTERSECTION_TOLERANCE) ⇒ Boolean
Check if lines nearly intersect (for cell detection).
-
#normalize! ⇒ Object
Normalize almost-horizontal and almost-vertical lines.
- #oblique? ⇒ Boolean
-
#p1 ⇒ Object
Get start and end points as Point objects.
- #p2 ⇒ Object
-
#position ⇒ Object
Position perpendicular to the line (y for horizontal, x for vertical).
- #position=(value) ⇒ Object
- #right ⇒ Object
-
#start ⇒ Object
Start point along the line direction.
- #to_s ⇒ Object
- #top ⇒ Object
- #vertical? ⇒ Boolean
- #width ⇒ Object
Constructor Details
#initialize(x1, y1, x2, y2) ⇒ Ruling
Returns a new instance of Ruling.
15 16 17 18 19 20 21 |
# File 'lib/tabula/core/ruling.rb', line 15 def initialize(x1, y1, x2, y2) @x1 = x1.to_f @y1 = y1.to_f @x2 = x2.to_f @y2 = y2.to_f normalize! end |
Instance Attribute Details
#x1 ⇒ Object
Returns the value of attribute x1.
13 14 15 |
# File 'lib/tabula/core/ruling.rb', line 13 def x1 @x1 end |
#x2 ⇒ Object
Returns the value of attribute x2.
13 14 15 |
# File 'lib/tabula/core/ruling.rb', line 13 def x2 @x2 end |
#y1 ⇒ Object
Returns the value of attribute y1.
13 14 15 |
# File 'lib/tabula/core/ruling.rb', line 13 def y1 @y1 end |
#y2 ⇒ Object
Returns the value of attribute y2.
13 14 15 |
# File 'lib/tabula/core/ruling.rb', line 13 def y2 @y2 end |
Class Method Details
.collapse_oriented_rulings(rulings, tolerance = 1.0) ⇒ Object
Collapse colinear rulings that are close together
250 251 252 253 254 255 256 257 258 259 260 261 |
# File 'lib/tabula/core/ruling.rb', line 250 def collapse_oriented_rulings(rulings, tolerance = 1.0) return [] if rulings.empty? # Separate horizontal and vertical horizontal = rulings.select(&:horizontal?).sort_by(&:y1) vertical = rulings.select(&:vertical?).sort_by(&:x1) collapsed = [] collapsed.concat(collapse_group(horizontal, tolerance)) collapsed.concat(collapse_group(vertical, tolerance)) collapsed end |
.crop_to_area(rulings, rect) ⇒ Object
Crop rulings to a rectangular area
264 265 266 |
# File 'lib/tabula/core/ruling.rb', line 264 def crop_to_area(rulings, rect) rulings.filter_map { |r| r.clip_to(rect) } end |
.find_intersections(horizontal_rulings, vertical_rulings) ⇒ Object
Find all intersection points between horizontal and vertical rulings Uses sweep line algorithm for O(n log n) performance
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/tabula/core/ruling.rb', line 230 def find_intersections(horizontal_rulings, vertical_rulings) intersections = {} horizontal_rulings.each do |h| vertical_rulings.each do |v| next unless h.intersects?(v) point = h.intersection_point(v) next unless point # Round to avoid floating point issues key = [point.x.round(2), point.y.round(2)] intersections[key] ||= point end end intersections.values end |
.from_bounds(top, left, width, height) ⇒ Object
Create from top, left, width, height (like Rectangle)
29 30 31 |
# File 'lib/tabula/core/ruling.rb', line 29 def self.from_bounds(top, left, width, height) new(left, top, left + width, top + height) end |
.from_points(p1, p2) ⇒ Object
Create from two points
24 25 26 |
# File 'lib/tabula/core/ruling.rb', line 24 def self.from_points(p1, p2) new(p1.x, p1.y, p2.x, p2.y) end |
Instance Method Details
#==(other) ⇒ Object Also known as: eql?
199 200 201 202 203 |
# File 'lib/tabula/core/ruling.rb', line 199 def ==(other) return false unless other.is_a?(Ruling) x1 == other.x1 && y1 == other.y1 && x2 == other.x2 && y2 == other.y2 end |
#angle ⇒ Object
Calculate angle in degrees (0 = horizontal, 90 = vertical)
131 132 133 |
# File 'lib/tabula/core/ruling.rb', line 131 def angle Math.atan2(y2 - y1, x2 - x1) * 180.0 / Math::PI end |
#bottom ⇒ Object
96 97 98 |
# File 'lib/tabula/core/ruling.rb', line 96 def bottom [y1, y2].max end |
#bounds ⇒ Object
Get bounding rectangle
117 118 119 |
# File 'lib/tabula/core/ruling.rb', line 117 def bounds Rectangle.new(top, left, width, height) end |
#clip_to(rect) ⇒ Object
Clip ruling to a rectangular area
184 185 186 |
# File 'lib/tabula/core/ruling.rb', line 184 def clip_to(rect) CohenSutherlandClipping.clip(self, rect) end |
#colinear_with?(other, tolerance = 1.0) ⇒ Boolean
Check if this ruling overlaps with another (for collapsing)
189 190 191 192 193 194 195 196 197 |
# File 'lib/tabula/core/ruling.rb', line 189 def colinear_with?(other, tolerance = 1.0) return false unless horizontal? == other.horizontal? if horizontal? (y1 - other.y1).abs < tolerance else (x1 - other.x1).abs < tolerance end end |
#dup ⇒ Object
210 211 212 |
# File 'lib/tabula/core/ruling.rb', line 210 def dup Ruling.new(x1, y1, x2, y2) end |
#end ⇒ Object
End point along the line direction
84 85 86 |
# File 'lib/tabula/core/ruling.rb', line 84 def end horizontal? ? x2 : y2 end |
#expand(amount) ⇒ Object
Expand the ruling by extending its endpoints
170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/tabula/core/ruling.rb', line 170 def (amount) if horizontal? Ruling.new(x1 - amount, y1, x2 + amount, y2) elsif vertical? Ruling.new(x1, y1 - amount, x2, y2 + amount) else # For oblique lines, expand in both directions dx = (x2 - x1) / length * amount dy = (y2 - y1) / length * amount Ruling.new(x1 - dx, y1 - dy, x2 + dx, y2 + dy) end end |
#hash ⇒ Object
206 207 208 |
# File 'lib/tabula/core/ruling.rb', line 206 def hash [x1, y1, x2, y2].hash end |
#height ⇒ Object
112 113 114 |
# File 'lib/tabula/core/ruling.rb', line 112 def height bottom - top end |
#horizontal? ⇒ Boolean
51 52 53 |
# File 'lib/tabula/core/ruling.rb', line 51 def horizontal? (y2 - y1).abs <= ORIENTATION_TOLERANCE end |
#inspect ⇒ Object
223 224 225 |
# File 'lib/tabula/core/ruling.rb', line 223 def inspect to_s end |
#intersection_point(other) ⇒ Object
Find intersection point with another ruling (only for orthogonal lines)
136 137 138 139 140 141 142 143 144 145 |
# File 'lib/tabula/core/ruling.rb', line 136 def intersection_point(other) return nil if horizontal? == other.horizontal? return nil if oblique? || other.oblique? if horizontal? Point.new(other.x1, y1) else Point.new(x1, other.y1) end end |
#intersects?(other, tolerance = INTERSECTION_TOLERANCE) ⇒ Boolean
Check if this ruling intersects another (with tolerance)
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/tabula/core/ruling.rb', line 148 def intersects?(other, tolerance = INTERSECTION_TOLERANCE) point = intersection_point(other) return false unless point # Check if intersection point lies within both line segments if horizontal? x_in_self = point.x.between?(left - tolerance, right + tolerance) y_in_other = point.y.between?(other.top - tolerance, other.bottom + tolerance) x_in_self && y_in_other else y_in_self = point.y.between?(top - tolerance, bottom + tolerance) x_in_other = point.x.between?(other.left - tolerance, other.right + tolerance) y_in_self && x_in_other end end |
#left ⇒ Object
100 101 102 |
# File 'lib/tabula/core/ruling.rb', line 100 def left [x1, x2].min end |
#length ⇒ Object
88 89 90 |
# File 'lib/tabula/core/ruling.rb', line 88 def length Math.sqrt(((x2 - x1)**2) + ((y2 - y1)**2)) end |
#nearly_intersects?(other, tolerance = INTERSECTION_TOLERANCE) ⇒ Boolean
Check if lines nearly intersect (for cell detection)
165 166 167 |
# File 'lib/tabula/core/ruling.rb', line 165 def nearly_intersects?(other, tolerance = INTERSECTION_TOLERANCE) intersects?(other, tolerance) end |
#normalize! ⇒ Object
Normalize almost-horizontal and almost-vertical lines
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/tabula/core/ruling.rb', line 34 def normalize! if horizontal? avg_y = (y1 + y2) / 2.0 @y1 = avg_y @y2 = avg_y # Ensure x1 < x2 @x1, @x2 = @x2, @x1 if x1 > x2 elsif vertical? avg_x = (x1 + x2) / 2.0 @x1 = avg_x @x2 = avg_x # Ensure y1 < y2 @y1, @y2 = @y2, @y1 if y1 > y2 end self end |
#oblique? ⇒ Boolean
59 60 61 |
# File 'lib/tabula/core/ruling.rb', line 59 def oblique? !horizontal? && !vertical? end |
#p1 ⇒ Object
Get start and end points as Point objects
122 123 124 |
# File 'lib/tabula/core/ruling.rb', line 122 def p1 Point.new(x1, y1) end |
#position ⇒ Object
Position perpendicular to the line (y for horizontal, x for vertical)
64 65 66 |
# File 'lib/tabula/core/ruling.rb', line 64 def position horizontal? ? y1 : x1 end |
#position=(value) ⇒ Object
68 69 70 71 72 73 74 75 76 |
# File 'lib/tabula/core/ruling.rb', line 68 def position=(value) if horizontal? @y1 = value @y2 = value else @x1 = value @x2 = value end end |
#right ⇒ Object
104 105 106 |
# File 'lib/tabula/core/ruling.rb', line 104 def right [x1, x2].max end |
#start ⇒ Object
Start point along the line direction
79 80 81 |
# File 'lib/tabula/core/ruling.rb', line 79 def start horizontal? ? x1 : y1 end |
#to_s ⇒ Object
214 215 216 217 218 219 220 221 |
# File 'lib/tabula/core/ruling.rb', line 214 def to_s orientation = if horizontal? 'H' else (vertical? ? 'V' : 'O') end "Ruling[#{orientation}](#{x1}, #{y1}) -> (#{x2}, #{y2})" end |
#top ⇒ Object
92 93 94 |
# File 'lib/tabula/core/ruling.rb', line 92 def top [y1, y2].min end |
#vertical? ⇒ Boolean
55 56 57 |
# File 'lib/tabula/core/ruling.rb', line 55 def vertical? (x2 - x1).abs <= ORIENTATION_TOLERANCE end |
#width ⇒ Object
108 109 110 |
# File 'lib/tabula/core/ruling.rb', line 108 def width right - left end |