Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,159 @@ let encryptedFromURLs = try await client.encryptPDFs(
- **User Password**: Required to open and view the PDF (required for encryption)
- **Owner Password**: Required for full access (editing, copying, printing) (optional)

### PDF File Embedding

GotenbergKit supports embedding files within PDFs in two ways:

1. **During Conversion**: Embed files while generating PDFs from HTML, Markdown, or office documents
2. **Post-Processing**: Use the dedicated embed route to add files to existing PDFs

This enables compliance with standards like ZUGFeRD/Factur-X that require XML invoices and other attachments to be embedded in the PDF.

#### 1. Embed Files During Conversion

```swift
// Create invoice XML for ZUGFeRD/Factur-X compliance
let invoiceXML = """
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<ExchangedDocument>
<ID>INV-12345</ID>
<TypeCode>380</TypeCode>
</ExchangedDocument>
</Invoice>
""".data(using: .utf8)!

let metadataJSON = """
{
"invoice_number": "INV-12345",
"amount": 1000.00,
"currency": "USD"
}
""".data(using: .utf8)!

// Embed files during HTML to PDF conversion
let options = ChromiumOptions(
embeds: [
"invoice.xml": invoiceXML,
"metadata.json": metadataJSON,
"logo.png": try Data(contentsOf: logoURL)
]
)

let response = try await client.convert(
html: htmlContent.data(using: .utf8)!,
options: options
)

// Embed files during LibreOffice conversion
let libreOptions = LibreOfficeConversionOptions(
embeds: [
"factur-x.xml": facturXData,
"additional-info.pdf": additionalPdfData
]
)

let response = try await client.convertWithLibreOffice(
documents: ["invoice.docx": docData],
options: libreOptions
)
```

#### 2. Embed Files During PDF Processing

```swift
// Embed files when encrypting existing PDFs
let pdfOptions = PDFEngineOptions(
userPassword: "password123",
embeds: [
"invoice.xml": invoiceXMLData,
"receipt.pdf": receiptPdfData
]
)

let encryptedResponse = try await client.encryptPDFs(
documents: ["document.pdf": pdfData],
options: pdfOptions
)

// Embed files when merging PDFs
let mergeOptions = PDFEngineOptions(
embeds: [
"source-data.xml": xmlData,
"attachments.zip": zipData
]
)

let mergedResponse = try await client.mergeWithPDFEngines(
documents: ["doc1.pdf": pdf1Data, "doc2.pdf": pdf2Data],
options: mergeOptions
)
```

#### 3. Dedicated Embed Route

For adding files to existing PDFs, use the dedicated `embedFiles` method that utilizes Gotenberg's `/forms/pdfengines/embed` route:

```swift
// Create files to embed
let invoiceXML = """
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<ExchangedDocument>
<ID>INV-2025-001</ID>
<TypeCode>380</TypeCode>
</ExchangedDocument>
</Invoice>
""".data(using: .utf8)!

let metadataJSON = """
{
"invoice_id": "INV-2025-001",
"amount": 1500.00,
"currency": "USD"
}
""".data(using: .utf8)!

// Embed files into existing PDFs
let embedOptions = PDFEngineOptions(
metadata: Metadata(
author: "Invoice System",
title: "Invoice with Embedded Data",
subject: "ZUGFeRD Compliant Invoice"
),
embeds: [
"factur-x.xml": invoiceXML,
"invoice-metadata.json": metadataJSON
]
)

let embeddedResponse = try await client.embedFiles(
documents: ["invoice.pdf": existingPdfData],
options: embedOptions
)

// Embed files from URLs
let embeddedFromUrls = try await client.embedFiles(
urls: [DownloadFrom(url: "https://example.com/document.pdf")],
options: embedOptions
)
```

**Key Features:**
- **Multiple Files**: Embed multiple files of different types (XML, JSON, PDF, images, etc.)
- **Standards Compliance**: Perfect for ZUGFeRD/Factur-X invoice standards
- **Two Approaches**: Embed during conversion or post-process existing PDFs
- **Flexible Input**: Support for file data and URLs
- **Metadata Control**: Override PDF metadata during embedding
- **Preserved Attachments**: Embedded files can be extracted by PDF readers that support attachments

**Use Cases:**
- **Electronic Invoicing**: ZUGFeRD/Factur-X compliant invoices with embedded XML
- **Legal Documents**: Contracts with embedded supporting documents
- **Document Archival**: Add source files to final PDFs
- **Compliance**: Meet regulatory requirements for embedded attachments
- **Post-Processing**: Add files to existing PDF workflows

### Authentication

Expand Down
7 changes: 6 additions & 1 deletion Sources/GotenbergKit/Chromium/ChromiumOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Stevenson Michel on 4/12/25.
//

import Foundation
import Logging

import class Foundation.JSONEncoder
Expand Down Expand Up @@ -111,6 +112,8 @@ public struct ChromiumOptions: Sendable {
public var userPassword: String?
/// Password for full access on the resulting PDF
public var ownerPassword: String?
/// Files to embed in the generated PDF (for ZUGFeRD/Factur-X compliance)
public var embeds: [String: Data]

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

Expand Down Expand Up @@ -148,7 +151,8 @@ public struct ChromiumOptions: Sendable {
metadata: Metadata? = nil,
generateTaggedPdf: Bool = false,
userPassword: String? = nil,
ownerPassword: String? = nil
ownerPassword: String? = nil,
embeds: [String: Data] = [:]
) {
self.singlePage = singlePage
self.paperWidth = paperWidth
Expand Down Expand Up @@ -184,6 +188,7 @@ public struct ChromiumOptions: Sendable {
self.generateTaggedPdf = generateTaggedPdf
self.userPassword = userPassword
self.ownerPassword = ownerPassword
self.embeds = embeds
}

var formValues: [String: String] {
Expand Down
20 changes: 20 additions & 0 deletions Sources/GotenbergKit/Chromium/GotenbergClient+Chromium.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ extension GotenbergClient {
)
}

// Add embed files
let embedFiles = processEmbedFiles(options.embeds)
files.append(contentsOf: embedFiles)

return try await sendFormRequest(
route: "/forms/chromium/convert/html",
files: files,
Expand Down Expand Up @@ -134,6 +138,10 @@ extension GotenbergClient {
)
}

// Add embed files
let embedFiles = processEmbedFiles(options.embeds)
files.append(contentsOf: embedFiles)

return try await sendFormRequest(
route: "/forms/chromium/convert/url",
files: files,
Expand Down Expand Up @@ -193,6 +201,10 @@ extension GotenbergClient {
)
}

// Add embed files
let embedFiles = processEmbedFiles(options.embeds)
files.append(contentsOf: embedFiles)

return try await sendFormRequest(
route: "/forms/chromium/convert/html",
files: files,
Expand Down Expand Up @@ -252,6 +264,10 @@ extension GotenbergClient {
)
}

// Add embed files
let embedFiles = processEmbedFiles(options.embeds)
files.append(contentsOf: embedFiles)

return try await sendFormRequest(
route: "/forms/chromium/convert/markdown",
files: files,
Expand Down Expand Up @@ -310,6 +326,10 @@ extension GotenbergClient {
)
}

// Add embed files
let embedFiles = processEmbedFiles(options.embeds)
formFiles.append(contentsOf: embedFiles)

return try await sendFormRequest(
route: "/forms/chromium/convert/markdown",
files: formFiles,
Expand Down
15 changes: 15 additions & 0 deletions Sources/GotenbergKit/GotenbergClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,21 @@ public struct GotenbergClient: Sendable {
)
throw GotenbergError.apiError(statusCode: response.status.code, message: errorMessage)
}

}

/// Helper function to process embed files into FormFile array
/// - Parameter embeds: Dictionary of filename to file data for embedding
/// - Returns: Array of FormFile objects for embed files
internal func processEmbedFiles(_ embeds: [String: Data]) -> [FormFile] {
embeds.map { filename, data in
FormFile(
name: "embeds",
filename: filename,
contentType: contentTypeForFilename(filename),
data: data
)
}
}

/// Convert an HTTPClientResponse into data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ extension GotenbergClient {
logger.debug("Document size: \(data.count) bytes")
}

// Add embed files
let embedFiles = processEmbedFiles(options.embeds)
files.append(contentsOf: embedFiles)

let values = options.formValues

return try await sendFormRequest(
Expand Down Expand Up @@ -87,9 +91,12 @@ extension GotenbergClient {
var values = options.formValues
values["downloadFrom"] = jsonString

// Add embed files
let embedFiles = processEmbedFiles(options.embeds)

return try await sendFormRequest(
route: "/forms/libreoffice/convert",
files: [],
files: embedFiles,
values: values,
headers: clientHTTPHeaders,
timeoutSeconds: Int64(waitTimeout)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
// LibreOfficeConversionOptions.swift
// gotenberg-kit
//
// Created by Stevenson Michel on 4/11/25.
// Created by Stevenson Michel on 4/14/25.
//

import Foundation
import Logging

import class Foundation.JSONEncoder
Expand Down Expand Up @@ -98,6 +99,8 @@ public struct LibreOfficeConversionOptions {
public var userPassword: String?
/// Password for full access on the resulting PDF
public var ownerPassword: String?
/// Files to embed in the generated PDF (for ZUGFeRD/Factur-X compliance)
public var embeds: [String: Data]

public struct PageRange {
public let from: Int
Expand Down Expand Up @@ -151,7 +154,8 @@ public struct LibreOfficeConversionOptions {
metadata: Metadata? = nil,
flatten: Bool = false,
userPassword: String? = nil,
ownerPassword: String? = nil
ownerPassword: String? = nil,
embeds: [String: Data] = [:]
) {
self.password = password
self.landscape = landscape
Expand Down Expand Up @@ -185,6 +189,7 @@ public struct LibreOfficeConversionOptions {
self.flatten = flatten
self.userPassword = userPassword
self.ownerPassword = ownerPassword
self.embeds = embeds
}

/// Convert options to form values for the API request
Expand Down
Loading
Loading