Rust SDK für Dateikonvertierung#

enconvert ist die offizielle Rust-Crate für die EnConvert-API. Sie ist ein blockierender Client auf Basis der Blocking-API von reqwest: keine Async-Runtime, kein tokio in deinem Abhängigkeitsbaum und nirgends ein .await in deinem Code. Zwölf Methoden auf Enconvert decken die Datei-Konvertierung ab: URL zu PDF, Screenshots, Markdown-Extraktion, Bild- und Dokumentformatpaare, Anything-to-PDF und Batches für ganze Websites. Ein zweiter Namespace, client.v2(), ergänzt dreiundzwanzig Methoden für Web-Intelligence: Perceive, Discover, Lookup, Distill, Ingest und Watch. Jedes Options-Struct leitet Default ab, und jeder Fehler landet in einem einzigen Error-Enum.

crates.io: enconvert (0.1.0) · Quelle: conversionapi/rust-sdk · Rust: Edition 2021 · Lizenz: MIT

Installation#

cargo add enconvert

Oder trage die Crate von Hand ein. distill und die schema-Extraktion von Perceive nehmen eine serde_json::Map<String, Value> entgegen, ergänze also auch serde_json, wenn du sie nutzen möchtest:

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

Die Crate zieht reqwest (blocking, json, multipart), serde, serde_json, thiserror und uuid mit herein. Es gibt keine optionalen Features zu aktivieren.


Schnellstart#

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

    // Eine URL in ein PDF konvertieren und das Ergebnis direkt auf die Platte schreiben.
    let pdf = client.convert_url_to_pdf("https://example.com", UrlToPdfOptions {
        save_to: Some("page.pdf".into()),
        ..Default::default()
    })?;
    println!("{}", pdf.presigned_url);

    // Dieselbe Seite so lesen, wie es dein Agent tun sollte, mit angehängtem Qualitätswert.
    let op = client.v2().perceive("https://example.com", PerceiveOptions {
        outputs: Some(vec![PerceiveOutputName::Markdown, PerceiveOutputName::Structured]),
        ..Default::default()
    })?;
    println!("{:?}", op.render_quality); // z. B. Some(0.93)
    Ok(())
}

Jede Methode blockiert den aufrufenden Thread. Damit lässt sich das SDK mühelos in ein CLI, ein Build-Skript, einen Worker-Thread oder einen synchronen Web-Handler einsetzen. Alle öffentlichen Typen werden am Crate-Root re-exportiert, die Beispiele unten brauchen also nur use enconvert::{...} und den client von oben.

Rufe das SDK nicht aus einem Thread einer Async-Runtime auf. Der blockierende Client von reqwest lässt sich nicht aus einem Thread heraus betreiben, der bereits einem Tokio-Reaktor (oder einem vergleichbaren) gehört. Aus Async-Code heraus verpackst du jeden Aufruf in tokio::task::spawn_blocking.

Was der Client bereitstellt#

Bereich Erreichbar über Was er abdeckt
Datei-Konvertierung client.<method> 12 Methoden: URL zu PDF, Screenshot und Markdown; Bild- und Dokumentpaare; Anything-to-Markdown und Anything-to-PDF; Batches für ganze Websites; Job- und Batch-Status
Web-Intelligence client.v2().<method> 23 Methoden über Perceive, Discover, Lookup, Distill, Ingest und Watch
Formattabellen valid_outputs_for, IMPLEMENTED_CONVERSIONS Die 43 implementierten {input}-to-{output}-Endpunkte, clientseitig geprüft, bevor eine Anfrage rausgeht
Fehler enconvert::Error Ein Enum, neun Varianten, mit is_authentication, is_quota, is_rate_limit, is_server_error und status_code

Nichts ist ein Builder und nichts ist async. Die idiomatische Aufrufform ist ein Struct-Literal mit ..Default::default().


Datei-Konvertierung#

convert_url_to_pdf#

