SDK Swift per la conversione dei file#

Enconvert è il client EnConvert ufficiale per Swift, distribuito tramite Swift Package Manager. È costruito su URLSession con async/await, non porta con sé alcuna dipendenza esterna e punta a Swift 5.9 e successivi su macOS 12, iOS 15, tvOS 15 e watchOS 8. Dodici metodi sul client coprono la conversione dei file e il rendering degli URL (da DOCX a PDF, da HEIC a WebP, da URL a PDF, da URL a Markdown, batch su interi siti), e il namespace client.v2 aggiunge ventitré metodi di web intelligence per percepire, scoprire, cercare, distillare, ingerire e monitorare le pagine.

Pacchetto: Enconvert · Sorgente: conversionapi/swift-sdk · Swift: 5.9+ · Piattaforme: macOS 12+, iOS 15+, tvOS 15+, watchOS 8+ · Dipendenze: nessuna

Installazione#

Aggiungi il pacchetto, poi elenca il prodotto nel target che lo usa:

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, usa File > Add Package Dependencies e incolla https://github.com/conversionapi/swift-sdk.git. Su Linux l'SDK importa FoundationNetworking in modo condizionale, quindi da parte tua non serve nulla di aggiuntivo.


Avvio rapido#

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 è throwing, non failable: un apiKey vuoto solleva EnconvertError.invalidArgument prima che qualcosa tocchi la rete. Ogni metodo di richiesta è async throws, e i metodi di conversione sono marcati @discardableResult, così una chiamata effettuata solo per l'effetto collaterale di saveTo non genera warning.


Che cosa espone il client#

Dodici metodi pendono da Enconvert e si mappano 1:1 sugli endpoint REST:

Metodo Endpoint Restituisce
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} (con polling) BatchStatus

client.v2 è un namespace EnconvertV2 che contiene altri ventitré metodi distribuiti su sei gruppi di capacità:

Gruppo Metodi Percorso base
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

Le opzioni vengono passate come una struct con parametri di inizializzazione dotati di valori predefiniti, quindi UrlToPdfOptions() significa "tutti i valori predefiniti" e tu nomini solo i campi che ti interessano. Swift richiede gli argomenti etichettati nell'ordine di dichiarazione, quindi tieni saveTo: prima di singlePage: e pdfOptions: quando ne imposti diversi insieme.


Conversione dei file#

Gli upload accettano un FileInput:

Caso A cosa serve
.path("report.docx") Un file su disco. Il nome base decide il formato di input e il tipo MIME.
.data(bytes) Byte grezzi senza nome. Caricati come upload.bin, application/octet-stream.
.wrapped(data: bytes, filename: "report.docx", contentType: nil) Byte grezzi più un nome file esplicito. Un contentType a nil viene dedotto dall'estensione.

convertUrlToPdf#

Esegue il rendering di qualsiasi URL pubblico in un 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)
))
Opzione Tipo Predefinito Descrizione
viewportWidth, viewportHeight Int? 1920, 1080 Dimensione del viewport del browser in pixel.
loadMedia, enableScroll Bool? true Attende immagini e video; scorre dall'alto verso il basso per attivare i caricamenti lazy.
outputFilename String? auto Sovrascrive il nome file generato.
auth, cookies, headers HttpBasicAuth?, [BrowserCookie]?, [String: String]? nessuno Credenziali HTTP Basic, cookie iniettati (massimo 50), header di richiesta aggiuntivi (massimo 20, hop-by-hop rifiutati).
saveTo String? nessuno Percorso locale su cui scrivere il PDF. Le directory padre vengono create.
singlePage Bool? true true produce una singola pagina continua. false impagina usando pdfOptions.pageSize.
pdfOptions PdfOptions? nessuno Geometria di pagina. Vedi Opzioni PDF.

Le pagine dietro un login accettano credenziali, cookie o header:

_ = 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"
))

Non combinare auth con una voce Authorization in headers. L'API rifiuta il conflitto.

convertUrlToScreenshot#

Cattura un PNG di qualsiasi URL.

let shot = try await client.convertUrlToScreenshot(
    "https://example.com",
    options: UrlToScreenshotOptions(viewportWidth: 1440, saveTo: "shot.png")
)

UrlToScreenshotOptions accetta gli stessi campi di viewport, media, scroll, nome file e accesso al browser di UrlToPdfOptions, senza singlePage e pdfOptions.

