Convert HTML to PDF on Apple platforms using WKWebView. Processes 1,939 PDFs/sec continuous mode with 35 MB steady-state memory. Swift 6 strict concurrency with actor-based resource pooling.
- Overview
- Features
- Installation
- Quick Start
- Usage Examples
- Performance
- Architecture
- Monitoring
- Documentation
- Testing
- Test Support
- Platform Support
- Related Packages
- Contributing
- License
- Acknowledgments
swift-html-to-pdf provides HTML to PDF conversion with actor-based resource pooling, streaming results, and Swift 6 strict concurrency. Built on WKWebView for native rendering quality and performance.
- Streaming PDF generation with AsyncStream for progressive results
- WebView resource pooling with automatic lifecycle management
- Swift 6 strict concurrency with Sendable guarantees
- Optional type-safe HTML DSL integration via swift-html
- Swift Metrics integration for production monitoring
- Performance: 1,939 PDFs/sec continuous mode, 677 PDFs/sec paginated mode
- Memory efficiency: 35 MB steady-state with 4-24 workers
- Support for both continuous and paginated rendering modes
Add swift-html-to-pdf to your Package.swift:
dependencies: [
.package(url: "https://github.com/coenttb/swift-html-to-pdf.git", from: "1.0.0")
]Add to your target:
.target(
name: "YourTarget",
dependencies: [
.product(name: "HtmlToPdf", package: "swift-html-to-pdf")
]
)To use the swift-html integration, enable the HTML trait:
dependencies: [
.package(
url: "https://github.com/coenttb/swift-html-to-pdf.git",
from: "1.0.0",
traits: ["HTML"]
)
]- Swift 6.2+
- macOS 14.0+ or iOS 17.0+
- Xcode 26.0+
Note: Swift 6.2 is required due to compiler bugs in Swift 6.0 and 6.1 that cause crashes during compilation (both in release mode and during macro expansion in debug mode). These issues are resolved in Swift 6.2.
import HtmlToPdf
import Dependencies
@Dependency(\.pdf) var pdf
// Render to file
try await pdf.render(html: "<h1>Invoice #1234</h1>", to: fileURL)
// Render to data (in-memory)
let pdfData = try await pdf.render(html: "<h1>Receipt</h1>")
// Batch processing with streaming results
let html = invoices.map { "<html><body>\($0.html)</body></html>" }
for try await result in try await pdf.render(html: html, to: directory) {
print("Generated \(result.url)")
}import HtmlToPdf
struct Invoice: HTML {
let number: Int
let total: Decimal
var body: some HTML {
h1 { "Invoice #\(number)" }
p { "Total: $\(total)" }
}
}
@Dependency(\.pdf) var pdf
try await pdf.render(html: Invoice(number: 1234, total: 99.99), to: fileURL)Or use inline HTML:
import HtmlToPdf
let invoice = HTMLDocument {
h1 { "Invoice #1234" }
p { "Total: $99.99" }
}
@Dependency(\.pdf) var pdf
try await pdf.render(html: invoice, to: fileURL)Process PDFs as they are generated:
for try await result in try await pdf.render(html: html, to: directory) {
// PDF is ready immediately
try await uploadToS3(result.url)
try await db.markComplete(result.index)
}Benefits: lower latency, constant memory usage, real-time progress tracking.
Customize paper size, margins, pagination, and concurrency:
try await withDependencies {
$0.pdf.render.configuration.paperSize = .letter
$0.pdf.render.configuration.margins = .wide
$0.pdf.render.configuration.paginationMode = .paginated
$0.pdf.render.configuration.concurrency = .automatic
} operation: {
try await pdf.render(html: html, to: fileURL)
}Available options:
- Paper sizes: .a4, .letter, .legal, .a3, .a5, or custom CGSize
- Margins: .none, .minimal, .standard, .comfortable, .wide, or custom EdgeInsets
- Pagination: .continuous (fast), .paginated (print-ready), .automatic
- Concurrency: .automatic (1x CPU), .fixed(n), or specific count
See Configuration Guide for details.
Continuous mode (single-page, maximum speed):
| Batch Size | Throughput | Avg Latency | Memory |
|---|---|---|---|
| 100 | 1,772/sec | 0.56ms | 146 MB |
| 1,000 | 1,939/sec | 0.52ms | 146 MB |
| 10,000 | 1,814/sec | 0.55ms | 148 MB |
Paginated mode (multi-page, print-ready):
| Batch Size | Throughput | Avg Latency | Memory |
|---|---|---|---|
| 100 | 142/sec | 7.05ms | 102 MB |
| 1,000 | 677/sec | 1.48ms | 110 MB |
| 10,000 | 485/sec | 2.06ms | 137 MB |
Test environment: macOS 26.0, Apple Silicon M1 (8 cores), 24 GB RAM, Swift 6.2
Memory usage remains constant across concurrency levels:
| Concurrency | Steady-State | Peak | Expected |
|---|---|---|---|
| 4 workers | 34 MB | 34 MB | 400 MB |
| 8 workers | 34 MB | 35 MB | 800 MB |
| 16 workers | 35 MB | 35 MB | 1,600 MB |
| 24 workers | 35 MB | 35 MB | 2,400 MB |
Shared WebKit infrastructure provides memory efficiency. Memory determined by pool overhead, not worker count.
Measured with 50+ PDF warmup and sustained rendering workload. See WebViewMemoryTests.swift for methodology.
- Pre-warmed WKWebView instances for immediate availability
- Automatic lifecycle management
- FIFO fairness under load
- Optimal concurrency: 1x CPU count (8 WebViews on 8-core Mac)
- Powered by swift-resource-pool
- Full type safety in concurrent code
- Sendable guarantees throughout
- Actor-isolated state management
- No data races possible
Export metrics to Prometheus, StatsD, or other systems via swift-metrics:
import Metrics
import Prometheus
// Bootstrap once at startup
MetricsSystem.bootstrap(PrometheusMetricsFactory())
// Use library normally - metrics automatically collected
@Dependency(\.pdf) var pdf
try await pdf.render(html: invoices, to: directory)Available metrics:
htmltopdf_pdfs_generated_total- Counterhtmltopdf_pdfs_failed_total- Counter (with reason dimension)htmltopdf_render_duration_seconds- Timer (with mode dimension; p50/p95/p99)htmltopdf_pool_replacements_total- Counterhtmltopdf_pool_utilization- Gaugehtmltopdf_throughput_pdfs_per_sec- Gauge
- Getting Started Guide - Installation, basic usage, first PDF
- Performance Guide - Optimization, benchmarks, tuning
- Configuration Guide - All configuration options
- API Documentation - Full DocC documentation
Generate docs locally:
swift package generate-documentation --open# All tests
swift test
# Performance benchmarks
swift test --filter PerformanceBenchmarks
# Memory analysis
swift test --filter WebViewMemoryTests
# Stress tests (10K-1M PDFs)
swift test --filter StressTestsThe PDFTestSupport module provides test utilities for applications using swift-html-to-pdf:
dependencies: [
.product(name: "PDFTestSupport", package: "swift-html-to-pdf")
]Features:
- HTML test fixtures (minimal, simple, rich formatting, unicode)
- Temporary directory management with automatic cleanup
- Metrics testing backends
- PDF output verification helpers
- Platform-specific test renderers
Example usage:
import PDFTestSupport
import Testing
@Test func generateTestPDF() async throws {
try await withTemporaryDirectory { dir in
let html = TestHTML.richFormatting
try await pdf.render(html: html, to: dir.appendingPathComponent("test.pdf"))
}
}See test files for additional examples.
| Platform | Status | Notes |
|---|---|---|
| macOS | Full support | Optimal performance, 8 concurrent workers (8-core) |
| iOS | Full support | 8 concurrent workers, mobile-optimized |
| Linux | Planned | Architecture ready, needs WebKit renderer |
| Windows | Possible | Pending WebKit integration |
- swift-html: The Swift library for domain-accurate and type-safe HTML & CSS.
- swift-logging-extras: A Swift package for integrating swift-logging with swift-dependencies.
- swift-resource-pool: A Swift package for actor-based resource pooling.
- swift-document-templates: A Swift package for data-driven business document creation.
- pointfreeco/swift-dependencies: A dependency management library for controlling dependencies in Swift.
- apple/swift-metrics: A Metrics API package for Swift.
Contributions welcome. Please:
- Add tests - 95%+ coverage maintained
- Follow conventions - Swift 6, strict concurrency, no force-unwraps
- Update docs - DocC comments and README updates
Areas for contribution:
- Linux support (implement WebKit renderer)
- Performance improvements
- Documentation and examples
- Bug reports with reproduction steps
Apache 2.0 - See LICENSE for details.
- Point-Free for swift-dependencies and HTML DSL foundations
- Apple for WKWebView and Swift 6
- The Swift Community for feedback and contributions