SDK de Conversión de Archivos para Rust#
enconvert es el crate oficial de Rust para la API de EnConvert. Es un cliente bloqueante construido sobre la API bloqueante de reqwest, así que no hay runtime asíncrono, no hay tokio en tu árbol de dependencias y no hay ningún .await en tu código. Doce métodos de Enconvert cubren la conversión de archivos: URL a PDF, capturas de pantalla, extracción de Markdown, pares de formatos de imagen y de documento, cualquier cosa a PDF y lotes de sitios completos. Un segundo espacio de nombres, client.v2(), añade veintitrés métodos de inteligencia web: perceive, discover, lookup, distill, ingest y watch. Cada struct de opciones deriva Default, y cada fallo es un único enum de error.
enconvert (0.1.0) · Fuente: conversionapi/rust-sdk · Rust: edición 2021 · Licencia: MIT
Instalación#
cargo add enconvert
O añádelo a mano. distill y la extracción por schema de perceive reciben un serde_json::Map<String, Value>, así que añade también serde_json si piensas usarlos:
[dependencies]
enconvert = "0.1"
serde_json = "1"
El crate arrastra reqwest (blocking, json, multipart), serde, serde_json, thiserror y uuid. No hay features opcionales que activar.
Inicio rápido#
use enconvert::{Enconvert, PerceiveOptions, PerceiveOutputName, UrlToPdfOptions};
fn main() -> Result<(), enconvert::Error> {
let key = std::env::var("ENCONVERT_API_KEY").expect("ENCONVERT_API_KEY is not set");
let client = Enconvert::new(key)?;
// Convierte una URL a PDF y escribe el resultado directamente en disco.
let pdf = client.convert_url_to_pdf("https://example.com", UrlToPdfOptions {
save_to: Some("page.pdf".into()),
..Default::default()
})?;
println!("{}", pdf.presigned_url);
// Lee la misma página como debería hacerlo tu agente, con una puntuación de calidad adjunta.
let op = client.v2().perceive("https://example.com", PerceiveOptions {
outputs: Some(vec![PerceiveOutputName::Markdown, PerceiveOutputName::Structured]),
..Default::default()
})?;
println!("{:?}", op.render_quality); // p. ej. Some(0.93)
Ok(())
}
Cada método bloquea el hilo que llama, lo que hace fácil meter el SDK en una CLI, en un build script, en un hilo trabajador o en un handler web síncrono. Todos los tipos públicos se reexportan en la raíz del crate, así que los fragmentos siguientes no necesitan más que use enconvert::{...} y el client de arriba.
reqwest no se puede accionar desde un hilo que ya pertenece a un reactor de Tokio (o similar). Desde código asíncrono, envuelve cada llamada en tokio::task::spawn_blocking.
Qué expone el cliente#
| Superficie | Cómo se accede | Qué cubre |
|---|---|---|
| Conversión de archivos | client.<method> |
12 métodos: URL a PDF, captura de pantalla y Markdown; pares de imagen y de documento; cualquier cosa a Markdown y cualquier cosa a PDF; lotes de sitios completos; estado de job y de lote |
| Inteligencia web | client.v2().<method> |
23 métodos repartidos entre perceive, discover, lookup, distill, ingest y watch |
| Tablas de formatos | valid_outputs_for, IMPLEMENTED_CONVERSIONS |
Los 43 endpoints {input}-to-{output} implementados, comprobados en el cliente antes de enviar una solicitud |
| Errores | enconvert::Error |
Un enum, nueve variantes, con is_authentication, is_quota, is_rate_limit, is_server_error y status_code |
Nada es un builder y nada es async. La forma idiomática de llamada es un literal de struct con ..Default::default().
Conversión de archivos#
convert_url_to_pdf#
Renderiza cualquier URL pública a PDF.
use enconvert::{PdfOptions, PdfOrientation, UrlToPdfOptions};
let result = client.convert_url_to_pdf("https://example.com", UrlToPdfOptions {
single_page: Some(false),
pdf_options: Some(PdfOptions {
page_size: Some("A4".to_string()),
orientation: Some(PdfOrientation::Landscape),
..Default::default()
}),
save_to: Some("report.pdf".into()),
..Default::default()
})?;
println!("{} ({:?} bytes)", result.filename, result.file_size);
| Campo | Tipo | Por defecto | Descripción |
|---|---|---|---|
save_to |
Option<PathBuf> |
- | Ruta local donde escribir el PDF. Los directorios padre se crean automáticamente. |
single_page |
Option<bool> |
true |
true produce una única página continua. false pagina usando pdf_options.page_size. |
pdf_options |
Option<PdfOptions> |
- | Geometría de página, escala, escala de grises, encabezado y pie. Consulta Opciones de PDF. |
render.viewport_width / render.viewport_height |
Option<u32> |
1920 / 1080 |
Viewport del navegador en píxeles. |
render.load_media / render.enable_scroll |
Option<bool> |
true |
Espera a imágenes y vídeo, y desplaza de arriba abajo para disparar los cargadores diferidos. |
render.output_filename |
Option<String> |
automático | Sobrescribe el nombre de archivo generado. |
render.auth / render.cookies / render.headers |
ver tipos | - | Autenticación HTTP Basic, hasta 50 cookies inyectadas, hasta 20 encabezados de solicitud adicionales. |
El bloque render es UrlRenderOptions, compartido por todas las conversiones de URL y de sitio web. No combines auth con un encabezado Authorization: la API rechaza el conflicto.
convert_url_to_screenshot#
Captura un PNG de cualquier URL. UrlToScreenshotOptions lleva el mismo bloque render más save_to, y nada más.
use enconvert::{UrlRenderOptions, UrlToScreenshotOptions};
client.convert_url_to_screenshot("https://example.com", UrlToScreenshotOptions {
render: UrlRenderOptions { viewport_width: Some(1440), ..Default::default() },
save_to: Some("shot.png".into()),
})?;
convert_url_to_markdown#
Extrae Markdown limpio con sabor GitHub a partir de una URL. Se eliminan la navegación, los pies de página, los anuncios y los scripts, se conserva el cuerpo principal del artículo y se antepone frontmatter YAML (title, description, url, links, images).
use enconvert::UrlToMarkdownOptions;
client.convert_url_to_markdown("https://example.com/article", UrlToMarkdownOptions {
save_to: Some("article.md".into()),
..Default::default()
})?;
Útil para pipelines de RAG, importaciones a un CMS y recolección de datos de entrenamiento. Para una lectura puntuada, con artefactos y extracción estructurada en una sola llamada, usa perceive en su lugar.
convert_image#
Convierte entre jpeg, png, svg, heic y webp (los 20 pares ordenados), o rasteriza un PDF a JPEG.
use enconvert::{ConvertImageOptions, NamedFile};
// Desde una ruta: el formato de entrada procede de la extensión.
client.convert_image("photo.heic", ConvertImageOptions {
output_format: "webp".to_string(),
save_to: Some("photo.webp".into()),
..Default::default()
})?;
// Desde bytes: proporciona un nombre de archivo para que la extensión se pueda seguir leyendo.
let data = std::fs::read("photo.heic")?;
client.convert_image(
NamedFile { data, filename: "photo.heic".to_string(), content_type: None },
ConvertImageOptions { output_format: "png".to_string(), ..Default::default() },
)?;
output_format es el único campo obligatorio; save_to y output_filename son opcionales. El primer argumento es cualquier cosa que se convierta en FileInput: una ruta &str o String, un &Path o PathBuf, un Vec<u8> o &[u8] de bytes crudos, o un NamedFile cuando tienes los bytes y quieres declarar tú mismo el nombre de archivo y el tipo de contenido.
convert_document#
Convierte documentos y formatos de datos. output_format es "pdf" por defecto, y yml, htm, md y jpg se normalizan a sus nombres canónicos. save_to, output_filename y pdf_options son opcionales.
use enconvert::{ConvertDocumentOptions, PdfOptions};
// docx a pdf (el formato de salida por defecto)
client.convert_document("report.docx", ConvertDocumentOptions {
save_to: Some("report.pdf".into()),
..Default::default()
})?;
// json a yaml
client.convert_document("data.json", ConvertDocumentOptions {
output_format: Some("yaml".to_string()),
save_to: Some("data.yaml".into()),
..Default::default()
})?;
// markdown a pdf con configuración de página personalizada
client.convert_document("README.md", ConvertDocumentOptions {
pdf_options: Some(PdfOptions { page_size: Some("A4".to_string()), ..Default::default() }),
..Default::default()
})?;
Extensiones de entrada reconocidas: .doc, .docx, .xls, .xlsx, .ppt, .pptx, .html, .htm, .odt, .ods, .odp, .ots, .pages, .numbers, .md, .markdown, .csv, .json, .xml, .yaml, .yml, .toml.
| Formato de entrada | Salidas válidas |
|---|---|
json |
csv, toml, xml, yaml |
xml |
csv, json |
csv |
json, xml |
yaml, toml |
json |
markdown |
html, pdf |
html |
pdf |
doc, excel, ppt, odt, ods, odp, ots, pages, numbers |
pdf |
jpeg, png, svg, heic, webp |
entre sí, los 20 pares |
pdf |
jpeg |
EPUB no tiene un par de documento dedicado. Envía los archivos .epub a través de convert_to_pdf o convert_to_markdown.
Tanto convert_image como convert_document resuelven el formato de entrada a partir de la extensión del archivo y comprueban el par contra la tabla del lado del cliente, así que un par no implementado devuelve Error::UnsupportedConversion con las salidas válidas enumeradas, antes de realizar ninguna solicitud HTTP. Consulta esa misma tabla directamente:
use enconvert::{valid_outputs_for, IMPLEMENTED_CONVERSIONS};
println!("{:?}", valid_outputs_for("json")); // ["csv", "toml", "xml", "yaml"]
println!("{}", IMPLEMENTED_CONVERSIONS.len()); // 43
convert_to_markdown#
Sube un documento de casi cualquier tipo y recibe Markdown limpio: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT y MD, y archivos de ofimática antiguos o en ODF. El formato se detecta en el servidor, así que no se ejecuta ninguna comprobación de extensión en el cliente. Las imágenes no están admitidas.
use enconvert::ConvertToMarkdownOptions;
client.convert_to_markdown("handbook.pdf", ConvertToMarkdownOptions {
save_to: Some("handbook.md".into()),
..Default::default()
})?;
La salida es un único archivo .md consciente de los encabezados, lo que lo convierte en una buena pieza de partida para el troceado de RAG: un chunker semántico puede dividir por la propia jerarquía de encabezados del documento en lugar de por recuentos arbitrarios de caracteres. Aquí no hay opciones de PDF, solo save_to y output_filename.
convert_to_pdf#
Sube casi cualquier cosa y recibe un PDF: ofimática, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, texto plano, imágenes rasterizadas, SVG, EPUB o un PDF existente que pasa de largo.
use enconvert::{ConvertToPdfOptions, PdfOptions};
client.convert_to_pdf("slides.pptx", ConvertToPdfOptions {
save_to: Some("slides.pdf".into()),
..Default::default()
})?;
// PDF de entrada, PDF en escala de grises de salida.
client.convert_to_pdf("scan.pdf", ConvertToPdfOptions {
pdf_options: Some(PdfOptions { grayscale: Some(true), ..Default::default() }),
save_to: Some("scan-gray.pdf".into()),
..Default::default()
})?;
pdf_options.grayscale. El endpoint de cualquier cosa a PDF detecta automáticamente la entrada y aplica su propia geometría. Para controlar el tamaño de página, la orientación, los márgenes, la escala, los encabezados y los pies, usa convert_url_to_pdf o convert_document con una entrada HTML o Markdown.
convert_website_to_pdf y convert_website_to_screenshot#
Descubre todas las páginas de un sitio web, convierte cada una en segundo plano y recoge un único ZIP. Ambos son solo asíncronos: devuelven un BatchSubmission, nunca un archivo terminado.
use enconvert::{CrawlMode, WaitForBatchOptions, WebsiteConversionOptions, WebsiteToPdfOptions};
let batch = client.convert_website_to_pdf("https://example.com", WebsiteToPdfOptions {
website: WebsiteConversionOptions {
crawl_mode: Some(CrawlMode::Sitemap),
exclude_patterns: Some(vec!["/blog/tag/".to_string()]),
notification_email: Some("[email protected]".to_string()),
..Default::default()
},
..Default::default()
})?;
// Bloquea hasta que el lote se resuelva y luego guarda el ZIP.
let status = client.wait_for_batch(&batch.batch_id, WaitForBatchOptions {
save_to: Some("site.zip".into()),
..Default::default()
})?;
println!("{} of {} pages converted", status.completed, status.total);
| Campo | Tipo | Por defecto | Descripción |
|---|---|---|---|
website.crawl_mode |
Option<CrawlMode> |
Auto |
Auto, Sitemap (solo sitemap.xml) o Full (sitemap más un rastreo en anchura). |
website.include_patterns / website.exclude_patterns |
Option<Vec<String>> |
- | Rastrea solo, o salta, las URL que coincidan con estos patrones. Solo en modo de rastreo completo. |
website.notification_email / website.callback_url |
Option<String> |
propietario del proyecto / - | Dirección a la que se envía el correo, y webhook al que se hace POST, cuando el lote termina. |
website.render |
UrlRenderOptions |
- | Los mismos campos de viewport, medios, desplazamiento, autenticación, cookies y encabezados que en las conversiones de una sola URL. |
single_page / pdf_options |
ver arriba | - | Solo en lotes de PDF. |
convert_website_to_screenshot recibe WebsiteToScreenshotOptions, un alias de tipo de WebsiteConversionOptions, y produce un ZIP de PNG. wait_for_batch sondea get_batch_status cada interval_ms (5000 por defecto) hasta que el lote deja el estado Processing, y después descarga opcionalmente el ZIP. Si primero se agota timeout_ms (1.800.000 por defecto, es decir, 30 minutos), devuelve Error::Api { status: 504, .. }.
get_job_status y get_batch_status#
use enconvert::JobStatusValue;
let status = client.get_job_status("job_abc123")?;
match status.status {
JobStatusValue::Success => println!("{:?}", status.presigned_url),
JobStatusValue::Failed => eprintln!("{:?}", status.error),
other => println!("still running: {other:?}"),
}
let batch = client.get_batch_status("batch_abc123")?; // recuentos agregados y elementos por URL
println!("{}/{} done, {} failed", batch.completed, batch.total, batch.failed);
get_job_status directamente. El SDK lo sondea por ti cuando una conversión síncrona devuelve 5xx. Consulta Recuperación de timeouts.
Inteligencia web (V2)#
Todo lo que hay bajo client.v2() convierte páginas web en datos listos para agentes. Lo único que toda lectura tiene en común es render_quality, una puntuación de 0.0 a 1.0 adjunta a cada página renderizada. Una puntuación baja significa que la página no se renderizó con limpieza: una página de desafío, un muro de cookies o de inicio de sesión, el armazón vacío de una SPA o un error HTTP. El contenido se sigue devolviendo, pero vuelve marcado, con un mapa deductions con nombres y una lista warnings, de modo que una mala lectura nunca entra en silencio en el contexto de tu agente. Los resultados de perceive, distill, lookup y watch lo llevan todos. Empieza por la visión general de V2 para conocer los conceptos detrás de las seis capacidades.
Perceive#
Renderiza una URL en los artefactos que pidas. Es síncrono: la llamada devuelve una operación completada con URL de artefacto firmadas durante 15 minutos. Consulta perceive.
use enconvert::{PerceiveExtractName, PerceiveOptions, PerceiveOutputName};
let op = client.v2().perceive("https://example.com", PerceiveOptions {
outputs: Some(vec![PerceiveOutputName::Markdown, PerceiveOutputName::Screenshot]),
extract: Some(vec![PerceiveExtractName::Tables, PerceiveExtractName::Metadata]),
..Default::default()
})?;
println!("{:?}", op.render_quality);
println!("{:?}", op.deductions); // p. ej. {"http_error": 0.7} cuando algo va mal
println!("{:?}", op.outputs.get("markdown").and_then(|a| a.url.as_ref()));
println!("{:?}", op.structured);
// Las URL de artefacto se vuelven a firmar en cada lectura de estado.
client.v2().get_perceive_operation(&op.operation_id)?;
| Opción | Tipo | Por defecto | Descripción |
|---|---|---|---|
outputs |
Option<Vec<PerceiveOutputName>> |
markdown, structured |
Markdown, HtmlCleaned, HtmlRaw, Screenshot, ScreenshotFullPage, Pdf, Links, Images, Structured. |
extract |
Option<Vec<PerceiveExtractName>> |
- | Tables, Prices, Contacts, Metadata, MainContent, Headings, StructuredData, Technologies, All. |
schema |
Option<Map<String, Value>> |
- | Schema JSON para la extracción estructurada. |
only_main_content |
Option<bool> |
true |
Elimina la navegación, el encabezado, el pie y los banners de cookies del artefacto Markdown y del extract main_content. |
wait_for / wait_timeout_ms / js_code |
ver tipos | - / 30000 / - |
Un selector CSS (opcionalmente css:...) o js:<expr> que esperar, cuánto esperar (de 0 a 60000) y JavaScript que ejecutar tras la navegación (máximo 20000 caracteres). |
viewport / mobile |
Option<PerceiveViewport> / Option<bool> |
1920x1080 | Ancho de 320 a 3840, alto de 240 a 2160, o un perfil de dispositivo móvil. |
cache_mode / block_resources |
ver tipos | Enabled / - |
Enabled cachea durante 1 hora, Bypass se salta la caché, Refresh vuelve a renderizar; más los tipos de recurso que el navegador no debería cargar. |
headers, cookies, auth, respect_robots, pdf_options |
ver UrlRenderOptions y PdfOptions |
- | Las mismas formas que las opciones de renderizado de V1. pdf_options solo importa cuando outputs incluye Pdf. |
perceive_direct devuelve en streaming un único artefacto como bytes crudos en lugar de un sobre JSON. Hay que pedir exactamente una salida que produzca artefacto, y el SDK lo comprueba localmente con Error::InvalidInput antes de enviar nada.
use enconvert::{PerceiveOptions, PerceiveOutputName};
let direct = client.v2().perceive_direct("https://example.com", PerceiveOptions {
outputs: Some(vec![PerceiveOutputName::Pdf]),
..Default::default()
})?;
std::fs::write(direct.filename.as_deref().unwrap_or("page.pdf"), &direct.content)?;
// Vuelve a descargar más tarde un artefacto almacenado. Pasa None cuando la operación produjo solo uno.
let saved = client
.v2()
.download_perceive_artifact(&direct.operation_id, Some(PerceiveOutputName::Pdf))?;
println!("{} bytes", saved.content.len());
PerceiveDirectResult lleva content, content_type, filename, operation_id, object_key, cache_hit, render_quality, source_status_code, content_hash y warnings_count, todos leídos de los encabezados de la respuesta. Un Error::Api { status: 410, .. } desde download_perceive_artifact significa que el artefacto ha superado su ventana de retención.
perceive_batch acepta hasta 1000 URL y un único bloque de opciones compartido. Los lotes pequeños se resuelven en línea; los más grandes vuelven con estado Queued, así que sondea get_perceive_batch con el job_id.
use enconvert::{PerceiveBatchOptions, PerceiveBatchOutputMode, PerceiveOptions, PerceiveOutputName};
let render = PerceiveOptions {
outputs: Some(vec![PerceiveOutputName::Markdown]),
..Default::default()
};
let batch = client.v2().perceive_batch(
vec!["https://example.com/a".to_string(), "https://example.com/b".to_string()],
PerceiveBatchOptions { render, output_mode: Some(PerceiveBatchOutputMode::Zip) },
)?;
let done = client.v2().get_perceive_batch(&batch.job_id)?;
for item in &done.items {
println!("{} {:?}", item.url, item.render_quality);
}
Discover#
Enumera las URL de un sitio sin renderizado de navegador alguno. Consulta discover.
use enconvert::{DiscoverMode, DiscoverOptions};
let found = client.v2().discover("https://example.com", DiscoverOptions {
mode: Some(DiscoverMode::Hybrid),
max_urls: Some(200),
exclude_patterns: Some(vec!["/tag/".to_string()]),
..Default::default()
})?;
println!("{} urls, truncated: {}", found.total, found.truncated);
mode es Hybrid por defecto (Sitemap y Crawl son las alternativas), max_urls es 100 (de 1 a 1000), max_depth es 2 (de 1 a 5) y same_domain_only es true. include_patterns y después exclude_patterns aplican filtrado por regex, con un máximo de 50 cada uno, y respect_robots respeta robots.txt. El resultado lleva urls, total, pages_crawled, truncated, robots_respected y un mapa sources con los recuentos crudos por fuente.
Lookup#
Ejecuta una búsqueda web categorizada y, opcionalmente, renderiza los primeros resultados en la misma llamada. Consulta lookup.
use enconvert::{LookupCategory, LookupOptions};
let search = client.v2().lookup("best static site generators", LookupOptions {
category: Some(LookupCategory::Web),
num_results: Some(10),
country: Some("us".to_string()),
perceive_top: Some(3),
..Default::default()
})?;
for hit in &search.results {
let quality = hit.perceive.as_ref().and_then(|p| p.render_quality);
println!("{:?} {:?} {quality:?}", hit.title, hit.url);
}
category es Web por defecto (News, Images, Scholar, Patents y Maps son las otras), num_results es 10 (de 1 a 100), page es 1 (de 1 a 10) y autocorrect es true. country y locale reciben códigos como "us" y "en", location recibe texto libre como "Austin, Texas", y time_filter acepta Hour, Day, Week, Month o Year. perceive_top (de 0 a 10, 0 por defecto) renderiza automáticamente las N primeras URL de resultado y adjunta un PerceiveResult completo a cada acierto. El resultado también lleva answer_box, knowledge_graph y perceive_operation_ids.
Distill#
Extracción estructurada guiada por schema: le das una forma y recibes esa forma para cada URL. Primero se ejecuta una pasada CSS opcional, y todo lo que se le escape escala al nivel LLM. Consulta distill.
use enconvert::{CssField, CssFieldType, CssSchema, DistillOptions};
use serde_json::json;
// CssField no tiene Default porque `field_type` es obligatorio.
fn text_field(name: &str, selector: &str) -> CssField {
CssField {
name: name.into(), field_type: CssFieldType::Text, selector: Some(selector.into()),
attribute: None, pattern: None, default: None, transform: None, fields: None,
}
}
let distilled = client.v2().distill(DistillOptions {
urls: Some(vec!["https://example.com/pricing".to_string()]),
schema: json!({ "plans": "list of plan names with monthly prices" })
.as_object().unwrap().clone(),
css_schema: Some(CssSchema {
base_selector: ".plan-card".to_string(),
fields: vec![text_field("name", "h3"), text_field("price", ".price")],
name: None,
target_field: None,
}),
..Default::default()
})?;
for item in &distilled.results {
println!("{:?} tier {:?} css {} llm {}",
item.data, item.extraction_tier, item.fields_from_css, item.fields_from_llm);
}
O descubre primero un sitio y destila cada página que encuentre:
use enconvert::{DistillDiscoverFrom, DistillOptions};
use serde_json::json;
client.v2().distill(DistillOptions {
discover_from: Some(DistillDiscoverFrom {
max_pages: Some(20), // de 1 a 50, limita tanto el descubrimiento como la destilación
..DistillDiscoverFrom::new("https://example.com")
}),
schema: json!({ "title": "page title" }).as_object().unwrap().clone(),
..Default::default()
})?;
Proporciona exactamente uno de urls (máximo 50) o discover_from. Ambos, o ninguno, devuelve Error::InvalidInput antes de enviar ninguna solicitud. schema es obligatorio y es o bien un objeto JSON-Schema o bien un mapa plano {field: description}. CssFieldType cubre Text, Attribute, Html, Regex, Nested, List y NestedList; transform acepta Lowercase, Uppercase o Strip; el anidamiento está limitado a una profundidad de 5.
Ingest#
Convierte un sitio entero, o un montón de documentos subidos, en JSONL troceado y listo para RAG a través de un único pipeline. Ingest es siempre asíncrono. Consulta ingest.
use enconvert::{
IngestChunkOptions, IngestFilesOptions, IngestMode, IngestOptions, IngestStatus, V2ListOptions,
};
// Desde un sitio.
let job = client.v2().ingest(IngestOptions {
mode: Some(IngestMode::Sitemap),
url: Some("https://docs.example.com".to_string()),
max_pages: Some(100),
chunk: Some(IngestChunkOptions { max_words: Some(512), sentence_overlap: Some(1) }),
webhook_url: Some("https://my.app/hooks/enconvert".to_string()),
..Default::default()
})?;
// O desde archivos subidos: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT y MD,
// y documentos de ofimática antiguos o en ODF.
let files = vec!["handbook.pdf".into(), "notes.docx".into()];
client.v2().ingest_files(files, IngestFilesOptions::default())?;
// Sondea, lista, cancela.
let status = client.v2().get_ingest_job(&job.job_id)?;
if status.status == IngestStatus::Completed {
println!("{:?}", status.output_url); // URL firmada del JSONL
}
client.v2().list_ingest_jobs(V2ListOptions { limit: Some(50), ..Default::default() })?;
client.v2().cancel_ingest_job(&job.job_id)?; // idempotente
mode es Urls por defecto, lo que exige una lista urls no vacía (máximo 1000) y rechaza url; Sitemap y Crawl son lo contrario, y exigen la url semilla. Ese emparejamiento se valida en el cliente, así que una discrepancia devuelve Error::InvalidInput de inmediato. max_pages es 50 por defecto (de 1 a 1000), max_depth es 2 (de 1 a 5), same_domain_only es true, chunk.max_words es 512 (de 32 a 4000) y chunk.sentence_overlap es 1 (de 0 a 10). include_patterns, exclude_patterns, respect_robots, wait_for y wait_timeout_ms se comportan igual que en discover y perceive. Los webhooks de finalización van firmados con HMAC:
let secret = client.v2().get_webhook_secret()?;
println!("{} {} {}", secret.secret, secret.signature_header, secret.signature_scheme);
client.v2().rotate_webhook_secret()?; // las firmas antiguas dejan de verificarse al instante
client.v2().retry_ingest_webhook("ing_abc123")?; // reenvía el webhook de un job completado
Watch#
Vuelve a renderizar una URL con una cadencia fija y descubre qué ha cambiado. Consulta watch.
use enconvert::{
SnapshotListOptions, V2ListOptions, WatchCreateOptions, WatchDiffMode, WatchUpdateStatus,
WatcherUpdate,
};
let watcher = client.v2().create_watcher("https://example.com/pricing", WatchCreateOptions {
frequency_minutes: Some(60),
diff_mode: Some(WatchDiffMode::Auto),
webhook_url: Some("https://my.app/hooks/changes".to_string()),
notify_email: Some(true),
..Default::default()
})?;
println!("{} next check {:?}", watcher.watcher_id, watcher.next_check_at);
// Lee el historial de comprobaciones, de la más reciente a la más antigua.
let history = client
.v2()
.get_watcher_snapshots(&watcher.watcher_id, SnapshotListOptions { limit: Some(10) })?;
for snap in &history.snapshots {
println!("{} changed: {} similarity: {:?}", snap.checked_at, snap.has_changes, snap.similarity);
}
client.v2().list_watchers(V2ListOptions::default())?;
client.v2().get_watcher(&watcher.watcher_id)?;
// Ponlo en pausa. Pasar Some(String::new()) en webhook_url borraría el webhook.
client.v2().update_watcher(&watcher.watcher_id, WatcherUpdate {
status: Some(WatchUpdateStatus::Paused),
..Default::default()
})?;
client.v2().delete_watcher(&watcher.watcher_id)?; // borrado lógico, idempotente
frequency_minutes es 60 por defecto y acepta de 60 a 43200, así que el suelo horario es rígido. diff_mode es Auto por defecto y también acepta Text, Structured, Tables y Metadata, con track_fields para limitar el motor de diferencias a un subconjunto de campos o selectores. notify_email es true por defecto y avisa por correo al propietario del proyecto cuando hay cambios; webhook_url añade un webhook de cambios firmado con HMAC.
update_watcher exige al menos un campo y devuelve Error::InvalidInput ante un WatcherUpdate con todo en None. Las entradas changes de un snapshot contienen contenido de página no confiable, así que escápalas antes de renderizarlas en cualquier sitio.
Opciones de PDF#
PdfOptions es compartido por convert_url_to_pdf, convert_document, convert_to_pdf y la salida Pdf de perceive. Todos los campos son opcionales y solo se envían cuando se establecen.
use enconvert::{PdfHeaderFooter, PdfMargins, PdfOptions, PdfOrientation, UrlToPdfOptions};
fn block(text: &str, height: f64) -> PdfHeaderFooter {
PdfHeaderFooter { content: Some(text.into()), height: Some(height) }
}
client.convert_url_to_pdf("https://example.com", UrlToPdfOptions {
pdf_options: Some(PdfOptions {
page_size: Some("A4".to_string()),
orientation: Some(PdfOrientation::Landscape),
margins: Some(PdfMargins {
top: Some(10.0), bottom: Some(10.0), left: Some(15.0), right: Some(15.0),
}),
scale: Some(0.9),
header: Some(block("Quarterly Report", 15.0)),
footer: Some(block("Confidential", 12.0)),
..Default::default()
}),
..Default::default()
})?;
| Campo | Tipo | Descripción |
|---|---|---|
page_size |
Option<String> |
"A4", "A3", "Letter", "Legal" y similares. |
page_width / page_height |
Option<f64> |
Dimensiones personalizadas. Establecidas juntas, prevalecen sobre page_size. |
orientation |
Option<PdfOrientation> |
Portrait o Landscape. |
margins |
Option<PdfMargins> |
top, bottom, left, right, cada uno opcional. |
scale |
Option<f64> |
Escala de renderizado, por ejemplo 0.9 para el 90 por ciento. |
grayscale |
Option<bool> |
Posprocesa el PDF a escala de grises. |
header / footer |
Option<PdfHeaderFooter> |
content (máximo 2000 caracteres) y height. |
La semántica completa de los parámetros está en parámetros y opciones.
Manejo de errores#
Hay un único tipo de error, enconvert::Error. Implementa std::error::Error mediante thiserror, así que ? se propaga a cualquier Box<dyn Error> o anyhow::Result.
use enconvert::{Error, UrlToPdfOptions};
match client.convert_url_to_pdf("https://example.com", UrlToPdfOptions::default()) {
Ok(result) => println!("{}", result.presigned_url),
Err(e) if e.is_authentication() => eprintln!("check ENCONVERT_API_KEY"),
Err(e) if e.is_rate_limit() => eprintln!("too many requests, back off"),
Err(e) if e.is_server_error() => eprintln!("gateway problem, retry later"),
Err(Error::UnsupportedConversion(msg)) => eprintln!("{msg}"),
Err(Error::Api { status, message }) => eprintln!("API error [{status}]: {message}"),
Err(e) => return Err(e),
}
| Variante | Se devuelve en | Código de estado |
|---|---|---|
Error::Authentication(String) |
Clave de API inválida, ausente o revocada | 401, 403 |
Error::Quota(String) |
Se lanza ante un HTTP 402 | 402 |
Error::RateLimit(String) |
Límite de tasa superado | 429 |
Error::Api { status, message } |
Cualquier otra respuesta 4xx o 5xx | el código real |
Error::UnsupportedConversion(String) |
Un par {input}-to-{output} que la API no implementa, detectado en el cliente |
- |
Error::InvalidInput(String) |
Argumentos incorrectos detectados antes de la solicitud, como una clave de API vacía | - |
Error::Http(reqwest::Error) |
Fallo de conexión, TLS, timeout o decodificación de la respuesta | el del transporte, cuando existe |
Error::Io(std::io::Error) |
Leer un archivo para subirlo, o escribir una descarga | - |
Error::Json(serde_json::Error) |
Serializar un cuerpo de solicitud | - |
Cuatro predicados mantienen cortos los brazos del match: is_authentication(), is_quota(), is_rate_limit() e is_server_error(). status_code() devuelve Option<u16> para cada variante que lleve uno. El mapa completo de mensajes está en la referencia de códigos de error.
Recuperación de timeouts#
Los renderizados largos de URL a PDF y las conversiones de documentos grandes pueden sobrevivir al timeout de un proxy inverso incluso cuando la conversión en sí tiene éxito en el servidor. El SDK se recupera por su cuenta:
- Antes de cada solicitud genera un UUID y lo envía como
job_iden el cuerpo o en el formulario multipart. - Si esa solicitud vuelve como un
Error::Api5xx, el SDK pasa en silencio a sondearGET /v1/convert/status/{job_id}cada 3 segundos. - En cuanto el job marca
success, devuelve el resultado. En cuanto marcafailed, devuelveError::Apicon el mensaje del servidor. - El plazo de sondeo es de 5 minutos. Pasado ese punto obtienes
Error::Api { status: 504, message: "Conversion timed out" }.
La recuperación cubre convert_url_to_pdf, convert_url_to_screenshot, convert_url_to_markdown, convert_image, convert_document, convert_to_markdown y convert_to_pdf. Deliberadamente no cubre los envíos de lotes de sitios web: esos no tienen una fila por job, así que un 5xx ahí significa que falló el propio envío y se expone directamente. Los endpoints V2 responden directamente y tampoco tienen respaldo por job. A las respuestas que omiten job_id se les rellena el generado por el cliente, así que result.job_id siempre es algo que puedes pasarle después a get_job_status.
Configuración#
use enconvert::Enconvert;
use std::time::Duration;
let client = Enconvert::with_options(
std::env::var("ENCONVERT_API_KEY").expect("ENCONVERT_API_KEY is not set"),
Some("https://api.enconvert.com"), // sobrescribe la URL base
Some(Duration::from_secs(300)), // timeout de la solicitud
)?;
println!("enconvert {}", enconvert::VERSION);
| Argumento | Tipo | Por defecto | Descripción |
|---|---|---|---|
api_key |
impl Into<String> |
obligatorio | Clave de API privada. Una cadena vacía devuelve Error::InvalidInput, y también una clave que contenga caracteres inválidos para un encabezado. |
base_url |
Option<&str> |
https://api.enconvert.com |
Las barras finales se eliminan. |
timeout |
Option<Duration> |
300 segundos | Se aplica a la solicitud completa. |
Enconvert::new(api_key) es la forma abreviada de with_options(api_key, None, None).
X-API-Key como sensible para que no aparezca en la salida de depuración, y descarga las URL firmadas mediante un segundo cliente HTTP sin autenticar, de modo que tu clave nunca se envía al almacenamiento de objetos. Genera y rota claves en el panel de control, y consulta autenticación para conocer los tipos de clave.
Forma del resultado#
Todos los métodos de conversión de un solo archivo devuelven un ConversionResult:
pub struct ConversionResult {
pub presigned_url: String,
pub object_key: String,
pub filename: String,
pub file_size: Option<u64>,
pub conversion_time_seconds: Option<f64>,
pub job_id: Option<String>,
}
Descarga el archivo tú mismo desde presigned_url, o pasa save_to y deja que el SDK lo escriba por ti. Las URL firmadas caducan, así que guarda en tu propio bucket todo lo que necesites conservar.
Las rutas asíncronas devuelven sus propias formas. JobStatus lleva status (Processing, Success, Failed o Unknown(String)), presigned_url, object_key y error. BatchSubmission lleva batch_id, status, url_count, total_discovered, discovery_method y output_format. BatchStatus añade los contadores total, completed, failed e in_progress, más output_mode, zip_download_url y un Vec<BatchItem> con las filas por URL.
Las lecturas V2 devuelven PerceiveResult, cuyo mapa outputs está indexado por nombre de salida ("markdown", "screenshot_full_page", y así) con valores V2OutputArtifact { url, object_key, size_bytes, content_type, expires_in }. Esas URL de artefacto están firmadas durante 15 minutos y se vuelven a firmar en cada llamada a get_perceive_operation. Junto a ellas están render_quality, status_code, deductions, cache_hit, structured, extraction_tier, tokens, cost_cents, duration_ms, warnings y options_echo. Cada enum de respuesta lleva una variante Unknown(String), así que un valor de estado añadido en el servidor después de compilar tu binario se parsea sin problemas en lugar de fallar.
Código fuente e incidencias#
- crates.io: enconvert
- GitHub: conversionapi/rust-sdk
- Licencia: MIT
- Otros clientes: todos los SDK y la referencia de endpoints REST
Preguntas frecuentes#
¿Cómo convierto archivos en Rust con un paquete de crates.io?#
Ejecuta cargo add enconvert, construye un cliente con Enconvert::new(api_key)? y llama a un método como convert_url_to_pdf, convert_image o convert_document. Cada struct de opciones deriva Default, así que escribes un literal de struct con ..Default::default() y estableces solo los campos que te importan. Pasa save_to para que el SDK escriba la salida directamente en disco.
¿El SDK de Rust necesita tokio o un runtime asíncrono?#
No. Está construido sobre la API bloqueante de reqwest, así que cada método bloquea el hilo que llama y no hay .await, ni executor, ni tokio en tu árbol de dependencias. Si tu aplicación ya es asíncrona, llama al SDK desde tokio::task::spawn_blocking en lugar de hacerlo directamente en un hilo del reactor, porque el cliente bloqueante de reqwest no puede ejecutarse dentro del contexto de un runtime asíncrono.
¿Cómo convierto una URL a PDF en Rust?#
Llama a convert_url_to_pdf("https://example.com", UrlToPdfOptions { save_to: Some("page.pdf".into()), ..Default::default() }). Establece single_page: Some(false) para paginar en lugar de producir una única página continua, y pasa pdf_options para el tamaño de página, la orientación, los márgenes, la escala, la escala de grises, los encabezados y los pies.
¿Cómo convierto DOCX a PDF en Rust?#
Llama a convert_document("report.docx", ConvertDocumentOptions { save_to: Some("report.pdf".into()), ..Default::default() }). PDF es la salida por defecto, así que output_format se puede dejar sin establecer. El mismo método se encarga de XLSX, PPTX, ODT, ODS, ODP, OTS, Pages, Numbers, HTML y Markdown a PDF, además de pares de formatos de datos como JSON a YAML y CSV a XML.
¿Cómo convierto HEIC a WebP en Rust?#
Llama a convert_image("photo.heic", ConvertImageOptions { output_format: "webp".to_string(), ..Default::default() }). El formato de entrada procede de la extensión del archivo, y están implementados los 20 pares ordenados entre jpeg, png, svg, heic y webp, más pdf a jpeg para rasterizar. Un par no admitido devuelve Error::UnsupportedConversion antes de cualquier llamada de red.
¿Cómo extraigo una página web a Markdown limpio desde Rust?#
De dos maneras. convert_url_to_markdown devuelve un único archivo Markdown con frontmatter YAML. client.v2().perceive(url, PerceiveOptions { outputs: Some(vec![PerceiveOutputName::Markdown]), ..Default::default() }) devuelve el mismo contenido con una puntuación render_quality, deducciones con nombre, avisos y, opcionalmente, capturas de pantalla, enlaces o extracción estructurada del mismo renderizado. Usa perceive cuando un agente vaya a leer el resultado.
¿Cómo sé si una página se renderizó de verdad?#
Lee render_quality, una puntuación de 0.0 a 1.0 presente en cada lectura V2. Una puntuación baja significa que el renderizado no fue limpio: una página de desafío, un muro de cookies o de inicio de sesión, el armazón vacío de una SPA o un error HTTP. El mapa deductions nombra cada penalización que se activó, status_code da el estado HTTP de origen y warnings enumera qué salió mal. El contenido se sigue devolviendo, solo que marcado.
¿Qué pasa cuando una conversión tarda más que el timeout del proxy?#
El SDK se encarga. Cada solicitud lleva un job_id generado por el cliente; si la solicitud devuelve 5xx, el SDK sondea GET /v1/convert/status/{job_id} cada 3 segundos durante hasta 5 minutos y devuelve el resultado terminado como si nada hubiera pasado. Pasado el plazo obtienes Error::Api { status: 504, message: "Conversion timed out" }. Los envíos de lotes de sitios completos son la excepción, y wait_for_batch es la herramienta para esos casos.
¿Puedo usar el SDK de Rust desde un navegador o un target WASM?#
No. Se autentica con una clave de API privada que nunca debe llegar a un cliente, y depende del cliente bloqueante de reqwest con TLS nativo e hilos, y ninguna de esas dos cosas existe en wasm32-unknown-unknown. Ejecútalo en un servidor, en una CLI o en un worker, y haz que tu frontend hable con tu propio backend.