convertUrlToMarkdown#

Estrae Markdown pulito in stile GitHub-Flavored da un URL. Il convertitore rimuove navigazione, footer, pubblicità e script, mantiene il corpo principale dell'articolo e antepone un frontmatter YAML con titolo, descrizione, url, link e immagini. Utile per pipeline RAG, per importare contenuti di terze parti in un CMS o per generare dati di addestramento.

_ = try await client.convertUrlToMarkdown("https://example.com/article", options: UrlToMarkdownOptions(saveTo: "article.md"))

convertImage#

Converte tra jpeg, png, svg, heic e webp, oppure rasterizza un PDF in JPEG.

let result = try await client.convertImage(.path("photo.heic"), options: ConvertImageOptions(outputFormat: "webp", saveTo: "photo.webp"))

Il formato di input viene ricavato dall'estensione del nome file (.jpg, .jpeg, .png, .svg, .heic, .webp e .pdf per la rasterizzazione). outputFormat è obbligatorio e accetta gli alias jpg, yml, htm e md.

Opzione Tipo Obbligatorio Descrizione
outputFormat String Formato di destinazione, ad esempio "webp".
saveTo String? no Percorso locale su cui scrivere il risultato.
outputFilename String? no Sovrascrive il nome file generato.

convertDocument#

Converte documenti e formati di testo strutturato. outputFormat vale "pdf" per impostazione predefinita.

// da docx a pdf
_ = try await client.convertDocument(.path("report.docx"), options: ConvertDocumentOptions(saveTo: "report.pdf"))

// da json a yaml
_ = try await client.convertDocument(.path("data.json"), options: ConvertDocumentOptions(outputFormat: "yaml", saveTo: "data.yaml"))

// da markdown a pdf con impostazioni di pagina personalizzate
_ = try await client.convertDocument(.path("README.md"), options: ConvertDocumentOptions(
    saveTo: "readme.pdf",
    pdfOptions: PdfOptions(pageSize: "A4", margins: PdfMargins(top: 20, bottom: 20))
))

Input supportati: .doc, .docx, .xls, .xlsx, .ppt, .pptx, .html, .htm, .odt, .ods, .odp, .ots, .pages, .numbers, .md, .markdown, .csv, .json, .xml, .yaml, .yml, .toml. EPUB non ha una coppia documentale dedicata, quindi passa invece i file .epub attraverso convertToPdf oppure convertToMarkdown.

L'SDK include la tabella completa delle conversioni del gateway e valida localmente ogni coppia {input}-to-{output}, così una coppia non supportata solleva EnconvertError.invalidArgument con l'elenco degli output validi invece di pagare un giro di rete destinato a fallire. Le coppie implementate sono 43:

Input Output
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 ciascuno verso gli altri quattro (20 coppie ordinate)
pdf jpeg

Puoi interrogare tu stesso quella tabella senza effettuare alcuna richiesta:

validOutputsFor("json")                           // ["csv", "toml", "xml", "yaml"]
IMPLEMENTED_CONVERSIONS.contains("heic-to-webp")  // true

convertToMarkdown#

Converte in Markdown pulito un documento caricato di quasi qualsiasi formato, con il formato rilevato automaticamente lato server. Un buon primo stadio per una pipeline RAG.

_ = try await client.convertToMarkdown(.path("handbook.docx"), options: ConvertToMarkdownOptions(saveTo: "handbook.md"))

Sono accettati PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT e MD, e file office legacy o ODF. Le immagini no. Su questo endpoint non ci sono opzioni PDF.

convertToPdf#

Converte in PDF un file caricato di quasi qualsiasi formato: office, ODF, Pages, Numbers, RTF, CSV, HTML, Markdown, testo semplice, immagini raster, SVG, EPUB, oppure un PDF già esistente passato in passthrough e normalizzato.

_ = try await client.convertToPdf(.path("slides.pptx"), options: ConvertToPdfOptions(saveTo: "slides.pdf"))

// Passthrough di un PDF, convertito in scala di grigi
_ = try await client.convertToPdf(
    .path("scan.pdf"),
    options: ConvertToPdfOptions(saveTo: "scan-gray.pdf", pdfOptions: PdfOptions(grayscale: true))
)
Qui viene rispettato solo grayscale. convertToPdf inoltra pdfOptions, ma l'endpoint anything-to-pdf legge grayscale e ignora il resto. Usa convertDocument oppure convertUrlToPdf quando ti servono dimensione della pagina, orientamento, margini, scala, intestazioni o piè di pagina.