Rendere jede öffentliche URL zu einem 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);
Feld Typ Standard Beschreibung
save_to Option<PathBuf> - Lokaler Pfad, unter dem das PDF geschrieben wird. Übergeordnete Verzeichnisse werden automatisch angelegt.
single_page Option<bool> true true erzeugt eine einzige durchgehende Seite. false paginiert anhand von pdf_options.page_size.
pdf_options Option<PdfOptions> - Seitengeometrie, Skalierung, Graustufen, Kopf- und Fußzeile. Siehe PDF-Optionen.
render.viewport_width / render.viewport_height Option<u32> 1920 / 1080 Browser-Viewport in Pixeln.
render.load_media / render.enable_scroll Option<bool> true Auf Bilder und Videos warten und von oben nach unten scrollen, damit Lazy Loader auslösen.
render.output_filename Option<String> automatisch Überschreibt den generierten Dateinamen.
render.auth / render.cookies / render.headers siehe Typen - HTTP Basic Auth, bis zu 50 eingeschleuste Cookies, bis zu 20 zusätzliche Request-Header.

Der render-Block ist UrlRenderOptions und wird von jeder URL- und Website-Konvertierung geteilt. Kombiniere auth nicht mit einem Authorization-Header: Die API weist diesen Konflikt zurück.

convert_url_to_screenshot#

Nimm ein PNG von einer beliebigen URL auf. UrlToScreenshotOptions trägt denselben render-Block plus save_to, und sonst nichts.

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#

Extrahiere sauberes GitHub-Flavored Markdown aus einer URL. Navigation, Fußzeilen, Werbung und Skripte werden entfernt, der eigentliche Artikeltext bleibt erhalten, und ein YAML-Frontmatter (Titel, Beschreibung, URL, Links, Bilder) wird vorangestellt.

use enconvert::UrlToMarkdownOptions;

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

Nützlich für RAG-Pipelines, CMS-Importe und das Sammeln von Trainingsdaten. Für einen bewerteten Lesevorgang mit Artefakten und strukturierter Extraktion in einem einzigen Aufruf nimmst du stattdessen Perceive.

convert_image#

Konvertiere zwischen jpeg, png, svg, heic und webp (alle 20 geordneten Paare) oder rastere ein PDF nach JPEG.

use enconvert::{ConvertImageOptions, NamedFile};

// Aus einem Pfad: Das Eingabeformat ergibt sich aus der Dateiendung.
client.convert_image("photo.heic", ConvertImageOptions {
    output_format: "webp".to_string(),
    save_to: Some("photo.webp".into()),
    ..Default::default()
})?;

// Aus Bytes: Gib einen Dateinamen an, damit die Endung weiterhin lesbar ist.
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 ist das einzige Pflichtfeld; save_to und output_filename sind optional. Das erste Argument ist alles, was sich in FileInput umwandeln lässt: ein &str- oder String-Pfad, ein &Path oder PathBuf, ein Vec<u8> oder &[u8] mit Rohbytes, oder ein NamedFile, wenn du Bytes hast und Dateiname und Content-Type selbst festlegen willst.

convert_document#

Konvertiere Dokumente und Datenformate. output_format ist standardmäßig "pdf", und yml, htm, md sowie jpg werden auf ihre kanonischen Namen normalisiert. save_to, output_filename und pdf_options sind optional.

use enconvert::{ConvertDocumentOptions, PdfOptions};

// docx zu pdf (das Standard-Ausgabeformat)
client.convert_document("report.docx", ConvertDocumentOptions {
    save_to: Some("report.pdf".into()),
    ..Default::default()
})?;

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

// markdown zu pdf mit eigener Seiteneinrichtung
client.convert_document("README.md", ConvertDocumentOptions {
    pdf_options: Some(PdfOptions { page_size: Some("A4".to_string()), ..Default::default() }),
    ..Default::default()
})?;

Erkannte Eingabe-Dateiendungen: .doc, .docx, .xls, .xlsx, .ppt, .pptx, .html, .htm, .odt, .ods, .odp, .ots, .pages, .numbers, .md, .markdown, .csv, .json, .xml, .yaml, .yml, .toml.

Eingabeformat Gültige Ausgaben
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 jeweils untereinander, alle 20 Paare
pdf jpeg

EPUB hat kein eigenes Dokumentpaar. Schicke .epub-Dateien stattdessen durch convert_to_pdf oder convert_to_markdown.

convert_image und convert_document leiten das Eingabeformat beide aus der Dateiendung ab und prüfen das Paar gegen die clientseitige Tabelle. Ein nicht implementiertes Paar liefert also Error::UnsupportedConversion samt Liste der gültigen Ausgaben, bevor überhaupt eine HTTP-Anfrage gestellt wird. Dieselbe Tabelle kannst du direkt abfragen:

use enconvert::{valid_outputs_for, IMPLEMENTED_CONVERSIONS};

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

convert_to_markdown#

Lade ein Dokument nahezu beliebigen Typs hoch und erhalte sauberes Markdown zurück: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT und MD sowie ältere oder ODF-Office-Dateien. Das Format wird serverseitig erkannt, es läuft also keine clientseitige Endungsprüfung. Bilder werden nicht unterstützt.

use enconvert::ConvertToMarkdownOptions;

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

Die Ausgabe ist eine einzige überschriftenbewusste .md-Datei, was sie zu einem guten Baustein für RAG-Chunking macht: Ein semantischer Chunker kann anhand der Überschriftenhierarchie des Dokuments selbst trennen statt an willkürlichen Zeichenzahlen. Hier gibt es keine PDF-Optionen, nur save_to und output_filename.

convert_to_pdf#

Lade fast alles hoch und erhalte ein PDF zurück: Office, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, reinen Text, Rasterbilder, SVG, EPUB oder ein bestehendes PDF zum Durchreichen.

use enconvert::{ConvertToPdfOptions, PdfOptions};

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

// PDF rein, Graustufen-PDF raus.
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()
})?;
Hier wird ausschließlich pdf_options.grayscale berücksichtigt. Der Anything-to-PDF-Endpunkt erkennt die Eingabe selbst und wendet seine eigene Geometrie an. Für Kontrolle über Seitengröße, Ausrichtung, Ränder, Skalierung, Kopf- und Fußzeilen nutzt du convert_url_to_pdf oder convert_document mit einer HTML- oder Markdown-Eingabe.

convert_website_to_pdf und convert_website_to_screenshot#

Ermittle jede Seite einer Website, konvertiere jede einzelne im Hintergrund und sammle alles in einem ZIP. Beide arbeiten ausschließlich asynchron: Sie liefern ein BatchSubmission zurück, nie eine fertige Datei.

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

// Blockieren, bis der Batch durch ist, dann das ZIP speichern.
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);
Feld Typ Standard Beschreibung
website.crawl_mode Option<CrawlMode> Auto Auto, Sitemap (nur sitemap.xml) oder Full (Sitemap plus Breitensuche-Crawl).
website.include_patterns / website.exclude_patterns Option<Vec<String>> - Nur URLs crawlen, die auf diese Muster passen, oder sie überspringen. Nur im Full-Crawl-Modus.
website.notification_email / website.callback_url Option<String> Projektinhaber / - Adresse, an die eine E-Mail geht, und Webhook, der aufgerufen wird, sobald der Batch fertig ist.
website.render UrlRenderOptions - Dieselben Felder für Viewport, Medien, Scrollen, Auth, Cookies und Header wie bei Einzel-URL-Konvertierungen.
single_page / pdf_options siehe oben - Nur für PDF-Batches.

convert_website_to_screenshot nimmt WebsiteToScreenshotOptions entgegen, ein Typalias für WebsiteConversionOptions, und erzeugt ein ZIP mit PNGs. wait_for_batch fragt get_batch_status alle interval_ms ab (Standard 5000), bis der Batch den Zustand Processing verlässt, und lädt anschließend optional das ZIP herunter. Läuft vorher timeout_ms ab (Standard 1.800.000, also 30 Minuten), kommt Error::Api { status: 504, .. } zurück.

get_job_status und 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")?; // aggregierte Zähler und Einträge pro URL
println!("{}/{} done, {} failed", batch.completed, batch.total, batch.failed);
Du brauchst get_job_status selten direkt. Das SDK fragt ihn für dich ab, wenn eine synchrone Konvertierung mit 5xx antwortet. Siehe Timeout-Recovery.

Web-Intelligence (V2)#

