Class: Rpdfium::Util::WordExtractor

Inherits:
Object
  • Object
show all
Defined in:
lib/rpdfium/util/word_extractor.rb

Overview

Estrae “words” da una lista di char, fedelmente a pdfplumber.WordExtractor.

Algoritmo:

1. Ordina i char per (top, x0): righe top-to-bottom, char left-to-right
   dentro ogni riga.
2. Cluster per top con `y_tolerance` → "righe logiche" di char.
3. Dentro ogni riga, cluster per gap orizzontale: due char sono nella
   stessa word se `next.x0 - prev.x1 <= x_tolerance`. Anche un char
   whitespace separa la word (a meno che `keep_blank_chars`).
4. Per ogni cluster di char emette una word: text concatenato, bbox.

Differenze da pdfplumber (semplificazioni accettabili per il nostro uso):

- Non gestiamo `line_dir`/`char_dir` rotated (testo ruotato non
  orizzontale ltr): non rilevante per i casi d'uso correnti.
- Non gestiamo `use_text_flow` (ordering basato sul content stream):
  i nostri char arrivano già da PDFium nell'ordine geometrico via
  `chars` (top, x0).
- Non gestiamo `expand_ligatures`: PDFium di solito espande i
  codepoint correttamente già a livello char.

Queste differenze sono documentate; se mai necessarie si aggiungono come feature toggles senza cambiare il path di default.

Constant Summary collapse

DEFAULT_X_TOLERANCE =
3.0
DEFAULT_Y_TOLERANCE =
3.0

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(x_tolerance: DEFAULT_X_TOLERANCE, y_tolerance: DEFAULT_Y_TOLERANCE, keep_blank_chars: false, extra_attrs: nil) ⇒ WordExtractor

Returns a new instance of WordExtractor.



33
34
35
36
37
38
39
40
41
# File 'lib/rpdfium/util/word_extractor.rb', line 33

def initialize(x_tolerance: DEFAULT_X_TOLERANCE,
               y_tolerance: DEFAULT_Y_TOLERANCE,
               keep_blank_chars: false,
               extra_attrs: nil)
  @x_tolerance = x_tolerance.to_f
  @y_tolerance = y_tolerance.to_f
  @keep_blank_chars = keep_blank_chars
  @extra_attrs = extra_attrs || []
end

Instance Attribute Details

#keep_blank_charsObject (readonly)

Returns the value of attribute keep_blank_chars.



31
32
33
# File 'lib/rpdfium/util/word_extractor.rb', line 31

def keep_blank_chars
  @keep_blank_chars
end

#x_toleranceObject (readonly)

Returns the value of attribute x_tolerance.



31
32
33
# File 'lib/rpdfium/util/word_extractor.rb', line 31

def x_tolerance
  @x_tolerance
end

#y_toleranceObject (readonly)

Returns the value of attribute y_tolerance.



31
32
33
# File 'lib/rpdfium/util/word_extractor.rb', line 31

def y_tolerance
  @y_tolerance
end

Instance Method Details

#extract_words(chars) ⇒ Object

Restituisce un Array di Hash: { text:, x0:, x1:, top:, bottom:, chars: }. Se ‘extra_attrs` è non vuoto, ogni word splitta anche al cambio di questi attributi (es. fontname/size diversi → word diverse).



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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/rpdfium/util/word_extractor.rb', line 46

def extract_words(chars)
  return [] if chars.empty?

  # Fast path: 1 solo char → 1 word triviale (se non whitespace).
  if chars.size == 1
    c = chars.first
    return [] if blank?(c) && !@keep_blank_chars

    return [build_word([c])]
  end

  # 1. Ordina per (top, x0). Top-down, left-to-right.
  sorted = chars.sort_by { |c| [c[:top], c[:x0]] }

  # 2. Cluster in righe per `top`.
  # `presorted: true`: sorted è già ordinato per [top, x0], quindi
  # implicitamente anche per top — cluster_objects salta il proprio
  # sort interno.
  rows = Cluster.cluster_objects(sorted, :top,
                                  tolerance: @y_tolerance,
                                  presorted: true)

  words = []
  rows.each do |row|
    # Re-sort per x0 dentro ogni riga clusterizzata.
    #
    # NOTA: in linea di principio l'input `sorted` è già ordinato per
    # [top, x0], quindi i cluster di top dovrebbero essere già in
    # ordine x0. MA il sort globale `[top, x0]` rispetta strettamente
    # l'ordine per top — se due char della stessa riga visiva hanno
    # top diversi entro tolerance (es. la "i" minuscola spesso ha
    # top più alto di 0.008pt rispetto alle altre lettere a causa di
    # come PDFium calcola la bbox), il sort globale li interfoglia.
    # Il cluster_objects per :top non riordina internamente i char,
    # quindi un char con top leggermente minore finisce DAVANTI a
    # tutte le altre lettere della parola.
    #
    # Esempio reale: "Categoria" dove "i" ha top=414.9789 e le altre
    # 414.9869 → output `iCategora` invece di `Categoria`.
    # Il fix è semplicemente ri-sortare per x0 dentro la riga.
    row_sorted = row.sort_by { |c| c[:x0] }

    word_chars = []
    row_sorted.each do |c|
      if char_begins_new_word?(word_chars.last, c)
        words << build_word(word_chars) unless word_chars.empty?
        word_chars = []
      end
      # Whitespace: per default lo usiamo come separatore (lo scartiamo).
      # Con keep_blank_chars=true lo includiamo nella word corrente.
      if blank?(c) && !@keep_blank_chars
        words << build_word(word_chars) unless word_chars.empty?
        word_chars = []
      else
        word_chars << c
      end
    end
    words << build_word(word_chars) unless word_chars.empty?
  end

  words
end