Skip to content

Commit ee25644

Browse files
authored
Add PDF encryption support and tests (#33)
Introduces password protection (user and owner passwords) for PDFs across Chromium, LibreOffice, and PDF engine options. Adds dedicated encryptPDFs methods to GotenbergClient for both file and URL sources. Updates error handling for missing parameters, extends documentation with encryption usage and best practices, and adds comprehensive tests for encryption features. Also updates Docker image to gotenberg/gotenberg:8.25 and refines docker-compose configuration.
1 parent ad8988f commit ee25644

File tree

11 files changed

+711
-34
lines changed

11 files changed

+711
-34
lines changed

README.md

Lines changed: 155 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ A modern Swift SDK for [Gotenberg](https://gotenberg.dev/) that provides a type-
1515
- 📄 **Multiple input formats** (HTML, Markdown, URLs, Office docs)
1616
- 🖼️ **Screenshot capture** from web pages
1717
- 📋 **PDF manipulation** (merge, split, metadata)
18-
- 🔐 **Authentication support** (Basic Auth + custom headers)
18+
- 🔐 **PDF encryption** (during conversion + dedicated endpoint)
19+
- 🔑 **Authentication support** (Basic Auth + custom headers)
1920
- 📱 **Cross-platform** (iOS, macOS, Linux, Windows)
2021

2122
## Quick Start
@@ -174,7 +175,7 @@ let response = try await client.splitPDF(
174175
)
175176
```
176177

177-
#### PDF Metadata
178+
### PDF Metadata
178179

179180
Read and write PDF metadata:
180181

@@ -196,6 +197,116 @@ let response = try await client.writePDFMetadata(
196197
)
197198
```
198199

200+
### PDF Encryption
201+
202+
GotenbergKit provides two ways to encrypt PDFs with password protection:
203+
204+
#### 1. Encrypt During Conversion
205+
206+
Add password protection while converting documents to PDF:
207+
208+
```swift
209+
// Encrypt HTML to PDF with both user and owner passwords
210+
let options = ConversionOptions(
211+
userPassword: "user123", // Password for opening/viewing the PDF
212+
ownerPassword: "owner456" // Password for full access/editing
213+
)
214+
215+
let response = try await client.convertHTMLToPDF(
216+
htmlContent: "<h1>Confidential Document</h1>",
217+
options: options
218+
)
219+
220+
// Encrypt office documents
221+
let libreOptions = LibreOfficeConversionOptions(
222+
password: "source_password", // Password for opening source file (if encrypted)
223+
userPassword: "view_password", // Password for viewing output PDF
224+
ownerPassword: "edit_password" // Password for editing output PDF
225+
)
226+
227+
let response = try await client.convertWithLibreOffice(
228+
documents: ["confidential.docx": docData],
229+
options: libreOptions
230+
)
231+
232+
// Encrypt when processing existing PDFs
233+
let pdfOptions = PDFEngineOptions(
234+
userPassword: "reader_access",
235+
ownerPassword: "full_access"
236+
)
237+
238+
let encryptedPDF = try await client.mergeWithPDFEngines(
239+
documents: ["file1.pdf": data1, "file2.pdf": data2],
240+
options: pdfOptions
241+
)
242+
```
243+
244+
#### 2. Encrypt Existing PDFs
245+
246+
Use the dedicated encryption endpoint to add password protection to existing PDF files with full metadata override support:
247+
248+
```swift
249+
// Basic encryption with passwords only
250+
let encryptedResponse = try await client.encryptPDFs(
251+
documents: [
252+
"document1.pdf": try Data(contentsOf: pdf1URL),
253+
"document2.pdf": try Data(contentsOf: pdf2URL)
254+
],
255+
options: PDFEngineOptions(
256+
userPassword: "viewer_password",
257+
ownerPassword: "admin_password" // Optional
258+
)
259+
)
260+
261+
// Encrypt with custom metadata override
262+
let customMetadata = Metadata(
263+
author: "Secure Author",
264+
copyright: "Company Confidential",
265+
creator: "GotenbergKit",
266+
marked: true,
267+
producer: "Swift PDF Processor",
268+
subject: "Encrypted Document",
269+
title: "Confidential Report"
270+
)
271+
272+
let encryptedWithMetadata = try await client.encryptPDFs(
273+
documents: ["report.pdf": reportData],
274+
options: PDFEngineOptions(
275+
metadata: customMetadata, // Override metadata during encryption
276+
userPassword: "user123",
277+
ownerPassword: "owner456",
278+
flatten: true, // Additional PDF processing options
279+
pdfua: true
280+
)
281+
)
282+
283+
// Encrypt PDFs from URLs
284+
let pdfURLs = [
285+
DownloadFrom(url: "https://example.com/file1.pdf"),
286+
DownloadFrom(url: "https://example.com/file2.pdf")
287+
]
288+
289+
let encryptedFromURLs = try await client.encryptPDFs(
290+
urls: pdfURLs,
291+
options: PDFEngineOptions(
292+
userPassword: "required_password"
293+
)
294+
)
295+
```
296+
297+
**Key Features:**
298+
- **Password Protection**: User password (required) and owner password (optional)
299+
- **Metadata Override**: Set custom metadata during encryption process
300+
- **PDF Processing**: Support for flattening, PDF/UA compliance, and format options
301+
- **Flexible Input**: Encrypt from file data or URLs
302+
- **Consistent API**: Uses PDFEngineOptions like other PDF operations
303+
304+
**Password Types:**
305+
- **User Password**: Required to open and view the PDF (required for encryption)
306+
- **Owner Password**: Required for full access (editing, copying, printing) (optional)
307+
308+
309+
199310
### Authentication
200311

201312
#### Basic Authentication
@@ -272,6 +383,48 @@ let response = try await client.convertHTMLToPDF(
272383
)
273384
```
274385

386+
#### Encryption Best Practices
387+
388+
```swift
389+
// Use strong, unique passwords for conversion
390+
let options = ConversionOptions(
391+
userPassword: generateSecurePassword(length: 12),
392+
ownerPassword: generateSecurePassword(length: 16)
393+
)
394+
395+
// For dedicated encryption endpoint with metadata control
396+
let encryptionOptions = PDFEngineOptions(
397+
metadata: Metadata(
398+
author: "Document Owner",
399+
copyright: "Confidential",
400+
creator: "Secure App",
401+
marked: true,
402+
subject: "Encrypted Content",
403+
title: "Protected Document"
404+
),
405+
userPassword: generateSecurePassword(length: 12),
406+
ownerPassword: generateSecurePassword(length: 16),
407+
flatten: true // Prevent form modifications
408+
)
409+
410+
// User password only (allows viewing)
411+
let viewOnlyOptions = PDFEngineOptions(
412+
userPassword: "view_password"
413+
)
414+
415+
// Both passwords for maximum control
416+
let secureOptions = PDFEngineOptions(
417+
userPassword: "user_access", // Required to open
418+
ownerPassword: "admin_access" // Full permissions
419+
)
420+
421+
// Apply encryption to existing PDFs
422+
let encrypted = try await client.encryptPDFs(
423+
documents: ["sensitive.pdf": pdfData],
424+
options: encryptionOptions
425+
)
426+
```
427+
275428
## Advanced Features
276429

277430
### Batch Processing

Sources/GotenbergKit/Chromium/ChromiumOptions.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ public struct ChromiumOptions: Sendable {
107107
public var metadata: Metadata?
108108
/// Tags provide a logical structure that governs how the content of the PDF is presented through assistive technology
109109
public var generateTaggedPdf: Bool
110+
/// Password for opening the resulting PDF
111+
public var userPassword: String?
112+
/// Password for full access on the resulting PDF
113+
public var ownerPassword: String?
110114

111115
private let logger = Logger(label: "com.gotenbergkit.chromiumoptions")
112116

@@ -142,7 +146,9 @@ public struct ChromiumOptions: Sendable {
142146
pdfFormat: PDFFormat? = nil,
143147
pdfua: Bool = false,
144148
metadata: Metadata? = nil,
145-
generateTaggedPdf: Bool = false
149+
generateTaggedPdf: Bool = false,
150+
userPassword: String? = nil,
151+
ownerPassword: String? = nil
146152
) {
147153
self.singlePage = singlePage
148154
self.paperWidth = paperWidth
@@ -176,6 +182,8 @@ public struct ChromiumOptions: Sendable {
176182
self.pdfua = pdfua
177183
self.metadata = metadata
178184
self.generateTaggedPdf = generateTaggedPdf
185+
self.userPassword = userPassword
186+
self.ownerPassword = ownerPassword
179187
}
180188

181189
var formValues: [String: String] {
@@ -281,6 +289,14 @@ public struct ChromiumOptions: Sendable {
281289
}
282290
}
283291

292+
if let userPassword = userPassword {
293+
values["userPassword"] = userPassword
294+
}
295+
296+
if let ownerPassword = ownerPassword {
297+
values["ownerPassword"] = ownerPassword
298+
}
299+
284300
return values
285301
}
286302
}

Sources/GotenbergKit/Common/GotenbergError.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public enum GotenbergError: Error {
2020
case paperHeightTooSmall
2121
case marginTooSmall
2222
case pageRangeInvalid
23+
case missingRequiredParameter(String)
2324

2425
public var errorDescription: String {
2526
switch self {
@@ -45,6 +46,8 @@ public enum GotenbergError: Error {
4546
return "Margin must be at least 0 inches"
4647
case .pageRangeInvalid:
4748
return "Page range must be in format start-end abd with positive integers for start and end and start <= end."
49+
case .missingRequiredParameter(let parameter):
50+
return "Missing required parameter: \(parameter)"
4851
}
4952
}
5053
}

Sources/GotenbergKit/LibreOffice/LibreOfficeConversionOptions.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ public struct LibreOfficeConversionOptions {
9494
/// Flatten the resulting PDF.
9595
/// default false
9696
public var flatten: Bool
97+
/// Password for opening the resulting PDF
98+
public var userPassword: String?
99+
/// Password for full access on the resulting PDF
100+
public var ownerPassword: String?
97101

98102
public struct PageRange {
99103
public let from: Int
@@ -145,7 +149,9 @@ public struct LibreOfficeConversionOptions {
145149
pdfFormat: PDFFormat? = nil,
146150
pdfua: Bool = false,
147151
metadata: Metadata? = nil,
148-
flatten: Bool = false
152+
flatten: Bool = false,
153+
userPassword: String? = nil,
154+
ownerPassword: String? = nil
149155
) {
150156
self.password = password
151157
self.landscape = landscape
@@ -177,6 +183,8 @@ public struct LibreOfficeConversionOptions {
177183
self.pdfua = pdfua
178184
self.metadata = metadata
179185
self.flatten = flatten
186+
self.userPassword = userPassword
187+
self.ownerPassword = ownerPassword
180188
}
181189

182190
/// Convert options to form values for the API request
@@ -258,14 +266,22 @@ public struct LibreOfficeConversionOptions {
258266
values["metadata"] = String(decoding: data, as: UTF8.self)
259267
} catch {
260268
logger.error(
261-
"Error serializing metadata",
269+
"Failed to serialize metadata",
262270
metadata: [
263271
"error": .string(error.localizedDescription)
264272
]
265273
)
266274
}
267275
}
268276

277+
if let userPassword = userPassword {
278+
values["userPassword"] = userPassword
279+
}
280+
281+
if let ownerPassword = ownerPassword {
282+
values["ownerPassword"] = ownerPassword
283+
}
284+
269285
return values
270286
}
271287
}

0 commit comments

Comments
 (0)