Swift SDK für Dateikonvertierung#
Enconvert ist der offizielle EnConvert-Client für Swift, verteilt über den Swift Package Manager. Er basiert auf URLSession mit async/await, hat null externe Abhängigkeiten und zielt auf Swift 5.9 und neuer unter macOS 12, iOS 15, tvOS 15 und watchOS 8. Zwölf Methoden am Client decken Datei-Konvertierung und URL-Rendering ab (DOCX zu PDF, HEIC zu WebP, URL zu PDF, URL zu Markdown, Batches für ganze Websites), und der Namespace client.v2 ergänzt dreiundzwanzig Web-Intelligence-Methoden für Perceive, Discover, Lookup, Distill, Ingest und Watch.
Enconvert · Quelle: conversionapi/swift-sdk · Swift: 5.9+ · Plattformen: macOS 12+, iOS 15+, tvOS 15+, watchOS 8+ · Abhängigkeiten: keine
Installation#
Füge das Paket hinzu und liste danach das Produkt in dem Target auf, das es nutzt:
dependencies: [
.package(url: "https://github.com/conversionapi/swift-sdk.git", from: "0.0.1")
],
targets: [
.target(name: "MyApp", dependencies: [
.product(name: "Enconvert", package: "swift-sdk")
])
]
In Xcode nutzt du File > Add Package Dependencies und fügst https://github.com/conversionapi/swift-sdk.git ein. Unter Linux importiert das SDK FoundationNetworking bedingt, du musst also nichts zusätzlich tun.
Schnellstart#
import Enconvert
let apiKey = ProcessInfo.processInfo.environment["ENCONVERT_API_KEY"] ?? ""
let client = try Enconvert(apiKey: apiKey)
let result = try await client.convertUrlToPdf("https://example.com", options: UrlToPdfOptions(saveTo: "page.pdf"))
print(result.filename, result.presignedUrl)
Enconvert.init wirft, es ist kein Failable Initializer: Ein leerer apiKey löst EnconvertError.invalidArgument aus, bevor irgendetwas das Netzwerk berührt. Jede Anfragemethode ist async throws, und die Konvertierungsmethoden sind mit @discardableResult markiert, sodass ein Aufruf, der nur wegen des saveTo-Nebeneffekts erfolgt, keine Warnung erzeugt.
Was der Client bereitstellt#
Zwölf Methoden hängen an Enconvert und bilden REST-Endpunkte 1:1 ab:
| Methode | Endpunkt | Rückgabe |
|---|---|---|
convertUrlToPdf(_:options:) |
POST /v1/convert/url-to-pdf |
ConversionResult |
convertUrlToScreenshot(_:options:) |
POST /v1/convert/url-to-screenshot |
ConversionResult |
convertUrlToMarkdown(_:options:) |
POST /v1/convert/url-to-markdown |
ConversionResult |
convertImage(_:options:) |
POST /v1/convert/{from}-to-{to} |
ConversionResult |
convertDocument(_:options:) |
POST /v1/convert/{from}-to-{to} |
ConversionResult |
convertToMarkdown(_:options:) |
POST /v1/convert/anything-to-markdown |
ConversionResult |
convertToPdf(_:options:) |
POST /v1/convert/anything-to-pdf |
ConversionResult |
convertWebsiteToPdf(_:options:) |
POST /v1/convert/website-to-pdf |
BatchSubmission |
convertWebsiteToScreenshot(_:options:) |
POST /v1/convert/website-to-screenshot |
BatchSubmission |
getJobStatus(_:) |
GET /v1/convert/status/{jobId} |
JobStatus |
getBatchStatus(_:) |
GET /v1/convert/batch/{batchId} |
BatchStatus |
waitForBatch(_:options:) |
GET /v1/convert/batch/{batchId} (mit Polling) |
BatchStatus |
client.v2 ist ein EnconvertV2-Namespace mit dreiundzwanzig weiteren Methoden in sechs Fähigkeitsgruppen:
| Gruppe | Methoden | Basispfad |
|---|---|---|
| Perceive | perceive, perceiveDirect, getPerceiveOperation, downloadPerceiveArtifact, perceiveBatch, getPerceiveBatch |
/v2/perceive |
| Discover | discover |
/v2/discover |
| Lookup | lookup |
/v2/lookup |
| Distill | distill |
/v2/distill |
| Ingest | ingest, ingestFiles, getIngestJob, listIngestJobs, cancelIngestJob, retryIngestWebhook, getWebhookSecret, rotateWebhookSecret |
/v2/ingest |
| Watch | createWatcher, listWatchers, getWatcher, getWatcherSnapshots, updateWatcher, deleteWatcher |
/v2/watch |
Optionen werden als Struct mit vorbelegten Initializer-Parametern übergeben, UrlToPdfOptions() heißt also "alle Standardwerte", und du benennst nur die Felder, die dich interessieren. Swift verlangt benannte Argumente in Deklarationsreihenfolge, halte saveTo: also vor singlePage: und pdfOptions:, wenn du mehrere auf einmal setzt.
Datei-Konvertierung#
Uploads nehmen ein FileInput entgegen:
| Case | Wofür du ihn nutzt |
|---|---|
.path("report.docx") |
Eine Datei auf der Platte. Der Basisname bestimmt Eingabeformat und MIME-Typ. |
.data(bytes) |
Rohbytes ohne Namen. Werden als upload.bin, application/octet-stream hochgeladen. |
.wrapped(data: bytes, filename: "report.docx", contentType: nil) |
Rohbytes plus einen expliziten Dateinamen. Ein contentType von nil wird aus der Endung abgeleitet. |
convertUrlToPdf#
Rendere jede öffentliche URL zu einem PDF.
let result = try await client.convertUrlToPdf("https://example.com", options: UrlToPdfOptions(
viewportWidth: 1440,
saveTo: "report.pdf",
singlePage: false,
pdfOptions: PdfOptions(pageSize: "A4", orientation: .landscape)
))
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
viewportWidth, viewportHeight |
Int? |
1920, 1080 |
Größe des Browser-Viewports in Pixeln. |
loadMedia, enableScroll |
Bool? |
true |
Auf Bilder und Videos warten; von oben nach unten scrollen, damit Lazy Loader auslösen. |
outputFilename |
String? |
automatisch | Überschreibt den generierten Dateinamen. |
auth, cookies, headers |
HttpBasicAuth?, [BrowserCookie]?, [String: String]? |
keine | HTTP-Basic-Zugangsdaten, eingeschleuste Cookies (max. 50), zusätzliche Request-Header (max. 20, Hop-by-Hop wird abgelehnt). |
saveTo |
String? |
keiner | Lokaler Pfad, unter dem das PDF geschrieben wird. Übergeordnete Verzeichnisse werden angelegt. |
singlePage |
Bool? |
true |
true erzeugt eine einzige durchgehende Seite. false paginiert anhand von pdfOptions.pageSize. |
pdfOptions |
PdfOptions? |
keine | Seitengeometrie. Siehe PDF-Optionen. |
Seiten hinter einem Login nehmen Zugangsdaten, Cookies oder Header entgegen:
_ = try await client.convertUrlToPdf("https://internal.example.com/report", options: UrlToPdfOptions(
auth: HttpBasicAuth(username: "user", password: "pass"),
cookies: [BrowserCookie(name: "session", value: "abc123", domain: "internal.example.com")],
headers: ["X-Tenant": "acme"],
saveTo: "report.pdf"
))
Kombiniere auth nicht mit einem Authorization-Eintrag in headers. Die API weist den Konflikt zurück.
convertUrlToScreenshot#
Nimm ein PNG von einer beliebigen URL auf.
let shot = try await client.convertUrlToScreenshot(
"https://example.com",
options: UrlToScreenshotOptions(viewportWidth: 1440, saveTo: "shot.png")
)
UrlToScreenshotOptions akzeptiert dieselben Felder für Viewport, Medien, Scrollen, Dateiname und Browser-Zugriff wie UrlToPdfOptions, nur ohne singlePage und pdfOptions.
convertUrlToMarkdown#
Extrahiere sauberes GitHub-Flavored Markdown aus einer URL. Der Konverter entfernt Navigation, Fußzeilen, Werbung und Skripte, behält den eigentlichen Artikeltext und stellt ein YAML-Frontmatter mit Titel, Beschreibung, URL, Links und Bildern voran. Nützlich für RAG-Pipelines, den Import fremder Inhalte in ein CMS oder das Erzeugen von Trainingsdaten.
_ = try await client.convertUrlToMarkdown("https://example.com/article", options: UrlToMarkdownOptions(saveTo: "article.md"))
convertImage#
Konvertiere zwischen jpeg, png, svg, heic und webp, oder rastere ein PDF nach JPEG.
let result = try await client.convertImage(.path("photo.heic"), options: ConvertImageOptions(outputFormat: "webp", saveTo: "photo.webp"))
Das Eingabeformat ergibt sich aus der Dateiendung (.jpg, .jpeg, .png, .svg, .heic, .webp sowie .pdf für die Rasterung). outputFormat ist erforderlich und akzeptiert die Aliasse jpg, yml, htm und md.
| Option | Typ | Erforderlich | Beschreibung |
|---|---|---|---|
outputFormat |
String |
Ja | Zielformat, zum Beispiel "webp". |
saveTo |
String? |
nein | Lokaler Pfad, unter dem das Ergebnis geschrieben wird. |
outputFilename |
String? |
nein | Überschreibt den generierten Dateinamen. |
convertDocument#
Konvertiere Dokumente und strukturierte Textformate. outputFormat steht standardmäßig auf "pdf".
// docx zu pdf
_ = try await client.convertDocument(.path("report.docx"), options: ConvertDocumentOptions(saveTo: "report.pdf"))
// json zu yaml
_ = try await client.convertDocument(.path("data.json"), options: ConvertDocumentOptions(outputFormat: "yaml", saveTo: "data.yaml"))
// markdown zu pdf mit eigener Seiteneinrichtung
_ = try await client.convertDocument(.path("README.md"), options: ConvertDocumentOptions(
saveTo: "readme.pdf",
pdfOptions: PdfOptions(pageSize: "A4", margins: PdfMargins(top: 20, bottom: 20))
))
Unterstützte Eingaben: .doc, .docx, .xls, .xlsx, .ppt, .pptx, .html, .htm, .odt, .ods, .odp, .ots, .pages, .numbers, .md, .markdown, .csv, .json, .xml, .yaml, .yml, .toml. EPUB hat kein eigenes Dokumentpaar, schicke .epub-Dateien also stattdessen durch convertToPdf oder convertToMarkdown.
Das SDK bringt die vollständige Konvertierungstabelle des Gateways mit und prüft jedes {input}-to-{output}-Paar lokal. Ein nicht unterstütztes Paar wirft also EnconvertError.invalidArgument samt Liste der gültigen Ausgaben, statt für einen Roundtrip zu zahlen, der garantiert scheitert. Es gibt 43 implementierte Paare:
| Eingabe | Ausgaben |
|---|---|
json |
csv, toml, xml, yaml |
xml |
csv, json |
yaml |
json |
csv |
json, xml |
toml |
json |
markdown |
html, pdf |
html |
pdf |
doc, excel, ppt, odt, ods, odp, ots, pages, numbers |
pdf |
jpeg, png, svg, heic, webp |
jeweils in die anderen vier (20 geordnete Paare) |
pdf |
jpeg |
Diese Tabelle kannst du auch selbst abfragen, ohne eine Anfrage zu stellen:
validOutputsFor("json") // ["csv", "toml", "xml", "yaml"]
IMPLEMENTED_CONVERSIONS.contains("heic-to-webp") // true
convertToMarkdown#
Konvertiere ein hochgeladenes Dokument nahezu beliebigen Formats in sauberes Markdown, wobei das Format serverseitig automatisch erkannt wird. Eine gute erste Stufe für eine RAG-Pipeline.
_ = try await client.convertToMarkdown(.path("handbook.docx"), options: ConvertToMarkdownOptions(saveTo: "handbook.md"))
Akzeptiert werden PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT und MD sowie ältere oder ODF-Office-Dateien. Bilder nicht. Dieser Endpunkt kennt keine PDF-Optionen.
convertToPdf#
Konvertiere eine hochgeladene Datei nahezu beliebigen Formats nach PDF: Office, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, reiner Text, Rasterbilder, SVG, EPUB oder ein bestehendes PDF, das durchgereicht und normalisiert wird.
_ = try await client.convertToPdf(.path("slides.pptx"), options: ConvertToPdfOptions(saveTo: "slides.pdf"))
// PDF durchgereicht und in Graustufen umgewandelt
_ = try await client.convertToPdf(
.path("scan.pdf"),
options: ConvertToPdfOptions(saveTo: "scan-gray.pdf", pdfOptions: PdfOptions(grayscale: true))
)
grayscale berücksichtigt. convertToPdf reicht pdfOptions weiter, aber der Anything-to-PDF-Endpunkt liest nur grayscale und ignoriert den Rest. Nutze convertDocument oder convertUrlToPdf, wenn du Seitengröße, Ausrichtung, Ränder, Skalierung, Kopf- oder Fußzeilen brauchst.
convertWebsiteToPdf und convertWebsiteToScreenshot#
Ermittle jede Seite einer Website, konvertiere jede einzelne im Hintergrund und sammle alles in einem ZIP. Beide Methoden arbeiten ausschließlich asynchron und verlangen einen privaten API-Key mit Crawl-Zugriff.
let batch = try await client.convertWebsiteToPdf("https://example.com", options: WebsiteToPdfOptions(
crawlMode: .sitemap,
excludePatterns: ["/blog/tag/"]
))
print(batch.batchId, batch.urlCount, batch.discoveryMethod ?? "")
// Blockieren, bis der Batch durch ist, und das ZIP speichern
let status = try await client.waitForBatch(batch.batchId, options: WaitForBatchOptions(saveTo: "site.zip"))
print("\(status.completed) of \(status.total) pages converted")
// Oder selbst abfragen
let snapshot = try await client.getBatchStatus(batch.batchId)
if snapshot.status != .processing {
print(snapshot.zipDownloadUrl ?? "")
}
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
crawlMode |
CrawlMode? |
.auto |
.auto, .sitemap (nur sitemap.xml) oder .full (Sitemap plus BFS-Crawl). |
includePatterns, excludePatterns |
[String]? |
keine | Positivliste und danach Sperrliste für gefundene URLs. Nur im Full-Crawl-Modus. |
notificationEmail |
String? |
Projektinhaber | Adresse, die benachrichtigt wird, wenn der Batch fertig ist. |
callbackUrl |
String? |
keine | Webhook, der per POST aufgerufen wird, wenn der Batch fertig ist. |
singlePage, pdfOptions |
Bool?, PdfOptions? |
siehe oben | Nur für PDF-Batches. |
Beide Methoden nehmen außerdem die unter convertUrlToPdf gelisteten Felder für Viewport, Medien, Scrollen und Browser-Zugriff entgegen und wenden sie auf jede Seite an. waitForBatch fragt standardmäßig alle 5 Sekunden mit einer Frist von 30 Minuten ab; überschreiben kannst du das mit WaitForBatchOptions(intervalMs:timeoutMs:saveTo:). Wird die Frist überschritten, wirft es EnconvertError.api(statusCode: 504, ...). convertWebsiteToScreenshot verhält sich identisch und erzeugt ein ZIP mit PNGs.
Web-Intelligence (V2)#
Jeder V2-Lesevorgang trägt einen renderQuality-Wert von 0.0 bis 1.0, bereitgestellt als Double? auf PerceiveResult, PerceiveDirectResult, DistillItem und WatcherSnapshot. Ein niedriger Wert bedeutet, dass die Seite nicht ehrlich gerendert hat: eine Bot-Challenge, eine Login-Wall, ein Cookie-Banner über einer leeren SPA-Hülle, ein HTTP-Fehlerstatus. Der Inhalt kommt trotzdem zurück, markiert, neben einem deductions-Dictionary, das jeden ausgelösten Abzug benennt, und einem warnings-Array, sodass ein schlechter Lesevorgang nie unbemerkt in den Kontext deines Agenten rutscht. Prüfe den Wert, bevor du irgendetwas vertraust:
if let quality = op.renderQuality, quality < 0.6 {
print("low quality read of \(op.url): \(op.deductions)")
}
Perceive#
Rendere eine URL in genau die Artefakte, die du anforderst. Synchron, mit signierten Artefakt-URLs, die 15 Minuten gültig sind. Jede V2-Methode braucht einen privaten API-Key; öffentliche Keys werden abgelehnt.
let op = try await client.v2.perceive("https://example.com", options: PerceiveOptions(
outputs: [.markdown, .screenshot, .structured],
extract: [.tables, .metadata],
viewport: PerceiveViewport(width: 1440)
))
print(op.operationId, op.renderQuality ?? 0, op.outputs["markdown"]?.url ?? "")
print(op.structured ?? [:], op.extractionTier ?? .heuristic)
// Die Artefakt-URLs später neu signieren
let again = try await client.v2.getPerceiveOperation(op.operationId)
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
outputs |
[PerceiveOutputName]? |
[.markdown, .structured] |
.markdown, .htmlCleaned, .htmlRaw, .screenshot, .screenshotFullPage, .pdf, .links, .images, .structured. |
extract |
[PerceiveExtractName]? |
keine | .tables, .prices, .contacts, .metadata, .mainContent, .headings, .structuredData, .technologies, .all. |
schema |
JSONObject? |
keines | JSON-Schema für die strukturierte Extraktion über die LLM-Stufe. |
waitFor, waitTimeoutMs |
String?, Int? |
keiner, 30000 |
Ein CSS-Selektor (optional mit Präfix css:) oder js:<expr>, auf den gewartet wird, und dessen Budget in ms (0 bis 60000). |
jsCode |
String? |
keiner | JavaScript, das nach der Navigation ausgeführt wird, max. 20000 Zeichen. |
viewport |
PerceiveViewport? |
1920 mal 1080 | width 320 bis 3840, height 240 bis 2160. |
headers, cookies, auth |
[String: String]?, [BrowserCookie]?, HttpBasicAuth? |
keine | Request-Header, eingeschleuste Cookies, HTTP-Basic-Zugangsdaten. |
cacheMode |
PerceiveCacheMode? |
.enabled |
.enabled (1 Stunde Cache), .bypass, .refresh. |
pdfOptions |
PdfOptions? |
keine | Nur relevant, wenn outputs .pdf enthält. |
blockResources |
[PerceiveResourceType]? |
keine | .image, .media, .font, .stylesheet, .script, .xhr, .fetch, .websocket, .manifest, .other. |
respectRobots, mobile |
Bool? |
Server-Standard | robots.txt beachten; ein Mobilgerät emulieren. |
onlyMainContent |
Bool? |
true |
Entfernt Navigation, Header, Footer und Cookie-Banner aus dem Markdown-Artefakt und dem main_content-Extrakt. Setze false für die ganze Seite. |
directDownload |
Bool? |
false |
Streamt Rohbytes statt eines JSON-Envelopes. Bevorzuge perceiveDirect. |
proxyUrl, geolocation und actionChain existieren auf PerceiveOptions und werden auch übertragen, der Server antwortet aber derzeit für alle drei mit 422. Lass sie auf nil.
Ein einzelnes Artefakt direkt auf die Platte zu streamen, spart den JSON-Envelope und den Umweg über die signierte URL. perceiveDirect prüft lokal, dass du genau eine artefakterzeugende Ausgabe angefordert hast, ein Fehler kostet also nichts:
let direct = try await client.v2.perceiveDirect("https://example.com", options: PerceiveOptions(outputs: [.pdf]))
try direct.content.write(to: URL(fileURLWithPath: direct.filename ?? "page.pdf"))
// Ein gespeichertes Artefakt später erneut laden. Übergib nil, wenn die Operation nur eines erzeugt hat.
let saved = try await client.v2.downloadPerceiveArtifact(direct.operationId, output: .pdf)
PerceiveDirectResult trägt content, contentType, filename, operationId, objectKey, cacheHit, renderQuality, sourceStatusCode, contentHash und warningsCount, alle aus den Response-Headern gelesen. Ein 410 von downloadPerceiveArtifact bedeutet, dass das Artefakt aus seiner Aufbewahrungsfrist herausgefallen ist.
Batches nehmen bis zu 1000 URLs mit einem gemeinsamen Optionsblock. Kleine Batches laufen inline durch; größere kommen in der Warteschlange zurück, und du fragst sie ab:
let batch = try await client.v2.perceiveBatch(
["https://a.example", "https://b.example"],
options: PerceiveBatchOptions(outputs: [.markdown], outputMode: .zip)
)
let done = try await client.v2.getPerceiveBatch(batch.jobId)
if done.status == .completed, let zip = done.zip {
print(zip.url ?? "")
}
outputMode ist .manifest (Standard) oder .zip. directDownload wird bei Batches mit 422 abgelehnt.
Discover#
Zähle die URLs einer Website ohne Browser-Rendering auf. Schnell, und es läuft nie ein Rendering.
let opts = DiscoverOptions(mode: .hybrid, maxUrls: 200, excludePatterns: ["/tag/"])
let found = try await client.v2.discover("https://example.com", options: opts)
print(found.total, found.truncated, found.sources, found.urls)
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
mode |
DiscoverMode? |
.hybrid |
.sitemap, .crawl oder .hybrid (Sitemap plus HTTP-Crawl). |
maxUrls, maxDepth |
Int? |
100, 2 |
1 bis 1000 URLs; Crawl-Tiefe 1 bis 5. |
includePatterns, excludePatterns |
[String]? |
keine | Regex-Positivliste, danach die Sperrliste. Jeweils max. 50 Muster. |
sameDomainOnly |
Bool? |
true |
Auf der Domain der Start-URL bleiben. |
respectRobots |
Bool? |
Server-Standard | robots.txt beachten. |
DiscoverResult meldet außerdem pagesCrawled, robotsRespected und warnings, und sources enthält die rohen Zähler je Quelle, erfasst vor der Deduplizierung.
Lookup#
Führe eine kategorisierte Websuche aus und rendere im selben Aufruf optional die besten Treffer.
let search = try await client.v2.lookup(
"best static site generators",
options: LookupOptions(category: .web, numResults: 10, perceiveTop: 3)
)
for hit in search.results {
print(hit.position ?? 0, hit.title ?? "", hit.url ?? "", hit.perceive?.renderQuality ?? 0)
}
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
category |
LookupCategory? |
.web |
.web, .news, .images, .scholar, .patents, .maps. |
country, locale |
String? |
keine | Google-Ländercode gl ("us", "in") und Oberflächensprache hl ("en"). |
timeFilter |
LookupTimeFilter? |
keiner | .hour, .day, .week, .month, .year. |
numResults, page |
Int? |
10, 1 |
1 bis 100 Ergebnisse; Seite 1 bis 10. |
location, autocorrect |
String?, Bool? |
keiner, true |
Ort als freier Text wie "Austin, Texas"; den Anbieter die Anfrage korrigieren lassen. |
perceiveTop |
Int? |
0 |
Rendert die obersten N Ergebnis-URLs automatisch, 0 bis 10. Jede davon startet ein vollständiges Browser-Rendering. |
LookupResult stellt außerdem answerBox, knowledgeGraph, perceiveOperationIds und credits bereit.
Distill#
Hole strukturierte Daten aus Seiten heraus, gegen ein Schema, das du selbst definierst.
let extraction = try await client.v2.distill(DistillOptions(
urls: ["https://example.com/pricing"],
schema: ["plans": .string("list of plan names with monthly prices")],
cssSchema: CssSchema(baseSelector: ".plan-card", fields: [
CssField(name: "name", type: .text, selector: "h3"),
CssField(name: "price", type: .text, selector: ".price")
])
))
let first = extraction.results[0]
print(first.data ?? [:], first.extractionTier, first.fieldsFromCss, first.fieldsFromLlm)
schema ist ein JSONObject, also ein [String: JSONValue], es funktioniert also sowohl eine flache {field: description}-Map als auch ein vollständiges JSON-Schema-Objekt. Das optionale cssSchema läuft zuerst und beantwortet alles, was einfache Selektoren erreichen; nur die Felder, die es verfehlt, eskalieren an die LLM-Stufe, und extractionTier meldet, welche Stufen tatsächlich geantwortet haben (.css, .llm, .mixed oder .none). CssField.type ist eines von .text, .attribute, .html, .regex, .nested, .list oder .nestedList, verschachtelt bis zu 5 Ebenen tief.
Tausche urls gegen discoverFrom, um in einem Aufruf erst zu ermitteln und dann zu destillieren. DistillDiscoverFrom nimmt url, mode (Standard .hybrid) und maxPages (1 bis 50, Standard 10, begrenzt Discovery und Distillation zugleich):
_ = try await client.v2.distill(DistillOptions(
discoverFrom: DistillDiscoverFrom(url: "https://example.com", mode: .sitemap, maxPages: 10),
schema: ["title": .string("page title"), "summary": .string("one-line summary")]
))
Gibst du sowohl urls als auch discoverFrom an oder keines von beidem, wirft es EnconvertError.invalidArgument, bevor eine Anfrage rausgeht.
Ingest#
Verwandle eine Website, eine URL-Liste oder einen Stapel hochgeladener Dokumente in gechunktes, RAG-fertiges JSONL. Immer asynchron.
let job = try await client.v2.ingest(IngestOptions(
mode: .sitemap,
url: "https://docs.example.com",
maxPages: 100,
chunk: IngestChunkOptions(maxWords: 512, sentenceOverlap: 1),
webhookUrl: "https://my.app/hooks/enconvert"
))
let status = try await client.v2.getIngestJob(job.jobId)
if status.status == .completed {
print(status.totalChunks, status.outputUrl ?? "")
}
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
mode |
IngestMode? |
.urls |
.urls, .sitemap oder .crawl. Der vierte Fall, .files, ist das, was ingestFiles an seinem Job zurückmeldet; übergib ihn hier nicht. |
url |
String? |
keine | Start-URL. Erforderlich für .sitemap und .crawl, verboten für .urls. |
urls |
[String]? |
keine | Explizite URLs, max. 1000. Erforderlich für .urls, sonst verboten. |
maxPages, maxDepth |
Int? |
50, 2 |
Discovery-Obergrenze für .sitemap und .crawl, 1 bis 1000; Tiefe 1 bis 5. |
sameDomainOnly |
Bool? |
true |
Auf der Domain der Start-URL bleiben. |
includePatterns, excludePatterns |
[String]? |
keine | Regex-Positivliste, danach die Sperrliste. |
respectRobots |
Bool? |
Server-Standard | robots.txt beachten. |
waitFor, waitTimeoutMs |
String?, Int? |
30000 ms |
Selektor oder js:-Ausdruck, auf den pro Seite gewartet wird, und dessen Budget (0 bis 60000). |
chunk |
IngestChunkOptions? |
keine | maxWords 32 bis 4000, Standard 512. sentenceOverlap 0 bis 10, Standard 1. |
webhookUrl |
String? |
keine | Abschluss-Webhook, HMAC-signiert. |
Die obigen Regeln zu Modus und URL werden clientseitig erzwungen: ingest wirft EnconvertError.invalidArgument, statt eine aussichtslose Anfrage zu stellen, wenn du urls mit mode: .sitemap übergibst. Hochgeladene Dateien laufen durch dieselbe Pipeline und denselben Job-Lebenszyklus:
let files: [FileInput] = [.path("handbook.pdf"), .path("notes.docx")]
let fileJob = try await client.v2.ingestFiles(files, options: IngestFilesOptions(chunk: IngestChunkOptions(maxWords: 512)))
Akzeptiert werden PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT und MD sowie ältere oder ODF-Office-Dateien, und mindestens eine Datei ist erforderlich. Job-Verwaltung und Webhook-Infrastruktur:
let list = try await client.v2.listIngestJobs(V2ListOptions(limit: 20))
let canceled = try await client.v2.cancelIngestJob(job.jobId) // idempotent
let secret = try await client.v2.getWebhookSecret()
print(secret.signatureHeader, secret.signatureScheme, secret.replayToleranceSeconds)
_ = try await client.v2.rotateWebhookSecret() // alte Signaturen gelten sofort nicht mehr
let retry = try await client.v2.retryIngestWebhook(job.jobId)
print(retry.delivered, retry.attempts, retry.detail)
retryIngestWebhook antwortet mit 409, wenn der Job nicht abgeschlossen ist, und mit 400, wenn kein Webhook konfiguriert ist. V2ListOptions nimmt skip und limit (1 bis 100, Standard 20).
Watch#
Rendere eine Seite in festem Takt erneut und lass dich benachrichtigen, wenn sie sich ändert.
let watcher = try await client.v2.createWatcher("https://example.com/pricing", options: WatchCreateOptions(
frequencyMinutes: 60,
diffMode: .auto,
webhookUrl: "https://my.app/hooks/changes",
notifyEmail: true
))
let history = try await client.v2.getWatcherSnapshots(watcher.watcherId, options: SnapshotListOptions(limit: 10))
for snapshot in history.snapshots where snapshot.hasChanges {
print(snapshot.checkedAt, snapshot.changeCount, snapshot.similarity ?? 0)
}
| Option | Typ | Standard | Beschreibung |
|---|---|---|---|
frequencyMinutes |
Int? |
60 |
60 bis 43200. Die stündliche Untergrenze ist hart. |
diffMode |
WatchDiffMode? |
.auto |
.auto, .text, .structured, .tables, .metadata. |
trackFields |
JSONObject? |
keine | Teilmenge von Feldern oder Selektoren, die an die Diff-Engine übergeben wird. |
webhookUrl |
String? |
keiner | Webhook für Änderungsbenachrichtigungen, HMAC-signiert. |
notifyEmail |
Bool? |
true |
Schickt dem Projektinhaber bei Änderungen eine E-Mail. |
// Ein leerer String löscht den Webhook; nil lässt ihn unangetastet.
_ = try await client.v2.updateWatcher(watcher.watcherId, updates: WatcherUpdate(webhookUrl: "", status: .paused))
_ = try await client.v2.listWatchers()
_ = try await client.v2.getWatcher(watcher.watcherId)
_ = try await client.v2.deleteWatcher(watcher.watcherId) // Soft Delete, idempotent
updateWatcher verlangt mindestens ein Feld und wirft EnconvertError.invalidArgument bei einem leeren WatcherUpdate. WatchUpdateStatus akzeptiert nur .active oder .paused; das Löschen läuft über deleteWatcher, das den auf tot gesetzten Watcher mit Status .deleted zurückgibt.
WatcherSnapshot.changes ist ein Array roher JSON-Objekte, die von der beobachteten Seite stammen. Escape die Werte, bevor du sie irgendwo renderst.
PDF-Optionen#
PdfOptions wird von convertUrlToPdf, convertDocument, convertWebsiteToPdf, PerceiveOptions und (nur für grayscale) convertToPdf gemeinsam genutzt. Es werden nur die Felder gesendet, die du setzt.
let pdf = PdfOptions(
pageSize: "A4",
orientation: .landscape,
margins: PdfMargins(top: 10, bottom: 10, left: 15, right: 15),
scale: 0.9,
grayscale: false,
header: PdfHeaderFooter(content: "Quarterly Report", height: 15),
footer: PdfHeaderFooter(content: "Confidential", height: 12)
)
| Feld | Typ | Beschreibung |
|---|---|---|
pageSize |
String? |
"A4", "A3", "Letter", "Legal" und Verwandte. |
pageWidth, pageHeight |
Double? |
Überschreiben pageSize, wenn beide gemeinsam gesetzt sind. |
orientation |
PdfOrientation? |
.portrait oder .landscape. Standard ist Hochformat. |
margins |
PdfMargins? |
top, bottom, left, right, jeweils ein Double?. Alle vier optional. |
scale |
Double? |
Render-Skalierung, zum Beispiel 0.9 für 90%. |
grayscale |
Bool? |
Wandelt das PDF nachträglich in Graustufen um. |
header |
PdfHeaderFooter? |
content (max. 2000 Zeichen) und height. |
footer |
PdfHeaderFooter? |
Dieselbe Form wie header. |
Fehlerbehandlung#
Swift bekommt genau einen Fehlertyp, EnconvertError, modelliert als Enum statt als Klassenhierarchie. Fange ihn mit catch-Mustern ab:
do {
_ = try await client.v2.perceive("https://example.com")
} catch EnconvertError.authentication(let message) {
print("invalid or missing API key: \(message)")
} catch EnconvertError.rateLimit(let message) {
print("too many requests, back off and retry: \(message)")
} catch let error as EnconvertError {
print("api error: \(error)") // wird als "[<status>] <message>" dargestellt
}
| Case | Ausgelöst bei | Statuscode |
|---|---|---|
.authentication(message:) |
Ungültiger, fehlender oder widerrufener Key | 401, 403 (beide melden 401) |
.quota(message:) |
Jede Antwort, die die API mit 402 beantwortet |
402 |
.rateLimit(message:) |
Rate-Limit überschritten | 429 |
.api(statusCode:message:) |
Jede andere 4xx oder 5xx | der tatsächliche Code |
.invalidArgument(_:) |
Clientseitige Validierung, vor jeder Anfrage | keiner |
EnconvertError erfüllt CustomStringConvertible und LocalizedError, String(describing:), localizedDescription und String-Interpolation stellen also alle "[<status>] <message>" dar. Zwei Convenience-Eigenschaften lesen dieselben Werte ohne Pattern Matching: error.statusCode (Int?, nil bei .invalidArgument) und error.message (der Text ohne das Präfix in eckigen Klammern). Eine wohlgeformte 2xx-Antwort, in der ein vom SDK benötigtes Feld fehlt, erscheint als .api(statusCode: 0, ...), was fehlerhafte Nutzdaten von einem echten HTTP-Fehler trennt.
Nicht unterstützte Konvertierungspaare, ein distill-Aufruf mit sowohl urls als auch discoverFrom, ein perceiveDirect-Aufruf, der zwei Artefakte anfordert, und ein leeres WatcherUpdate werfen alle .invalidArgument, bevor das Netzwerk berührt wird. Die Antwortcodes sind in der Fehlercode-Referenz katalogisiert.
Timeout-Recovery#
Lange URL-Renderings und große Dokumentkonvertierungen können die Obergrenze von 60 bis 120 Sekunden eines Reverse Proxy überdauern, selbst wenn der Job auf dem Server sauber durchläuft. Das SDK fragt sich da wieder heraus, ganz ohne Code von dir:
- Vor jeder Einzeldatei- und Einzel-URL-Konvertierung erzeugt der Client eine UUIDv4, entfernt die Bindestriche und sendet sie als
job_id. - Kommt diese Anfrage mit einem Status ab 500 zurück, wechselt der Client still auf
GET /v1/convert/status/{job_id}und fragt alle 3 Sekunden ab. Ein404heißt dort "noch nicht erfasst" und hält die Schleife am Laufen. - Bei
successgibt er das Ergebnis zurück. Beifailedwirft er.api(statusCode: 500, message:)mit der Meldung des Servers. Die Abfragefrist beträgt 5 Minuten, danach bekommst du.api(statusCode: 504, message: "Conversion timed out").
ConversionResult.jobId wird vom Client auch dann nachgetragen, wenn der synchrone Pfad erfolgreich war und die Antwort sie weggelassen hat, du kannst sie also selbst an getJobStatus übergeben:
let status = try await client.getJobStatus(result.jobId ?? "")
if status.status == .success {
print(status.presignedUrl ?? "")
} else if status.status == .failed {
print(status.error ?? "conversion failed")
}
convertWebsiteToPdf und convertWebsiteToScreenshot haben keine Job-Zeile zum Abfragen, ein 5xx kommt dort also sofort durch, statt wiederholt zu werden. V2-Methoden nutzen den Job-Fallback ebenfalls nicht.
Konfiguration#
let client = try Enconvert(
apiKey: ProcessInfo.processInfo.environment["ENCONVERT_API_KEY"] ?? "",
baseURL: "https://api.enconvert.com",
timeout: 300
)
| Parameter | Typ | Standard | Beschreibung |
|---|---|---|---|
apiKey |
String |
erforderlich | Privater API-Key. Ein leerer String wirft EnconvertError.invalidArgument. |
baseURL |
String |
https://api.enconvert.com |
Überschreibung für ein selbst gehostetes Gateway. Abschließende Schrägstriche werden entfernt. |
timeout |
TimeInterval |
300 |
Sekunden. Setzt sowohl timeoutIntervalForRequest als auch timeoutIntervalForResource an der internen URLSession. |
Der Key reist bei jedem API-Aufruf als Header X-API-Key mit. Vorsignierte Downloads gehen bewusst ohne ihn raus, denn eine signierte Storage-URL authentifiziert sich selbst, und den Key an einen Storage-Host weiterzureichen würde ihn preisgeben. Für das Abbrechen einzelner Aufrufe verpackst du den Aufruf in einen Task und brichst diesen ab: Jede Methode ist eine ganz normale async throws-Funktion. Enconvert speichert nur let-Eigenschaften über einer URLSession, baue also beim Start einen einzigen Client und nutze ihn wieder; client.v2 ist ein dünner Namespace über demselben Transport.
Ergebnisform#
Einzeldatei- und Einzel-URL-Konvertierungen liefern ein ConversionResult:
public struct ConversionResult: Codable, Equatable, Sendable {
public let presignedUrl: String
public let objectKey: String
public let filename: String
public let fileSize: Int?
public let conversionTimeSeconds: Double?
public let jobId: String?
}
Vorsignierte URLs sind kurzlebig. Übergib saveTo, damit das SDK die Bytes für dich auf die Platte streamt und dabei übergeordnete Verzeichnisse anlegt, oder hole die URL selbst und speichere die Datei für den langfristigen Zugriff in deinem eigenen Bucket.
V2-Artefakte kommen als V2OutputArtifact-Werte an, geschlüsselt nach Ausgabename, jeder mit url (String?, 15 Minuten lang vorsigniert und bei jedem Status-GET neu signiert), objectKey, sizeBytes, contentType und expiresIn (Sekunden, standardmäßig 900). PerceiveResult umschließt sie mit den Ehrlichkeitsmetadaten: renderQuality, statusCode, deductions, cacheHit, warnings, contentHash, urlFinal, structured, extractionTier, tokens, costCents, durationMs und optionsEcho, das die vom Server tatsächlich berücksichtigten Optionen zurückspiegelt, wobei Geheimnisse auf Booleans reduziert sind. Vom Aufrufer definierte Nutzdaten (Extraktionsschemata, destillierte data, trackFields eines Watchers, changes eines Diffs, extra einer Suche) laufen über JSONValue hin und zurück, ein Enum mit den Fällen .null, .bool, .number, .string, .array und .object, dazu der Alias JSONObject für [String: JSONValue]. Jeder Ergebnistyp ist Codable, Equatable und Sendable, ein geparstes Ergebnis auf die Platte zu cachen und später neu zu laden funktioniert also sofort.
Quelle und Issues#
- Paket:
Enconvert, über den Swift Package Manager. Die Version steht zur Laufzeit als modulweite KonstanteVERSIONbereit - GitHub: conversionapi/swift-sdk
- Lizenz: MIT. Abhängigkeiten: keine, nur
URLSessionund Foundation
Weiterführend: alle SDKs, V2-Übersicht, Perceive, Discover, Lookup, Distill, Ingest, Watch, Endpunkt-Übersicht, Parameter und Optionen und dein Dashboard für die Keys.
Häufig gestellte Fragen#
Wie konvertiere ich Dateien in Swift?#
Füge https://github.com/conversionapi/swift-sdk.git den Abhängigkeiten in deiner Package.swift hinzu, baue einen Client mit try Enconvert(apiKey:) und rufe dann eine typisierte Methode wie convertDocument, convertImage oder convertUrlToPdf auf. Übergib saveTo im Options-Struct, und das SDK streamt die fertige Datei direkt an diesen Pfad und legt dabei übergeordnete Verzeichnisse an.
Wie konvertiere ich eine URL in Swift in ein PDF?#
Rufe try await client.convertUrlToPdf("https://example.com", options: UrlToPdfOptions(saveTo: "page.pdf")) auf. Setze singlePage: false, um zu paginieren statt eine einzige durchgehende Seite zu erzeugen, und übergib pdfOptions: für Seitengröße, Ausrichtung, Ränder, Skalierung, Graustufen, Kopf- und Fußzeilen. Denk daran, dass Swift die Labels in Deklarationsreihenfolge erwartet, saveTo: kommt also vor singlePage: und pdfOptions:.
Wie konvertiere ich DOCX in Swift nach PDF?#
try await client.convertDocument(.path("report.docx"), options: ConvertDocumentOptions(saveTo: "report.pdf")). Das Ausgabeformat ist standardmäßig "pdf", outputFormat kann also entfallen. Dieselbe Methode verarbeitet Eingaben in XLSX, PPTX, ODT, ODS, ODP, OTS, Pages, Numbers, HTML, Markdown, CSV, JSON, XML, YAML und TOML.
Wie konvertiere ich HEIC in Swift nach WebP?#
try await client.convertImage(.path("photo.heic"), options: ConvertImageOptions(outputFormat: "webp", saveTo: "photo.webp")). Das Eingabeformat wird aus der Dateiendung gelesen, und alle 20 geordneten Paare unter jpeg, png, svg, heic und webp funktionieren genauso. Nicht unterstützte Paare werfen lokal EnconvertError.invalidArgument, bevor eine Anfrage rausgeht.
Zieht das Swift SDK irgendwelche Fremdabhängigkeiten mit?#
Nein. Package.swift deklariert ein leeres dependencies-Array. Alles läuft über URLSession, JSONSerialization und Foundation, wobei FoundationNetworking bedingt importiert wird, damit das Paket unter Linux ebenso baut wie auf Apple-Plattformen.
Wie hole ich eine Webseite in Swift als sauberes Markdown?#
Zwei Möglichkeiten. client.convertUrlToMarkdown liefert GitHub-Flavored Markdown mit YAML-Frontmatter und ist der einfachste Weg. client.v2.perceive mit outputs: [.markdown] liefert dasselbe Markdown plus einen renderQuality-Wert, eine deductions-Map, warnings und die Möglichkeit, Screenshots, Links oder strukturierte Extraktion im selben Rendering zu ergänzen.
Was bedeutet Render-Qualität, und warum sollte ich sie prüfen?#
renderQuality ist ein Double? von 0.0 bis 1.0, das an jedem V2-Lesevorgang hängt. Es sinkt, wenn die Seite nicht ehrlich gerendert hat: eine Bot-Challenge, eine Login-Wall, ein Cookie-Banner über einer leeren Hülle oder ein HTTP-Fehlerstatus. Der Inhalt wird trotzdem zurückgegeben statt verschluckt, prüfe also den Wert und das deductions-Dictionary, das jeden Abzug benennt, bevor du den Text an ein Modell gibst.
Kann ich das Swift SDK innerhalb einer iOS- oder macOS-App nutzen?#
Nur hinter deinem eigenen Backend. Das Paket baut für iOS 15, tvOS 15, watchOS 8 und macOS 12, damit du Modellcode über Targets hinweg teilen kannst, aber es authentifiziert sich mit einem privaten API-Key, und V2-Endpunkte lehnen öffentliche Keys rundweg ab. Diesen Key in einem App-Binary auszuliefern, gibt ihn jedem in die Hand, der das Bundle entpackt. Rufe aus der App deinen eigenen Server auf und EnConvert vom Server aus.
Was passiert, wenn eine lange Konvertierung das Proxy-Timeout erreicht?#
Das SDK sendet mit jeder Einzeldatei- und Einzel-URL-Konvertierung eine clientseitig erzeugte job_id. Antwortet die Anfrage mit 500 oder höher, fragt es GET /v1/convert/status/{job_id} alle 3 Sekunden für bis zu 5 Minuten ab, gibt bei success das Ergebnis zurück und wirft bei failed .api(statusCode: 500, ...). Wird die Frist überschritten, bekommst du .api(statusCode: 504, message: "Conversion timed out"). Batch-Einreichungen für ganze Websites überspringen diesen Fallback absichtlich.
Wie erfahre ich vor dem Senden einer Anfrage, welche Konvertierungen unterstützt werden?#
Rufe validOutputsFor("json") für die Ausgaben auf, die ein bestimmtes Eingabeformat unterstützt, oder prüfe die Mitgliedschaft in IMPLEMENTED_CONVERSIONS, der Menge aller 43 implementierten {input}-to-{output}-Endpunkte. convertImage und convertDocument führen intern dieselbe Prüfung aus und werfen EnconvertError.invalidArgument samt Liste der für diese Eingabe gültigen Ausgaben, bevor eine Anfrage rausgeht.