SDK Rust per la conversione dei file#

enconvert è il crate Rust ufficiale per l'API EnConvert. È un client bloccante costruito sull'API blocking di reqwest, quindi niente runtime async, niente tokio nel tuo albero delle dipendenze e nessun .await nel tuo codice. Dodici metodi su Enconvert coprono la conversione dei file: da URL a PDF, screenshot, estrazione Markdown, coppie di formati per immagini e documenti, anything-to-PDF e batch su interi siti. Un secondo namespace, client.v2(), aggiunge ventitré metodi di web intelligence: perceive, discover, lookup, distill, ingest e watch. Ogni struct di opzioni deriva Default, e ogni errore è un unico enum.

crates.io: enconvert (0.1.0) · Sorgente: conversionapi/rust-sdk · Rust: edizione 2021 · Licenza: MIT

Installazione#

cargo add enconvert

Oppure aggiungilo a mano. distill e l'estrazione con schema di perceive accettano una serde_json::Map<String, Value>, quindi aggiungi anche serde_json se pensi di usarli:

[dependencies]
enconvert = "0.1"
serde_json = "1"

Il crate porta con sé reqwest (blocking, json, multipart), serde, serde_json, thiserror e uuid. Non ci sono feature opzionali da abilitare.


Avvio rapido#

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)?;

    // Converte un URL in PDF e scrive il risultato direttamente su disco.
    let pdf = client.convert_url_to_pdf("https://example.com", UrlToPdfOptions {
        save_to: Some("page.pdf".into()),
        ..Default::default()
    })?;
    println!("{}", pdf.presigned_url);

    // Legge la stessa pagina come dovrebbe farlo il tuo agente, con un punteggio di qualità allegato.
    let op = client.v2().perceive("https://example.com", PerceiveOptions {
        outputs: Some(vec![PerceiveOutputName::Markdown, PerceiveOutputName::Structured]),
        ..Default::default()
    })?;
    println!("{:?}", op.render_quality); // es. Some(0.93)
    Ok(())
}

Ogni metodo blocca il thread chiamante, il che rende l'SDK facile da inserire in una CLI, in uno script di build, in un thread worker o in un handler web sincrono. Tutti i tipi pubblici sono ri-esportati alla radice del crate, quindi agli esempi qui sotto non serve nulla oltre a use enconvert::{...} e al client visto sopra.

Non chiamare l'SDK dall'interno di un thread di un runtime async. Il client blocking di reqwest non può essere pilotato da un thread già posseduto da un reactor Tokio (o simile). Da codice async, avvolgi ogni chiamata in tokio::task::spawn_blocking.

Che cosa espone il client#

Superficie Si raggiunge come Che cosa copre
Conversione file client.<method> 12 metodi: da URL a PDF, screenshot e Markdown; coppie di immagini e documenti; anything-to-Markdown e anything-to-PDF; batch su interi siti; stato di job e batch
Web intelligence client.v2().<method> 23 metodi tra perceive, discover, lookup, distill, ingest e watch
Tabelle dei formati valid_outputs_for, IMPLEMENTED_CONVERSIONS I 43 endpoint {input}-to-{output} implementati, verificati lato client prima che una richiesta parta
Errori enconvert::Error Un solo enum, nove varianti, con is_authentication, is_quota, is_rate_limit, is_server_error e status_code

Non ci sono builder e non c'è nulla di async. La forma di chiamata idiomatica è un literal di struct con ..Default::default().


Conversione dei file#

convert_url_to_pdf#

Esegue il rendering di qualsiasi URL pubblico in un 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 Predefinito Descrizione
save_to Option<PathBuf> - Percorso locale su cui scrivere il PDF. Le directory padre vengono create automaticamente.
single_page Option<bool> true true produce una singola pagina continua. false impagina usando pdf_options.page_size.
pdf_options Option<PdfOptions> - Geometria di pagina, scala, scala di grigi, intestazione e piè di pagina. Vedi Opzioni PDF.
render.viewport_width / render.viewport_height Option<u32> 1920 / 1080 Viewport del browser in pixel.
render.load_media / render.enable_scroll Option<bool> true Attende immagini e video, e scorre dall'alto verso il basso per attivare i caricamenti lazy.
render.output_filename Option<String> auto Sovrascrive il nome file generato.
render.auth / render.cookies / render.headers vedi i tipi - HTTP Basic Auth, fino a 50 cookie iniettati, fino a 20 header di richiesta aggiuntivi.