convertWebsiteToPdf e convertWebsiteToScreenshot#

Individua ogni pagina di un sito, converte ciascuna in background e raccoglie tutto in un unico ZIP. Entrambi i metodi sono solo asincroni e richiedono una chiave API privata con accesso alla scansione.

let batch = try await client.convertWebsiteToPdf("https://example.com", options: WebsiteToPdfOptions(
    crawlMode: .sitemap,
    excludePatterns: ["/blog/tag/"]
))
print(batch.batchId, batch.urlCount, batch.discoveryMethod ?? "")

// Blocca finché il batch non si assesta e salva lo ZIP
let status = try await client.waitForBatch(batch.batchId, options: WaitForBatchOptions(saveTo: "site.zip"))
print("\(status.completed) of \(status.total) pages converted")

// Oppure interrogalo tu stesso
let snapshot = try await client.getBatchStatus(batch.batchId)
if snapshot.status != .processing {
    print(snapshot.zipDownloadUrl ?? "")
}
Opzione Tipo Predefinito Descrizione
crawlMode CrawlMode? .auto .auto, .sitemap (solo sitemap.xml) oppure .full (sitemap più scansione BFS).
includePatterns, excludePatterns [String]? nessuno Allowlist e poi denylist per gli URL individuati. Solo in modalità di scansione completa.
notificationEmail String? proprietario del progetto Email avvisata quando il batch termina.
callbackUrl String? nessuno Webhook chiamato in POST quando il batch termina.
singlePage, pdfOptions Bool?, PdfOptions? vedi sopra Solo per i batch PDF.

Entrambi i metodi accettano anche i campi di viewport, media, scroll e accesso al browser elencati sotto convertUrlToPdf, applicati a ogni pagina. waitForBatch interroga ogni 5 secondi con un limite di 30 minuti per impostazione predefinita; puoi sovrascriverlo con WaitForBatchOptions(intervalMs:timeoutMs:saveTo:). Superare il limite solleva EnconvertError.api(statusCode: 504, ...). convertWebsiteToScreenshot si comporta in modo identico e produce uno ZIP di PNG.


Web intelligence (V2)#

Ogni lettura V2 porta con sé un punteggio renderQuality da 0.0 a 1.0, esposto come Double? su PerceiveResult, PerceiveDirectResult, DistillItem e WatcherSnapshot. Un punteggio basso significa che la pagina non si è renderizzata onestamente: una sfida anti-bot, un muro di login, un banner dei cookie sopra un guscio SPA vuoto, uno stato di errore HTTP. Il contenuto torna comunque, segnalato, accanto a un dizionario deductions che dà un nome a ciascuna penalità scattata e a un array warnings, così una lettura sbagliata non entra mai in silenzio nel contesto del tuo agente. Verificalo prima di fidarti di qualsiasi cosa:

if let quality = op.renderQuality, quality < 0.6 {
    print("low quality read of \(op.url): \(op.deductions)")
}

Perceive#

Renderizza un URL negli artefatti che chiedi. Sincrono, con URL firmati agli artefatti validi 15 minuti. Ogni metodo V2 richiede una chiave API privata; le chiavi pubbliche vengono rifiutate.

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)

