---
seo_title: SDK Rust de conversion de fichiers (crates.io) | EnConvert
meta_desc: SDK Rust officiel d'EnConvert : client bloquant bâti sur reqwest, sans runtime asynchrone, couvrant la conversion de fichiers et l'espace de noms V2 d'intelligence web.
keywords: sdk rust conversion de fichiers, convertir des fichiers en rust, url vers pdf en rust, crate rust api scraping web, docx vers pdf en rust, enconvert sdk rust, html vers pdf crate rust, heic vers webp en rust, capture d'écran de site web en rust, client api bloquant reqwest, extraire du markdown depuis une url en rust
---

# SDK Rust de conversion de fichiers

`enconvert` est la crate Rust officielle de l'API EnConvert. C'est un client bloquant bâti sur l'API blocking de `reqwest` : pas de runtime asynchrone, pas de `tokio` dans votre arbre de dépendances, et aucun `.await` dans votre code. Douze méthodes sur `Enconvert` couvrent la conversion de fichiers : URL vers PDF, captures d'écran, extraction Markdown, paires de formats d'images et de documents, anything-to-PDF et lots à l'échelle d'un site entier. Un second espace de noms, `client.v2()`, ajoute vingt-trois méthodes d'intelligence web : perceive, discover, lookup, distill, ingest et watch. Chaque struct d'options dérive `Default`, et chaque échec passe par une seule énumération d'erreur.

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

---

## Installation

```bash
cargo add enconvert
```

Ou ajoutez-la à la main. `distill` et l'extraction `schema` de perceive prennent un `serde_json::Map<String, Value>`, ajoutez donc aussi `serde_json` si vous comptez les utiliser :

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

La crate embarque `reqwest` (blocking, json, multipart), `serde`, `serde_json`, `thiserror` et `uuid`. Il n'y a aucune feature optionnelle à activer.

---

## Démarrage rapide

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

    // Convertit une URL en PDF et écrit le résultat directement sur le disque.
    let pdf = client.convert_url_to_pdf("https://example.com", UrlToPdfOptions {
        save_to: Some("page.pdf".into()),
        ..Default::default()
    })?;
    println!("{}", pdf.presigned_url);

    // Lit la même page comme votre agent devrait le faire, avec un score de qualité attaché.
    let op = client.v2().perceive("https://example.com", PerceiveOptions {
        outputs: Some(vec![PerceiveOutputName::Markdown, PerceiveOutputName::Structured]),
        ..Default::default()
    })?;
    println!("{:?}", op.render_quality); // p. ex. Some(0.93)
    Ok(())
}
```

Chaque méthode bloque le thread appelant, ce qui rend le SDK facile à intégrer dans une CLI, un script de build, un thread de travail ou un handler web synchrone. Tous les types publics sont réexportés à la racine de la crate : les extraits ci-dessous n'ont besoin de rien d'autre que `use enconvert::{...}` et du `client` ci-dessus.

<div class="alert alert-warning">
<strong>N'appelez pas le SDK depuis un thread d'un runtime asynchrone.</strong> Le client bloquant de <code>reqwest</code> ne peut pas être piloté depuis un thread déjà détenu par un réacteur Tokio (ou équivalent). Depuis du code asynchrone, enveloppez chaque appel dans <code>tokio::task::spawn_blocking</code>.
</div>

---

## Ce que le client expose

| Surface | Accessible via | Ce qu'elle couvre |
|---------|-----------|----------------|
| Conversion de fichiers | `client.<method>` | 12 méthodes : URL vers PDF, capture d'écran et Markdown ; paires d'images et de documents ; anything-to-Markdown et anything-to-PDF ; lots à l'échelle d'un site ; statut des jobs et des lots |
| Intelligence web | `client.v2().<method>` | 23 méthodes réparties sur perceive, discover, lookup, distill, ingest et watch |
| Tables de formats | `valid_outputs_for`, `IMPLEMENTED_CONVERSIONS` | Les 43 endpoints `{input}-to-{output}` implémentés, vérifiés côté client avant l'envoi d'une requête |
| Erreurs | `enconvert::Error` | Une énumération, neuf variantes, avec `is_authentication`, `is_quota`, `is_rate_limit`, `is_server_error` et `status_code` |

Rien n'est un builder et rien n'est `async`. La forme d'appel idiomatique est un littéral de struct suivi de `..Default::default()`.

---

## Conversion de fichiers

### `convert_url_to_pdf`

Effectue le rendu de n'importe quelle URL publique en 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);
```