Alles unter client.v2() verwandelt Webseiten in agentenfertige Daten. Was jeder Lesevorgang gemeinsam hat, ist render_quality, ein Wert von 0.0 bis 1.0, der an jede gerenderte Seite angehängt wird. Ein niedriger Wert bedeutet, dass die Seite nicht sauber gerendert hat: eine Challenge-Seite, eine Cookie- oder Login-Wall, eine leere SPA-Hülle oder ein HTTP-Fehler. Der Inhalt kommt trotzdem zurück, aber markiert, mit einer benannten deductions-Map und einer warnings-Liste, sodass ein schlechter Lesevorgang nie unbemerkt in den Kontext deines Agenten rutscht. Ergebnisse von Perceive, Distill, Lookup und Watch tragen den Wert alle. Beginne mit der V2-Übersicht für die Konzepte hinter den sechs Fähigkeiten.

Perceive#

Rendere eine URL in genau die Artefakte, die du anforderst. Synchron: Der Aufruf liefert eine abgeschlossene Operation mit 15 Minuten gültigen signierten Artefakt-URLs zurück. Siehe 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); // z. B. {"http_error": 0.7}, wenn etwas nicht stimmt
println!("{:?}", op.outputs.get("markdown").and_then(|a| a.url.as_ref()));
println!("{:?}", op.structured);

// Artefakt-URLs werden bei jeder Statusabfrage neu signiert.
client.v2().get_perceive_operation(&op.operation_id)?;
Option Typ Standard Beschreibung
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>> - JSON-Schema für die strukturierte Extraktion.
only_main_content Option<bool> true Entfernt Navigation, Header, Footer und Cookie-Banner aus dem Markdown-Artefakt und dem main_content-Extrakt.
wait_for / wait_timeout_ms / js_code siehe Typen - / 30000 / - Ein CSS-Selektor (optional css:...) oder js:<expr>, auf den gewartet wird, die Wartedauer (0 bis 60000) und JavaScript, das nach der Navigation ausgeführt wird (max. 20000 Zeichen).
viewport / mobile Option<PerceiveViewport> / Option<bool> 1920x1080 Breite 320 bis 3840, Höhe 240 bis 2160, oder ein Mobilgeräte-Profil.
cache_mode / block_resources siehe Typen Enabled / - Enabled cacht für 1 Stunde, Bypass überspringt den Cache, Refresh rendert neu; dazu die Ressourcentypen, die der Browser nicht laden soll.
headers, cookies, auth, respect_robots, pdf_options siehe UrlRenderOptions und PdfOptions - Dieselben Formen wie bei den V1-Render-Optionen. pdf_options spielt nur eine Rolle, wenn outputs Pdf enthält.

perceive_direct streamt ein einzelnes Artefakt als Rohbytes zurück statt eines JSON-Envelopes. Es muss genau eine artefakterzeugende Ausgabe angefordert werden, und das SDK erzwingt das lokal mit Error::InvalidInput, bevor irgendetwas gesendet wird.

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

// Ein gespeichertes Artefakt später erneut laden. Übergib None, wenn die Operation nur eines erzeugt hat.
let saved = client
    .v2()
    .download_perceive_artifact(&direct.operation_id, Some(PerceiveOutputName::Pdf))?;
println!("{} bytes", saved.content.len());

PerceiveDirectResult trägt content, content_type, filename, operation_id, object_key, cache_hit, render_quality, source_status_code, content_hash und warnings_count, alle aus den Response-Headern gelesen. Error::Api { status: 410, .. } von download_perceive_artifact bedeutet, dass das Artefakt seine Aufbewahrungsfrist überschritten hat.

perceive_batch nimmt bis zu 1000 URLs und einen gemeinsamen Optionsblock. Kleine Batches laufen inline; größere kommen mit Status Queued zurück, frage sie also über get_perceive_batch mit der job_id ab.

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#

Zähle die URLs einer Website auf, ganz ohne Browser-Rendering. Siehe 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 ist standardmäßig Hybrid (Sitemap und Crawl sind die Alternativen), max_urls steht auf 100 (1 bis 1000), max_depth auf 2 (1 bis 5) und same_domain_only auf true. include_patterns und danach exclude_patterns filtern per Regex, jeweils maximal 50 Einträge, und respect_robots berücksichtigt robots.txt. Das Ergebnis trägt urls, total, pages_crawled, truncated, robots_respected und eine sources-Map mit den rohen Zählern je Quelle.

Lookup#