Il blocco render è UrlRenderOptions, condiviso da ogni conversione di URL e di sito. Non combinare auth con un header Authorization: l'API rifiuta il conflitto.

convert_url_to_screenshot#

Cattura un PNG di qualsiasi URL. UrlToScreenshotOptions porta con sé lo stesso blocco render più save_to, e nient'altro.

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#

Estrae Markdown pulito in stile GitHub-Flavored da un URL. Navigazione, footer, pubblicità e script vengono rimossi, il corpo principale dell'articolo viene mantenuto e viene anteposto un frontmatter YAML (titolo, descrizione, url, link, immagini).

use enconvert::UrlToMarkdownOptions;

client.convert_url_to_markdown("https://example.com/article", UrlToMarkdownOptions {
    save_to: Some("article.md".into()),
    ..Default::default()
})?;

Utile per pipeline RAG, importazioni in un CMS e raccolta di dati di addestramento. Per una lettura con punteggio, artefatti ed estrazione strutturata in un'unica chiamata, usa invece perceive.

convert_image#

Converte tra jpeg, png, svg, heic e webp (tutte e 20 le coppie ordinate), oppure rasterizza un PDF in JPEG.

use enconvert::{ConvertImageOptions, NamedFile};

// Da un percorso: il formato di input viene ricavato dall'estensione.
client.convert_image("photo.heic", ConvertImageOptions {
    output_format: "webp".to_string(),
    save_to: Some("photo.webp".into()),
    ..Default::default()
})?;

// Dai byte: fornisci un nome file, così l'estensione resta leggibile.
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 è l'unico campo obbligatorio; save_to e output_filename sono opzionali. Il primo argomento è qualsiasi cosa convertibile in FileInput: un percorso &str o String, un &Path o PathBuf, un Vec<u8> o &[u8] di byte grezzi, oppure un NamedFile quando hai i byte e vuoi dichiarare tu stesso nome file e content type.

convert_document#

Converte documenti e formati di dati. output_format vale "pdf" per impostazione predefinita, e yml, htm, md e jpg vengono normalizzati ai rispettivi nomi canonici. save_to, output_filename e pdf_options sono opzionali.

use enconvert::{ConvertDocumentOptions, PdfOptions};

// da docx a pdf (il formato di output predefinito)
client.convert_document("report.docx", ConvertDocumentOptions {
    save_to: Some("report.pdf".into()),
    ..Default::default()
})?;

// da json a yaml
client.convert_document("data.json", ConvertDocumentOptions {
    output_format: Some("yaml".to_string()),
    save_to: Some("data.yaml".into()),
    ..Default::default()
})?;

// da markdown a pdf con impostazioni di pagina personalizzate
client.convert_document("README.md", ConvertDocumentOptions {
    pdf_options: Some(PdfOptions { page_size: Some("A4".to_string()), ..Default::default() }),
    ..Default::default()
})?;

Estensioni di input riconosciute: .doc, .docx, .xls, .xlsx, .ppt, .pptx, .html, .htm, .odt, .ods, .odp, .ots, .pages, .numbers, .md, .markdown, .csv, .json, .xml, .yaml, .yml, .toml.

Formato di input Output validi
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 l'uno verso l'altro, tutte e 20 le coppie
pdf jpeg

EPUB non ha una coppia documentale dedicata. Passa invece i file .epub attraverso convert_to_pdf oppure convert_to_markdown.

convert_image e convert_document ricavano entrambi il formato di input dall'estensione del file e verificano la coppia sulla tabella lato client, quindi una coppia non implementata restituisce Error::UnsupportedConversion con l'elenco degli output validi, prima che venga effettuata qualsiasi richiesta HTTP. Puoi interrogare la stessa tabella direttamente:

use enconvert::{valid_outputs_for, IMPLEMENTED_CONVERSIONS};

println!("{:?}", valid_outputs_for("json"));   // ["csv", "toml", "xml", "yaml"]
println!("{}", IMPLEMENTED_CONVERSIONS.len()); // 43

convert_to_markdown#

Carica un documento di quasi qualsiasi tipo e ottieni Markdown pulito: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT e MD, e file office legacy o ODF. Il formato viene rilevato lato server, quindi non viene eseguito alcun controllo dell'estensione lato client. Le immagini non sono supportate.

use enconvert::ConvertToMarkdownOptions;

client.convert_to_markdown("handbook.pdf", ConvertToMarkdownOptions {
    save_to: Some("handbook.md".into()),
    ..Default::default()
})?;

L'output è un unico file .md strutturato per intestazioni, il che ne fa un buon mattone per il chunking RAG: un chunker semantico può suddividere sulla gerarchia di intestazioni del documento invece che su conteggi arbitrari di caratteri. Qui non ci sono opzioni PDF, solo save_to e output_filename.

convert_to_pdf#

Carica quasi qualsiasi cosa e ottieni un PDF: office, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, testo semplice, immagini raster, SVG, EPUB, oppure un PDF già esistente in passthrough.

use enconvert::{ConvertToPdfOptions, PdfOptions};

client.convert_to_pdf("slides.pptx", ConvertToPdfOptions {
    save_to: Some("slides.pdf".into()),
    ..Default::default()
})?;

// PDF in ingresso, PDF in scala di grigi in uscita.
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()
})?;
Qui viene rispettata solo pdf_options.grayscale. L'endpoint anything-to-PDF rileva automaticamente l'input e applica la propria geometria. Per controllare dimensione della pagina, orientamento, margini, scala, intestazioni e piè di pagina, usa convert_url_to_pdf oppure convert_document con un input HTML o Markdown.

convert_website_to_pdf e convert_website_to_screenshot#

Individua ogni pagina di un sito web, converte ciascuna in background e raccoglie tutto in un unico ZIP. Entrambi sono solo asincroni: restituiscono un BatchSubmission, mai un file finito.

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()
})?;

// Blocca finché il batch non si assesta, poi salva lo 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 Predefinito Descrizione
website.crawl_mode Option<CrawlMode> Auto Auto, Sitemap (solo sitemap.xml) oppure Full (sitemap più una scansione in ampiezza).
website.include_patterns / website.exclude_patterns Option<Vec<String>> - Scansiona solo, oppure salta, gli URL che corrispondono a questi pattern. Solo in modalità di scansione completa.
website.notification_email / website.callback_url Option<String> proprietario del progetto / - Indirizzo a cui viene inviata l'email, e webhook chiamato, quando il batch termina.
website.render UrlRenderOptions - Stessi campi di viewport, media, scroll, auth, cookie e header delle conversioni di singoli URL.
single_page / pdf_options vedi sopra - Solo per i batch PDF.

convert_website_to_screenshot accetta WebsiteToScreenshotOptions, un alias di tipo per WebsiteConversionOptions, e produce uno ZIP di PNG. wait_for_batch interroga get_batch_status ogni interval_ms (predefinito 5000) finché il batch non esce da Processing, poi scarica facoltativamente lo ZIP. Se timeout_ms (predefinito 1.800.000, cioè 30 minuti) scade prima, restituisce Error::Api { status: 504, .. }.

get_job_status e 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")?; // conteggi aggregati e voci per singolo URL
println!("{}/{} done, {} failed", batch.completed, batch.total, batch.failed);
Raramente serve chiamare get_job_status direttamente. L'SDK lo interroga per te quando una conversione sincrona restituisce 5xx. Vedi Recupero dei timeout.

Web intelligence (V2)#

