Class: Tabula::Rectangle

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/tabula/core/rectangle.rb

Overview

Represents a rectangle with position and dimensions. Coordinates use PDF coordinate system (origin at bottom-left).

Direct Known Subclasses

Cell, Line, Page, Table, TextChunk, TextElement

Constant Summary collapse

VERTICAL_COMPARISON_THRESHOLD =

Threshold for vertical overlap comparison (40% overlap)

0.4

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(top, left, width, height) ⇒ Rectangle

Returns a new instance of Rectangle.



12
13
14
15
16
17
# File 'lib/tabula/core/rectangle.rb', line 12

def initialize(top, left, width, height)
  @top = top.to_f
  @left = left.to_f
  @width = width.to_f
  @height = height.to_f
end

Instance Attribute Details

#height ⇒ Object

Returns the value of attribute height.



10
11
12
# File 'lib/tabula/core/rectangle.rb', line 10

def height
  @height
end

#left ⇒ Object

Returns the value of attribute left.



10
11
12
# File 'lib/tabula/core/rectangle.rb', line 10

def left
  @left
end

#top ⇒ Object

Returns the value of attribute top.



10
11
12
# File 'lib/tabula/core/rectangle.rb', line 10

def top
  @top
end

#width ⇒ Object

Returns the value of attribute width.



10
11
12
# File 'lib/tabula/core/rectangle.rb', line 10

def width
  @width
end

Class Method Details

.bounding_box_of(rectangles) ⇒ Object

Compute bounding box of multiple rectangles



34
35
36
37
38
39
40
41
42
43
# File 'lib/tabula/core/rectangle.rb', line 34

def self.bounding_box_of(rectangles)
  return nil if rectangles.empty?

  top = rectangles.map(&:top).min
  left = rectangles.map(&:left).min
  bottom = rectangles.map(&:bottom).max
  right = rectangles.map(&:right).max

  from_bounds(top, left, bottom, right)
end

.from_bounds(top, left, bottom, right) ⇒ Object

Create rectangle from bounds [top, left, bottom, right]



20
21
22
# File 'lib/tabula/core/rectangle.rb', line 20

def self.from_bounds(top, left, bottom, right)
  new(top, left, right - left, bottom - top)
end

.from_points(p1, p2) ⇒ Object

Create rectangle from two points



25
26
27
28
29
30
31
# File 'lib/tabula/core/rectangle.rb', line 25

def self.from_points(p1, p2)
  top = [p1.y, p2.y].min
  left = [p1.x, p2.x].min
  bottom = [p1.y, p2.y].max
  right = [p1.x, p2.x].max
  from_bounds(top, left, bottom, right)
end

Instance Method Details

#<=>(other) ⇒ Object

Comparator for sorting by position (top to bottom, left to right)



209
210
211
212
213
214
# File 'lib/tabula/core/rectangle.rb', line 209

def <=>(other)
  result = top <=> other.top
  return result unless result.zero?

  left <=> other.left
end

#==(other) ⇒ Object Also known as: eql?



185
186
187
188
189
# File 'lib/tabula/core/rectangle.rb', line 185

def ==(other)
  return false unless other.is_a?(Rectangle)

  top == other.top && left == other.left && width == other.width && height == other.height
end

#area ⇒ Object



77
78
79
# File 'lib/tabula/core/rectangle.rb', line 77

def area
  width * height
end

#bottom ⇒ Object



45
46
47
# File 'lib/tabula/core/rectangle.rb', line 45

def bottom
  top + height
end

#bottom=(value) ⇒ Object



49
50
51
# File 'lib/tabula/core/rectangle.rb', line 49

def bottom=(value)
  @height = value - top
end

#bounds ⇒ Object



85
86
87
# File 'lib/tabula/core/rectangle.rb', line 85

def bounds
  [top, left, bottom, right]
end

#center ⇒ Object



81
82
83
# File 'lib/tabula/core/rectangle.rb', line 81

def center
  Point.new(left + (width / 2.0), top + (height / 2.0))
end

#contains?(other) ⇒ Boolean

Check if this rectangle fully contains another

Returns:

  • (Boolean)


144
145
146
# File 'lib/tabula/core/rectangle.rb', line 144

def contains?(other)
  left <= other.left && right >= other.right && top <= other.top && bottom >= other.bottom
end

#contains_point?(point) ⇒ Boolean

Check if this rectangle contains a point

Returns:

  • (Boolean)


139
140
141
# File 'lib/tabula/core/rectangle.rb', line 139

def contains_point?(point)
  point.x.between?(left, right) && point.y >= top && point.y <= bottom
end

#dup ⇒ Object



196
197
198
# File 'lib/tabula/core/rectangle.rb', line 196

def dup
  Rectangle.new(top, left, width, height)
end

#hash ⇒ Object



192
193
194
# File 'lib/tabula/core/rectangle.rb', line 192

def hash
  [top, left, width, height].hash
end

