---
seo_title: SDK Rust per Conversione File: Client crates.io | EnConvert
meta_desc: SDK Rust ufficiale di EnConvert: client bloccante basato su reqwest, senza runtime async, per la conversione di file e per il namespace V2 di web intelligence.
keywords: sdk rust conversione file, convertire file in rust, url in pdf rust, api web scraping rust, docx in pdf rust, enconvert sdk rust, crate rust html in pdf, heic in webp rust, api screenshot sito web rust, client api reqwest bloccante, estrarre markdown da url in rust
---

# 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.

<div class="alert alert-info">
<strong>crates.io:</strong> <code>enconvert</code> (0.1.0) &middot; <strong>Sorgente:</strong> <a href="https://github.com/conversionapi/rust-sdk">conversionapi/rust-sdk</a> &middot; <strong>Rust:</strong> edizione 2021 &middot; <strong>Licenza:</strong> MIT
</div>

---

## Installazione

```bash
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:

```toml
[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

```rust
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.

<div class="alert alert-warning">
<strong>Non chiamare l'SDK dall'interno di un thread di un runtime async.</strong> Il client blocking di <code>reqwest</code> non può essere pilotato da un thread già posseduto da un reactor Tokio (o simile). Da codice async, avvolgi ogni chiamata in <code>tokio::task::spawn_blocking</code>.
</div>

---

## 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.

```rust
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](#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.

```rust
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).

```rust
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](#perceive).

### `convert_image`

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

```rust
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.

```rust
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:

```rust
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.

```rust
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.

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

<div class="alert alert-warning">
<strong>Qui viene rispettata solo <code>pdf_options.grayscale</code>.</strong> 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 <code>convert_url_to_pdf</code> oppure <code>convert_document</code> con un input HTML o Markdown.
</div>

### `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.

```rust
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("ops@example.com".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`

```rust
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);
```

<div class="alert alert-info">
<strong>Raramente serve chiamare <code>get_job_status</code> direttamente.</strong> L'SDK lo interroga per te quando una conversione sincrona restituisce 5xx. Vedi <a href="#timeout-recovery">Recupero dei timeout</a>.
</div>

---

## 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](/it/docs/v2-overview) 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](/it/docs/v2-perceive).

```rust
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.

```rust
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`.

```rust
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](/it/docs/v2-discover).

```rust
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](/it/docs/v2-lookup).

```rust
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](/it/docs/v2-distill).

```rust
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:

```rust
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](/it/docs/v2-ingest).

```rust
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:

```rust
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](/it/docs/v2-watch).

```rust
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.

```rust
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](/it/docs/parameters-options).

---

## 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`.

```rust
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](/it/docs/error-codes).

---

## 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

```rust
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)`.

<div class="alert alert-warning">
<strong>Non inserire mai la chiave API direttamente nel codice.</strong> Leggila da una variabile d'ambiente o da un secret manager. L'SDK marca come sensibile il valore dell'header <code>X-API-Key</code> 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 <a href="/it/dashboard">dashboard</a>, e consulta l'<a href="/it/docs/authentication">autenticazione</a> per i tipi di chiave.
</div>

---

## Struttura del risultato

Ogni metodo di conversione di un singolo file restituisce un `ConversionResult`:

```rust
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

- **crates.io:** [enconvert](https://crates.io/crates/enconvert)
- **GitHub:** [conversionapi/rust-sdk](https://github.com/conversionapi/rust-sdk)
- **Licenza:** MIT
- **Altri client:** [tutti gli SDK](/it/docs/sdks) e il [riferimento degli endpoint REST](/it/docs/endpoints-overview)

---

## 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.