Tutto ciò che sta sotto client.v2() trasforma le pagine web in dati pronti per gli agenti. L'unica cosa che ogni lettura ha in comune è render_quality, un punteggio da 0.0 a 1.0 associato a ciascuna pagina renderizzata. Un punteggio basso significa che la pagina non si è renderizzata in modo pulito: una pagina anti-bot, un muro di cookie o di login, un guscio SPA vuoto, oppure un errore HTTP. Il contenuto torna comunque, ma torna segnalato, con una mappa deductions con nomi e una lista warnings, così una lettura sbagliata non entra mai in silenzio nel contesto del tuo agente. I risultati di perceive, distill, lookup e watch la riportano tutti. Parti dalla panoramica V2 per i concetti dietro alle sei capacità.

Perceive#

Renderizza un URL negli artefatti che chiedi. Sincrono: la chiamata restituisce un'operazione completata con URL firmati agli artefatti validi 15 minuti. Vedi 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); // es. {"http_error": 0.7} quando qualcosa non va
println!("{:?}", op.outputs.get("markdown").and_then(|a| a.url.as_ref()));
println!("{:?}", op.structured);

// Gli URL degli artefatti vengono rifirmati a ogni lettura dello stato.
client.v2().get_perceive_operation(&op.operation_id)?;
Opzione Tipo Predefinito Descrizione
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 per l'estrazione strutturata.
only_main_content Option<bool> true Rimuove navigazione, header, footer e banner dei cookie dall'artefatto Markdown e dall'estratto main_content.
wait_for / wait_timeout_ms / js_code vedi i tipi - / 30000 / - Un selettore CSS (facoltativamente css:...) oppure js:<expr> da attendere, per quanto tempo attendere (da 0 a 60000) e il JavaScript da eseguire dopo la navigazione (massimo 20000 caratteri).
viewport / mobile Option<PerceiveViewport> / Option<bool> 1920x1080 Larghezza da 320 a 3840, altezza da 240 a 2160, oppure un profilo di dispositivo mobile.
cache_mode / block_resources vedi i tipi Enabled / - Enabled mette in cache per 1 ora, Bypass salta la cache, Refresh rifà il rendering; più i tipi di risorsa che il browser non deve caricare.
headers, cookies, auth, respect_robots, pdf_options vedi UrlRenderOptions e PdfOptions - Stesse forme delle opzioni di render V1. pdf_options conta solo quando outputs include Pdf.

perceive_direct restituisce in streaming un singolo artefatto come byte grezzi invece di un envelope JSON. Deve essere richiesto esattamente un output che produca un artefatto, e l'SDK lo verifica localmente con Error::InvalidInput prima di inviare qualsiasi cosa.

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)?;

// Riscarica più tardi un artefatto archiviato. Passa None quando l'operazione ne ha prodotto uno solo.
let saved = client
    .v2()
    .download_perceive_artifact(&direct.operation_id, Some(PerceiveOutputName::Pdf))?;
println!("{} bytes", saved.content.len());

PerceiveDirectResult porta con sé content, content_type, filename, operation_id, object_key, cache_hit, render_quality, source_status_code, content_hash e warnings_count, tutti letti dagli header della risposta. Un Error::Api { status: 410, .. } da download_perceive_artifact significa che l'artefatto ha superato la propria finestra di conservazione.

perceive_batch accetta fino a 1000 URL e un unico blocco di opzioni condiviso. I batch piccoli vengono eseguiti inline; quelli più grandi tornano con stato Queued, quindi interroga get_perceive_batch con il 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 gli URL di un sito senza alcun rendering nel browser. Vedi 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 vale Hybrid per impostazione predefinita (Sitemap e Crawl sono le alternative), max_urls 100 (da 1 a 1000), max_depth 2 (da 1 a 5) e same_domain_only è true. include_patterns e poi exclude_patterns applicano un filtro con espressioni regolari, massimo 50 ciascuno, e respect_robots rispetta robots.txt. Il risultato riporta urls, total, pages_crawled, truncated, robots_respected e una mappa sources con i conteggi grezzi per singola fonte.

Lookup#

