Module: Rpdfium::Table::Cells
- Defined in:
- lib/rpdfium/table/cells.rb
Overview
Costruisce celle da intersezioni e tabelle da celle. Algoritmi 1:1 con pdfplumber.intersections_to_cells e pdfplumber.cells_to_tables.
Class Method Summary collapse
-
.cells_to_tables(cells) ⇒ Object
Raggruppa celle in tabelle in base ai corner condivisi.
-
.intersections_to_cells(intersections) ⇒ Object
Ricerca della “smallest cell” per ogni intersezione: dato un punto ‘pt = (x, y)`, cerca il rettangolo minimo i cui 4 corner sono intersezioni e i cui 4 lati hanno edge che le connettono.
Class Method Details
.cells_to_tables(cells) ⇒ Object
Raggruppa celle in tabelle in base ai corner condivisi.
Algoritmo: Union-Find (disjoint set) sui corner — O(n α(n)) invece del greedy fixed-point O(n²) di pdfplumber. Il risultato è identico: due celle finiscono nello stesso gruppo se condividono almeno un corner.
Filtro finale: scarta tabelle con UNA SOLA cella (rumore).
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/rpdfium/table/cells.rb', line 101 def cells_to_tables(cells) return [] if cells.empty? n = cells.size parent = Array.new(n) { |i| i } find = lambda do |i| i = parent[i] = parent[parent[i]] while parent[i] != i i end union = ->(a, b) { parent[find.call(a)] = find.call(b) } # Per ogni corner, raccoglie gli indici delle celle che lo condividono # e le unisce nel medesimo componente. corner_to_cells = Hash.new { |h, k| h[k] = [] } cells.each_with_index do |cell, idx| x0, top, x1, bottom = cell [[x0, top], [x0, bottom], [x1, top], [x1, bottom]].each do |corner| corner_to_cells[corner] << idx end end corner_to_cells.each_value do |idxs| idxs.each_cons(2) { |a, b| union.call(a, b) } end # Raggruppa per root del Union-Find groups = Hash.new { |h, k| h[k] = [] } cells.each_with_index { |cell, i| groups[find.call(i)] << cell } # Sort top-to-bottom, left-to-right; filtra single-cell. groups.values .sort_by { |t| t.map { |c| [c[1], c[0]] }.min } .reject { |t| t.size <= 1 } end |
.intersections_to_cells(intersections) ⇒ Object
Ricerca della “smallest cell” per ogni intersezione: dato un punto ‘pt = (x, y)`, cerca il rettangolo minimo i cui 4 corner sono intersezioni e i cui 4 lati hanno edge che le connettono.
Il vincolo “edge connect” è cruciale: due intersezioni con stessa x non bastano — devono CONDIVIDERE almeno un edge verticale (cioè appartenere a uno stesso segmento continuo). Idem orizzontale. Questo evita falsi positivi tipo “due colonne lontane allineate accidentalmente”.
‘intersections` è il Hash prodotto da Edges.edges_to_intersections, con chiavi `[x, y]` e valori `{ v: [edges…], h: [edges…] }`.
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'lib/rpdfium/table/cells.rb', line 23 def intersections_to_cells(intersections) return [] if intersections.empty? # Indici di adiacenza: per ogni edge (oggetto Hash, identità di # ruby), quali intersection points contiene? Pdfplumber lo fa # confrontando bbox degli edge — noi abbiamo accesso diretto agli # oggetti edge dentro `intersections[pt]`, basta usare l'identity. # Per "stesso edge" usiamo `equal?` (identità d'oggetto). edge_ids = intersections.transform_values do |val| { v: val[:v].map(&:object_id).to_set, h: val[:h].map(&:object_id).to_set } end edge_connects = lambda do |p1, p2| if p1[0] == p2[0] return !(edge_ids[p1][:v] & edge_ids[p2][:v]).empty? end if p1[1] == p2[1] return !(edge_ids[p1][:h] & edge_ids[p2][:h]).empty? end false end points = intersections.keys.sort npoints = points.size # Indici spaziali: precomputa punti per colonna (stessa x) e per riga # (stessa y), già ordinati perché `points` è sorted. # Permette lookup O(log n) via bsearch invece di O(n) via select. by_x = Hash.new { |h, k| h[k] = [] } by_y = Hash.new { |h, k| h[k] = [] } points.each { |p| by_x[p[0]] << p; by_y[p[1]] << p } cells = [] points.each_with_index do |pt, i| next if i == npoints - 1 # Punti direttamente sotto `pt` (stessa x, y maggiore) col = by_x[pt[0]] below_start = col.bsearch_index { |q| q[1] > pt[1] } || col.size below = col[below_start..] # Punti direttamente a destra di `pt` (stessa y, x maggiore) row_pts = by_y[pt[1]] right_start = row_pts.bsearch_index { |q| q[0] > pt[0] } || row_pts.size right = row_pts[right_start..] # Cerca il PRIMO (== più piccolo per via dell'ordinamento) bottom-right # i cui 4 corner sono presenti e gli edge connettono. found = nil below.each do |b| next unless edge_connects.call(pt, b) right.each do |r| next unless edge_connects.call(pt, r) br = [r[0], b[1]] next unless intersections.key?(br) next unless edge_connects.call(br, r) next unless edge_connects.call(br, b) found = [pt[0], pt[1], br[0], br[1]] break end break if found end cells << found if found end cells end |