Führe eine kategorisierte Websuche aus und rendere im selben Aufruf optional die besten Treffer. Siehe 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 ist standardmäßig Web (News, Images, Scholar, Patents und Maps sind die übrigen), num_results steht auf 10 (1 bis 100), page auf 1 (1 bis 10) und autocorrect auf true. country und locale nehmen Codes wie "us" und "en", location nimmt freien Text wie "Austin, Texas", und time_filter akzeptiert Hour, Day, Week, Month oder Year. perceive_top (0 bis 10, Standard 0) rendert automatisch die obersten N Ergebnis-URLs und hängt jedem Treffer ein vollständiges PerceiveResult an. Das Ergebnis trägt außerdem answer_box, knowledge_graph und perceive_operation_ids.

Distill#

Schemagesteuerte strukturierte Extraktion: Du gibst eine Form vor und bekommst genau diese Form für jede URL zurück. Optional läuft zuerst ein CSS-Durchgang, und alles, was er verfehlt, eskaliert an die LLM-Stufe. Siehe Distill.

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

// CssField hat kein Default, weil `field_type` erforderlich ist.
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);
}

Oder ermittle zuerst die Seiten einer Website und destilliere jede gefundene Seite:

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

client.v2().distill(DistillOptions {
    discover_from: Some(DistillDiscoverFrom {
        max_pages: Some(20), // 1 bis 50, begrenzt Discovery und Distillation zugleich
        ..DistillDiscoverFrom::new("https://example.com")
    }),
    schema: json!({ "title": "page title" }).as_object().unwrap().clone(),
    ..Default::default()
})?;

Gib genau eines von beiden an: urls (maximal 50) oder discover_from. Beides oder keines von beidem liefert Error::InvalidInput, bevor eine Anfrage rausgeht. schema ist erforderlich und ist entweder ein JSON-Schema-Objekt oder eine flache {field: description}-Map. CssFieldType deckt Text, Attribute, Html, Regex, Nested, List und NestedList ab; transform akzeptiert Lowercase, Uppercase oder Strip; die Verschachtelung ist auf Tiefe 5 begrenzt.

Ingest#

Verwandle eine ganze Website oder einen Stapel hochgeladener Dokumente über eine einzige Pipeline in gechunktes, RAG-fertiges JSONL. Ingest arbeitet immer asynchron. Siehe Ingest.

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

// Von einer Website.
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()
})?;

// Oder aus hochgeladenen Dateien: PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT und MD,
// sowie ältere oder ODF-Office-Dokumente.
let files = vec!["handbook.pdf".into(), "notes.docx".into()];
client.v2().ingest_files(files, IngestFilesOptions::default())?;

// Abfragen, auflisten, abbrechen.
let status = client.v2().get_ingest_job(&job.job_id)?;
if status.status == IngestStatus::Completed {
    println!("{:?}", status.output_url); // signierte URL zum JSONL
}
client.v2().list_ingest_jobs(V2ListOptions { limit: Some(50), ..Default::default() })?;
client.v2().cancel_ingest_job(&job.job_id)?; // idempotent

mode steht standardmäßig auf Urls, was eine nicht leere urls-Liste (maximal 1000) verlangt und url ablehnt; Sitemap und Crawl verhalten sich umgekehrt und benötigen die Start-url. Diese Kopplung wird clientseitig geprüft, eine Fehlpaarung liefert also sofort Error::InvalidInput. max_pages steht standardmäßig auf 50 (1 bis 1000), max_depth auf 2 (1 bis 5), same_domain_only auf true, chunk.max_words auf 512 (32 bis 4000) und chunk.sentence_overlap auf 1 (0 bis 10). include_patterns, exclude_patterns, respect_robots, wait_for und wait_timeout_ms verhalten sich wie bei Discover und Perceive. Abschluss-Webhooks sind HMAC-signiert:

let secret = client.v2().get_webhook_secret()?;
println!("{} {} {}", secret.secret, secret.signature_header, secret.signature_scheme);
client.v2().rotate_webhook_secret()?;            // alte Signaturen gelten sofort nicht mehr
client.v2().retry_ingest_webhook("ing_abc123")?; // Webhook eines fertigen Jobs erneut zustellen

Watch#

Rendere eine URL in festem Takt erneut und lass dir sagen, was sich geändert hat. Siehe 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);

// Die Prüfhistorie lesen, neueste zuerst.
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)?;