Esegue una ricerca web per categorie e, facoltativamente, renderizza i primi risultati nella stessa chiamata. Vedi 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 vale Web per impostazione predefinita (News, Images, Scholar, Patents e Maps sono le altre), num_results 10 (da 1 a 100), page 1 (da 1 a 10) e autocorrect è true. country e locale accettano codici come "us" e "en", location accetta testo libero come "Austin, Texas" e time_filter accetta Hour, Day, Week, Month oppure Year. perceive_top (da 0 a 10, predefinito 0) renderizza automaticamente i primi N URL dei risultati e allega un PerceiveResult completo a ciascun risultato. Il risultato porta con sé anche answer_box, knowledge_graph e perceive_operation_ids.

Distill#

Estrazione strutturata guidata da uno schema: gli dai una forma e ottieni quella forma per ogni URL. Un passaggio CSS facoltativo viene eseguito per primo, e tutto quello che gli sfugge sale al livello LLM. Vedi distill.

use enconvert::{CssField, CssFieldType, CssSchema, DistillOptions};
use serde_json::json;

// CssField non ha Default perché `field_type` è obbligatorio.
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);
}

Oppure individua prima gli URL di un sito e distilla ogni pagina trovata:

use enconvert::{DistillDiscoverFrom, DistillOptions};
use serde_json::json;

client.v2().distill(DistillOptions {
    discover_from: Some(DistillDiscoverFrom {
        max_pages: Some(20), // da 1 a 50, limita sia la scoperta sia la distillazione
        ..DistillDiscoverFrom::new("https://example.com")
    }),
    schema: json!({ "title": "page title" }).as_object().unwrap().clone(),
    ..Default::default()
})?;

Fornisci esattamente uno tra urls (massimo 50) e discover_from. Entrambi, o nessuno dei due, restituisce Error::InvalidInput prima che venga inviata qualsiasi richiesta. schema è obbligatorio ed è o un oggetto JSON-Schema o una mappa piatta {field: description}. CssFieldType copre Text, Attribute, Html, Regex, Nested, List e NestedList; transform accetta Lowercase, Uppercase oppure Strip; l'annidamento è limitato a una profondità di 5.

Ingest#

Trasforma un intero sito, o una pila di documenti caricati, in JSONL suddiviso in chunk e pronto per il RAG attraverso un'unica pipeline. Ingest è sempre asincrono. Vedi ingest.

use enconvert::{
    IngestChunkOptions, IngestFilesOptions, IngestMode, IngestOptions, IngestStatus, V2ListOptions,
};

// Da un sito.
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()
})?;

// Oppure da file caricati: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT e MD,
// e documenti office legacy o ODF.
let files = vec!["handbook.pdf".into(), "notes.docx".into()];
client.v2().ingest_files(files, IngestFilesOptions::default())?;

// Interroga, elenca, annulla.
let status = client.v2().get_ingest_job(&job.job_id)?;
if status.status == IngestStatus::Completed {
    println!("{:?}", status.output_url); // URL firmato verso il JSONL
}
client.v2().list_ingest_jobs(V2ListOptions { limit: Some(50), ..Default::default() })?;
client.v2().cancel_ingest_job(&job.job_id)?; // idempotente

mode vale Urls per impostazione predefinita, il che richiede una lista urls non vuota (massimo 1000) e rifiuta url; Sitemap e Crawl fanno il contrario, richiedendo l'url di partenza. Quell'abbinamento è validato lato client, quindi una discrepanza restituisce subito Error::InvalidInput. max_pages vale 50 per impostazione predefinita (da 1 a 1000), max_depth 2 (da 1 a 5), same_domain_only è true, chunk.max_words vale 512 (da 32 a 4000) e chunk.sentence_overlap vale 1 (da 0 a 10). include_patterns, exclude_patterns, respect_robots, wait_for e wait_timeout_ms si comportano come su discover e perceive. I webhook di completamento sono firmati in HMAC:

let secret = client.v2().get_webhook_secret()?;
println!("{} {} {}", secret.secret, secret.signature_header, secret.signature_scheme);
client.v2().rotate_webhook_secret()?;            // le vecchie firme smettono subito di essere valide
client.v2().retry_ingest_webhook("ing_abc123")?; // rinvia il webhook di un job completato

Watch#

Rifà il rendering di un URL a cadenza fissa e ti dice che cosa è cambiato. Vedi 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);

// Legge lo storico dei controlli, dal più recente.
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)?;

// Mettilo in pausa. Passare Some(String::new()) per webhook_url azzererebbe il webhook.
client.v2().update_watcher(&watcher.watcher_id, WatcherUpdate {
    status: Some(WatchUpdateStatus::Paused),
    ..Default::default()
})?;
client.v2().delete_watcher(&watcher.watcher_id)?; // cancellazione logica, idempotente

frequency_minutes vale 60 per impostazione predefinita e accetta valori da 60 a 43200, quindi il minimo orario è invalicabile. diff_mode vale Auto per impostazione predefinita e accetta anche Text, Structured, Tables e Metadata, con track_fields a restringere il motore di diff a un sottoinsieme di campi o selettori. notify_email è true per impostazione predefinita e invia un'email al proprietario del progetto quando ci sono cambiamenti; webhook_url aggiunge un webhook di modifica firmato in HMAC.

update_watcher richiede almeno un campo e restituisce Error::InvalidInput per un WatcherUpdate con tutti i campi a None. Le voci changes degli snapshot contengono contenuto di pagina non attendibile, quindi effettua l'escape prima di renderizzarle da qualsiasi parte.


Opzioni PDF#

PdfOptions è condiviso da convert_url_to_pdf, convert_document, convert_to_pdf e dall'output Pdf di perceive. Ogni campo è opzionale e viene inviato solo quando è impostato.

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 Descrizione
page_size Option<String> "A4", "A3", "Letter", "Legal" e così via.
page_width / page_height Option<f64> Dimensioni personalizzate. Impostati insieme, hanno la precedenza su page_size.
orientation Option<PdfOrientation> Portrait oppure Landscape.
margins Option<PdfMargins> top, bottom, left, right, ciascuno opzionale.
scale Option<f64> Scala di rendering, ad esempio 0.9 per il 90 percento.
grayscale Option<bool> Post-elabora il PDF convertendolo in scala di grigi.
header / footer Option<PdfHeaderFooter> content (massimo 2000 caratteri) e height.

La semantica completa dei parametri si trova in parametri e opzioni.


Gestione degli errori#

Esiste un solo tipo di errore, enconvert::Error. Implementa std::error::Error tramite thiserror, quindi ? propaga in qualsiasi 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 Restituita per Codice di stato
Error::Authentication(String) Chiave API non valida, mancante o revocata 401, 403
Error::Quota(String) Sollevata su HTTP 402 402
Error::RateLimit(String) Rate limit superato 429
Error::Api { status, message } Qualsiasi altra risposta 4xx o 5xx il codice effettivo
Error::UnsupportedConversion(String) Una coppia {input}-to-{output} che l'API non implementa, intercettata lato client -
Error::InvalidInput(String) Argomenti errati intercettati prima della richiesta, come una chiave API vuota -
Error::Http(reqwest::Error) Errore di connessione, TLS, timeout o decodifica della risposta dal trasporto, quando presente
Error::Io(std::io::Error) Lettura di un file da caricare, o scrittura di un download -
Error::Json(serde_json::Error) Serializzazione del corpo di una richiesta -

Quattro predicati mantengono corti i rami del match: is_authentication(), is_quota(), is_rate_limit() e is_server_error(). status_code() restituisce Option<u16> per ogni variante che ne porta uno. La mappa completa dei messaggi si trova nel riferimento dei codici di errore.


Recupero dei timeout#