#horizontal_overlap(other) ⇒ Object

Calculate horizontal overlap with another rectangle



104
105
106
# File 'lib/tabula/core/rectangle.rb', line 104

def horizontal_overlap(other)
  [0, [right, other.right].min - [left, other.left].max].max
end

#horizontally_overlaps?(other, threshold = 0.0) ⇒ Boolean

Check if rectangles overlap horizontally

Returns:

  • (Boolean)


118
119
120
121
122
123
124
125
# File 'lib/tabula/core/rectangle.rb', line 118

def horizontally_overlaps?(other, threshold = 0.0)
  overlap = horizontal_overlap(other)
  min_width = [width, other.width].min
  return true if min_width.zero? && overlap.zero?
  return false if min_width.zero?

  (overlap / min_width) > threshold
end

#inspect ⇒ Object



204
205
206
# File 'lib/tabula/core/rectangle.rb', line 204

def inspect
  to_s
end

#intersection(other) ⇒ Object

Return intersection rectangle, or nil if no intersection



174
175
176
177
178
179
180
181
182
183
# File 'lib/tabula/core/rectangle.rb', line 174

def intersection(other)
  return nil unless intersects?(other)

  Rectangle.from_bounds(
    [top, other.top].max,
    [left, other.left].max,
    [bottom, other.bottom].min,
    [right, other.right].min
  )
end

#intersects?(other) ⇒ Boolean

Check if this rectangle intersects another

Returns:

  • (Boolean)


149
150
151
# File 'lib/tabula/core/rectangle.rb', line 149

def intersects?(other)
  !(other.left > right || other.right < left || other.top > bottom || other.bottom < top)
end

#merge(other) ⇒ Object

Merge this rectangle with another, returning the bounding box



154
155
156
157
158
159
160
161
# File 'lib/tabula/core/rectangle.rb', line 154

def merge(other)
  Rectangle.from_bounds(
    [top, other.top].min,
    [left, other.left].min,
    [bottom, other.bottom].max,
    [right, other.right].max
  )
end

#merge!(other) ⇒ Object

Merge in place



164
165
166
167
168
169
170
171
# File 'lib/tabula/core/rectangle.rb', line 164

def merge!(other)
  merged = merge(other)
  @top = merged.top
  @left = merged.left
  @width = merged.width
  @height = merged.height
  self
end

#overlap_ratio(other) ⇒ Object

Calculate overlap ratio (intersection area / union area)



128
129
130
131
132
133
134
135
136
# File 'lib/tabula/core/rectangle.rb', line 128

def overlap_ratio(other)
  intersection_area = vertical_overlap(other) * horizontal_overlap(other)
  return 0.0 if intersection_area.zero?

  union_area = area + other.area - intersection_area
  return 0.0 if union_area.zero?

  intersection_area / union_area
end

#points ⇒ Object



89
90
91
92
93
94
95
96
# File 'lib/tabula/core/rectangle.rb', line 89

def points
  [
    Point.new(left, top),
    Point.new(right, top),
    Point.new(right, bottom),
    Point.new(left, bottom)
  ]
end

#right ⇒ Object



53
54
55
# File 'lib/tabula/core/rectangle.rb', line 53

def right
  left + width
end

#right=(value) ⇒ Object



57
58
59
# File 'lib/tabula/core/rectangle.rb', line 57

def right=(value)
  @width = value - left
end

#to_s ⇒ Object



200
201
202
# File 'lib/tabula/core/rectangle.rb', line 200

def to_s
  "Rectangle[top=#{top}, left=#{left}, width=#{width}, height=#{height}]"
end

#vertical_overlap(other) ⇒ Object

Calculate vertical overlap with another rectangle



99
100
101
# File 'lib/tabula/core/rectangle.rb', line 99

def vertical_overlap(other)
  [0, [bottom, other.bottom].min - [top, other.top].max].max
end

#vertically_overlaps?(other, threshold = VERTICAL_COMPARISON_THRESHOLD) ⇒ Boolean

Check if rectangles overlap vertically

Returns:

  • (Boolean)


109
110
111
112
113
114
115
# File 'lib/tabula/core/rectangle.rb', line 109

def vertically_overlaps?(other, threshold = VERTICAL_COMPARISON_THRESHOLD)
  overlap = vertical_overlap(other)
  min_height = [height, other.height].min
  return false if min_height.zero?

  (overlap / min_height) >= threshold
end

#x ⇒ Object



61
62
63
# File 'lib/tabula/core/rectangle.rb', line 61

def x
  left
end

#x=(value) ⇒ Object



65
66
67
# File 'lib/tabula/core/rectangle.rb', line 65

def x=(value)
  self.left = value
end

#y ⇒ Object



69
70
71
# File 'lib/tabula/core/rectangle.rb', line 69

def y
  top
end

#y=(value) ⇒ Object



73
74
75
# File 'lib/tabula/core/rectangle.rb', line 73

def y=(value)
  self.top = value
end