// Pausieren. Some(String::new()) für webhook_url würde den Webhook löschen.
client.v2().update_watcher(&watcher.watcher_id, WatcherUpdate {
    status: Some(WatchUpdateStatus::Paused),
    ..Default::default()
})?;
client.v2().delete_watcher(&watcher.watcher_id)?; // Soft Delete, idempotent

frequency_minutes steht standardmäßig auf 60 und akzeptiert 60 bis 43200, die stündliche Untergrenze ist also hart. diff_mode steht standardmäßig auf Auto und akzeptiert außerdem Text, Structured, Tables und Metadata, wobei track_fields die Diff-Engine auf eine Teilmenge von Feldern oder Selektoren einschränkt. notify_email steht standardmäßig auf true und schickt dem Projektinhaber bei Änderungen eine E-Mail; webhook_url ergänzt einen HMAC-signierten Änderungs-Webhook.

update_watcher verlangt mindestens ein Feld und liefert Error::InvalidInput für ein WatcherUpdate, in dem alles None ist. Die changes-Einträge eines Snapshots enthalten nicht vertrauenswürdige Seiteninhalte, escape sie also, bevor du sie irgendwo darstellst.


PDF-Optionen#

PdfOptions wird von convert_url_to_pdf, convert_document, convert_to_pdf und der Pdf-Ausgabe von Perceive gemeinsam genutzt. Jedes Feld ist optional und wird nur gesendet, wenn es gesetzt ist.

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()
})?;
Feld Typ Beschreibung
page_size Option<String> "A4", "A3", "Letter", "Legal" und so weiter.
page_width / page_height Option<f64> Eigene Maße. Gemeinsam gesetzt überschreiben sie page_size.
orientation Option<PdfOrientation> Portrait oder Landscape.
margins Option<PdfMargins> top, bottom, left, right, jeweils optional.
scale Option<f64> Render-Skalierung, zum Beispiel 0.9 für 90 Prozent.
grayscale Option<bool> Wandelt das PDF nachträglich in Graustufen um.
header / footer Option<PdfHeaderFooter> content (max. 2000 Zeichen) und height.

Die vollständige Semantik aller Parameter findest du unter Parameter und Optionen.


Fehlerbehandlung#

Es gibt genau einen Fehlertyp, enconvert::Error. Er implementiert std::error::Error über thiserror, ? propagiert also in jedes Box<dyn Error> oder 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 Ausgelöst bei Statuscode
Error::Authentication(String) Ungültiger, fehlender oder widerrufener API-Key 401, 403
Error::Quota(String) Wird bei HTTP 402 ausgelöst 402
Error::RateLimit(String) Rate-Limit überschritten 429
Error::Api { status, message } Jede andere 4xx- oder 5xx-Antwort der tatsächliche Code
Error::UnsupportedConversion(String) Ein {input}-to-{output}-Paar, das die API nicht implementiert, clientseitig abgefangen -
Error::InvalidInput(String) Fehlerhafte Argumente, vor der Anfrage abgefangen, etwa ein leerer API-Key -
Error::Http(reqwest::Error) Verbindungs-, TLS-, Timeout- oder Antwort-Decodierungsfehler vom Transport, sofern vorhanden
Error::Io(std::io::Error) Lesen einer hochzuladenden Datei oder Schreiben eines Downloads -
Error::Json(serde_json::Error) Serialisieren eines Request-Bodys -

Vier Prädikate halten die Match-Arme kurz: is_authentication(), is_quota(), is_rate_limit() und is_server_error(). status_code() liefert Option<u16> für jede Variante, die einen Code trägt. Die vollständige Zuordnung der Meldungen steht in der Fehlercode-Referenz.


Timeout-Recovery#

Lange URL-zu-PDF-Renderings und große Dokumentkonvertierungen können ein Reverse-Proxy-Timeout überdauern, selbst wenn die Konvertierung auf dem Server erfolgreich ist. Das SDK fängt sich von allein wieder:

  1. Vor jeder Anfrage erzeugt es eine UUID und sendet sie als job_id im Body oder im Multipart-Formular.
  2. Kommt diese Anfrage als 5xx-Error::Api zurück, wechselt das SDK still auf das Abfragen von GET /v1/convert/status/{job_id} alle 3 Sekunden.
  3. Sobald der Job success meldet, gibt es das Ergebnis zurück. Sobald er failed meldet, gibt es Error::Api mit der Meldung des Servers zurück.
  4. Die Abfragefrist beträgt 5 Minuten. Danach bekommst du Error::Api { status: 504, message: "Conversion timed out" }.