I rendering lunghi da URL a PDF e le conversioni di documenti di grandi dimensioni possono superare il timeout di un reverse proxy anche quando la conversione stessa riesce sul server. L'SDK si recupera da solo:

  1. Prima di ogni richiesta genera un UUID e lo invia come job_id nel corpo oppure nel form multipart.
  2. Se quella richiesta torna come Error::Api 5xx, l'SDK passa silenziosamente al polling di GET /v1/convert/status/{job_id} ogni 3 secondi.
  3. Non appena il job risulta success, restituisce il risultato. Non appena risulta failed, restituisce Error::Api con il messaggio del server.
  4. Il limite di tempo per il polling è di 5 minuti. Superato quello ottieni Error::Api { status: 504, message: "Conversion timed out" }.

Il recupero copre convert_url_to_pdf, convert_url_to_screenshot, convert_url_to_markdown, convert_image, convert_document, convert_to_markdown e convert_to_pdf. Deliberatamente non copre gli invii batch di siti web: quelli non hanno una riga per singolo job, quindi un 5xx lì significa che l'invio stesso è fallito e viene riportato direttamente. Anche gli endpoint V2 rispondono direttamente e non hanno alcun fallback sui job. Le risposte che omettono job_id ricevono il riempimento con quello generato dal client, così result.job_id è sempre qualcosa che puoi passare più tardi a get_job_status.


Configurazione#

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"), // sovrascrittura dell'URL base
    Some(Duration::from_secs(300)),    // timeout della richiesta
)?;
println!("enconvert {}", enconvert::VERSION);
Argomento Tipo Predefinito Descrizione
api_key impl Into<String> obbligatorio Chiave API privata. Una stringa vuota restituisce Error::InvalidInput, e così anche una chiave che contiene caratteri non validi negli header.
base_url Option<&str> https://api.enconvert.com Gli slash finali vengono rimossi.
timeout Option<Duration> 300 secondi Applicato all'intera richiesta.

Enconvert::new(api_key) è una scorciatoia per with_options(api_key, None, None).

Non inserire mai la chiave API direttamente nel codice. Leggila da una variabile d'ambiente o da un secret manager. L'SDK marca come sensibile il valore dell'header X-API-Key così resta fuori dall'output di debug, e scarica gli URL firmati tramite un secondo client HTTP non autenticato, così la tua chiave non viene mai inviata all'object storage. Genera e ruota le chiavi nella dashboard, e consulta l'autenticazione per i tipi di chiave.

Struttura del risultato#

Ogni metodo di conversione di un singolo file restituisce 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>,
}

Scarica tu stesso il file da presigned_url, oppure passa save_to e lascia che sia l'SDK a scriverlo per te. Gli URL firmati scadono, quindi archivia nel tuo bucket tutto ciò che vuoi conservare.

I percorsi asincroni restituiscono forme proprie. JobStatus porta con sé status (Processing, Success, Failed oppure Unknown(String)), presigned_url, object_key ed error. BatchSubmission porta con sé batch_id, status, url_count, total_discovered, discovery_method e output_format. BatchStatus aggiunge i contatori total, completed, failed e in_progress, più output_mode, zip_download_url e un Vec<BatchItem> di righe per singolo URL.

Le letture V2 restituiscono PerceiveResult, la cui mappa outputs è indicizzata per nome di output ("markdown", "screenshot_full_page" e così via) con valori V2OutputArtifact { url, object_key, size_bytes, content_type, expires_in }. Quegli URL degli artefatti sono firmati per 15 minuti e vengono rifirmati a ogni chiamata di get_perceive_operation. Accanto a loro stanno render_quality, status_code, deductions, cache_hit, structured, extraction_tier, tokens, cost_cents, duration_ms, warnings e options_echo. Ogni enum di risposta porta con sé una variante Unknown(String), così un valore di stato aggiunto sul server dopo il rilascio della tua build viene interpretato senza problemi invece di far fallire il parsing.


Sorgente e problemi#


Domande frequenti#

Come converto i file in Rust con un pacchetto crates.io?#

Esegui cargo add enconvert, costruisci un client con Enconvert::new(api_key)? e chiama un metodo come convert_url_to_pdf, convert_image oppure convert_document. Ogni struct di opzioni deriva Default, quindi scrivi un literal di struct con ..Default::default() e imposti solo i campi che ti interessano. Passa save_to per far scrivere l'output direttamente su disco dall'SDK.

