@capgo/capacitor-pdf-generator 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,292 @@
1
+ import Capacitor
2
+ import Foundation
3
+ import WebKit
4
+
5
+ @objc(PdfGeneratorPlugin)
6
+ public class PdfGeneratorPlugin: CAPPlugin, CAPBridgedPlugin {
7
+ public let identifier = "PdfGeneratorPlugin"
8
+ public let jsName = "PdfGenerator"
9
+ public let pluginMethods: [CAPPluginMethod] = [
10
+ CAPPluginMethod(name: "fromURL", returnType: CAPPluginReturnPromise),
11
+ CAPPluginMethod(name: "fromData", returnType: CAPPluginReturnPromise),
12
+ ]
13
+
14
+ private var tasks: [PdfGenerationTask] = []
15
+
16
+ @objc func fromURL(_ call: CAPPluginCall) {
17
+ guard let urlString = call.getString("url"), let url = URL(string: urlString) else {
18
+ call.reject("A valid 'url' is required.")
19
+ return
20
+ }
21
+
22
+ let options = PdfGeneratorOptions(from: call)
23
+ let task = PdfGenerationTask(plugin: self, call: call, source: .url(url), options: options)
24
+ enqueue(task)
25
+ }
26
+
27
+ @objc func fromData(_ call: CAPPluginCall) {
28
+ guard let htmlData = call.getString("data") else {
29
+ call.reject("The 'data' option is required.")
30
+ return
31
+ }
32
+
33
+ let options = PdfGeneratorOptions(from: call)
34
+ let task = PdfGenerationTask(plugin: self, call: call, source: .html(htmlData, options.baseUrl), options: options)
35
+ enqueue(task)
36
+ }
37
+
38
+ private func enqueue(_ task: PdfGenerationTask) {
39
+ tasks.append(task)
40
+ task.start()
41
+ }
42
+
43
+ fileprivate func taskDidComplete(_ task: PdfGenerationTask) {
44
+ tasks.removeAll { $0 === task }
45
+ }
46
+
47
+ fileprivate func handle(pdfData: Data, for task: PdfGenerationTask) {
48
+ switch task.options.outputType {
49
+ case .base64:
50
+ resolveBase64(data: pdfData, for: task)
51
+ case .share:
52
+ share(pdfData: pdfData, for: task)
53
+ }
54
+ }
55
+
56
+ private func resolveBase64(data: Data, for task: PdfGenerationTask) {
57
+ let base64 = data.base64EncodedString()
58
+ DispatchQueue.main.async {
59
+ task.call.resolve([
60
+ "type": "base64",
61
+ "base64": base64,
62
+ ])
63
+ task.finish()
64
+ }
65
+ }
66
+
67
+ private func share(pdfData: Data, for task: PdfGenerationTask) {
68
+ let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(task.options.fileName)
69
+
70
+ do {
71
+ try pdfData.write(to: temporaryURL, options: .atomic)
72
+ } catch {
73
+ task.call.reject("Failed to write PDF data: \(error.localizedDescription)")
74
+ task.finish()
75
+ return
76
+ }
77
+
78
+ DispatchQueue.main.async { [weak self] in
79
+ guard let self = self, let presenter = self.bridge?.viewController else {
80
+ task.call.reject("Unable to present share sheet.")
81
+ try? FileManager.default.removeItem(at: temporaryURL)
82
+ task.finish()
83
+ return
84
+ }
85
+
86
+ let activity = UIActivityViewController(activityItems: [temporaryURL], applicationActivities: nil)
87
+ activity.completionWithItemsHandler = { _, completed, _, error in
88
+ if let error = error {
89
+ task.call.reject("Share failed: \(error.localizedDescription)")
90
+ } else {
91
+ task.call.resolve([
92
+ "type": "share",
93
+ "completed": completed,
94
+ ])
95
+ }
96
+ try? FileManager.default.removeItem(at: temporaryURL)
97
+ task.finish()
98
+ }
99
+
100
+ if let popover = activity.popoverPresentationController, let view = presenter.view {
101
+ popover.sourceView = view
102
+ popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
103
+ popover.permittedArrowDirections = []
104
+ }
105
+
106
+ presenter.present(activity, animated: true)
107
+ }
108
+ }
109
+ }
110
+
111
+ private final class PdfGenerationTask: NSObject, WKNavigationDelegate {
112
+ enum Source {
113
+ case url(URL)
114
+ case html(String, URL?)
115
+ }
116
+
117
+ let call: CAPPluginCall
118
+ let options: PdfGeneratorOptions
119
+
120
+ private weak var plugin: PdfGeneratorPlugin?
121
+ private let source: Source
122
+ private let webView: WKWebView
123
+ private var didFinish = false
124
+
125
+ init(plugin: PdfGeneratorPlugin, call: CAPPluginCall, source: Source, options: PdfGeneratorOptions) {
126
+ self.plugin = plugin
127
+ self.call = call
128
+ self.source = source
129
+ self.options = options
130
+ let configuration = WKWebViewConfiguration()
131
+ configuration.preferences.javaScriptEnabled = true
132
+ self.webView = WKWebView(frame: .zero, configuration: configuration)
133
+ super.init()
134
+ self.webView.navigationDelegate = self
135
+ }
136
+
137
+ func start() {
138
+ DispatchQueue.main.async {
139
+ switch self.source {
140
+ case let .url(url):
141
+ let request = URLRequest(url: url)
142
+ self.webView.load(request)
143
+ case let .html(html, baseUrl):
144
+ self.webView.loadHTMLString(html, baseURL: baseUrl)
145
+ }
146
+ }
147
+ }
148
+
149
+ func finish() {
150
+ guard !didFinish else { return }
151
+ didFinish = true
152
+ cleanup()
153
+ }
154
+
155
+ private func cleanup() {
156
+ DispatchQueue.main.async {
157
+ self.webView.stopLoading()
158
+ self.webView.navigationDelegate = nil
159
+ self.webView.removeFromSuperview()
160
+ }
161
+ plugin?.taskDidComplete(self)
162
+ }
163
+
164
+ private func fail(with message: String) {
165
+ guard !didFinish else { return }
166
+ didFinish = true
167
+ DispatchQueue.main.async {
168
+ self.call.reject(message)
169
+ }
170
+ cleanup()
171
+ }
172
+
173
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
174
+ generatePdf()
175
+ }
176
+
177
+ func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
178
+ fail(with: "Failed to load content: \(error.localizedDescription)")
179
+ }
180
+
181
+ func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
182
+ fail(with: "Failed to load content: \(error.localizedDescription)")
183
+ }
184
+
185
+ private func generatePdf() {
186
+ let configuration = WKPDFConfiguration()
187
+ configuration.rect = CGRect(origin: .zero, size: options.pageSize)
188
+
189
+ webView.createPDF(configuration: configuration) { [weak self] result in
190
+ guard let self else { return }
191
+ switch result {
192
+ case let .success(data):
193
+ self.plugin?.handle(pdfData: data, for: self)
194
+ case let .failure(error):
195
+ self.fail(with: "Failed to generate PDF: \(error.localizedDescription)")
196
+ }
197
+ }
198
+ }
199
+ }
200
+
201
+ private struct PdfGeneratorOptions {
202
+ enum OutputType {
203
+ case base64
204
+ case share
205
+
206
+ init(string: String?) {
207
+ switch string?.lowercased() {
208
+ case "share":
209
+ self = .share
210
+ default:
211
+ self = .base64
212
+ }
213
+ }
214
+ }
215
+
216
+ enum DocumentSize {
217
+ case a3
218
+ case a4
219
+
220
+ init(string: String?) {
221
+ switch string?.uppercased() {
222
+ case "A3":
223
+ self = .a3
224
+ default:
225
+ self = .a4
226
+ }
227
+ }
228
+
229
+ private var portraitSize: CGSize {
230
+ let pointsPerMillimetre = 72.0 / 25.4
231
+ switch self {
232
+ case .a3:
233
+ return CGSize(width: 297.0 * pointsPerMillimetre, height: 420.0 * pointsPerMillimetre)
234
+ case .a4:
235
+ return CGSize(width: 210.0 * pointsPerMillimetre, height: 297.0 * pointsPerMillimetre)
236
+ }
237
+ }
238
+
239
+ func size(isLandscape: Bool) -> CGSize {
240
+ let size = portraitSize
241
+ return isLandscape ? CGSize(width: size.height, height: size.width) : size
242
+ }
243
+ }
244
+
245
+ let documentSize: DocumentSize
246
+ let isLandscape: Bool
247
+ let outputType: OutputType
248
+ let fileName: String
249
+ let baseUrl: URL?
250
+
251
+ var pageSize: CGSize {
252
+ documentSize.size(isLandscape: isLandscape)
253
+ }
254
+
255
+ init(from call: CAPPluginCall) {
256
+ let orientationValue = PdfGeneratorOptions.orientationString(from: call)
257
+ self.documentSize = DocumentSize(string: call.getString("documentSize"))
258
+ self.isLandscape = orientationValue == "landscape"
259
+ self.outputType = OutputType(string: call.getString("type"))
260
+ self.fileName = PdfGeneratorOptions.sanitizedFileName(from: call.getString("fileName"))
261
+ self.baseUrl = PdfGeneratorOptions.baseUrl(from: call.getString("baseUrl"))
262
+ }
263
+
264
+ private static func orientationString(from call: CAPPluginCall) -> String {
265
+ if let landscapeFlag = call.options["landscape"] as? Bool {
266
+ return landscapeFlag ? "landscape" : "portrait"
267
+ }
268
+
269
+ let orientation = call.getString("orientation") ?? call.getString("landscape") ?? "portrait"
270
+ return orientation.lowercased()
271
+ }
272
+
273
+ private static func sanitizedFileName(from raw: String?) -> String {
274
+ let trimmed = raw?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
275
+ var name = trimmed.isEmpty ? "default.pdf" : trimmed
276
+ name = name.replacingOccurrences(of: "[/\\\\:]", with: "_", options: .regularExpression)
277
+ if !name.lowercased().hasSuffix(".pdf") {
278
+ name += ".pdf"
279
+ }
280
+ return name
281
+ }
282
+
283
+ private static func baseUrl(from raw: String?) -> URL? {
284
+ guard let raw = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else {
285
+ return nil
286
+ }
287
+ if raw == "BUNDLE" {
288
+ return Bundle.main.bundleURL
289
+ }
290
+ return URL(string: raw)
291
+ }
292
+ }
@@ -0,0 +1,7 @@
1
+ import XCTest
2
+
3
+ final class PdfGeneratorPluginTests: XCTestCase {
4
+ func testExample() {
5
+ XCTAssertTrue(true)
6
+ }
7
+ }
package/package.json ADDED
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@capgo/capacitor-pdf-generator",
3
+ "version": "7.0.0",
4
+ "description": "Generate PDF files from HTML strings or URLs on iOS and Android.",
5
+ "main": "dist/plugin.cjs.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "unpkg": "dist/plugin.js",
9
+ "files": [
10
+ "android/src/main/",
11
+ "android/build.gradle",
12
+ "dist/",
13
+ "ios/Sources",
14
+ "ios/Tests",
15
+ "Package.swift",
16
+ "CapgoCapacitorPdfGenerator.podspec"
17
+ ],
18
+ "author": "Martin Donadieu <martin@capgo.app>",
19
+ "license": "MIT",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/Cap-go/capacitor-pdf-generator.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/Cap-go/pdf-generator/issues"
26
+ },
27
+ "keywords": [
28
+ "capacitor",
29
+ "plugin",
30
+ "pdf",
31
+ "html",
32
+ "generator"
33
+ ],
34
+ "scripts": {
35
+ "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
36
+ "verify:ios": "xcodebuild -scheme CapgoCapacitorPdfGenerator -destination generic/platform=iOS",
37
+ "verify:android": "cd android && ./gradlew clean build test && cd ..",
38
+ "verify:web": "npm run build",
39
+ "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
40
+ "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --autocorrect --format",
41
+ "eslint": "eslint . --ext .ts",
42
+ "prettier": "prettier \"**/*.{css,html,ts,js,java,kt,swift}\" --plugin=prettier-plugin-java",
43
+ "swiftlint": "node-swiftlint",
44
+ "docgen": "docgen --api PdfGeneratorPlugin --output-readme README.md --output-json dist/docs.json",
45
+ "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
46
+ "clean": "rimraf ./dist",
47
+ "watch": "tsc --watch",
48
+ "prepublishOnly": "npm run build"
49
+ },
50
+ "devDependencies": {
51
+ "@capacitor/android": "^7.0.0",
52
+ "@capacitor/cli": "^7.0.0",
53
+ "@capacitor/core": "^7.0.0",
54
+ "@capacitor/docgen": "^0.3.0",
55
+ "@capacitor/ios": "^7.0.0",
56
+ "@ionic/eslint-config": "^0.4.0",
57
+ "@ionic/prettier-config": "^4.0.0",
58
+ "@ionic/swiftlint-config": "^2.0.0",
59
+ "@types/node": "^22.13.1",
60
+ "eslint": "^8.57.0",
61
+ "eslint-plugin-import": "^2.31.0",
62
+ "husky": "^9.1.7",
63
+ "prettier": "^3.4.2",
64
+ "prettier-plugin-java": "^2.6.7",
65
+ "rimraf": "^6.0.1",
66
+ "rollup": "^4.34.6",
67
+ "swiftlint": "^2.0.0",
68
+ "typescript": "^5.7.3"
69
+ },
70
+ "peerDependencies": {
71
+ "@capacitor/core": ">=7.0.0"
72
+ },
73
+ "eslintConfig": {
74
+ "extends": "@ionic/eslint-config/recommended"
75
+ },
76
+ "prettier": "@ionic/prettier-config",
77
+ "swiftlint": "@ionic/swiftlint-config",
78
+ "capacitor": {
79
+ "ios": {
80
+ "src": "ios"
81
+ },
82
+ "android": {
83
+ "src": "android"
84
+ }
85
+ }
86
+ }