Die Recovery greift bei convert_url_to_pdf, convert_url_to_screenshot, convert_url_to_markdown, convert_image, convert_document, convert_to_markdown und convert_to_pdf. Bewusst ausgenommen sind die Website-Batch-Einreichungen: Sie haben keine Job-Zeile, ein 5xx bedeutet dort also, dass die Einreichung selbst fehlgeschlagen ist, und das kommt direkt durch. Auch V2-Endpunkte antworten direkt und haben keinen Job-Fallback. Antworten ohne job_id bekommen die clientseitig erzeugte nachgetragen, result.job_id ist also immer etwas, das du später an get_job_status übergeben kannst.


Konfiguration#

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"), // Basis-URL überschreiben
    Some(Duration::from_secs(300)),    // Anfrage-Timeout
)?;
println!("enconvert {}", enconvert::VERSION);
Argument Typ Standard Beschreibung
api_key impl Into<String> erforderlich Privater API-Key. Ein leerer String liefert Error::InvalidInput, ebenso ein Key mit ungültigen Header-Zeichen.
base_url Option<&str> https://api.enconvert.com Abschließende Schrägstriche werden entfernt.
timeout Option<Duration> 300 Sekunden Gilt für die gesamte Anfrage.

Enconvert::new(api_key) ist die Kurzform von with_options(api_key, None, None).

Schreibe den API-Key niemals fest in den Code. Lies ihn aus einer Umgebungsvariable oder einem Secret-Manager. Das SDK markiert den Wert des X-API-Key-Headers als sensibel, damit er nicht in Debug-Ausgaben landet, und lädt signierte URLs über einen zweiten, nicht authentifizierten HTTP-Client herunter, sodass dein Key nie an den Objektspeicher geht. Keys erzeugst und rotierst du im Dashboard, und die Key-Typen erklärt die Authentifizierung.

Ergebnisform#

Jede Einzeldatei-Konvertierungsmethode liefert ein 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>,
}

Lade die Datei selbst von presigned_url herunter, oder übergib save_to und lass sie das SDK für dich schreiben. Signierte URLs laufen ab, speichere also alles, was du behalten willst, in deinem eigenen Bucket.

Die asynchronen Pfade liefern eigene Formen. JobStatus trägt status (Processing, Success, Failed oder Unknown(String)), presigned_url, object_key und error. BatchSubmission trägt batch_id, status, url_count, total_discovered, discovery_method und output_format. BatchStatus ergänzt die Zähler total, completed, failed und in_progress sowie output_mode, zip_download_url und ein Vec<BatchItem> mit einer Zeile pro URL.

V2-Lesevorgänge liefern PerceiveResult, dessen outputs-Map nach Ausgabenamen ("markdown", "screenshot_full_page" und so weiter) mit Werten vom Typ V2OutputArtifact { url, object_key, size_bytes, content_type, expires_in } geschlüsselt ist. Diese Artefakt-URLs sind 15 Minuten lang signiert und werden bei jedem get_perceive_operation-Aufruf neu signiert. Daneben stehen render_quality, status_code, deductions, cache_hit, structured, extraction_tier, tokens, cost_cents, duration_ms, warnings und options_echo. Jedes Antwort-Enum trägt eine Unknown(String)-Variante, sodass ein Statuswert, der serverseitig nach dem Build deiner Anwendung hinzukam, sauber geparst wird statt zu scheitern.


Quelle und Issues#


Häufig gestellte Fragen#

Wie konvertiere ich Dateien in Rust mit einem crates.io-Paket?#

Führe cargo add enconvert aus, baue einen Client mit Enconvert::new(api_key)? und rufe eine Methode wie convert_url_to_pdf, convert_image oder convert_document auf. Jedes Options-Struct leitet Default ab, du schreibst also ein Struct-Literal mit ..Default::default() und setzt nur die Felder, die dich interessieren. Übergib save_to, damit das SDK die Ausgabe direkt auf die Platte schreibt.

Braucht das Rust SDK tokio oder eine Async-Runtime?#

