Module: Rpdfium::Table::Edges
- Defined in:
- lib/rpdfium/table/edges.rb
Overview
Operazioni su edges (segmenti orizzontali/verticali) usate dal TableFinder. Mappa diretta su ‘pdfplumber/table.py`.
Convenzioni interne (allineate a pdfplumber):
- Ogni edge è un Hash con :orientation ("v" | "h"),
:x0, :x1, :top, :bottom (in coordinate top-down).
- Edge orizzontale: top == bottom, x0 < x1.
- Edge verticale: x0 == x1, top < bottom.
Le edges possono provenire da:
- linee vettoriali del PDF (path segments)
- rettangoli (decomposti in 4 lati)
- line "implicite" dedotte dall'allineamento di words (strategia :text)
- line specificate dall'utente (strategia :explicit)
Constant Summary collapse
- DEFAULT_MIN_WORDS_VERTICAL =
words → edges (strategia :text)
3- DEFAULT_MIN_WORDS_HORIZONTAL =
1
Class Method Summary collapse
-
.edges_to_intersections(edges, x_tolerance: 1.0, y_tolerance: 1.0) ⇒ Object
Per ogni coppia (h, v) che si interseca entro tolerance, registra un’intersezione ‘(v.x0, h.top)` con i puntatori agli edge sorgenti.
-
.filter_edges(edges, orientation: nil, min_length: 1.0) ⇒ Object
Filtra edges troppo corti.
-
.join_edge_group(edges, orientation, tolerance: 3.0) ⇒ Object
Join: dato un gruppo di edges sulla stessa retta infinita (stessa top per orizzontali, stessa x0 per verticali), fonde quelli i cui estremi sono entro ‘tolerance`.
-
.merge_edges(edges, snap_x_tolerance: 3.0, snap_y_tolerance: 3.0, join_x_tolerance: 3.0, join_y_tolerance: 3.0) ⇒ Object
Pipeline completa: snap + join.
- .move_to_avg(cluster, orientation) ⇒ Object
-
.snap_edges(edges, x_tolerance: 3.0, y_tolerance: 3.0) ⇒ Object
Snap: cluster di edges quasi-collineari → coordinata media comune.
-
.words_to_edges_h(words, word_threshold: DEFAULT_MIN_WORDS_HORIZONTAL) ⇒ Object
Per ogni cluster di word allineate “in alto” (stessa top, entro tol=1) con almeno ‘word_threshold` membri, emette DUE edges orizzontali (top e bottom della bbox di quel cluster).
-
.words_to_edges_v(words, word_threshold: DEFAULT_MIN_WORDS_VERTICAL) ⇒ Object
Tre cluster di word per x: x0, x1, centerpoint.
Class Method Details
.edges_to_intersections(edges, x_tolerance: 1.0, y_tolerance: 1.0) ⇒ Object
Per ogni coppia (h, v) che si interseca entro tolerance, registra un’intersezione ‘(v.x0, h.top)` con i puntatori agli edge sorgenti. Il valore in `intersections[(x, y)] = { v: […], h: […] }` permette poi al cell-builder di verificare “edge connect”.
Ottimizzazione rispetto al loop naïve O(|v|×|h|): sorted_h è ordinato per top; per ogni edge verticale si usa bsearch per trovare il primo h candidato e si esce appena h supera v + y_tolerance, riducendo le iterazioni al solo sottoinsieme verticalmente rilevante.
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/rpdfium/table/edges.rb', line 194 def edges_to_intersections(edges, x_tolerance: 1.0, y_tolerance: 1.0) v_edges, h_edges = edges.partition { |e| e[:orientation] == "v" } intersections = {} sorted_v = v_edges.sort_by { |v| [v[:x0], v[:top]] } sorted_h = h_edges.sort_by { |h| [h[:top], h[:x0]] } h_tops = sorted_h.map { |h| h[:top] } sorted_v.each do |v| v_top_min = v[:top] - y_tolerance v_top_max = v[:bottom] + y_tolerance # Salta tutti gli h il cui top è ancora sotto la finestra verticale. start_idx = h_tops.bsearch_index { |t| t >= v_top_min } || sorted_h.size sorted_h[start_idx..].each do |h| # Gli h rimanenti sono oltre la finestra: esci subito. break if h[:top] > v_top_max next unless v[:x0] >= h[:x0] - x_tolerance next unless v[:x0] <= h[:x1] + x_tolerance key = [v[:x0], h[:top]] entry = intersections[key] ||= { v: [], h: [] } entry[:v] << v entry[:h] << h end end intersections end |
.filter_edges(edges, orientation: nil, min_length: 1.0) ⇒ Object
Filtra edges troppo corti.
93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/rpdfium/table/edges.rb', line 93 def filter_edges(edges, orientation: nil, min_length: 1.0) edges.reject do |e| next true if orientation && e[:orientation] != orientation length = if e[:orientation] == "h" e[:x1] - e[:x0] else e[:bottom] - e[:top] end length < min_length end end |
.join_edge_group(edges, orientation, tolerance: 3.0) ⇒ Object
Join: dato un gruppo di edges sulla stessa retta infinita (stessa top per orizzontali, stessa x0 per verticali), fonde quelli i cui estremi sono entro ‘tolerance`.
Match esatto del comportamento di pdfplumber.join_edge_group: scorre sorted per minprop, estende il “current” se overlap/contiguità entro tolerance, altrimenti apre nuovo current.
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/rpdfium/table/edges.rb', line 52 def join_edge_group(edges, orientation, tolerance: 3.0) return [] if edges.empty? min_prop, max_prop = orientation == "h" ? [:x0, :x1] : [:top, :bottom] sorted = edges.sort_by { |e| e[min_prop] } joined = [sorted.first.dup] sorted[1..].each do |e| last = joined.last if e[min_prop] <= last[max_prop] + tolerance last[max_prop] = e[max_prop] if e[max_prop] > last[max_prop] else joined << e.dup end end joined end |
.merge_edges(edges, snap_x_tolerance: 3.0, snap_y_tolerance: 3.0, join_x_tolerance: 3.0, join_y_tolerance: 3.0) ⇒ Object
Pipeline completa: snap + join. Fedele a pdfplumber.merge_edges.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/rpdfium/table/edges.rb', line 72 def merge_edges(edges, snap_x_tolerance: 3.0, snap_y_tolerance: 3.0, join_x_tolerance: 3.0, join_y_tolerance: 3.0) if snap_x_tolerance.positive? || snap_y_tolerance.positive? edges = snap_edges(edges, x_tolerance: snap_x_tolerance, y_tolerance: snap_y_tolerance) end # Raggruppa per (orientation, "valore della retta") # h → top, v → x0 groups = edges.group_by do |e| e[:orientation] == "h" ? ["h", e[:top]] : ["v", e[:x0]] end groups.flat_map do |(orient, _key), group| tol = orient == "h" ? join_x_tolerance : join_y_tolerance join_edge_group(group, orient, tolerance: tol) end end |
.move_to_avg(cluster, orientation) ⇒ Object
34 35 36 37 38 39 40 41 42 43 |
# File 'lib/rpdfium/table/edges.rb', line 34 def move_to_avg(cluster, orientation) case orientation when "h" mean = cluster.sum { |e| e[:top] } / cluster.size.to_f cluster.map { |e| e.merge(top: mean, bottom: mean) } when "v" mean = cluster.sum { |e| e[:x0] } / cluster.size.to_f cluster.map { |e| e.merge(x0: mean, x1: mean) } end end |
.snap_edges(edges, x_tolerance: 3.0, y_tolerance: 3.0) ⇒ Object
Snap: cluster di edges quasi-collineari → coordinata media comune. Per orizzontali snappa la ‘top` (== `bottom`); per verticali la `x0`.
24 25 26 27 28 29 30 31 32 |
# File 'lib/rpdfium/table/edges.rb', line 24 def snap_edges(edges, x_tolerance: 3.0, y_tolerance: 3.0) v_edges, h_edges = edges.partition { |e| e[:orientation] == "v" } snapped_v = Util::Cluster.cluster_objects(v_edges, :x0, tolerance: x_tolerance) .flat_map { |g| move_to_avg(g, "v") } snapped_h = Util::Cluster.cluster_objects(h_edges, :top, tolerance: y_tolerance) .flat_map { |g| move_to_avg(g, "h") } snapped_v + snapped_h end |
.words_to_edges_h(words, word_threshold: DEFAULT_MIN_WORDS_HORIZONTAL) ⇒ Object
Per ogni cluster di word allineate “in alto” (stessa top, entro tol=1) con almeno ‘word_threshold` membri, emette DUE edges orizzontali (top e bottom della bbox di quel cluster). Avere il bottom oltre al top è critico: garantisce che l’ultima riga di ogni tabella abbia un edge orizzontale di chiusura.
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/rpdfium/table/edges.rb', line 118 def words_to_edges_h(words, word_threshold: DEFAULT_MIN_WORDS_HORIZONTAL) by_top = Util::Cluster.cluster_objects(words, :top, tolerance: 1.0) large = by_top.select { |g| g.size >= word_threshold } rects = large.map { |g| Util::Cluster.objects_to_rect(g) } return [] if rects.empty? min_x0 = rects.map { |r| r[:x0] }.min max_x1 = rects.map { |r| r[:x1] }.max rects.flat_map do |r| [ { x0: min_x0, x1: max_x1, top: r[:top], bottom: r[:top], orientation: "h" }, { x0: min_x0, x1: max_x1, top: r[:bottom], bottom: r[:bottom], orientation: "h" } ] end end |
.words_to_edges_v(words, word_threshold: DEFAULT_MIN_WORDS_VERTICAL) ⇒ Object
Tre cluster di word per x: x0, x1, centerpoint. Cluster con almeno ‘word_threshold` membri sono candidati colonna. Le bbox di ciascun cluster vengono “condensate”: se una bbox si sovrappone a un’altra già selezionata (più popolata), viene scartata.
Per ogni bbox condensata emetto un edge verticale al suo x0 (left della colonna). In aggiunta, emetto un edge “right” finale al max x1 di tutte le bbox: chiude visivamente la tabella sulla destra.
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/rpdfium/table/edges.rb', line 143 def words_to_edges_v(words, word_threshold: DEFAULT_MIN_WORDS_VERTICAL) by_x0 = Util::Cluster.cluster_objects(words, :x0, tolerance: 1.0) by_x1 = Util::Cluster.cluster_objects(words, :x1, tolerance: 1.0) center_fn = ->(w) { (w[:x0] + w[:x1]) / 2.0 } by_center = Util::Cluster.cluster_objects(words, center_fn, tolerance: 1.0) clusters = by_x0 + by_x1 + by_center # Più popolati prima sorted = clusters.sort_by { |c| -c.size } large = sorted.select { |c| c.size >= word_threshold } bboxes = large.map { |c| Util::Cluster.objects_to_bbox(c) } condensed_bboxes = bboxes.each_with_object([]) do |b, acc| acc << b unless acc.any? { |c| Util::Cluster.bbox_overlaps?(b, c) } end return [] if condensed_bboxes.empty? # Sort left-to-right per emettere edges in ordine geometrico. condensed_rects = condensed_bboxes.map do |b| { x0: b[0], top: b[1], x1: b[2], bottom: b[3] } end.sort_by { |r| r[:x0] } max_x1, min_top, max_bottom = condensed_rects.each_with_object( [-Float::INFINITY, Float::INFINITY, -Float::INFINITY] ) do |r, acc| acc[0] = r[:x1] if r[:x1] > acc[0] acc[1] = r[:top] if r[:top] < acc[1] acc[2] = r[:bottom] if r[:bottom] > acc[2] end # Edge "left" di ogni colonna + un edge finale "right". left_edges = condensed_rects.map do |r| { x0: r[:x0], x1: r[:x0], top: min_top, bottom: max_bottom, orientation: "v" } end right_edge = { x0: max_x1, x1: max_x1, top: min_top, bottom: max_bottom, orientation: "v" } left_edges + [right_edge] end |