| Champ | Type | Défaut | Description |
|-------|------|---------|-------------|
| `save_to` | `Option<PathBuf>` | - | Chemin local où écrire le PDF. Les répertoires parents sont créés automatiquement. |
| `single_page` | `Option<bool>` | `true` | `true` produit une seule page continue. `false` pagine en utilisant `pdf_options.page_size`. |
| `pdf_options` | `Option<PdfOptions>` | - | Géométrie de page, échelle, niveaux de gris, en-tête et pied de page. Voir [Options PDF](#options-pdf). |
| `render.viewport_width` / `render.viewport_height` | `Option<u32>` | `1920` / `1080` | Fenêtre d'affichage du navigateur, en pixels. |
| `render.load_media` / `render.enable_scroll` | `Option<bool>` | `true` | Attend les images et les vidéos, et fait défiler la page de haut en bas pour déclencher le chargement paresseux. |
| `render.output_filename` | `Option<String>` | auto | Remplace le nom de fichier généré. |
| `render.auth` / `render.cookies` / `render.headers` | voir les types | - | Authentification HTTP Basic, jusqu'à 50 cookies injectés, jusqu'à 20 en-têtes de requête supplémentaires. |

Le bloc `render` est un `UrlRenderOptions`, partagé par toutes les conversions d'URL et de sites. Ne combinez pas `auth` avec un en-tête `Authorization` : l'API rejette le conflit.

### `convert_url_to_screenshot`

Capture un PNG de n'importe quelle URL. `UrlToScreenshotOptions` porte le même bloc `render` plus `save_to`, et rien d'autre.

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

Extrait un Markdown GitHub-Flavored propre à partir d'une URL. La navigation, les pieds de page, les publicités et les scripts sont supprimés, le corps principal de l'article est conservé, et un frontmatter YAML (titre, description, url, liens, images) est ajouté en tête.

```rust
use enconvert::UrlToMarkdownOptions;

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

Utile pour les pipelines RAG, les imports CMS et la collecte de données d'entraînement. Pour une lecture scorée avec artefacts et extraction structurée en un seul appel, utilisez plutôt [perceive](#perceive).

### `convert_image`

Convertit entre `jpeg`, `png`, `svg`, `heic` et `webp` (les 20 paires ordonnées), ou rastérise un PDF en JPEG.

```rust
use enconvert::{ConvertImageOptions, NamedFile};

// Depuis un chemin : le format d'entrée provient de l'extension.
client.convert_image("photo.heic", ConvertImageOptions {
    output_format: "webp".to_string(),
    save_to: Some("photo.webp".into()),
    ..Default::default()
})?;

// Depuis des octets : fournissez un nom de fichier pour que l'extension reste lisible.
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` est le seul champ obligatoire ; `save_to` et `output_filename` sont facultatifs. Le premier argument accepte tout ce qui se convertit en `FileInput` : un chemin `&str` ou `String`, un `&Path` ou `PathBuf`, un `Vec<u8>` ou `&[u8]` d'octets bruts, ou un `NamedFile` lorsque vous disposez d'octets et souhaitez déclarer vous-même le nom de fichier et le type de contenu.

### `convert_document`

Convertit des documents et des formats de données. `output_format` vaut `"pdf"` par défaut, et `yml`, `htm`, `md` et `jpg` sont normalisés vers leurs noms canoniques. `save_to`, `output_filename` et `pdf_options` sont facultatifs.

```rust
use enconvert::{ConvertDocumentOptions, PdfOptions};

// docx vers pdf (le format de sortie par défaut)
client.convert_document("report.docx", ConvertDocumentOptions {
    save_to: Some("report.pdf".into()),
    ..Default::default()
})?;

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

// markdown vers pdf avec une mise en page personnalisée
client.convert_document("README.md", ConvertDocumentOptions {
    pdf_options: Some(PdfOptions { page_size: Some("A4".to_string()), ..Default::default() }),
    ..Default::default()
})?;
```

**Extensions d'entrée reconnues :** `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx`, `.html`, `.htm`, `.odt`, `.ods`, `.odp`, `.ots`, `.pages`, `.numbers`, `.md`, `.markdown`, `.csv`, `.json`, `.xml`, `.yaml`, `.yml`, `.toml`.

| Format d'entrée | Sorties valides |
|--------------|---------------|
| `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` | les uns vers les autres, les 20 paires |
| `pdf` | `jpeg` |

EPUB n'a pas de paire documentaire dédiée. Faites plutôt passer les fichiers `.epub` par `convert_to_pdf` ou `convert_to_markdown`.

`convert_image` et `convert_document` résolvent tous deux le format d'entrée à partir de l'extension du fichier et vérifient la paire dans la table côté client : une paire non implémentée renvoie donc `Error::UnsupportedConversion` avec la liste des sorties valides, avant qu'aucune requête HTTP ne soit émise. Vous pouvez interroger cette même table directement :

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

Envoyez un document de presque n'importe quel type et récupérez du Markdown propre : PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT et MD, ainsi que les formats bureautiques hérités ou ODF. Le format est détecté côté serveur, aucune vérification d'extension n'a donc lieu côté client. Les images ne sont pas prises en charge.

```rust
use enconvert::ConvertToMarkdownOptions;

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

La sortie est un unique fichier `.md` qui respecte la hiérarchie des titres, ce qui en fait une bonne brique de base pour le découpage RAG : un découpeur sémantique peut segmenter sur la hiérarchie de titres propre au document plutôt que sur un nombre de caractères arbitraire. Il n'y a pas d'options PDF ici, seulement `save_to` et `output_filename`.

### `convert_to_pdf`

Envoyez à peu près n'importe quoi et récupérez un PDF : bureautique, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, texte brut, images matricielles, SVG, EPUB, ou un PDF existant en passthrough.

```rust
use enconvert::{ConvertToPdfOptions, PdfOptions};

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

// PDF en entrée, PDF en niveaux de gris en sortie.
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>Seul <code>pdf_options.grayscale</code> est pris en compte ici.</strong> L'endpoint anything-to-PDF détecte l'entrée automatiquement et applique sa propre géométrie. Pour contrôler la taille de page, l'orientation, les marges, l'échelle, les en-têtes et les pieds de page, utilisez <code>convert_url_to_pdf</code> ou <code>convert_document</code> avec une entrée HTML ou Markdown.
</div>

### `convert_website_to_pdf` et `convert_website_to_screenshot`

Découvrez toutes les pages d'un site, convertissez-les en arrière-plan et récupérez une archive ZIP unique. Les deux méthodes sont exclusivement asynchrones : elles renvoient un `BatchSubmission`, jamais un fichier terminé.

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

// Bloque jusqu'à ce que le lot se stabilise, puis enregistre le 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);
```

| Champ | Type | Défaut | Description |
|-------|------|---------|-------------|
| `website.crawl_mode` | `Option<CrawlMode>` | `Auto` | `Auto`, `Sitemap` (sitemap.xml uniquement) ou `Full` (sitemap plus une exploration en largeur). |
| `website.include_patterns` / `website.exclude_patterns` | `Option<Vec<String>>` | - | N'explore que les URL correspondant à ces motifs, ou les ignore. Mode d'exploration complète. |
| `website.notification_email` / `website.callback_url` | `Option<String>` | propriétaire du projet / - | Adresse notifiée par e-mail, et webhook appelé, à la fin du lot. |
| `website.render` | `UrlRenderOptions` | - | Mêmes champs de fenêtre d'affichage, médias, défilement, authentification, cookies et en-têtes que les conversions d'URL unique. |
| `single_page` / `pdf_options` | voir ci-dessus | - | Lots PDF uniquement. |

`convert_website_to_screenshot` prend un `WebsiteToScreenshotOptions`, un alias de type pour `WebsiteConversionOptions`, et produit un ZIP de PNG. `wait_for_batch` interroge `get_batch_status` toutes les `interval_ms` (5000 par défaut) jusqu'à ce que le lot quitte l'état `Processing`, puis télécharge éventuellement le ZIP. Si `timeout_ms` (1 800 000 par défaut, soit 30 minutes) s'écoule avant, la méthode renvoie `Error::Api { status: 504, .. }`.

### `get_job_status` et `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")?; // compteurs agrégés et éléments par URL
println!("{}/{} done, {} failed", batch.completed, batch.total, batch.failed);
```

<div class="alert alert-info">
<strong>Vous avez rarement besoin de <code>get_job_status</code> directement.</strong> Le SDK l'interroge pour vous lorsqu'une conversion synchrone renvoie une 5xx. Voir <a href="#timeout-recovery">Récupération après timeout</a>.
</div>

---

## Intelligence web (V2)

Tout ce qui se trouve sous `client.v2()` transforme des pages web en données prêtes pour un agent. Le point commun de chaque lecture est `render_quality`, un score de 0.0 à 1.0 attaché à chaque page rendue. Un score faible signifie que la page n'a pas été rendue proprement : page anti-bot, mur de cookies ou de connexion, coquille SPA vide, ou erreur HTTP. Le contenu revient quand même, mais il revient signalé, avec une table `deductions` nommée et une liste `warnings` : une mauvaise lecture n'entre donc jamais discrètement dans le contexte de votre agent. Les résultats de perceive, distill, lookup et watch le portent tous. Commencez par la [vue d'ensemble V2](/fr/docs/v2-overview) pour les concepts derrière les six capacités.

### Perceive

Effectue le rendu d'une URL dans les artefacts que vous demandez. Synchrone : l'appel renvoie une opération terminée avec des URL d'artefacts signées pour 15 minutes. Voir [perceive](/fr/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); // p. ex. {"http_error": 0.7} quand quelque chose cloche
println!("{:?}", op.outputs.get("markdown").and_then(|a| a.url.as_ref()));
println!("{:?}", op.structured);

// Les URL d'artefacts sont resignées à chaque lecture du statut.
client.v2().get_perceive_operation(&op.operation_id)?;
```

| Option | Type | Défaut | Description |
|--------|------|---------|-------------|
| `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>>` | - | Schéma JSON pour l'extraction structurée. |
| `only_main_content` | `Option<bool>` | `true` | Supprime la navigation, l'en-tête, le pied de page et les bandeaux de cookies de l'artefact Markdown et de l'extrait `main_content`. |
| `wait_for` / `wait_timeout_ms` / `js_code` | voir les types | - / `30000` / - | Un sélecteur CSS (éventuellement `css:...`) ou `js:<expr>` à attendre, la durée d'attente (0 à 60000), et le JavaScript à exécuter après la navigation (20000 caractères maximum). |
| `viewport` / `mobile` | `Option<PerceiveViewport>` / `Option<bool>` | 1920x1080 | Largeur de 320 à 3840, hauteur de 240 à 2160, ou un profil d'appareil mobile. |
| `cache_mode` / `block_resources` | voir les types | `Enabled` / - | `Enabled` met en cache pendant 1 heure, `Bypass` l'ignore, `Refresh` refait le rendu ; plus les types de ressources que le navigateur ne doit pas charger. |
| `headers`, `cookies`, `auth`, `respect_robots`, `pdf_options` | voir `UrlRenderOptions` et `PdfOptions` | - | Mêmes formes que les options de rendu V1. `pdf_options` ne compte que lorsque `outputs` inclut `Pdf`. |

`perceive_direct` renvoie un artefact unique en octets bruts au lieu d'une enveloppe JSON. Il faut demander exactement une sortie produisant un artefact, et le SDK le vérifie localement avec `Error::InvalidInput` avant tout envoi.

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

// Retélécharge un artefact stocké plus tard. Passez None si l'opération n'en a produit qu'un.
let saved = client
    .v2()
    .download_perceive_artifact(&direct.operation_id, Some(PerceiveOutputName::Pdf))?;
println!("{} bytes", saved.content.len());
```

`PerceiveDirectResult` porte `content`, `content_type`, `filename`, `operation_id`, `object_key`, `cache_hit`, `render_quality`, `source_status_code`, `content_hash` et `warnings_count`, tous lus depuis les en-têtes de réponse. Un `Error::Api { status: 410, .. }` renvoyé par `download_perceive_artifact` signifie que l'artefact a dépassé sa fenêtre de rétention.

`perceive_batch` accepte jusqu'à 1000 URL et un bloc d'options partagé. Les petits lots s'exécutent en ligne ; les plus gros reviennent avec le statut `Queued`, interrogez donc `get_perceive_batch` avec le `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

Énumère les URL d'un site sans aucun rendu navigateur. Voir [discover](/fr/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` vaut `Hybrid` par défaut (`Sitemap` et `Crawl` sont les alternatives), `max_urls` vaut 100 (1 à 1000), `max_depth` vaut 2 (1 à 5), et `same_domain_only` vaut true. `include_patterns` puis `exclude_patterns` appliquent un filtrage par expressions régulières, 50 au maximum chacun, et `respect_robots` respecte robots.txt. Le résultat porte `urls`, `total`, `pages_crawled`, `truncated`, `robots_respected`, ainsi qu'une table `sources` des comptages bruts par source.

### Lookup

Lance une recherche web catégorisée et, si vous le souhaitez, effectue le rendu des premiers résultats dans le même appel. Voir [lookup](/fr/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` vaut `Web` par défaut (`News`, `Images`, `Scholar`, `Patents` et `Maps` sont les autres valeurs), `num_results` vaut 10 (1 à 100), `page` vaut 1 (1 à 10), et `autocorrect` vaut true. `country` et `locale` prennent des codes comme `"us"` et `"en"`, `location` prend du texte libre comme `"Austin, Texas"`, et `time_filter` accepte `Hour`, `Day`, `Week`, `Month` ou `Year`. `perceive_top` (0 à 10, 0 par défaut) effectue automatiquement le rendu des N premières URL de résultat et attache un `PerceiveResult` complet à chaque occurrence. Le résultat porte aussi `answer_box`, `knowledge_graph` et `perceive_operation_ids`.

### Distill

Extraction structurée pilotée par schéma : donnez-lui une forme, récupérez cette forme pour chaque URL. Une passe CSS optionnelle s'exécute d'abord, et tout ce qu'elle manque est escaladé vers le niveau LLM. Voir [distill](/fr/docs/v2-distill).

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

// CssField n'a pas de Default parce que `field_type` est obligatoire.
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);
}
```

Ou découvrez d'abord un site et distillez chaque page trouvée :

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

client.v2().distill(DistillOptions {
    discover_from: Some(DistillDiscoverFrom {
        max_pages: Some(20), // 1 à 50, plafonne à la fois la découverte et la distillation
        ..DistillDiscoverFrom::new("https://example.com")
    }),
    schema: json!({ "title": "page title" }).as_object().unwrap().clone(),
    ..Default::default()
})?;
```

Fournissez exactement l'un des deux : `urls` (50 au maximum) ou `discover_from`. Les deux, ou aucun, renvoie `Error::InvalidInput` avant l'envoi de toute requête. `schema` est obligatoire et est soit un objet JSON Schema, soit une table plate `{field: description}`. `CssFieldType` couvre `Text`, `Attribute`, `Html`, `Regex`, `Nested`, `List` et `NestedList` ; `transform` accepte `Lowercase`, `Uppercase` ou `Strip` ; l'imbrication est plafonnée à une profondeur de 5.

### Ingest

Transforme un site entier, ou une pile de documents envoyés, en JSONL découpé et prêt pour le RAG à travers un seul pipeline. Ingest est toujours asynchrone. Voir [ingest](/fr/docs/v2-ingest).

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

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

// Ou depuis des fichiers envoyés : PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT et MD,
// ainsi que les documents bureautiques hérités ou ODF.
let files = vec!["handbook.pdf".into(), "notes.docx".into()];
client.v2().ingest_files(files, IngestFilesOptions::default())?;

// Interroger, lister, annuler.
let status = client.v2().get_ingest_job(&job.job_id)?;
if status.status == IngestStatus::Completed {
    println!("{:?}", status.output_url); // URL signée vers le JSONL
}
client.v2().list_ingest_jobs(V2ListOptions { limit: Some(50), ..Default::default() })?;
client.v2().cancel_ingest_job(&job.job_id)?; // idempotent
```

`mode` vaut `Urls` par défaut, ce qui exige une liste `urls` non vide (1000 au maximum) et rejette `url` ; `Sitemap` et `Crawl` font l'inverse et exigent l'`url` de départ. Cet appariement est validé côté client, un décalage renvoie donc immédiatement `Error::InvalidInput`. `max_pages` vaut 50 par défaut (1 à 1000), `max_depth` vaut 2 (1 à 5), `same_domain_only` vaut true, `chunk.max_words` vaut 512 (32 à 4000), et `chunk.sentence_overlap` vaut 1 (0 à 10). `include_patterns`, `exclude_patterns`, `respect_robots`, `wait_for` et `wait_timeout_ms` se comportent comme sur discover et perceive. Les webhooks de fin sont signés en HMAC :

```rust
let secret = client.v2().get_webhook_secret()?;
println!("{} {} {}", secret.secret, secret.signature_header, secret.signature_scheme);
client.v2().rotate_webhook_secret()?;            // les anciennes signatures cessent aussitôt d'être valides
client.v2().retry_ingest_webhook("ing_abc123")?; // renvoie le webhook d'un job terminé
```

### Watch

Refait le rendu d'une URL à intervalle fixe et vous signale ce qui a changé. Voir [watch](/fr/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);

// Lit l'historique des vérifications, de la plus récente à la plus ancienne.
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)?;

// Le met en pause. Passer Some(String::new()) pour webhook_url effacerait le webhook.
client.v2().update_watcher(&watcher.watcher_id, WatcherUpdate {
    status: Some(WatchUpdateStatus::Paused),
    ..Default::default()
})?;
client.v2().delete_watcher(&watcher.watcher_id)?; // suppression logique, idempotente
```

`frequency_minutes` vaut 60 par défaut et accepte de 60 à 43200 : le plancher horaire est donc strict. `diff_mode` vaut `Auto` par défaut et accepte aussi `Text`, `Structured`, `Tables` et `Metadata`, `track_fields` restreignant le moteur de diff à un sous-ensemble de champs ou de sélecteurs. `notify_email` vaut true par défaut et notifie le propriétaire du projet par e-mail en cas de changement ; `webhook_url` ajoute un webhook de changement signé en HMAC.

`update_watcher` exige au moins un champ et renvoie `Error::InvalidInput` pour un `WatcherUpdate` entièrement à `None`. Les entrées `changes` des snapshots contiennent du contenu de page non fiable : échappez-les avant tout affichage.

---

## Options PDF

`PdfOptions` est partagé par `convert_url_to_pdf`, `convert_document`, `convert_to_pdf` et la sortie `Pdf` de perceive. Chaque champ est facultatif et n'est envoyé que lorsqu'il est défini.

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

| Champ | Type | Description |
|-------|------|-------------|
| `page_size` | `Option<String>` | `"A4"`, `"A3"`, `"Letter"`, `"Legal"`, et ainsi de suite. |
| `page_width` / `page_height` | `Option<f64>` | Dimensions personnalisées. Définies ensemble, elles remplacent `page_size`. |
| `orientation` | `Option<PdfOrientation>` | `Portrait` ou `Landscape`. |
| `margins` | `Option<PdfMargins>` | `top`, `bottom`, `left`, `right`, chacun facultatif. |
| `scale` | `Option<f64>` | Échelle de rendu, par exemple `0.9` pour 90 pour cent. |
| `grayscale` | `Option<bool>` | Post-traite le PDF en niveaux de gris. |
| `header` / `footer` | `Option<PdfHeaderFooter>` | `content` (2000 caractères maximum) et `height`. |

La sémantique complète des paramètres se trouve dans [paramètres et options](/fr/docs/parameters-options).

---

## Gestion des erreurs

Il n'y a qu'un seul type d'erreur, `enconvert::Error`. Il implémente `std::error::Error` via `thiserror`, `?` se propage donc dans n'importe quel `Box<dyn Error>` ou `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 | Renvoyée sur | Code de statut |
|---------|-------------|-------------|
| `Error::Authentication(String)` | Clé API invalide, absente ou révoquée | `401`, `403` |
| `Error::Quota(String)` | Levée sur un HTTP 402 | `402` |
| `Error::RateLimit(String)` | Limite de débit dépassée | `429` |
| `Error::Api { status, message }` | Toute autre réponse 4xx ou 5xx | le code réel |
| `Error::UnsupportedConversion(String)` | Une paire `{input}-to-{output}` que l'API n'implémente pas, détectée côté client | - |
| `Error::InvalidInput(String)` | Arguments incorrects détectés avant la requête, par exemple une clé API vide | - |
| `Error::Http(reqwest::Error)` | Échec de connexion, de TLS, de timeout ou de décodage de la réponse | issu du transport, lorsqu'il est présent |
| `Error::Io(std::io::Error)` | Lecture d'un fichier à envoyer, ou écriture d'un téléchargement | - |
| `Error::Json(serde_json::Error)` | Sérialisation d'un corps de requête | - |

Quatre prédicats gardent les bras de `match` courts : `is_authentication()`, `is_quota()`, `is_rate_limit()` et `is_server_error()`. `status_code()` renvoie un `Option<u16>` pour chaque variante qui en porte un. La table complète des messages se trouve dans la [référence des codes d'erreur](/fr/docs/error-codes).

---

## Récupération après timeout

Les rendus URL vers PDF longs et les conversions de gros documents peuvent dépasser le timeout d'un reverse proxy même lorsque la conversion réussit côté serveur. Le SDK s'en sort tout seul :

1. Avant chaque requête, il génère un UUID et l'envoie comme `job_id` dans le corps ou dans le formulaire multipart.
2. Si cette requête revient en 5xx sous forme d'`Error::Api`, le SDK bascule silencieusement sur l'interrogation de `GET /v1/convert/status/{job_id}` toutes les 3 secondes.
3. Dès que le job passe à `success`, il renvoie le résultat. Dès qu'il passe à `failed`, il renvoie une `Error::Api` portant le message du serveur.
4. Le délai d'interrogation est de 5 minutes. Au-delà, vous obtenez `Error::Api { status: 504, message: "Conversion timed out" }`.

La récupération couvre `convert_url_to_pdf`, `convert_url_to_screenshot`, `convert_url_to_markdown`, `convert_image`, `convert_document`, `convert_to_markdown` et `convert_to_pdf`. Elle ne couvre délibérément pas les soumissions de lots de sites : celles-ci n'ont pas de ligne de job par unité, une 5xx y signifie donc que la soumission elle-même a échoué, et elle remonte directement. Les endpoints V2 répondent directement et n'ont pas non plus de repli par job. Les réponses qui omettent `job_id` reçoivent celui généré par le client, `result.job_id` est donc toujours quelque chose que vous pouvez passer plus tard à `get_job_status`.

---

## Configuration

```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"), // remplacement de l'URL de base
    Some(Duration::from_secs(300)),    // timeout de requête
)?;
println!("enconvert {}", enconvert::VERSION);
```

| Argument | Type | Défaut | Description |
|----------|------|---------|-------------|
| `api_key` | `impl Into<String>` | obligatoire | Clé API privée. Une chaîne vide renvoie `Error::InvalidInput`, tout comme une clé contenant des caractères d'en-tête invalides. |
| `base_url` | `Option<&str>` | `https://api.enconvert.com` | Les barres obliques finales sont supprimées. |
| `timeout` | `Option<Duration>` | 300 secondes | Appliqué à l'ensemble de la requête. |

`Enconvert::new(api_key)` est un raccourci pour `with_options(api_key, None, None)`.

<div class="alert alert-warning">
<strong>Ne codez jamais la clé API en dur.</strong> Lisez-la depuis une variable d'environnement ou un gestionnaire de secrets. Le SDK marque la valeur de l'en-tête <code>X-API-Key</code> comme sensible pour qu'elle reste hors des sorties de débogage, et il télécharge les URL signées via un second client HTTP non authentifié pour que votre clé ne soit jamais envoyée au stockage objet. Générez et faites tourner vos clés dans le <a href="/fr/dashboard">tableau de bord</a>, et consultez l'<a href="/fr/docs/authentication">authentification</a> pour les types de clés.
</div>

---

## Forme du résultat

Chaque méthode de conversion de fichier unique renvoie 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>,
}
```

Téléchargez le fichier vous-même depuis `presigned_url`, ou passez `save_to` et laissez le SDK l'écrire pour vous. Les URL signées expirent : stockez donc dans votre propre bucket tout ce que vous devez conserver.

Les chemins asynchrones renvoient leurs propres formes. `JobStatus` porte `status` (`Processing`, `Success`, `Failed` ou `Unknown(String)`), `presigned_url`, `object_key` et `error`. `BatchSubmission` porte `batch_id`, `status`, `url_count`, `total_discovered`, `discovery_method` et `output_format`. `BatchStatus` ajoute les compteurs `total`, `completed`, `failed` et `in_progress`, ainsi que `output_mode`, `zip_download_url` et un `Vec<BatchItem>` de lignes par URL.

Les lectures V2 renvoient un `PerceiveResult`, dont la table `outputs` est indexée par nom de sortie (`"markdown"`, `"screenshot_full_page"`, et ainsi de suite) avec des valeurs `V2OutputArtifact { url, object_key, size_bytes, content_type, expires_in }`. Ces URL d'artefacts sont signées pour 15 minutes et resignées à chaque appel de `get_perceive_operation`. À côté d'elles se trouvent `render_quality`, `status_code`, `deductions`, `cache_hit`, `structured`, `extraction_tier`, `tokens`, `cost_cents`, `duration_ms`, `warnings` et `options_echo`. Chaque énumération de réponse porte une variante `Unknown(String)` : une valeur de statut ajoutée sur le serveur après la publication de votre build se parse donc proprement au lieu d'échouer.

---

## Sources et signalements

- **crates.io :** [enconvert](https://crates.io/crates/enconvert)
- **GitHub :** [conversionapi/rust-sdk](https://github.com/conversionapi/rust-sdk)
- **Licence :** MIT
- **Autres clients :** [tous les SDK](/fr/docs/sdks) et la [référence des endpoints REST](/fr/docs/endpoints-overview)

---

## Questions fréquentes

### Comment convertir des fichiers en Rust avec un paquet crates.io ?

Exécutez `cargo add enconvert`, construisez un client avec `Enconvert::new(api_key)?`, puis appelez une méthode comme `convert_url_to_pdf`, `convert_image` ou `convert_document`. Chaque struct d'options dérive `Default` : vous écrivez donc un littéral de struct avec `..Default::default()` et ne renseignez que les champs qui vous intéressent. Passez `save_to` pour que le SDK écrive la sortie directement sur le disque.

### Le SDK Rust a-t-il besoin de tokio ou d'un runtime asynchrone ?

Non. Il est bâti sur l'API blocking de `reqwest` : chaque méthode bloque le thread appelant, et il n'y a ni `.await`, ni exécuteur, ni `tokio` dans votre arbre de dépendances. Si votre application est déjà asynchrone, appelez le SDK depuis `tokio::task::spawn_blocking` plutôt que directement sur un thread de réacteur, car le client bloquant de `reqwest` ne peut pas s'exécuter dans un contexte de runtime asynchrone.

### Comment convertir une URL en PDF en Rust ?

Appelez `convert_url_to_pdf("https://example.com", UrlToPdfOptions { save_to: Some("page.pdf".into()), ..Default::default() })`. Mettez `single_page: Some(false)` pour paginer au lieu de produire une seule page continue, et passez `pdf_options` pour la taille de page, l'orientation, les marges, l'échelle, les niveaux de gris, les en-têtes et les pieds de page.

### Comment convertir un DOCX en PDF en Rust ?

Appelez `convert_document("report.docx", ConvertDocumentOptions { save_to: Some("report.pdf".into()), ..Default::default() })`. PDF est la sortie par défaut, `output_format` peut donc rester non défini. La même méthode gère XLSX, PPTX, ODT, ODS, ODP, OTS, Pages, Numbers, HTML et Markdown vers PDF, ainsi que les paires de formats de données comme JSON vers YAML et CSV vers XML.

### Comment convertir du HEIC en WebP en Rust ?

Appelez `convert_image("photo.heic", ConvertImageOptions { output_format: "webp".to_string(), ..Default::default() })`. Le format d'entrée provient de l'extension du fichier, et les 20 paires ordonnées entre `jpeg`, `png`, `svg`, `heic` et `webp` sont implémentées, plus `pdf` vers `jpeg` pour la rastérisation. Une paire non prise en charge renvoie `Error::UnsupportedConversion` avant tout appel réseau.

### Comment récupérer une page web en Markdown propre depuis Rust ?

De deux façons. `convert_url_to_markdown` renvoie un fichier Markdown unique avec un frontmatter YAML. `client.v2().perceive(url, PerceiveOptions { outputs: Some(vec![PerceiveOutputName::Markdown]), ..Default::default() })` renvoie le même contenu avec un score `render_quality`, des déductions nommées, des avertissements, et éventuellement des captures d'écran, des liens ou une extraction structurée issus du même rendu. Utilisez perceive quand un agent va lire le résultat.

### Comment savoir si une page a réellement été rendue ?

Lisez `render_quality`, un score de 0.0 à 1.0 présent sur chaque lecture V2. Un score faible signifie que le rendu n'a pas été propre : page anti-bot, mur de cookies ou de connexion, coquille SPA vide, ou erreur HTTP. La table `deductions` nomme chaque pénalité qui s'est appliquée, `status_code` donne le statut HTTP en amont, et `warnings` liste ce qui a mal tourné. Le contenu est quand même renvoyé, simplement signalé.

### Que se passe-t-il quand une conversion dure plus longtemps que le timeout du proxy ?

Le SDK s'en charge. Chaque requête porte un `job_id` généré par le client ; si la requête renvoie une 5xx, le SDK interroge `GET /v1/convert/status/{job_id}` toutes les 3 secondes pendant 5 minutes au maximum et renvoie le résultat terminé comme si de rien n'était. Passé ce délai, vous obtenez `Error::Api { status: 504, message: "Conversion timed out" }`. Les soumissions de lots de sites entiers font exception, et `wait_for_batch` est l'outil adapté dans ce cas.

### Puis-je utiliser le SDK Rust depuis un navigateur ou une cible WASM ?

Non. Il s'authentifie avec une clé API privée qui ne doit jamais être livrée à un client, et il dépend du client bloquant de `reqwest`, avec du TLS natif et des threads, dont aucun n'existe sur `wasm32-unknown-unknown`. Exécutez-le sur un serveur, dans une CLI ou dans un worker, et faites dialoguer votre frontend avec votre propre backend.