// Rifirma più tardi gli URL degli artefatti
let again = try await client.v2.getPerceiveOperation(op.operationId)
Opzione Tipo Predefinito Descrizione
outputs [PerceiveOutputName]? [.markdown, .structured] .markdown, .htmlCleaned, .htmlRaw, .screenshot, .screenshotFullPage, .pdf, .links, .images, .structured.
extract [PerceiveExtractName]? nessuno .tables, .prices, .contacts, .metadata, .mainContent, .headings, .structuredData, .technologies, .all.
schema JSONObject? nessuno Schema JSON per l'estrazione strutturata tramite il livello LLM.
waitFor, waitTimeoutMs String?, Int? nessuno, 30000 Un selettore CSS (facoltativamente con prefisso css:) oppure js:<expr> da attendere, e il relativo budget in ms (da 0 a 60000).
jsCode String? nessuno JavaScript eseguito dopo la navigazione, massimo 20000 caratteri.
viewport PerceiveViewport? 1920 per 1080 width da 320 a 3840, height da 240 a 2160.
headers, cookies, auth [String: String]?, [BrowserCookie]?, HttpBasicAuth? nessuno Header di richiesta, cookie iniettati, credenziali HTTP Basic.
cacheMode PerceiveCacheMode? .enabled .enabled (cache di 1 ora), .bypass, .refresh.
pdfOptions PdfOptions? nessuno Ha senso solo quando outputs include .pdf.
blockResources [PerceiveResourceType]? nessuno .image, .media, .font, .stylesheet, .script, .xhr, .fetch, .websocket, .manifest, .other.
respectRobots, mobile Bool? predefinito del server Rispetta robots.txt; emula un dispositivo mobile.
onlyMainContent Bool? true Rimuove navigazione, header, footer e banner dei cookie dall'artefatto markdown e dall'estratto main_content. Imposta false per la pagina intera.
directDownload Bool? false Trasmette byte grezzi invece di un envelope JSON. Preferisci perceiveDirect.
Tre opzioni sono dichiarate ma non ancora attive. proxyUrl, geolocation e actionChain esistono su PerceiveOptions e vengono serializzate sulla rete, ma al momento il server risponde 422 per tutte e tre. Lasciale a nil.

Trasmettere un singolo artefatto direttamente su disco salta l'envelope JSON e il giro dell'URL firmato. perceiveDirect verifica localmente che tu abbia chiesto esattamente un output che produca un artefatto, così un errore non costa nulla:

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"))

// Riscarica più tardi un artefatto archiviato. Passa nil quando l'operazione ne ha prodotto uno solo.
let saved = try await client.v2.downloadPerceiveArtifact(direct.operationId, output: .pdf)

PerceiveDirectResult porta con sé content, contentType, filename, operationId, objectKey, cacheHit, renderQuality, sourceStatusCode, contentHash e warningsCount, tutti letti dagli header della risposta. Un 410 da downloadPerceiveArtifact significa che l'artefatto ha superato la propria finestra di conservazione.

I batch accettano fino a 1000 URL con un unico blocco di opzioni condiviso. I batch piccoli terminano inline; quelli più grandi tornano in coda e tu li interroghi:

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 è .manifest (predefinito) oppure .zip. directDownload viene rifiutato con 422 sui batch.

Discover#

Enumera gli URL di un sito senza rendering nel browser. Veloce, e non esegue mai un 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)
Opzione Tipo Predefinito Descrizione
mode DiscoverMode? .hybrid .sitemap, .crawl oppure .hybrid (sitemap più scansione HTTP).
maxUrls, maxDepth Int? 100, 2 Da 1 a 1000 URL; profondità di scansione da 1 a 5.
includePatterns, excludePatterns [String]? nessuno Allowlist con espressioni regolari, poi denylist applicata dopo di essa. Massimo 50 pattern ciascuna.
sameDomainOnly Bool? true Resta sul dominio dell'URL di partenza.
respectRobots Bool? predefinito del server Rispetta robots.txt.

DiscoverResult riporta anche pagesCrawled, robotsRespected e warnings, e sources contiene i conteggi grezzi per singola fonte rilevati prima della deduplicazione.

Lookup#

Esegue una ricerca web per categorie, renderizzando facoltativamente i primi risultati nella stessa chiamata.

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)
}
Opzione Tipo Predefinito Descrizione
category LookupCategory? .web .web, .news, .images, .scholar, .patents, .maps.
country, locale String? nessuno Codice paese gl di Google ("us", "in") e lingua dell'interfaccia hl ("en").
timeFilter LookupTimeFilter? nessuno .hour, .day, .week, .month, .year.
numResults, page Int? 10, 1 Da 1 a 100 risultati; pagina da 1 a 10.
location, autocorrect String?, Bool? nessuno, true Località in testo libero come "Austin, Texas"; lascia che il provider corregga la query.
perceiveTop Int? 0 Renderizza automaticamente i primi N URL dei risultati, da 0 a 10. Ciascuno esegue un rendering completo nel browser.

LookupResult espone anche answerBox, knowledgeGraph, perceiveOperationIds e credits.

Distill#

