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.
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 |
Sì | 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))
)
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. |
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.
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:
- 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. - 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. Un404lì significa "non ancora registrato" e mantiene attivo il ciclo. - Su
successrestituisce il risultato. Sufailedsolleva.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")
}
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.
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 moduloVERSION - GitHub: conversionapi/swift-sdk
- Licenza: MIT. Dipendenze: nessuna, solo
URLSessione 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.