Module: Rpdfium::Util::Cluster
- Defined in:
- lib/rpdfium/util/cluster.rb
Overview
Primitive di clustering 1D usate da tutto il pipeline tabellare. Mappa diretta su ‘pdfplumber.utils.clustering` (cluster_list, cluster_objects, make_cluster_dict).
PROPRIETÀ CHIAVE: questi cluster sono “1D agglomerative single-linkage”: due valori finiscono nello stesso cluster se sono entro ‘tolerance` da un valore qualsiasi del cluster. NON solo dal centro/media. Ne consegue che catene di valori ravvicinati possono estendere il cluster ben oltre `tolerance` (questo è esattamente il comportamento di pdfplumber, e su cui si appoggiano le sue euristiche edge/intersection).
Class Method Summary collapse
-
.bbox_overlap(a, b) ⇒ Object
bbox sovrapposti.
-
.bbox_overlaps?(a, b) ⇒ Boolean
True se due bbox si sovrappongono (anche solo a un punto è no, deve esserci area positiva).
-
.cluster_list(values, tolerance: 0) ⇒ Object
Raggruppa valori scalari in cluster.
-
.cluster_objects(objects, key_fn, tolerance: 0, presorted: false) ⇒ Object
Raggruppa oggetti (Hash) in cluster basandosi su una funzione di estrazione ‘key_fn` (oppure simbolo Hash key) e tolleranza.
-
.objects_to_bbox(objects) ⇒ Object
bbox = [x0, top, x1, bottom] (top-down).
-
.objects_to_rect(objects) ⇒ Object
Variante che ritorna un Hash invece di tuple — comoda nel contesto edge dove ci serve mescolare bbox+orientation.
Class Method Details
.bbox_overlap(a, b) ⇒ Object
bbox sovrapposti. None overlap => nil. Match pdfplumber’s get_bbox_overlap: ritorna la bbox di intersezione, oppure nil.
122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/rpdfium/util/cluster.rb', line 122 def bbox_overlap(a, b) ax0, atop, ax1, abot = a bx0, btop, bx1, bbot = b x0 = [ax0, bx0].max x1 = [ax1, bx1].min return nil if x0 >= x1 top = [atop, btop].max bot = [abot, bbot].min return nil if top >= bot [x0, top, x1, bot] end |
.bbox_overlaps?(a, b) ⇒ Boolean
True se due bbox si sovrappongono (anche solo a un punto è no, deve esserci area positiva).
138 139 140 |
# File 'lib/rpdfium/util/cluster.rb', line 138 def bbox_overlaps?(a, b) !bbox_overlap(a, b).nil? end |
.cluster_list(values, tolerance: 0) ⇒ Object
Raggruppa valori scalari in cluster. I valori dentro lo stesso cluster sono entro ‘tolerance` da almeno un altro valore del cluster.
Esempio:
cluster_list([1.0, 1.5, 2.0, 5.0], tolerance: 1.0)
#=> [[1.0, 1.5, 2.0], [5.0]]
NOTA: Catene “stepping stone”: [1, 2, 3, 4] con tol=1 fanno UN cluster solo, anche se 1 e 4 distano 3. Questo è il comportamento di pdfplumber, è documentato nei suoi issue come potenzialmente sorprendente ma intenzionale. Lo manteniamo identico.
29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/rpdfium/util/cluster.rb', line 29 def cluster_list(values, tolerance: 0) return [] if values.empty? sorted = values.sort clusters = [[sorted.first]] sorted[1..].each do |v| if (v - clusters.last.last).abs <= tolerance clusters.last << v else clusters << [v] end end clusters end |
.cluster_objects(objects, key_fn, tolerance: 0, presorted: false) ⇒ Object
Raggruppa oggetti (Hash) in cluster basandosi su una funzione di estrazione ‘key_fn` (oppure simbolo Hash key) e tolleranza.
Esempio:
cluster_objects(words, ->(w) { w[:top] }, tolerance: 1)
cluster_objects(words, :top, tolerance: 1) # syntactic sugar
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 93 94 95 96 97 |
# File 'lib/rpdfium/util/cluster.rb', line 50 def cluster_objects(objects, key_fn, tolerance: 0, presorted: false) return [] if objects.empty? # Fast path per il caso Symbol più comune (:top, :x0, :bottom): # accesso diretto Hash[symbol] è ~2× più veloce della lambda call. if key_fn.is_a?(Symbol) # Se il chiamante garantisce che l'input è già sortato per key_fn # (es. perché viene da un sort lessicografico [key_fn, ...]) si # può saltare il sort interno. Risparmio significativo quando # cluster_objects è chiamato in loop su molte righe piccole. sorted = presorted ? objects : objects.sort_by { |o| o[key_fn] } first = sorted.first last_key = first[key_fn] clusters = [[first]] tol = tolerance.to_f i = 1 n = sorted.size while i < n obj = sorted[i] curr_key = obj[key_fn] if (curr_key - last_key).abs <= tol clusters.last << obj else clusters << [obj] end last_key = curr_key i += 1 end return clusters end # Path generico con accessor callable accessor = key_fn sorted = presorted ? objects : objects.sort_by { |o| accessor.call(o) } last_key = accessor.call(sorted.first) clusters = [[sorted.first]] sorted[1..].each do |obj| curr_key = accessor.call(obj) if (curr_key - last_key).abs <= tolerance clusters.last << obj else clusters << [obj] end last_key = curr_key end clusters end |
.objects_to_bbox(objects) ⇒ Object
bbox = [x0, top, x1, bottom] (top-down). Ritorna la bbox che racchiude tutti gli oggetti passati. Usa min/max di x0/top/x1/bottom.
101 102 103 104 105 106 107 108 109 110 |
# File 'lib/rpdfium/util/cluster.rb', line 101 def objects_to_bbox(objects) objects.each_with_object( [Float::INFINITY, Float::INFINITY, -Float::INFINITY, -Float::INFINITY] ) do |o, acc| acc[0] = o[:x0] if o[:x0] < acc[0] acc[1] = o[:top] if o[:top] < acc[1] acc[2] = o[:x1] if o[:x1] > acc[2] acc[3] = o[:bottom] if o[:bottom] > acc[3] end end |
.objects_to_rect(objects) ⇒ Object
Variante che ritorna un Hash invece di tuple — comoda nel contesto edge dove ci serve mescolare bbox+orientation.
114 115 116 117 118 |
# File 'lib/rpdfium/util/cluster.rb', line 114 def objects_to_rect(objects) x0, top, x1, bottom = objects_to_bbox(objects) { x0: x0, top: top, x1: x1, bottom: bottom, width: x1 - x0, height: bottom - top } end |