Estrae dati strutturati dalle pagine seguendo uno schema che definisci tu.

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 è un JSONObject, cioè un [String: JSONValue], quindi funzionano sia una mappa piatta {field: description} sia un oggetto JSON-Schema completo. Il cssSchema facoltativo viene eseguito per primo e risponde a tutto ciò che i semplici selettori riescono a raggiungere; solo i campi che gli sfuggono salgono al livello LLM, ed extractionTier riporta quali livelli hanno effettivamente risposto (.css, .llm, .mixed oppure .none). CssField.type è uno tra .text, .attribute, .html, .regex, .nested, .list e .nestedList, annidabile fino a 5 livelli di profondità.

Sostituisci urls con discoverFrom per individuare e poi distillare in un'unica chiamata. DistillDiscoverFrom accetta url, mode (predefinito .hybrid) e maxPages (da 1 a 50, predefinito 10, che limita sia la scoperta sia la distillazione):

_ = 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")]
))

Passare sia urls sia discoverFrom, o nessuno dei due, solleva EnconvertError.invalidArgument prima che venga inviata qualsiasi richiesta.

Ingest#

Trasforma un sito, una lista di URL o una pila di documenti caricati in JSONL suddiviso in chunk e pronto per il RAG. Sempre asincrono.

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 ?? "")
}
Opzione Tipo Predefinito Descrizione
mode IngestMode? .urls .urls, .sitemap oppure .crawl. Il quarto caso, .files, è quello che ingestFiles riporta sul proprio job; non passarlo qui.
url String? nessuno URL di partenza. Obbligatorio per .sitemap e .crawl, vietato per .urls.
urls [String]? nessuno URL espliciti, massimo 1000. Obbligatori per .urls, vietati altrimenti.
maxPages, maxDepth Int? 50, 2 Limite di scoperta per .sitemap e .crawl, da 1 a 1000; profondità da 1 a 5.
sameDomainOnly Bool? true Resta sul dominio dell'URL di partenza.
includePatterns, excludePatterns [String]? nessuno Allowlist con espressioni regolari, poi denylist.
respectRobots Bool? predefinito del server Rispetta robots.txt.
waitFor, waitTimeoutMs String?, Int? 30000 ms Selettore o espressione js: attesa su ogni pagina, e il relativo budget (da 0 a 60000).
chunk IngestChunkOptions? nessuno maxWords da 32 a 4000, predefinito 512. sentenceOverlap da 0 a 10, predefinito 1.
webhookUrl String? nessuno Webhook di completamento, firmato in HMAC.

Le regole su modalità e URL qui sopra vengono applicate lato client: ingest solleva EnconvertError.invalidArgument invece di effettuare una richiesta condannata se passi urls con mode: .sitemap. I file caricati attraversano la stessa pipeline e lo stesso ciclo di vita del job:

let files: [FileInput] = [.path("handbook.pdf"), .path("notes.docx")]
let fileJob = try await client.v2.ingestFiles(files, options: IngestFilesOptions(chunk: IngestChunkOptions(maxWords: 512)))

Sono accettati PDF, DOCX, PPTX, XLSX, CSV, HTML, EPUB, TXT e MD, e file office legacy o ODF, ed è richiesto almeno un file. Gestione dei job e impianto dei webhook:

let list = try await client.v2.listIngestJobs(V2ListOptions(limit: 20))
let canceled = try await client.v2.cancelIngestJob(job.jobId)   // idempotente

let secret = try await client.v2.getWebhookSecret()
print(secret.signatureHeader, secret.signatureScheme, secret.replayToleranceSeconds)
_ = try await client.v2.rotateWebhookSecret()                   // le vecchie firme smettono subito di essere valide

let retry = try await client.v2.retryIngestWebhook(job.jobId)
print(retry.delivered, retry.attempts, retry.detail)

retryIngestWebhook risponde 409 quando il job non è completato e 400 quando non ha alcun webhook configurato. V2ListOptions accetta skip e limit (da 1 a 100, predefinito 20).

Watch#

Rifà il rendering di una pagina a cadenza fissa e ti avvisa quando cambia.

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)
}
Opzione Tipo Predefinito Descrizione
frequencyMinutes Int? 60 Da 60 a 43200. Il minimo orario è invalicabile.
diffMode WatchDiffMode? .auto .auto, .text, .structured, .tables, .metadata.
trackFields JSONObject? nessuno Sottoinsieme di campi o selettori passato al motore di diff.
webhookUrl String? nessuno Webhook di notifica delle modifiche, firmato in HMAC.
notifyEmail Bool? true Invia un'email al proprietario del progetto quando ci sono cambiamenti.
// Una stringa vuota azzera il webhook; nil lo lascia com'è.
_ = 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)   // cancellazione logica, idempotente

