Clasificar páginas de PDF — Texto vs. Escaneado
Antes de extraer texto, normalmente necesitas saber qué tipo de página tienes delante: ¿tiene una capa de texto nativa utilizable, o es un escaneado que requiere OCR? PDF Oxide responde esto con un preflight económico — classify_page y classify_document inspeccionan los internos del PDF (recuento de glifos, área de imagen, codec, proporción de texto invisible, proporción de glifos corruptos) sin ejecutar OCR y sin rasterizar la página.
La clasificación es explicable: cada veredicto incluye una puntuación de confianza, un código reason tipado y las signals en bruto que guiaron la decisión — lo que te permite enrutar páginas al extractor correcto (texto nativo vs. OCR) y registrar el porqué.
Cobertura de bindings. La clasificación está disponible en Rust, Go, C#, Swift y WASM/JavaScript. Los bindings de Python y Node N-API no exponen
classify_page/classify_documenten v0.3.69 — desde esos entornos, usa la ruta de extracción automática o haz un puente mediante el núcleo Rust / CLI.
¿Cómo clasifico una sola página de un PDF?
classify_page recibe un índice de página con base 0 y devuelve un PageClassification. En los bindings C-ABI (Go, C#, Swift, WASM) se devuelve como una cadena JSON que hay que deserializar.
Rust
use pdf_oxide::PdfDocument;
fn main() -> pdf_oxide::Result<()> {
let doc = PdfDocument::open("mixed.pdf")?;
// PdfDocument::classify_page(&self, page: usize)
// -> Result<pdf_oxide::extractors::auto::PageClassification>
let result = doc.classify_page(0)?;
println!("page {} is {:?} (confidence {:.2})",
result.page, result.kind, result.confidence);
println!("reason: {:?}", result.reason);
println!("glyphs={} image_area={:.2} garbled={:.2}",
result.signals.text_glyph_count,
result.signals.image_area_ratio,
result.signals.garbled_ratio);
Ok(())
}
Go
package main
import (
"encoding/json"
"fmt"
"log"
pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)
func main() {
doc, err := pdfoxide.Open("mixed.pdf")
if err != nil {
log.Fatal(err)
}
defer doc.Close()
// func (doc *PdfDocument) ClassifyPage(pageIndex int) (string, error)
raw, err := doc.ClassifyPage(0)
if err != nil {
log.Fatal(err)
}
var pc struct {
Page int `json:"page"`
Kind string `json:"kind"`
Confidence float64 `json:"confidence"`
Reason string `json:"reason"`
}
if err := json.Unmarshal([]byte(raw), &pc); err != nil {
log.Fatal(err)
}
fmt.Printf("page %d is %s (%.2f): %s\n", pc.Page, pc.Kind, pc.Confidence, pc.Reason)
}
C#
using System;
using System.Text.Json;
using PdfOxide.Core;
using var doc = PdfDocument.Open("mixed.pdf");
// string PdfDocument.ClassifyPage(int pageIndex)
string raw = doc.ClassifyPage(0);
using var json = JsonDocument.Parse(raw);
var root = json.RootElement;
Console.WriteLine(
$"page {root.GetProperty("page").GetInt32()} is " +
$"{root.GetProperty("kind").GetString()} " +
$"({root.GetProperty("confidence").GetDouble():F2}): " +
$"{root.GetProperty("reason").GetString()}");
Swift
import PdfOxide
let doc = try PdfDocument(path: "mixed.pdf")
// func classifyPage(_ pageIndex: Int) throws -> String (JSON)
let json = try doc.classifyPage(0)
print(json)
JavaScript (WASM)
import init, { WasmPdfDocument } from "pdf-oxide-wasm";
await init();
const bytes = new Uint8Array(await (await fetch("mixed.pdf")).arrayBuffer());
const doc = WasmPdfDocument.fromBytes(bytes);
// WasmPdfDocument.classifyPage(pageIndex) -> JSON string
const pc = JSON.parse(doc.classifyPage(0));
console.log(`page ${pc.page} is ${pc.kind} (${pc.confidence}): ${pc.reason}`);
Java
import fyi.oxide.pdf.PdfDocument;
import fyi.oxide.pdf.AutoExtractor;
import fyi.oxide.pdf.auto.PageClass;
try (PdfDocument doc = PdfDocument.open(java.nio.file.Path.of("mixed.pdf"))) {
AutoExtractor auto = AutoExtractor.of(doc);
// PageClass AutoExtractor.classifyPageKind(int pageIndex)
PageClass kind = auto.classifyPageKind(0);
System.out.println("page 0 is " + kind); // TEXT_LAYER / SCANNED / IMAGE_TEXT / MIXED / EMPTY
}
PHP
<?php
use PdfOxide\PdfDocument;
use PdfOxide\AutoExtractor;
$doc = PdfDocument::open('mixed.pdf');
$auto = AutoExtractor::of($doc);
// string AutoExtractor::classifyPageKind(int $pageIndex)
$kind = $auto->classifyPageKind(0);
echo "page 0 is {$kind}\n"; // text_layer / scanned / image_text / mixed / empty
Ruby
require 'pdf_oxide'
doc = PdfOxide::PdfDocument.open('mixed.pdf')
auto = PdfOxide::AutoExtractor.new(doc)
# AutoExtractor#classify_page(page_index)
# => { reason:, kind:, confidence:, classification: }
pc = auto.classify_page(0)
puts "page 0 is #{pc[:kind]} (#{pc[:confidence]}): #{pc[:reason]}"
C++
#include <pdf_oxide/pdf_oxide.hpp>
#include <iostream>
int main() {
auto doc = pdf_oxide::Document::open("mixed.pdf");
// std::string Document::classify_page(int page_index) — JSON
std::string json = doc.classify_page(0);
std::cout << json << '\n';
}
Dart
import 'package:pdf_oxide/pdf_oxide.dart';
void main() {
final doc = PdfDocument.open('mixed.pdf');
// String PdfDocument.classifyPage(int page) — JSON
final json = doc.classifyPage(0);
print(json);
}
R
library(pdfoxide)
doc <- pdf_open("mixed.pdf")
# pdf_classify_page(doc, page) — JSON PageClassification
json <- pdf_classify_page(doc, 0)
cat(json, "\n")
Julia
using PdfOxide
doc = open_document("mixed.pdf")
# classify_page(doc, page) -> JSON string
json = classify_page(doc, 0)
println(json)
Zig
const std = @import("std");
const pdf = @import("pdf_oxide");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
var doc = try pdf.Document.open("mixed.pdf");
defer doc.deinit();
// classifyPage(alloc, page_index) -> caller-owned JSON bytes
const json = try doc.classifyPage(alloc, 0);
defer alloc.free(json);
std.debug.print("{s}\n", .{json});
}
Objective-C
#import <POXPdfOxide.h>
NSError *err = nil;
POXDocument *doc = [POXDocument openPath:@"mixed.pdf" error:&err];
// -classifyPage:error: -> JSON NSString
NSString *json = [doc classifyPage:0 error:&err];
NSLog(@"%@", json);
Elixir
{:ok, doc} = PdfOxide.open("mixed.pdf")
# PdfOxide.classify_page(doc, page) -> JSON string
json = PdfOxide.classify_page(doc, 0)
IO.puts(json)
¿Cómo clasifico un documento PDF completo de una vez?
classify_document ejecuta el mismo preflight económico en cada página y consolida los resultados: una lista de kind por página, los índices pages_needing_ocr con base 0 y un summary agregado. La decisión es por página — PDF Oxide nunca impone un único modo de documento a un PDF mixto.
Rust
use pdf_oxide::PdfDocument;
fn main() -> pdf_oxide::Result<()> {
let doc = PdfDocument::open("report.pdf")?;
// PdfDocument::classify_document(&self)
// -> Result<pdf_oxide::extractors::auto::DocumentClassification>
let dc = doc.classify_document()?;
println!("summary: {:?}", dc.summary);
println!("pages needing OCR: {:?}", dc.pages_needing_ocr);
for (i, kind) in dc.pages.iter().enumerate() {
println!(" page {i}: {kind:?}");
}
Ok(())
}
Go
package main
import (
"encoding/json"
"fmt"
"log"
pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)
func main() {
doc, err := pdfoxide.Open("report.pdf")
if err != nil {
log.Fatal(err)
}
defer doc.Close()
// func (doc *PdfDocument) ClassifyDocument() (string, error)
raw, err := doc.ClassifyDocument()
if err != nil {
log.Fatal(err)
}
var dc struct {
Pages []string `json:"pages"`
PagesNeedingOCR []int `json:"pages_needing_ocr"`
Summary string `json:"summary"`
}
if err := json.Unmarshal([]byte(raw), &dc); err != nil {
log.Fatal(err)
}
fmt.Printf("summary=%s ocr_pages=%v\n", dc.Summary, dc.PagesNeedingOCR)
}
C#
using System;
using PdfOxide.Core;
using var doc = PdfDocument.Open("report.pdf");
// string PdfDocument.ClassifyDocument()
string raw = doc.ClassifyDocument();
Console.WriteLine(raw);
Swift
import PdfOxide
let doc = try PdfDocument(path: "report.pdf")
// func classifyDocument() throws -> String (JSON)
let json = try doc.classifyDocument()
print(json)
JavaScript (WASM)
import init, { WasmPdfDocument } from "pdf-oxide-wasm";
await init();
const bytes = new Uint8Array(await (await fetch("report.pdf")).arrayBuffer());
const doc = WasmPdfDocument.fromBytes(bytes);
// WasmPdfDocument.classifyDocument() -> JSON string
const dc = JSON.parse(doc.classifyDocument());
console.log("pages needing OCR:", dc.pages_needing_ocr);
Java
import fyi.oxide.pdf.PdfDocument;
import fyi.oxide.pdf.AutoExtractor;
import fyi.oxide.pdf.auto.PageClass;
import java.util.List;
try (PdfDocument doc = PdfDocument.open(java.nio.file.Path.of("report.pdf"))) {
AutoExtractor auto = AutoExtractor.of(doc);
// List<PageClass> AutoExtractor.classifyDocumentKinds()
List<PageClass> kinds = auto.classifyDocumentKinds();
for (int i = 0; i < kinds.size(); i++) {
System.out.println("page " + i + ": " + kinds.get(i));
}
}
PHP
<?php
use PdfOxide\PdfDocument;
use PdfOxide\AutoExtractor;
$doc = PdfDocument::open('report.pdf');
$auto = AutoExtractor::of($doc);
// array<int,string> AutoExtractor::classifyDocumentKinds()
$kinds = $auto->classifyDocumentKinds();
foreach ($kinds as $i => $kind) {
echo "page {$i}: {$kind}\n";
}
Ruby
require 'pdf_oxide'
doc = PdfOxide::PdfDocument.open('report.pdf')
auto = PdfOxide::AutoExtractor.new(doc)
# AutoExtractor#classify_document => decoded JSON envelope
dc = auto.classify_document
puts "pages needing OCR: #{dc['pages_needing_ocr']}"
C++
#include <pdf_oxide/pdf_oxide.hpp>
#include <iostream>
int main() {
auto doc = pdf_oxide::Document::open("report.pdf");
// std::string Document::classify_document() — JSON
std::string json = doc.classify_document();
std::cout << json << '\n';
}
Dart
import 'package:pdf_oxide/pdf_oxide.dart';
void main() {
final doc = PdfDocument.open('report.pdf');
// String PdfDocument.classifyDocument() — JSON
final json = doc.classifyDocument();
print(json);
}
R
library(pdfoxide)
doc <- pdf_open("report.pdf")
# pdf_classify_document(doc) — JSON DocumentClassification
json <- pdf_classify_document(doc)
cat(json, "\n")
Julia
using PdfOxide
doc = open_document("report.pdf")
# classify_document(doc) -> JSON string
json = classify_document(doc)
println(json)
Zig
const std = @import("std");
const pdf = @import("pdf_oxide");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
var doc = try pdf.Document.open("report.pdf");
defer doc.deinit();
// classifyDocument(alloc) -> caller-owned JSON bytes
const json = try doc.classifyDocument(alloc);
defer alloc.free(json);
std.debug.print("{s}\n", .{json});
}
Objective-C
#import <POXPdfOxide.h>
NSError *err = nil;
POXDocument *doc = [POXDocument openPath:@"report.pdf" error:&err];
// -classifyDocumentWithError: -> JSON NSString
NSString *json = [doc classifyDocumentWithError:&err];
NSLog(@"%@", json);
Elixir
{:ok, doc} = PdfOxide.open("report.pdf")
# PdfOxide.classify_document(doc) -> JSON string
json = PdfOxide.classify_document(doc)
IO.puts(json)
¿Cómo es el JSON de clasificación?
classify_page devuelve un PageClassification:
{
"page": 0,
"kind": "text_layer",
"confidence": 0.97,
"reason": "native_text_high_confidence",
"signals": {
"text_glyph_count": 1840,
"text_area_ratio": 0.62,
"image_area_ratio": 0.0,
"codec": "none",
"invisible_text_ratio": 0.0,
"garbled_ratio": 0.0,
"fragmented_word_ratio": 0.01,
"consecutive_repeat_ratio": 0.0,
"vector_path_density": 0.04,
"has_reliable_structure": true,
"producer_prior": "authoring",
"page_is_empty": false
}
}
classify_document devuelve un DocumentClassification:
{
"pages": ["text_layer", "scanned", "image_text"],
"pages_needing_ocr": [1],
"summary": "mixed"
}
Tipos de página (kind)
| Tipo | Significado | Ruta recomendada |
|---|---|---|
text_layer |
Predomina texto nativo utilizable | Extraer la capa de texto |
scanned |
Dominado por imágenes, sin texto o texto corrupto | Aplicar OCR a la página |
image_text |
Texto nativo y regiones de imagen con texto | Híbrido: nativo + OCR por región |
mixed |
Heterogéneo en la página (texto + tabla/figura como imagen) | Enrutamiento automático por región |
empty |
En blanco / casi vacío — no es un error | Omitir |
Resumen del documento (summary)
mostly_text, mostly_scanned, mixed o empty.
Códigos de motivo (reason)
El motivo es un token snake_case congelado y de solo adición. Valores comunes: ok, native_text_high_confidence, no_text_layer_present, text_layer_below_threshold, glyph_mapping_missing, encrypted_no_extract_permission, image_table_reconstructed, image_table_no_structure.
¿Cómo enruto la extracción según la clasificación?
El propósito del preflight económico es evitar pagar OCR en páginas que no lo necesitan. Clasifica primero, luego extrae solo las páginas OCR por la ruta más costosa:
Rust
use pdf_oxide::PdfDocument;
use pdf_oxide::extractors::auto::PageKind;
fn main() -> pdf_oxide::Result<()> {
let doc = PdfDocument::open("report.pdf")?;
let dc = doc.classify_document()?;
for (page, kind) in dc.pages.iter().enumerate() {
match kind {
PageKind::TextLayer => {
// Fast, free native path — no OCR cost.
let text = doc.extract_text(page)?;
println!("=== page {page} (native) ===\n{text}");
}
PageKind::Scanned | PageKind::ImageText | PageKind::Mixed => {
println!("=== page {page} needs OCR ===");
// route to your OCR / auto-extract pipeline here
}
PageKind::Empty => { /* skip */ }
}
}
Ok(())
}
Java
import fyi.oxide.pdf.PdfDocument;
import fyi.oxide.pdf.AutoExtractor;
import fyi.oxide.pdf.auto.PageClass;
import java.util.List;
try (PdfDocument doc = PdfDocument.open(java.nio.file.Path.of("report.pdf"))) {
AutoExtractor auto = AutoExtractor.of(doc);
List<PageClass> kinds = auto.classifyDocumentKinds();
for (int page = 0; page < kinds.size(); page++) {
switch (kinds.get(page)) {
case TEXT_LAYER -> {
// Fast, free native path — no OCR cost.
String text = doc.extractText(page);
System.out.println("=== page " + page + " (native) ===\n" + text);
}
case SCANNED, IMAGE_TEXT, MIXED ->
System.out.println("=== page " + page + " needs OCR ===");
case EMPTY -> { /* skip */ }
}
}
}
Ruby
require 'pdf_oxide'
doc = PdfOxide::PdfDocument.open('report.pdf')
auto = PdfOxide::AutoExtractor.new(doc)
dc = auto.classify_document
dc['pages'].each_with_index do |kind, page|
case kind
when 'text_layer'
# Fast, free native path — no OCR cost.
text = doc.extract_text(page)
puts "=== page #{page} (native) ===\n#{text}"
when 'scanned', 'image_text', 'mixed'
puts "=== page #{page} needs OCR ==="
when 'empty'
# skip
end
end
C++
#include <pdf_oxide/pdf_oxide.hpp>
#include <nlohmann/json.hpp> // any JSON lib
#include <iostream>
int main() {
auto doc = pdf_oxide::Document::open("report.pdf");
auto dc = nlohmann::json::parse(doc.classify_document());
int page = 0;
for (const auto& kind : dc["pages"]) {
if (kind == "text_layer") {
// Fast, free native path — no OCR cost.
std::cout << "=== page " << page << " (native) ===\n"
<< doc.extract_text(page) << '\n';
} else if (kind == "scanned" || kind == "image_text" || kind == "mixed") {
std::cout << "=== page " << page << " needs OCR ===\n";
}
++page;
}
}
PHP
<?php
use PdfOxide\PdfDocument;
use PdfOxide\AutoExtractor;
$doc = PdfDocument::open('report.pdf');
$auto = AutoExtractor::of($doc);
foreach ($auto->classifyDocumentKinds() as $page => $kind) {
if ($kind === 'text_layer') {
// Fast, free native path — no OCR cost.
echo "=== page {$page} (native) ===\n" . $doc->extractText($page) . "\n";
} elseif (in_array($kind, ['scanned', 'image_text', 'mixed'], true)) {
echo "=== page {$page} needs OCR ===\n";
}
}
Dart
import 'dart:convert';
import 'package:pdf_oxide/pdf_oxide.dart';
void main() {
final doc = PdfDocument.open('report.pdf');
final dc = jsonDecode(doc.classifyDocument()) as Map<String, dynamic>;
final pages = (dc['pages'] as List).cast<String>();
for (var page = 0; page < pages.length; page++) {
final kind = pages[page];
if (kind == 'text_layer') {
// Fast, free native path — no OCR cost.
print('=== page $page (native) ===\n${doc.extractText(page)}');
} else if (kind == 'scanned' || kind == 'image_text' || kind == 'mixed') {
print('=== page $page needs OCR ===');
}
}
}
R
library(pdfoxide)
library(jsonlite)
doc <- pdf_open("report.pdf")
dc <- fromJSON(pdf_classify_document(doc))
for (page in seq_along(dc$pages)) {
kind <- dc$pages[[page]]
idx <- page - 1L # 0-based page index
if (kind == "text_layer") {
# Fast, free native path — no OCR cost.
cat(sprintf("=== page %d (native) ===\n%s\n", idx, pdf_extract_text(doc, idx)))
} else if (kind %in% c("scanned", "image_text", "mixed")) {
cat(sprintf("=== page %d needs OCR ===\n", idx))
}
}
Julia
using PdfOxide
using JSON
doc = open_document("report.pdf")
dc = JSON.parse(classify_document(doc))
for (page, kind) in enumerate(dc["pages"])
idx = page - 1 # 0-based page index
if kind == "text_layer"
# Fast, free native path — no OCR cost.
println("=== page $idx (native) ===\n", extract_text(doc, idx))
elseif kind in ("scanned", "image_text", "mixed")
println("=== page $idx needs OCR ===")
end
end
Zig
const std = @import("std");
const pdf = @import("pdf_oxide");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const alloc = gpa.allocator();
var doc = try pdf.Document.open("report.pdf");
defer doc.deinit();
const dc = try doc.classifyDocument(alloc);
defer alloc.free(dc);
const parsed = try std.json.parseFromSlice(std.json.Value, alloc, dc, .{});
defer parsed.deinit();
const pages = parsed.value.object.get("pages").?.array;
for (pages.items, 0..) |kind_val, page| {
const kind = kind_val.string;
const idx: i32 = @intCast(page);
if (std.mem.eql(u8, kind, "text_layer")) {
// Fast, free native path — no OCR cost.
const text = try doc.extractText(alloc, idx);
defer alloc.free(text);
std.debug.print("=== page {d} (native) ===\n{s}\n", .{ idx, text });
} else if (std.mem.eql(u8, kind, "scanned") or
std.mem.eql(u8, kind, "image_text") or
std.mem.eql(u8, kind, "mixed"))
{
std.debug.print("=== page {d} needs OCR ===\n", .{idx});
}
}
}
Objective-C
#import <POXPdfOxide.h>
NSError *err = nil;
POXDocument *doc = [POXDocument openPath:@"report.pdf" error:&err];
NSString *json = [doc classifyDocumentWithError:&err];
NSDictionary *dc = [NSJSONSerialization JSONObjectWithData:[json dataUsingEncoding:NSUTF8StringEncoding]
options:0 error:&err];
NSArray<NSString *> *pages = dc[@"pages"];
[pages enumerateObjectsUsingBlock:^(NSString *kind, NSUInteger page, BOOL *stop) {
if ([kind isEqualToString:@"text_layer"]) {
// Fast, free native path — no OCR cost.
NSString *text = [doc extractText:(NSInteger)page error:nil];
NSLog(@"=== page %lu (native) ===\n%@", (unsigned long)page, text);
} else if ([kind isEqualToString:@"scanned"] ||
[kind isEqualToString:@"image_text"] ||
[kind isEqualToString:@"mixed"]) {
NSLog(@"=== page %lu needs OCR ===", (unsigned long)page);
}
}];
Elixir
{:ok, doc} = PdfOxide.open("report.pdf")
dc = doc |> PdfOxide.classify_document() |> Jason.decode!()
dc["pages"]
|> Enum.with_index()
|> Enum.each(fn {kind, page} ->
case kind do
"text_layer" ->
# Fast, free native path — no OCR cost.
IO.puts("=== page #{page} (native) ===\n#{PdfOxide.extract_text(doc, page)}")
k when k in ["scanned", "image_text", "mixed"] ->
IO.puts("=== page #{page} needs OCR ===")
_ ->
:ok
end
end)
El preflight de clasificación nativo es prácticamente gratuito en comparación con la extracción — nunca rasteriza ni ejecuta OCR, por lo que puedes ejecutarlo en un corpus completo para decidir qué páginas merecen el presupuesto de OCR. La propia extracción de texto nativo de PDF Oxide funciona a 0,8 ms de media / 100 % de tasa de éxito en el benchmark publicado, por lo que la ruta clasificar-luego-extraer mantiene rápido el caso habitual.
Nota sobre PDFs cifrados
classify_page y classify_document fallan de forma segura en un documento cifrado que aún no has autenticado — devuelven un error EncryptedPdf en lugar de informar silenciosamente empty. Autentica primero (ver Cifrar y descifrar PDFs) antes de clasificar. Los fallos por página no relacionados con la seguridad degradan con gracia a empty.
Preguntas frecuentes
¿La clasificación ejecuta OCR?
No. classify_page / classify_document son inspección pura de los internos del PDF — sin OCR, sin rasterización. Eso es lo que los hace lo suficientemente económicos para ejecutarlos en un corpus completo como preflight.
¿La clasificación está disponible en Python o Node? No en v0.3.69. Los métodos están disponibles en Rust, Go, C#, Swift y WASM/JavaScript. Desde Python/Node, usa la extracción automática o haz un puente mediante el núcleo Rust / CLI.
¿Qué tan precisa es la distinción text_layer vs. scanned?
El clasificador combina múltiples señales (recuento de glifos, área de imagen, codec raster, proporción de texto invisible, proporciones de glifos corruptos/fragmentados) y aplica un filtro de calidad de texto mejorado, de modo que una capa de texto born-digital inutilizable (columnas desordenadas, basura (cid:NN), fragmentación por glifo) se degrada a scanned con el motivo tipado en lugar de ser confiada.
¿Por qué el resultado es JSON en Go / C# / Swift? Esos bindings cruzan el C ABI, que devuelve la clasificación como una cadena JSON asignada con malloc. Deserialízala con tu biblioteca JSON estándar — los nombres de campos y los tokens de enumeración están congelados y son estables entre versiones.
Páginas relacionadas
- OCR en PDFs escaneados — extraer texto de las páginas marcadas como
scannedpor la clasificación - Gestión de modelos sin conexión — obtener previamente modelos OCR para enrutamiento air-gapped
- Perfiles de extracción — ajustar la extracción de texto nativo por tipo de documento
- Extraer texto de un PDF — la ruta nativa rápida para páginas
text_layer