L'SDK Rust richiede tokio o un runtime async?#

No. È costruito sull'API blocking di reqwest, quindi ogni metodo blocca il thread chiamante e non c'è alcun .await, nessun executor e nessun tokio nel tuo albero delle dipendenze. Se la tua applicazione è già async, chiama l'SDK da tokio::task::spawn_blocking invece che direttamente su un thread del reactor, perché il client blocking di reqwest non può girare dentro il contesto di un runtime async.

Come converto un URL in PDF in Rust?#

Chiama convert_url_to_pdf("https://example.com", UrlToPdfOptions { save_to: Some("page.pdf".into()), ..Default::default() }). Imposta single_page: Some(false) per impaginare invece di produrre una singola pagina continua, e passa pdf_options per dimensione della pagina, orientamento, margini, scala, scala di grigi, intestazioni e piè di pagina.

Come converto DOCX in PDF in Rust?#

Chiama convert_document("report.docx", ConvertDocumentOptions { save_to: Some("report.pdf".into()), ..Default::default() }). Il PDF è l'output predefinito, quindi output_format può restare non impostato. Lo stesso metodo gestisce XLSX, PPTX, ODT, ODS, ODP, OTS, Pages, Numbers, HTML e Markdown verso PDF, oltre alle coppie tra formati di dati come da JSON a YAML e da CSV a XML.

Come converto HEIC in WebP in Rust?#

Chiama convert_image("photo.heic", ConvertImageOptions { output_format: "webp".to_string(), ..Default::default() }). Il formato di input viene ricavato dall'estensione del file, e tutte e 20 le coppie ordinate tra jpeg, png, svg, heic e webp sono implementate, più pdf verso jpeg per la rasterizzazione. Una coppia non supportata restituisce Error::UnsupportedConversion prima di qualsiasi chiamata di rete.

Come estraggo Markdown pulito da una pagina web in Rust?#

In due modi. convert_url_to_markdown restituisce un unico file Markdown con frontmatter YAML. client.v2().perceive(url, PerceiveOptions { outputs: Some(vec![PerceiveOutputName::Markdown]), ..Default::default() }) restituisce lo stesso contenuto con un punteggio render_quality, detrazioni con nome, avvisi e, facoltativamente, screenshot, link o estrazione strutturata dallo stesso rendering. Usa perceive quando il risultato verrà letto da un agente.

Come faccio a sapere se una pagina si è davvero renderizzata?#

Leggi render_quality, un punteggio da 0.0 a 1.0 presente su ogni lettura V2. Un punteggio basso significa che il rendering non è stato pulito: una pagina anti-bot, un muro di cookie o di login, un guscio SPA vuoto, oppure un errore HTTP. La mappa deductions dà un nome a ciascuna penalità scattata, status_code riporta lo stato HTTP a monte e warnings elenca che cosa è andato storto. Il contenuto viene comunque restituito, solo segnalato.

Che cosa succede quando una conversione dura più del timeout del proxy?#

Ci pensa l'SDK. Ogni richiesta porta con sé un job_id generato dal client; se la richiesta restituisce 5xx, l'SDK interroga GET /v1/convert/status/{job_id} ogni 3 secondi per un massimo di 5 minuti e restituisce il risultato finito come se nulla fosse andato storto. Oltre quel limite ottieni Error::Api { status: 504, message: "Conversion timed out" }. Gli invii batch su interi siti sono l'eccezione, e per quelli lo strumento giusto è wait_for_batch.

Posso usare l'SDK Rust da un browser o da un target WASM?#

No. Si autentica con una chiave API privata che non deve mai finire su un client, e dipende dal client blocking di reqwest con TLS nativo e thread, nessuno dei quali esiste su wasm32-unknown-unknown. Eseguilo su un server, in una CLI o in un worker, e fai parlare il tuo frontend con il tuo backend.