updateWatcher richiede almeno un campo e solleva EnconvertError.invalidArgument su un WatcherUpdate vuoto. WatchUpdateStatus accetta solo .active oppure .paused; la cancellazione passa da deleteWatcher, che restituisce il watcher archiviato con stato .deleted.

I diff degli snapshot contengono contenuto di pagina non attendibile. WatcherSnapshot.changes è un array di oggetti JSON grezzi prelevati dalla pagina osservata. Effettua l'escape dei valori prima di renderizzarli da qualsiasi parte.

Opzioni PDF#

PdfOptions è condiviso da convertUrlToPdf, convertDocument, convertWebsiteToPdf, PerceiveOptions e (solo per grayscale) convertToPdf. Vengono inviati solo i campi che imposti.

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)
)
Campo Tipo Descrizione
pageSize String? "A4", "A3", "Letter", "Legal" e simili.
pageWidth, pageHeight Double? Hanno la precedenza su pageSize quando sono impostati entrambi insieme.
orientation PdfOrientation? .portrait oppure .landscape. Il valore predefinito è portrait.
margins PdfMargins? top, bottom, left, right, ciascuno un Double?. Tutti e quattro opzionali.
scale Double? Scala di rendering, ad esempio 0.9 per il 90%.
grayscale Bool? Post-elabora il PDF convertendolo in scala di grigi.
header PdfHeaderFooter? content (massimo 2000 caratteri) e height.
footer PdfHeaderFooter? Stessa forma di header.

Gestione degli errori#

Swift riceve un solo tipo di errore, EnconvertError, modellato come enum invece che come gerarchia di classi. Intercettalo con i pattern di catch:

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)")   // viene reso come "[<status>] <message>"
}
Caso Sollevato per Codice di stato
.authentication(message:) Chiave non valida, mancante o revocata 401, 403 (entrambi riportano 401)
.quota(message:) Qualsiasi risposta a cui l'API risponde con 402 402
.rateLimit(message:) Rate limit superato 429
.api(statusCode:message:) Qualsiasi altro 4xx o 5xx il codice effettivo
.invalidArgument(_:) Validazione lato client, prima di qualsiasi richiesta nessuno

EnconvertError è conforme a CustomStringConvertible e LocalizedError, quindi String(describing:), localizedDescription e l'interpolazione di stringa vengono tutti resi come "[<status>] <message>". Due proprietà di comodo leggono gli stessi valori senza pattern matching: error.statusCode (Int?, nil per .invalidArgument) ed error.message (il testo senza il prefisso tra parentesi quadre). Una risposta 2xx ben formata a cui manca un campo richiesto dall'SDK emerge come .api(statusCode: 0, ...), il che separa un payload malformato da un vero errore HTTP.

Coppie di conversione non supportate, una chiamata distill con sia urls sia discoverFrom, una chiamata perceiveDirect che chiede due artefatti e un WatcherUpdate vuoto sollevano tutti .invalidArgument prima che la rete venga toccata. I codici di risposta sono catalogati nel riferimento dei codici di errore.


Recupero dei timeout#

I rendering lunghi di URL e le conversioni di documenti di grandi dimensioni possono superare il tetto di 60-120 secondi di un reverse proxy anche quando il job termina correttamente sul server. L'SDK se ne tira fuori con il polling, senza che tu scriva codice:

  1. Prima di ogni conversione di un singolo file e di un singolo URL, il client genera un UUIDv4, ne rimuove i trattini e lo invia come job_id.
  2. Se quella richiesta torna con uno stato pari o superiore a 500, il client passa silenziosamente a GET /v1/convert/status/{job_id}, interrogandolo ogni 3 secondi. Un 404 lì significa "non ancora registrato" e mantiene attivo il ciclo.
  3. Su success restituisce il risultato. Su failed solleva .api(statusCode: 500, message:) con il messaggio del server. Il limite di tempo per il polling è di 5 minuti, superato il quale ottieni .api(statusCode: 504, message: "Conversion timed out").

ConversionResult.jobId viene riempito dal client anche quando il percorso sincrono è riuscito e la risposta lo ha omesso, così puoi passarlo tu stesso a getJobStatus:

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")
}
I batch dei siti web ne restano fuori di proposito. convertWebsiteToPdf e convertWebsiteToScreenshot non hanno una riga per singolo job da interrogare, quindi un 5xx lì emerge subito invece di essere ritentato. Nemmeno i metodi V2 usano il fallback sui job.

