rpdfium 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1870 -0
- data/LICENSE +19 -0
- data/README.md +599 -0
- data/lib/rpdfium/annotation/annotation.rb +114 -0
- data/lib/rpdfium/document.rb +226 -0
- data/lib/rpdfium/errors.rb +55 -0
- data/lib/rpdfium/form/form.rb +121 -0
- data/lib/rpdfium/image/embedded.rb +145 -0
- data/lib/rpdfium/io/png.rb +65 -0
- data/lib/rpdfium/page.rb +1623 -0
- data/lib/rpdfium/raw.rb +982 -0
- data/lib/rpdfium/search/search.rb +101 -0
- data/lib/rpdfium/structure/attachment.rb +40 -0
- data/lib/rpdfium/structure/element.rb +330 -0
- data/lib/rpdfium/structure/outline.rb +48 -0
- data/lib/rpdfium/structure/tree.rb +202 -0
- data/lib/rpdfium/table/cells.rb +137 -0
- data/lib/rpdfium/table/debugger.rb +122 -0
- data/lib/rpdfium/table/edges.rb +225 -0
- data/lib/rpdfium/table/extractor.rb +246 -0
- data/lib/rpdfium/table/table.rb +184 -0
- data/lib/rpdfium/util/cluster.rb +143 -0
- data/lib/rpdfium/util/column_inference.rb +139 -0
- data/lib/rpdfium/util/label_matcher.rb +214 -0
- data/lib/rpdfium/util/text_extraction.rb +49 -0
- data/lib/rpdfium/util/word_extractor.rb +151 -0
- data/lib/rpdfium/util/word_merger.rb +102 -0
- data/lib/rpdfium/version.rb +5 -0
- data/lib/rpdfium.rb +92 -0
- metadata +134 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rpdfium
|
|
4
|
+
# Ricerca testuale interna alla pagina, basata su FPDFText_Find*.
|
|
5
|
+
# Mantiene lo stato (cursor) e supporta forward/backward.
|
|
6
|
+
#
|
|
7
|
+
# Esempio:
|
|
8
|
+
# page.search("totale").each_match { |m| p m[:bbox], m[:text] }
|
|
9
|
+
class Search
|
|
10
|
+
include Enumerable
|
|
11
|
+
|
|
12
|
+
def initialize(page, query, match_case: false, whole_word: false, start_index: 0)
|
|
13
|
+
@page = page
|
|
14
|
+
@query = query
|
|
15
|
+
@start_index = start_index
|
|
16
|
+
flags = 0
|
|
17
|
+
flags |= Raw::FPDF_MATCHCASE if match_case
|
|
18
|
+
flags |= Raw::FPDF_MATCHWHOLEWORD if whole_word
|
|
19
|
+
|
|
20
|
+
utf16 = query.encode("UTF-16LE") + "\x00\x00".b
|
|
21
|
+
@query_buf = FFI::MemoryPointer.new(:uchar, utf16.bytesize)
|
|
22
|
+
@query_buf.put_bytes(0, utf16)
|
|
23
|
+
|
|
24
|
+
handle = Raw.FPDFText_FindStart(@page.text_page.handle, @query_buf,
|
|
25
|
+
flags, start_index)
|
|
26
|
+
raise Error, "FindStart failed" if handle.null?
|
|
27
|
+
|
|
28
|
+
@state = { handle: handle, closed: false }
|
|
29
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@state))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.finalizer(state)
|
|
33
|
+
proc do
|
|
34
|
+
next if state[:closed]
|
|
35
|
+
next if state[:handle].null?
|
|
36
|
+
|
|
37
|
+
Raw.FPDFText_FindClose(state[:handle])
|
|
38
|
+
state[:closed] = true
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def handle
|
|
43
|
+
@state[:handle]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Itera tutte le occorrenze in avanti. Ritorna hash con :char_index, :length,
|
|
47
|
+
# :text, :rects (array di bbox top-down: una per riga di testo).
|
|
48
|
+
def each_match
|
|
49
|
+
return enum_for(:each_match) unless block_given?
|
|
50
|
+
|
|
51
|
+
while Raw.FPDFText_FindNext(@state[:handle]) == 1
|
|
52
|
+
yield current_match
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
alias each each_match
|
|
56
|
+
|
|
57
|
+
def current_match
|
|
58
|
+
idx = Raw.FPDFText_GetSchResultIndex(@state[:handle])
|
|
59
|
+
n = Raw.FPDFText_GetSchCount(@state[:handle])
|
|
60
|
+
{
|
|
61
|
+
char_index: idx,
|
|
62
|
+
length: n,
|
|
63
|
+
text: extract_text(idx, n),
|
|
64
|
+
rects: extract_rects(idx, n)
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def close
|
|
69
|
+
return if @state[:closed]
|
|
70
|
+
|
|
71
|
+
Raw.FPDFText_FindClose(@state[:handle]) unless @state[:handle].null?
|
|
72
|
+
@state[:handle] = FFI::Pointer::NULL
|
|
73
|
+
@state[:closed] = true
|
|
74
|
+
ObjectSpace.undefine_finalizer(self)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def extract_text(idx, n)
|
|
80
|
+
buf = FFI::MemoryPointer.new(:ushort, n + 1)
|
|
81
|
+
Raw.FPDFText_GetText(@page.text_page.handle, idx, n, buf)
|
|
82
|
+
buf.read_bytes((n + 1) * 2).force_encoding("UTF-16LE")
|
|
83
|
+
.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
84
|
+
.delete("\x00")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def extract_rects(idx, n)
|
|
88
|
+
cnt = Raw.FPDFText_CountRects(@page.text_page.handle, idx, n)
|
|
89
|
+
h = @page.height
|
|
90
|
+
Array.new(cnt) do |ri|
|
|
91
|
+
l = FFI::MemoryPointer.new(:double)
|
|
92
|
+
t = FFI::MemoryPointer.new(:double)
|
|
93
|
+
r = FFI::MemoryPointer.new(:double)
|
|
94
|
+
b = FFI::MemoryPointer.new(:double)
|
|
95
|
+
Raw.FPDFText_GetRect(@page.text_page.handle, ri, l, t, r, b)
|
|
96
|
+
{ x0: l.read_double, x1: r.read_double,
|
|
97
|
+
top: h - t.read_double, bottom: h - b.read_double }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rpdfium
|
|
4
|
+
# File embedded nel PDF (allegati). PDFium li espone via FPDFDoc_GetAttachment.
|
|
5
|
+
class Attachment
|
|
6
|
+
attr_reader :document, :index, :handle
|
|
7
|
+
|
|
8
|
+
def initialize(document, index)
|
|
9
|
+
@document = document
|
|
10
|
+
@index = index
|
|
11
|
+
@handle = Raw.FPDFDoc_GetAttachment(document.handle, index)
|
|
12
|
+
raise Error, "Attachment #{index} not found" if @handle.null?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def name
|
|
16
|
+
Raw.read_utf16_string(:FPDFAttachment_GetName, @handle)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Ritorna i bytes del file allegato. Pattern probe-then-fetch.
|
|
20
|
+
def bytes
|
|
21
|
+
out_size = FFI::MemoryPointer.new(:ulong)
|
|
22
|
+
Raw.FPDFAttachment_GetFile(@handle, FFI::Pointer::NULL, 0, out_size)
|
|
23
|
+
n = out_size.read_ulong
|
|
24
|
+
return "" if n.zero?
|
|
25
|
+
|
|
26
|
+
buf = FFI::MemoryPointer.new(:uchar, n)
|
|
27
|
+
Raw.FPDFAttachment_GetFile(@handle, buf, n, out_size)
|
|
28
|
+
# Leggo n byte (la dimensione del MIO buffer), non out_size.read_ulong:
|
|
29
|
+
# PDFium può aggiornare out_size con un valore diverso da n (es. dim
|
|
30
|
+
# totale necessaria) che leggerebbe oltre il buffer → IndexError.
|
|
31
|
+
# Se la write effettiva è < n, riempie il resto con NUL.
|
|
32
|
+
buf.read_bytes(n)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def save(path)
|
|
36
|
+
File.binwrite(path, bytes)
|
|
37
|
+
path
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rpdfium
|
|
4
|
+
module Structure
|
|
5
|
+
# Element di un PDF tagged StructTree.
|
|
6
|
+
#
|
|
7
|
+
# Un Element rappresenta un nodo della struttura logica del documento:
|
|
8
|
+
# `Document`, `P` (paragrafo), `H1`..`H6` (headings), `Table`, `TR`,
|
|
9
|
+
# `TH`, `TD`, `Figure`, `Span`, `Lbl`, `LI`, `Caption`, ecc. Vedi
|
|
10
|
+
# PDF spec §14.8 per la tassonomia completa.
|
|
11
|
+
#
|
|
12
|
+
# Gli element non hanno una vita autonoma: appartengono al Tree che li
|
|
13
|
+
# ha generati. Quando il Tree viene chiuso, gli element diventano
|
|
14
|
+
# invalidi. Non chiamare metodi su un element dopo `tree.close`.
|
|
15
|
+
#
|
|
16
|
+
# Tutti i metodi sono read-only: PDFium non espone API per modificare
|
|
17
|
+
# il StructTree (è una struttura "di sola lettura" anche nel suo C API
|
|
18
|
+
# pubblico).
|
|
19
|
+
class Element
|
|
20
|
+
attr_reader :handle, :tree
|
|
21
|
+
|
|
22
|
+
def initialize(tree, handle)
|
|
23
|
+
@tree = tree
|
|
24
|
+
@handle = handle
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Tipo strutturale dell'element (es. "P", "H1", "Table", "TR", "TD").
|
|
28
|
+
# Nil se PDFium non riesce a leggerlo (element placeholder).
|
|
29
|
+
def type
|
|
30
|
+
read_utf16_string(:FPDF_StructElement_GetType)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Tipo dell'oggetto PDF sottostante: di solito "StructElem", ma può
|
|
34
|
+
# essere "MCR" (Marked Content Reference) o "OBJR" (Object Reference)
|
|
35
|
+
# per nodi specializzati. La maggior parte degli utenti usa `type`.
|
|
36
|
+
def obj_type
|
|
37
|
+
read_utf16_string(:FPDF_StructElement_GetObjType)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Title attribute (raro, usato in alcuni documenti per dare un nome
|
|
41
|
+
# parlante all'element, es. "Capitolo 1").
|
|
42
|
+
def title
|
|
43
|
+
read_utf16_string(:FPDF_StructElement_GetTitle)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# ID univoco dell'element (se dichiarato nel /ID dictionary del
|
|
47
|
+
# StructTreeRoot). Permette riferimenti cross-element (es. Headers
|
|
48
|
+
# attribute di una cella TD che punta a un TH per id).
|
|
49
|
+
def id
|
|
50
|
+
read_utf16_string(:FPDF_StructElement_GetID)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Lingua dichiarata sull'element (es. "it-IT", "en-US"). Ereditata
|
|
54
|
+
# dal parent se non sovrascritta. Utile per pipeline language-aware.
|
|
55
|
+
def lang
|
|
56
|
+
read_utf16_string(:FPDF_StructElement_GetLang)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# ActualText: override del testo "logico" per l'element. Risolve
|
|
60
|
+
# legature (PDF mostra `fi` ma actual_text dice "fi"), simboli math
|
|
61
|
+
# ("∫" → "integral"), abbreviazioni. Se presente, ha priorità sul
|
|
62
|
+
# testo grafico per accessibility e ricerca.
|
|
63
|
+
def actual_text
|
|
64
|
+
read_utf16_string(:FPDF_StructElement_GetActualText)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# AltText: testo alternativo per Figure / Formula / immagini. PDF/UA
|
|
68
|
+
# richiede che ogni Figure abbia un alt_text non vuoto.
|
|
69
|
+
def alt_text
|
|
70
|
+
read_utf16_string(:FPDF_StructElement_GetAltText)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Expansion text per abbreviazioni (es. element type "Span" con
|
|
74
|
+
# contenuto "Dr." e expansion "Doctor"). Usato per text-to-speech.
|
|
75
|
+
def expansion
|
|
76
|
+
read_utf16_string(:FPDF_StructElement_GetExpansion)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Marked Content IDs collegati a questo element. Un element ha tipicamente
|
|
80
|
+
# 1 MCID (es. una `<P>` ha tutto il testo del paragrafo dentro un BDC con
|
|
81
|
+
# mcid=N) oppure 0 (element strutturale puro: `<Document>`, `<Table>`,
|
|
82
|
+
# `<TR>` — i loro MCID stanno nei figli foglia).
|
|
83
|
+
#
|
|
84
|
+
# Per collegare un MCID al testo della pagina: leggi i page object e
|
|
85
|
+
# raggruppa per `FPDFPageObj_GetMarkedContentID`. Vedi `Element#text`.
|
|
86
|
+
def marked_content_ids
|
|
87
|
+
first = Raw.FPDF_StructElement_GetMarkedContentID(@handle)
|
|
88
|
+
count = Raw.FPDF_StructElement_GetMarkedContentIdCount(@handle)
|
|
89
|
+
# Casi: GetMarkedContentIdCount ritorna -1 quando non ci sono MCID
|
|
90
|
+
# diretti (element strutturale). GetMarkedContentID ritorna -1
|
|
91
|
+
# nello stesso caso.
|
|
92
|
+
return [] if count <= 0 && first < 0
|
|
93
|
+
|
|
94
|
+
# Quando esiste un solo MCID, GetMarkedContentIdCount può ritornare
|
|
95
|
+
# 0 o -1 mentre GetMarkedContentID dà il valore. Coalescenza:
|
|
96
|
+
if count <= 0
|
|
97
|
+
first >= 0 ? [first] : []
|
|
98
|
+
else
|
|
99
|
+
(0...count).filter_map do |i|
|
|
100
|
+
mcid = Raw.FPDF_StructElement_GetMarkedContentIdAtIndex(@handle, i)
|
|
101
|
+
mcid >= 0 ? mcid : nil
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Figli diretti dell'element. Ordinati come dichiarati nel PDF
|
|
107
|
+
# (top-to-bottom, left-to-right per reading order).
|
|
108
|
+
def children
|
|
109
|
+
n = Raw.FPDF_StructElement_CountChildren(@handle)
|
|
110
|
+
return [] if n <= 0
|
|
111
|
+
|
|
112
|
+
(0...n).filter_map do |i|
|
|
113
|
+
child_handle = Raw.FPDF_StructElement_GetChildAtIndex(@handle, i)
|
|
114
|
+
child_handle.null? ? nil : Element.new(@tree, child_handle)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Parent. Nil per gli element root (figli diretti del StructTree).
|
|
119
|
+
def parent
|
|
120
|
+
h = Raw.FPDF_StructElement_GetParent(@handle)
|
|
121
|
+
return nil if h.null?
|
|
122
|
+
|
|
123
|
+
Element.new(@tree, h)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Walk depth-first dell'intero sub-tree a partire da questo element.
|
|
127
|
+
# Visita prima self, poi ricorsivamente i figli.
|
|
128
|
+
# Senza block ritorna un Enumerator.
|
|
129
|
+
def walk(&block)
|
|
130
|
+
return enum_for(:walk) unless block
|
|
131
|
+
|
|
132
|
+
yield self
|
|
133
|
+
children.each { |c| c.walk(&block) }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Foglie del sub-tree (element senza figli). Sono i nodi che
|
|
137
|
+
# tipicamente hanno il MCID diretto.
|
|
138
|
+
def leaves
|
|
139
|
+
return [self] if children.empty?
|
|
140
|
+
|
|
141
|
+
children.flat_map(&:leaves)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Testo dell'element, ricostruito dalla pagina via MCID. Risoluzione:
|
|
145
|
+
# 1. Se `actual_text` è presente, lo usa (gestisce legature/abbreviazioni).
|
|
146
|
+
# 2. Altrimenti raccoglie tutti gli MCID del sub-tree (questo element
|
|
147
|
+
# + ricorsivamente i figli) e concatena il testo dei page objects
|
|
148
|
+
# con quei MCID, in document order.
|
|
149
|
+
#
|
|
150
|
+
# Per element strutturali puri (`Table`, `TR`) il testo è la
|
|
151
|
+
# concatenazione di tutti i discendenti — utile come "summary".
|
|
152
|
+
def text
|
|
153
|
+
return actual_text if actual_text && !actual_text.empty?
|
|
154
|
+
|
|
155
|
+
# Raccoglie MCID di tutto il sub-tree depth-first
|
|
156
|
+
all_mcids = []
|
|
157
|
+
walk { |el| all_mcids.concat(el.marked_content_ids) }
|
|
158
|
+
return "" if all_mcids.empty?
|
|
159
|
+
|
|
160
|
+
mcid_map = @tree.send(:mcid_text_map)
|
|
161
|
+
all_mcids.filter_map { |id| mcid_map[id] }.join
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Attributi PDF strutturali. Ritorna un Hash { name => value } con
|
|
165
|
+
# tutti gli attributi dichiarati su questo element (RowSpan, ColSpan,
|
|
166
|
+
# Scope, Headers, BBox, ecc.). I valori sono Ruby-native: Integer,
|
|
167
|
+
# Float, String, true/false, o Array per attributi "Headers" che
|
|
168
|
+
# contengono liste di ID.
|
|
169
|
+
def attributes
|
|
170
|
+
result = {}
|
|
171
|
+
attr_count = Raw.FPDF_StructElement_GetAttributeCount(@handle)
|
|
172
|
+
return result if attr_count <= 0
|
|
173
|
+
|
|
174
|
+
(0...attr_count).each do |ai|
|
|
175
|
+
attr = Raw.FPDF_StructElement_GetAttributeAtIndex(@handle, ai)
|
|
176
|
+
next if attr.null?
|
|
177
|
+
|
|
178
|
+
key_count = Raw.FPDF_StructElement_Attr_GetCount(attr)
|
|
179
|
+
(0...key_count).each do |ki|
|
|
180
|
+
name = read_attr_name(attr, ki)
|
|
181
|
+
next if name.nil? || name.empty?
|
|
182
|
+
|
|
183
|
+
value = read_attr_value(attr, name)
|
|
184
|
+
result[name] = value unless value.nil?
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
result
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def to_s
|
|
191
|
+
parts = ["<#{type || obj_type || '?'}>"]
|
|
192
|
+
mcids = marked_content_ids
|
|
193
|
+
parts << "mcid=#{mcids.first}" if mcids.size == 1
|
|
194
|
+
parts << "mcids=#{mcids.inspect}" if mcids.size > 1
|
|
195
|
+
parts << "lang=#{lang.inspect}" if lang
|
|
196
|
+
parts << "actual_text=#{actual_text.inspect[0, 30]}" if actual_text
|
|
197
|
+
parts << "alt_text=#{alt_text.inspect[0, 30]}" if alt_text
|
|
198
|
+
parts.join(" ")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def inspect
|
|
202
|
+
"#<Rpdfium::Structure::Element #{self}>"
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
private
|
|
206
|
+
|
|
207
|
+
# Helper UTF-16 string read con probe-then-fetch corretto. PDFium
|
|
208
|
+
# restituisce il numero di byte necessari (incluso null terminator),
|
|
209
|
+
# anche se il buffer è troppo piccolo.
|
|
210
|
+
def read_utf16_string(fn_name)
|
|
211
|
+
needed = Raw.send(fn_name, @handle, FFI::Pointer::NULL, 0)
|
|
212
|
+
return nil if needed < 2
|
|
213
|
+
|
|
214
|
+
buf = FFI::MemoryPointer.new(:uint8, needed)
|
|
215
|
+
written = Raw.send(fn_name, @handle, buf, needed)
|
|
216
|
+
return nil if written < 2
|
|
217
|
+
|
|
218
|
+
# Clamp: leggi al massimo il buffer allocato meno il null terminator.
|
|
219
|
+
payload = [written - 2, needed - 2].min
|
|
220
|
+
return nil if payload <= 0
|
|
221
|
+
|
|
222
|
+
s = buf.read_bytes(payload)
|
|
223
|
+
.force_encoding("UTF-16LE")
|
|
224
|
+
.encode("UTF-8")
|
|
225
|
+
.delete("\u0000")
|
|
226
|
+
s.empty? ? nil : s
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def read_attr_name(attr, index)
|
|
230
|
+
len_buf = FFI::MemoryPointer.new(:ulong)
|
|
231
|
+
name_buf = FFI::MemoryPointer.new(:uint8, 128)
|
|
232
|
+
ok = Raw.FPDF_StructElement_Attr_GetName(attr, index, name_buf, 128, len_buf)
|
|
233
|
+
return nil if ok == 0
|
|
234
|
+
|
|
235
|
+
n = len_buf.read_ulong
|
|
236
|
+
return nil if n.zero?
|
|
237
|
+
|
|
238
|
+
# GetName ritorna ASCII (latin-1), non UTF-16
|
|
239
|
+
name_buf.read_bytes(n).force_encoding("UTF-8").delete("\u0000")
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def read_attr_value(attr, name)
|
|
243
|
+
val_handle = Raw.FPDF_StructElement_Attr_GetValue(attr, name)
|
|
244
|
+
return nil if val_handle.null?
|
|
245
|
+
|
|
246
|
+
type = Raw.FPDF_StructElement_Attr_GetType(val_handle)
|
|
247
|
+
# Type codes da fpdf_structtree.h:
|
|
248
|
+
# 1 = Boolean, 2 = Number, 3 = String, 4 = Blob,
|
|
249
|
+
# 5 = Name, 6 = Array, 7 = Dictionary
|
|
250
|
+
case type
|
|
251
|
+
when 1 # Boolean
|
|
252
|
+
buf = FFI::MemoryPointer.new(:int)
|
|
253
|
+
Raw.FPDF_StructElement_Attr_GetBooleanValue(val_handle, buf) == 1 ? buf.read_int != 0 : nil
|
|
254
|
+
when 2 # Number
|
|
255
|
+
buf = FFI::MemoryPointer.new(:float)
|
|
256
|
+
Raw.FPDF_StructElement_Attr_GetNumberValue(val_handle, buf) == 1 ? buf.read_float : nil
|
|
257
|
+
when 3, 5 # String / Name
|
|
258
|
+
read_attr_string_value(val_handle)
|
|
259
|
+
when 4 # Blob (raw bytes)
|
|
260
|
+
read_attr_blob_value(val_handle)
|
|
261
|
+
when 6 # Array → ricorsivamente raccolgo i figli
|
|
262
|
+
n = Raw.FPDF_StructElement_Attr_CountChildren(val_handle)
|
|
263
|
+
(0...n).filter_map do |i|
|
|
264
|
+
child = Raw.FPDF_StructElement_Attr_GetChildAtIndex(val_handle, i)
|
|
265
|
+
next nil if child.null?
|
|
266
|
+
|
|
267
|
+
# Per ogni child applico la stessa lettura via type. Ma non ho
|
|
268
|
+
# un "name" per accedere a Attr_GetValue su un child; il child
|
|
269
|
+
# È già una FPDF_STRUCTELEMENT_ATTR_VALUE. Leggi direttamente.
|
|
270
|
+
read_attr_value_handle(child)
|
|
271
|
+
end
|
|
272
|
+
else
|
|
273
|
+
nil
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def read_attr_value_handle(val_handle)
|
|
278
|
+
type = Raw.FPDF_StructElement_Attr_GetType(val_handle)
|
|
279
|
+
case type
|
|
280
|
+
when 1
|
|
281
|
+
buf = FFI::MemoryPointer.new(:int)
|
|
282
|
+
Raw.FPDF_StructElement_Attr_GetBooleanValue(val_handle, buf) == 1 ? buf.read_int != 0 : nil
|
|
283
|
+
when 2
|
|
284
|
+
buf = FFI::MemoryPointer.new(:float)
|
|
285
|
+
Raw.FPDF_StructElement_Attr_GetNumberValue(val_handle, buf) == 1 ? buf.read_float : nil
|
|
286
|
+
when 3, 5
|
|
287
|
+
read_attr_string_value(val_handle)
|
|
288
|
+
when 4
|
|
289
|
+
read_attr_blob_value(val_handle)
|
|
290
|
+
else
|
|
291
|
+
nil
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def read_attr_string_value(val_handle)
|
|
296
|
+
len_buf = FFI::MemoryPointer.new(:ulong)
|
|
297
|
+
# Probe size
|
|
298
|
+
Raw.FPDF_StructElement_Attr_GetStringValue(val_handle,
|
|
299
|
+
FFI::Pointer::NULL, 0, len_buf)
|
|
300
|
+
n = len_buf.read_ulong
|
|
301
|
+
return nil if n < 2
|
|
302
|
+
|
|
303
|
+
buf = FFI::MemoryPointer.new(:uint8, n)
|
|
304
|
+
ok = Raw.FPDF_StructElement_Attr_GetStringValue(val_handle, buf, n, len_buf)
|
|
305
|
+
return nil if ok == 0
|
|
306
|
+
|
|
307
|
+
written = len_buf.read_ulong
|
|
308
|
+
payload = [written - 2, n - 2].min
|
|
309
|
+
return nil if payload <= 0
|
|
310
|
+
|
|
311
|
+
buf.read_bytes(payload).force_encoding("UTF-16LE")
|
|
312
|
+
.encode("UTF-8").delete("\u0000")
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def read_attr_blob_value(val_handle)
|
|
316
|
+
len_buf = FFI::MemoryPointer.new(:ulong)
|
|
317
|
+
Raw.FPDF_StructElement_Attr_GetBlobValue(val_handle,
|
|
318
|
+
FFI::Pointer::NULL, 0, len_buf)
|
|
319
|
+
n = len_buf.read_ulong
|
|
320
|
+
return nil if n.zero?
|
|
321
|
+
|
|
322
|
+
buf = FFI::MemoryPointer.new(:uint8, n)
|
|
323
|
+
ok = Raw.FPDF_StructElement_Attr_GetBlobValue(val_handle, buf, n, len_buf)
|
|
324
|
+
return nil if ok == 0
|
|
325
|
+
|
|
326
|
+
buf.read_bytes(len_buf.read_ulong)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rpdfium
|
|
4
|
+
# Albero di bookmark (outline) del documento. Costruito ricorsivamente.
|
|
5
|
+
class Outline
|
|
6
|
+
attr_reader :title, :page_index, :children
|
|
7
|
+
|
|
8
|
+
def initialize(title, page_index, children)
|
|
9
|
+
@title = title
|
|
10
|
+
@page_index = page_index
|
|
11
|
+
@children = children
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.from_document(document)
|
|
15
|
+
first = Raw.FPDFBookmark_GetFirstChild(document.handle, FFI::Pointer::NULL)
|
|
16
|
+
build_siblings(document, first)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.build_siblings(doc, bookmark_handle)
|
|
20
|
+
result = []
|
|
21
|
+
ptr = bookmark_handle
|
|
22
|
+
until ptr.null?
|
|
23
|
+
title = Raw.read_utf16_string(:FPDFBookmark_GetTitle, ptr)
|
|
24
|
+
dest = Raw.FPDFBookmark_GetDest(doc.handle, ptr)
|
|
25
|
+
idx = dest.null? ? nil : Raw.FPDFDest_GetDestPageIndex(doc.handle, dest)
|
|
26
|
+
idx = nil if idx == -1
|
|
27
|
+
children_handle = Raw.FPDFBookmark_GetFirstChild(doc.handle, ptr)
|
|
28
|
+
children = build_siblings(doc, children_handle)
|
|
29
|
+
result << new(title, idx, children)
|
|
30
|
+
ptr = Raw.FPDFBookmark_GetNextSibling(doc.handle, ptr)
|
|
31
|
+
end
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Iteratore flat preorder: utile per generare un sommario lineare.
|
|
36
|
+
def self.flatten(outline_tree, depth = 0, &block)
|
|
37
|
+
outline_tree.each do |item|
|
|
38
|
+
block.call(item, depth)
|
|
39
|
+
flatten(item.children, depth + 1, &block)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def to_h
|
|
44
|
+
{ title: @title, page: @page_index,
|
|
45
|
+
children: @children.map(&:to_h) }
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|