Nein. Es basiert auf der Blocking-API von reqwest, jede Methode blockiert also den aufrufenden Thread, und es gibt kein .await, keinen Executor und kein tokio in deinem Abhängigkeitsbaum. Ist deine Anwendung bereits async, rufe das SDK aus tokio::task::spawn_blocking auf statt direkt aus einem Reaktor-Thread, denn der blockierende Client von reqwest kann nicht innerhalb eines Async-Runtime-Kontexts laufen.

Wie konvertiere ich eine URL in Rust in ein PDF?#

Rufe convert_url_to_pdf("https://example.com", UrlToPdfOptions { save_to: Some("page.pdf".into()), ..Default::default() }) auf. Setze single_page: Some(false), um zu paginieren statt eine einzige durchgehende Seite zu erzeugen, und übergib pdf_options für Seitengröße, Ausrichtung, Ränder, Skalierung, Graustufen, Kopf- und Fußzeilen.

Wie konvertiere ich DOCX in Rust nach PDF?#

Rufe convert_document("report.docx", ConvertDocumentOptions { save_to: Some("report.pdf".into()), ..Default::default() }) auf. PDF ist die Standardausgabe, output_format kann also ungesetzt bleiben. Dieselbe Methode verarbeitet XLSX, PPTX, ODT, ODS, ODP, OTS, Pages, Numbers, HTML und Markdown nach PDF sowie Datenformatpaare wie JSON zu YAML und CSV zu XML.

Wie konvertiere ich HEIC in Rust nach WebP?#

Rufe convert_image("photo.heic", ConvertImageOptions { output_format: "webp".to_string(), ..Default::default() }) auf. Das Eingabeformat ergibt sich aus der Dateiendung, und alle 20 geordneten Paare unter jpeg, png, svg, heic und webp sind implementiert, dazu pdf zu jpeg für die Rasterung. Ein nicht unterstütztes Paar liefert Error::UnsupportedConversion, bevor irgendein Netzwerkaufruf stattfindet.

Wie hole ich eine Webseite aus Rust als sauberes Markdown?#

Auf zwei Wegen. convert_url_to_markdown liefert eine einzelne Markdown-Datei mit YAML-Frontmatter. client.v2().perceive(url, PerceiveOptions { outputs: Some(vec![PerceiveOutputName::Markdown]), ..Default::default() }) liefert denselben Inhalt mit einem render_quality-Wert, benannten Abzügen, Warnungen und optional Screenshots, Links oder strukturierter Extraktion aus demselben Rendering. Nimm Perceive, wenn ein Agent das Ergebnis lesen wird.

Woran erkenne ich, ob eine Seite tatsächlich gerendert hat?#

Lies render_quality, einen Wert von 0.0 bis 1.0, der bei jedem V2-Lesevorgang vorhanden ist. Ein niedriger Wert bedeutet, dass das Rendering nicht sauber war: eine Challenge-Seite, eine Cookie- oder Login-Wall, eine leere SPA-Hülle oder ein HTTP-Fehler. Die deductions-Map benennt jeden ausgelösten Abzug, status_code liefert den HTTP-Status der Gegenstelle, und warnings listet auf, was schiefgelaufen ist. Der Inhalt wird trotzdem zurückgegeben, nur eben markiert.

Was passiert, wenn eine Konvertierung länger dauert als das Proxy-Timeout?#

Das SDK regelt das. Jede Anfrage trägt eine clientseitig erzeugte job_id; antwortet die Anfrage mit 5xx, fragt das SDK GET /v1/convert/status/{job_id} alle 3 Sekunden für bis zu 5 Minuten ab und gibt das fertige Ergebnis zurück, als wäre nichts gewesen. Nach Ablauf der Frist bekommst du Error::Api { status: 504, message: "Conversion timed out" }. Die Ausnahme sind Batch-Einreichungen für ganze Websites, dafür ist wait_for_batch das richtige Werkzeug.

Kann ich das Rust SDK aus einem Browser oder einem WASM-Target nutzen?#

Nein. Es authentifiziert sich mit einem privaten API-Key, der niemals an einen Client ausgeliefert werden darf, und es hängt am blockierenden Client von reqwest mit nativem TLS und Threads, die es beide auf wasm32-unknown-unknown nicht gibt. Betreibe es auf einem Server, in einem CLI oder in einem Worker, und lass dein Frontend mit deinem eigenen Backend sprechen.