Configurazione#

let client = try Enconvert(
    apiKey: ProcessInfo.processInfo.environment["ENCONVERT_API_KEY"] ?? "",
    baseURL: "https://api.enconvert.com",
    timeout: 300
)
Parametro Tipo Predefinito Descrizione
apiKey String obbligatorio Chiave API privata. Una stringa vuota solleva EnconvertError.invalidArgument.
baseURL String https://api.enconvert.com Sovrascrittura per un gateway self-hosted. Gli slash finali vengono rimossi.
timeout TimeInterval 300 Secondi. Imposta sia timeoutIntervalForRequest sia timeoutIntervalForResource sull'URLSession interna.

La chiave viaggia come header X-API-Key su ogni chiamata all'API. I download presigned escono deliberatamente senza di essa, dato che un URL di storage firmato si autentica da solo e inoltrare la chiave a un host di storage la esporrebbe. Per annullare una singola chiamata, avvolgila in un Task e annulla quello: ogni metodo è una semplice funzione async throws. Enconvert conserva solo proprietà let su una URLSession, quindi costruisci un unico client all'avvio e riusalo; client.v2 è un sottile namespace sullo stesso trasporto.

Non inserire mai la chiave API direttamente nel codice, e non distribuirla mai in un bundle applicativo. Leggila dall'ambiente o dal tuo secret manager e tieni il client su un server che controlli. Il pacchetto compila per iOS, tvOS e watchOS così puoi condividere il codice dei modelli tra i target, ma il binario di un'app è un artefatto pubblico: chiunque estragga la tua chiave privata può eseguire conversioni a carico del tuo progetto. Fai chiamare al tuo backend dall'app, e lascia che sia il backend a chiamare EnConvert. Consulta l'[autenticazione](/it/docs/authentication) per i tipi di chiave e la rotazione.

Struttura del risultato#

Le conversioni di un singolo file e di un singolo URL restituiscono un 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?
}

Gli URL presigned hanno vita breve. Passa saveTo per far trasmettere i byte su disco dall'SDK, che crea le directory padre quando serve, oppure recupera tu stesso l'URL e archivia il file nel tuo bucket per un accesso a lungo termine.

Gli artefatti V2 arrivano come valori V2OutputArtifact indicizzati per nome di output, ciascuno con url (String?, firmato per 15 minuti e rifirmato a ogni GET di stato), objectKey, sizeBytes, contentType ed expiresIn (secondi, 900 per impostazione predefinita). PerceiveResult li avvolge con i metadati di trasparenza: renderQuality, statusCode, deductions, cacheHit, warnings, contentHash, urlFinal, structured, extractionTier, tokens, costCents, durationMs e optionsEcho, che restituisce le opzioni effettivamente rispettate dal server con i segreti ridotti a booleani. I payload definiti dal chiamante (schemi di estrazione, data distillati, trackFields dei watcher, changes dei diff, extra delle ricerche) fanno andata e ritorno attraverso JSONValue, un enum con i casi .null, .bool, .number, .string, .array e .object, più l'alias JSONObject per [String: JSONValue]. Ogni tipo di risultato è Codable, Equatable e Sendable, quindi mettere in cache su disco un risultato già interpretato e ricaricarlo più tardi funziona senza alcun lavoro aggiuntivo.


Sorgente e problemi#

  • Pacchetto: Enconvert, tramite Swift Package Manager. La versione è esposta a runtime nella costante a livello di modulo VERSION
  • GitHub: conversionapi/swift-sdk
  • Licenza: MIT. Dipendenze: nessuna, solo URLSession e Foundation

Letture correlate: tutti gli SDK, panoramica V2, perceive, discover, lookup, distill, ingest, watch, panoramica degli endpoint, parametri e opzioni, e la tua dashboard per le chiavi.


Domande frequenti#

Come converto i file in Swift?#

Aggiungi https://github.com/conversionapi/swift-sdk.git alle dipendenze del tuo Package.swift, costruisci un client con try Enconvert(apiKey:), poi chiama un metodo tipizzato come convertDocument, convertImage oppure convertUrlToPdf. Passa saveTo nella struct delle opzioni e l'SDK trasmette il file finito direttamente su quel percorso, creando le directory padre quando serve.

