https://github.com/coenttb/swift-html-to-pdf
The Swift package for printing HTML to PDF.
https://github.com/coenttb/swift-html-to-pdf
actor-based concurrency document-generation html ios macos pdf pdf-generation pdf-printer printing server-side-swift swift swift6 wkhtmltopdf wkwebview
Last synced: 3 months ago
JSON representation
The Swift package for printing HTML to PDF.
- Host: GitHub
- URL: https://github.com/coenttb/swift-html-to-pdf
- Owner: coenttb
- License: apache-2.0
- Created: 2024-07-15T13:54:56.000Z (almost 2 years ago)
- Default Branch: main
- Last Pushed: 2026-01-27T11:53:16.000Z (4 months ago)
- Last Synced: 2026-01-27T23:38:18.348Z (4 months ago)
- Topics: actor-based, concurrency, document-generation, html, ios, macos, pdf, pdf-generation, pdf-printer, printing, server-side-swift, swift, swift6, wkhtmltopdf, wkwebview
- Language: Swift
- Homepage: https://coenttb.com
- Size: 474 KB
- Stars: 69
- Watchers: 4
- Forks: 8
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Funding: .github/FUNDING.yml
- License: LICENSE
Awesome Lists containing this project
README
# swift-html-to-pdf
[](https://github.com/coenttb/swift-html-to-pdf/actions/workflows/ci.yml)
[](https://swift.org)
[](https://github.com/coenttb/swift-html-to-pdf)
[](LICENSE)
**The fastest HTML to PDF library for Swift**
⚡ **1,939 PDFs/sec** • 💾 **35 MB memory (4-24 workers)** • 🎯 **Type-safe** • 🧪 **Swift 6**
---
## Why
Every other solution makes you choose: **fast** *or* **safe** *or* **easy**.
This library gives you all three.
```swift
@Dependency(\.pdf) var pdf
try await pdf.render(html: "
Invoice #1234
", to: fileURL)
```
**One line. Zero configuration. Production-ready.**
---
## The Numbers
### Performance
**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 Efficiency
Memory usage **doesn't scale** with concurrency:
| 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 |
**Why?** Shared WebKit infrastructure. Memory determined by pool overhead, not worker count.
*Measured empirically with 50+ PDF warmup, sustained rendering workload. See `WebViewMemoryTests.swift` for methodology.*
**Memory stays constant during extended batches:**
- 500 PDFs with 8 concurrent: Peak 98 MB, range 74 MB (includes startup), steady-state variance <5 MB
- No leaks. No accumulation. Constant memory regardless of batch size.
---
## Quick Start
### HTML String → PDF
```swift
import HtmlToPdf
import Dependencies
@Dependency(\.pdf) var pdf
// To file
try await pdf.render(html: "
Invoice #1234
", to: fileURL)
// To data (in-memory)
let pdfData = try await pdf.render(html: "
Receipt
")
// Batch processing
let html = invoices.map { "\($0.html)" }
for try await result in try await pdf.render(html: html, to: directory) {
print("Generated \(result.url)")
}
```
**That's it.** No setup. No configuration.
### Type-Safe HTML (Optional)
For compile-time safety, enable the HTML trait to use [swift-html](https://github.com/coenttb/swift-html):
**Package.swift:**
```swift
dependencies: [
.package(
url: "https://github.com/coenttb/swift-html-to-pdf.git",
from: "1.0.0",
traits: ["HTML"] // ← Enable HTML trait
)
]
```
**Usage:**
```swift
import HtmlToPdf
struct Invoice: HTMLDocument {
let number: Int
let total: Decimal
var head: some HTML {
title { "Invoice #\(number)" }
}
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)
```
**Invalid HTML?** Won't compile. **Type safety** all the way down.
---
## Why It's Different
### 1. Streaming Results
Process PDFs as they're generated. Don't wait for the batch to finish.
```swift
for try await result in try await pdf.render(html: html, to: directory) {
// This PDF is ready NOW
try await uploadToS3(result.url) // Upload immediately
try await db.markComplete(result.index) // Update database
}
```
**Benefits:** Lower latency, constant memory, real-time progress.
### 2. WebView Resource Pooling
- Pre-warmed WKWebView instances (instant availability)
- Automatic lifecycle management
- FIFO fairness under load
- Optimal concurrency: 1x CPU count (8 WebViews on 8-core Mac)
- Powered by [swift-resource-pool](https://github.com/coenttb/swift-resource-pool)
### 3. Swift 6 Strict Concurrency
- Full type safety in concurrent code
- Sendable guarantees throughout
- Actor-isolated state management
- No data races possible
---
## Installation
### Swift Package Manager
Add to your `Package.swift`:
```swift
dependencies: [
.package(url: "https://github.com/coenttb/swift-html-to-pdf.git", from: "1.0.0")
]
```
Add to your target:
```swift
.target(
name: "YourTarget",
dependencies: [
.product(name: "HtmlToPdf", package: "swift-html-to-pdf")
]
)
```
**Optional: Enable type-safe HTML DSL**
To use the [swift-html](https://github.com/coenttb/swift-html) integration, enable the HTML trait:
```swift
dependencies: [
.package(
url: "https://github.com/coenttb/swift-html-to-pdf.git",
from: "1.0.0",
traits: ["HTML"] // ← Enable HTML trait
)
]
```
### Requirements
- **Swift 6.0+**
- **macOS 14.0+** or **iOS 17.0+**
- **Xcode 16.0+**
---
## Configuration
Need to customize? Full configuration available:
```swift
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)
}
```
**Common configurations:**
- **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](Sources/HtmlToPdf/Documentation.docc/ConfigurationGuide.md) for all options.
---
## Production Metrics
Export metrics to Prometheus, StatsD, or other monitoring systems via [swift-metrics](https://github.com/apple/swift-metrics):
```swift
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` - Counter
- `htmltopdf_pdfs_failed_total` - Counter (with `reason` dimension)
- `htmltopdf_render_duration_seconds` - Timer (with `mode` dimension; p50/p95/p99)
- `htmltopdf_pool_replacements_total` - Counter
- `htmltopdf_pool_utilization` - Gauge
- `htmltopdf_throughput_pdfs_per_sec` - Gauge
---
## Documentation
- **[Getting Started Guide](Sources/HtmlToPdf/Documentation.docc/GettingStarted.md)** - Installation, basic usage, first PDF
- **[Performance Guide](Sources/HtmlToPdf/Documentation.docc/PerformanceGuide.md)** - Optimization, benchmarks, tuning
- **[Configuration Guide](Sources/HtmlToPdf/Documentation.docc/ConfigurationGuide.md)** - All configuration options
- **[API Documentation](https://coenttb.github.io/swift-html-to-pdf/)** - Full DocC documentation
Generate docs locally:
```bash
swift package generate-documentation --open
```
---
## Testing
```bash
# All tests
swift test
# Performance benchmarks
swift test --filter PerformanceBenchmarks
# Memory analysis
swift test --filter WebViewMemoryTests
# Stress tests (10K-1M PDFs)
swift test --filter StressTests
```
---
## Platform Support
| Platform | Status | Notes |
|-------------|------------------|--------------------------------------------|
| **macOS** | ✅ Full support | Optimal performance, 8 concurrent workers (8-core) |
| **iOS** | ✅ Full support | 8 concurrent workers, mobile-optimized |
| **Linux** | 🚧 Coming soon | Architecture ready, needs WebKit renderer |
| **Windows** | 🚧 Possible | Pending WebKit integration |
---
## Contributing
Contributions welcome! Please:
1. **Add tests** - We have 95%+ coverage
2. **Follow conventions** - Swift 6, strict concurrency, no force-unwraps
3. **Update docs** - DocC comments + README updates
**Areas for contribution:**
- Linux support (implement WebKit renderer)
- Performance improvements
- Documentation and examples
- Bug reports with reproduction steps
---
## Related Projects
Part of the [coenttb Swift ecosystem](https://github.com/coenttb), and optionally integrates with [swift-html](https://github.com/coenttb/swift-html) - Type-safe HTML & CSS DSL.
Built on [Point-Free](https://www.pointfree.co)'s [swift-dependencies](https://github.com/pointfreeco/swift-dependencies), and integrates with [swift-metrics](https://github.com/apple/swift-metrics).
---
## License
Apache 2.0 - See [LICENSE](LICENSE) for details.
---
## Acknowledgments
- **Point-Free** for swift-dependencies and HTML DSL foundations
- **Apple** for WKWebView and Swift 6
- **The Swift Community** for feedback and contributions
---
**Questions?**
- **Issues:** [GitHub Issues](https://github.com/coenttb/swift-html-to-pdf/issues)
- **Discussions:** [GitHub Discussions](https://github.com/coenttb/swift-html-to-pdf/discussions)
- **Email:** [coen@coenttb.com](mailto:coen@coenttb.com)
---
**Made with ❤️ by [Coen ten Thije Boonkkamp](https://coenttb.com)**
⚡ **Fast** • 💾 **Efficient** • 🎯 **Type-Safe** • 🧪 **Production-Ready**