Come converto un URL in PDF in Swift?#

Chiama try await client.convertUrlToPdf("https://example.com", options: UrlToPdfOptions(saveTo: "page.pdf")). Imposta singlePage: false per impaginare invece di produrre una singola pagina continua, e passa pdfOptions: per dimensione della pagina, orientamento, margini, scala, scala di grigi, intestazioni e piè di pagina. Ricorda che Swift vuole le etichette nell'ordine di dichiarazione, quindi saveTo: viene prima di singlePage: e pdfOptions:.

Come converto DOCX in PDF in Swift?#

try await client.convertDocument(.path("report.docx"), options: ConvertDocumentOptions(saveTo: "report.pdf")). Il formato di output vale "pdf" per impostazione predefinita, quindi outputFormat può essere omesso. Lo stesso metodo gestisce input XLSX, PPTX, ODT, ODS, ODP, OTS, Pages, Numbers, HTML, Markdown, CSV, JSON, XML, YAML e TOML.

Come converto HEIC in WebP in Swift?#

try await client.convertImage(.path("photo.heic"), options: ConvertImageOptions(outputFormat: "webp", saveTo: "photo.webp")). Il formato di input viene letto dall'estensione del nome file, e tutte e 20 le coppie ordinate tra jpeg, png, svg, heic e webp funzionano allo stesso modo. Le coppie non supportate sollevano EnconvertError.invalidArgument localmente, prima che venga inviata qualsiasi richiesta.

L'SDK Swift porta con sé dipendenze di terze parti?#

No. Package.swift dichiara un array dependencies vuoto. Tutto gira su URLSession, JSONSerialization e Foundation, con FoundationNetworking importato in modo condizionale così il pacchetto compila anche su Linux oltre che sulle piattaforme Apple.

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

Ci sono due opzioni. client.convertUrlToMarkdown restituisce Markdown in stile GitHub-Flavored con frontmatter YAML ed è il percorso più semplice. client.v2.perceive con outputs: [.markdown] ti dà lo stesso Markdown più un punteggio renderQuality, una mappa deductions, dei warnings e la possibilità di aggiungere screenshot, link o estrazione strutturata nello stesso rendering.

Che cosa significa la qualità del rendering e perché dovrei controllarla?#

renderQuality è un Double? da 0.0 a 1.0 allegato a ogni lettura V2. Scende quando la pagina non si è renderizzata onestamente: una sfida anti-bot, un muro di login, un banner dei cookie sopra un guscio vuoto, oppure uno stato di errore HTTP. Il contenuto viene comunque restituito invece di essere scartato, quindi controlla il punteggio e il dizionario deductions che dà un nome a ciascuna penalità prima di dare il testo in pasto a un modello.

Posso usare l'SDK Swift dentro un'app iOS o macOS?#

Solo dietro al tuo backend. Il pacchetto compila per iOS 15, tvOS 15, watchOS 8 e macOS 12 così puoi condividere il codice dei modelli tra i target, ma si autentica con una chiave API privata e gli endpoint V2 rifiutano categoricamente le chiavi pubbliche. Distribuire quella chiave in un binario applicativo significa consegnarla a chiunque scompatti il bundle. Fai chiamare il tuo server dall'app, e chiama EnConvert dal server.

Che cosa succede quando una conversione lunga incontra il timeout del proxy?#

L'SDK invia un job_id generato dal client a ogni conversione di un singolo file e di un singolo URL. Se la richiesta restituisce 500 o più, interroga GET /v1/convert/status/{job_id} ogni 3 secondi per un massimo di 5 minuti, restituendo il risultato su success e sollevando .api(statusCode: 500, ...) su failed. Superare il limite di tempo produce .api(statusCode: 504, message: "Conversion timed out"). Gli invii batch dei siti web saltano deliberatamente questo fallback.

Come faccio a sapere quali conversioni sono supportate prima di inviare una richiesta?#

Chiama validOutputsFor("json") per gli output che un dato formato di input supporta, oppure verifica l'appartenenza a IMPLEMENTED_CONVERSIONS, l'insieme di tutti i 43 endpoint {input}-to-{output} implementati. convertImage e convertDocument eseguono internamente lo stesso controllo e sollevano EnconvertError.invalidArgument con gli output validi elencati per quell'input, prima che venga inviata qualsiasi richiesta.