diff --git a/Package.pins b/Package.pins deleted file mode 100644 index cc9d30e..0000000 --- a/Package.pins +++ /dev/null @@ -1,102 +0,0 @@ -{ - "autoPin": true, - "pins": [ - { - "package": "BCrypt", - "reason": null, - "repositoryURL": "https://github.com/vapor/bcrypt.git", - "version": "1.0.0" - }, - { - "package": "Bits", - "reason": null, - "repositoryURL": "https://github.com/vapor/bits.git", - "version": "1.0.0" - }, - { - "package": "Console", - "reason": null, - "repositoryURL": "https://github.com/vapor/console.git", - "version": "2.0.0" - }, - { - "package": "Core", - "reason": null, - "repositoryURL": "https://github.com/vapor/core.git", - "version": "2.0.0" - }, - { - "package": "Crypto", - "reason": null, - "repositoryURL": "https://github.com/vapor/crypto.git", - "version": "2.0.0" - }, - { - "package": "CTLS", - "reason": null, - "repositoryURL": "https://github.com/vapor/ctls.git", - "version": "1.0.0" - }, - { - "package": "Debugging", - "reason": null, - "repositoryURL": "https://github.com/vapor/debugging.git", - "version": "1.0.0" - }, - { - "package": "Engine", - "reason": null, - "repositoryURL": "https://github.com/vapor/engine.git", - "version": "2.0.0" - }, - { - "package": "JSON", - "reason": null, - "repositoryURL": "https://github.com/vapor/json.git", - "version": "2.0.0" - }, - { - "package": "Multipart", - "reason": null, - "repositoryURL": "https://github.com/vapor/multipart.git", - "version": "2.0.0" - }, - { - "package": "Node", - "reason": null, - "repositoryURL": "https://github.com/vapor/node.git", - "version": "2.0.0" - }, - { - "package": "Random", - "reason": null, - "repositoryURL": "https://github.com/vapor/random.git", - "version": "1.0.0" - }, - { - "package": "Routing", - "reason": null, - "repositoryURL": "https://github.com/vapor/routing.git", - "version": "2.0.0" - }, - { - "package": "Sockets", - "reason": null, - "repositoryURL": "https://github.com/vapor/sockets.git", - "version": "2.0.0" - }, - { - "package": "TLS", - "reason": null, - "repositoryURL": "https://github.com/vapor/tls.git", - "version": "2.0.0" - }, - { - "package": "Vapor", - "reason": null, - "repositoryURL": "https://github.com/vapor/vapor.git", - "version": "2.0.0" - } - ], - "version": 1 -} \ No newline at end of file diff --git a/Package.swift b/Package.swift index befbd42..14a86b4 100644 --- a/Package.swift +++ b/Package.swift @@ -1,8 +1,25 @@ +// swift-tools-version:4.2 import PackageDescription let package = Package( name: "wkhtmltopdf", + products: [ + .library( + name: "wkhtmltopdf", + targets: ["wkhtmltopdf"]), + ], dependencies: [ - .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2), + .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), + .package(url: "https://github.com/vapor/service.git", from: "1.0.0") + ], + targets: [ + .target( + name: "wkhtmltopdf", + dependencies: [ + "Service" + ]), + .testTarget( + name: "wkhtmltopdfTests", + dependencies: ["wkhtmltopdf", "Vapor"]), ] ) diff --git a/README.md b/README.md index de5be57..2dc842f 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,19 @@ # wkhtmltopdf -![Swift](http://img.shields.io/badge/swift-3.1-brightgreen.svg) -![Vapor](http://img.shields.io/badge/vapor-2.0-brightgreen.svg) +![Swift](http://img.shields.io/badge/swift-4.2-brightgreen.svg) +![Vapor](http://img.shields.io/badge/vapor-3.0-brightgreen.svg) ![Travis](https://travis-ci.org/vapor-community/wkhtmltopdf.svg?branch=master) -Vapor 2 library for converting HTML (Leaf or otherwise) into PDF files using +Vapor 3 library for converting HTML (Leaf or otherwise) into PDF files using [wkhtmltopdf](http://wkhtmltopdf.org/). +## Getting Started + +Add the following in your `Package.swift` file +```Swift +.package(url: "https://github.com/vapor-community/wkhtmltopdf.git", from: "2.0.0"), +``` + ## 📘 Overview First, install [wkhtmltopdf](http://wkhtmltopdf.org/downloads.html). This @@ -15,31 +22,44 @@ library is tested on version 0.12.4. Your binary should be installed at installed correctly. To create a PDF, create and configure a `Document`, add one or more `Page`s, -and then call `generatePDF()`. Here is a full example: +and then call `generatePDF(on: Request)`. Here is a full example: ```Swift import wkhtmltopdf -// Create document. Margins in mm, can be set individually or all at once. -// If no margins are set, the default is 20mm. -let document = Document(margins: 15) -// Create a page from an HTML string. -let page1 = Page("
Page from direct HTML
") -// Create a page from a Leaf template. -let page2 = Page(drop, view: "page_from_leaf_template") -// Create a page from a Leaf template with Context variables. -let page3 = Page(drop, view: "page_from_leaf_template", [ - "firstName": "Peter", - "lastName": "Pan" -]) -// Add the pages to the document -document.pages = [page1, page2, page3] -// Render to a PDF -let pdf = try document.generatePDF() -// Now you can return the PDF as a response, if you want -let response = Response(status: .ok, body: .data(pdf)) -response.headers["Content-Type"] = "application/pdf" -return response +func pdf(_ req: Request) -> FuturePage from direct HTML
") + + // Create a page from a Leaf template. + let page2 = try req.view().render("page_from_leaf_template") + + // Create a page from a Leaf template with Context variables. + let page3 = try req.view().render("page_from_leaf_template", [ "firstName": "Peter", + "lastName": "Pan"]) + let pages = [ page2, page3].flatten(on: req) + .map { views in + return views.map { Page($0.data) } + } + + return pages.flatMap { pages in + // Add the pages to the document + document.pages = [page1] + pages + // Render to a PDF + let pdf = try document.generatePDF(on: req) + // Now you can return the PDF as a response, if you want + return pdf.map { data -> Response in + let http = HTTPResponse(status: .ok, + headers: HTTPHeaders([("Content-Type", "application/pdf")]), + body: data) + return Response(http: http, + using: req) + } + } +} ``` In your Leaf file, you may want to load resources such as images, CSS diff --git a/Sources/wkhtmltopdf/Document+Generate.swift b/Sources/wkhtmltopdf/Document+Generate.swift index cd752a7..bdea7d6 100644 --- a/Sources/wkhtmltopdf/Document+Generate.swift +++ b/Sources/wkhtmltopdf/Document+Generate.swift @@ -1,48 +1,48 @@ import Foundation -import Core - -#if os(Linux) && !swift(>=3.1) -typealias Process = Task -#endif +import Service extension Document { - public func generatePDF() throws -> Bytes { - // Create the temp folder if it doesn't already exist - let workDir = "/tmp/vapor-wkhtmltopdf" - try FileManager().createDirectory(atPath: workDir, withIntermediateDirectories: true) - // Save input pages to temp files, and build up args to wkhtmltopdf - var wkArgs: [String] = [ - "--zoom", Document.zoom, - "--quiet", - "-s", paperSize, - "-T", "\(topMargin)mm", - "-R", "\(rightMargin)mm", - "-B", "\(bottomMargin)mm", - "-L", "\(leftMargin)mm", - ] - let fm = DataFile(workDir: workDir) - let pageFiles: [String] = try pages.map { page in - let fileName = "\(workDir)/\(UUID().uuidString).html" - try fm.write(page.content, to: fileName) - return fileName - } - defer { - pageFiles.forEach { path in - try? fm.delete(at: path) - } - } - wkArgs += pageFiles - // Call wkhtmltopdf and retrieve the result data - let wk = Process() - let stdout = Pipe() - wk.launchPath = "/usr/local/bin/wkhtmltopdf" - wk.arguments = wkArgs - wk.arguments?.append("-") // output to stdout - wk.standardOutput = stdout - wk.launch() - let pdf = stdout.fileHandleForReading.readDataToEndOfFile() - return pdf.makeBytes() - } + public func generatePDF(on container: Container) throws -> Future { + let sharedThreadPool = try container.make(BlockingIOThreadPool.self) + return sharedThreadPool.runIfActive(eventLoop: container.eventLoop) { () -> Data in + let fileManager = FileManager.default + // Create the temp folder if it doesn't already exist + let workDir = "/tmp/vapor-wkhtmltopdf" + try fileManager.createDirectory(atPath: workDir, withIntermediateDirectories: true) + // Save input pages to temp files, and build up args to wkhtmltopdf + var wkArgs: [String] = [ + "--zoom", self.zoom, + "--quiet", + "-s", self.paperSize, + "-T", "\(self.topMargin)mm", + "-R", "\(self.rightMargin)mm", + "-B", "\(self.bottomMargin)mm", + "-L", "\(self.leftMargin)mm", + ] + + let pageFiles: [String] = try self.pages.map { page in + let name = UUID().uuidString + ".html" + let filename = "\(workDir)/\(name)" + try page.content.write(to: URL(fileURLWithPath: filename)) + return filename + } + defer { + try? pageFiles.forEach(fileManager.removeItem) + } + + wkArgs += pageFiles + // Call wkhtmltopdf and retrieve the result data + let wk = Process() + let stdout = Pipe() + wk.launchPath = "/usr/local/bin/wkhtmltopdf" + wk.arguments = wkArgs + wk.arguments?.append("-") // output to stdout + wk.standardOutput = stdout + wk.launch() + let pdf = stdout.fileHandleForReading.readDataToEndOfFile() + return pdf + } + } } diff --git a/Sources/wkhtmltopdf/Document.swift b/Sources/wkhtmltopdf/Document.swift index 165c15e..af4c7b3 100644 --- a/Sources/wkhtmltopdf/Document.swift +++ b/Sources/wkhtmltopdf/Document.swift @@ -1,23 +1,22 @@ public class Document { - // This may need changing across different platforms and deployments. - static var zoom: String = "1.3" + let zoom: String + let topMargin: Int + let rightMargin: Int + let bottomMargin: Int + let leftMargin: Int - let topMargin: Int - let rightMargin: Int - let bottomMargin: Int - let leftMargin: Int + let paperSize: String - let paperSize: String - - public var pages: [Page] = [] - - public init(size: String = "A4", margins all: Int? = nil, top: Int? = nil, right: Int? = nil, bottom: Int? = nil, left: Int? = nil) { - paperSize = size - topMargin = all ?? top ?? 20 - rightMargin = all ?? right ?? 20 - bottomMargin = all ?? bottom ?? 20 - leftMargin = all ?? left ?? 20 - } + public var pages: [Page] = [] + + public init(size: String = "A4", zoom: String? = nil, margins all: Int? = nil, top: Int? = nil, right: Int? = nil, bottom: Int? = nil, left: Int? = nil) { + self.zoom = zoom ?? "1.3" + paperSize = size + topMargin = all ?? top ?? 20 + rightMargin = all ?? right ?? 20 + bottomMargin = all ?? bottom ?? 20 + leftMargin = all ?? left ?? 20 + } } diff --git a/Sources/wkhtmltopdf/Page+View.swift b/Sources/wkhtmltopdf/Page+View.swift deleted file mode 100644 index 26bc3ef..0000000 --- a/Sources/wkhtmltopdf/Page+View.swift +++ /dev/null @@ -1,13 +0,0 @@ -import Vapor - -extension Page { - - public init(_ drop: Droplet, view path: String, _ context: Node? = nil) throws { - var ctx = context ?? Node.object([:]) - ctx["workDir"] = drop.config.workDir.makeNode(in: nil) - ctx["publicDir"] = (drop.config.workDir + "Public/").makeNode(in: nil) - let view = try drop.view.make(path, ctx) - self.init(view.data) - } - -} diff --git a/Sources/wkhtmltopdf/Page.swift b/Sources/wkhtmltopdf/Page.swift index c90c830..d05d38c 100644 --- a/Sources/wkhtmltopdf/Page.swift +++ b/Sources/wkhtmltopdf/Page.swift @@ -1,15 +1,13 @@ -import Bits +import Foundation public struct Page { + let content: Data - let content: Bytes - - public init(_ content: Bytes) { - self.content = content - } - - public init(_ content: String) { - self.content = content.makeBytes() - } + public init(_ content: Data) { + self.content = content + } + public init(_ content: String) { + self.content = Data(content.utf8) + } } diff --git a/Tests/wkhtmltopdfTests/wkhtmltopdfTests.swift b/Tests/wkhtmltopdfTests/wkhtmltopdfTests.swift index 8df3bae..42e235b 100644 --- a/Tests/wkhtmltopdfTests/wkhtmltopdfTests.swift +++ b/Tests/wkhtmltopdfTests/wkhtmltopdfTests.swift @@ -1,22 +1,32 @@ import XCTest +import Vapor @testable import wkhtmltopdf -import Core class wkhtmltopdfTests: XCTestCase { - static var allTests = [ - ("testStringPDF", testStringPDF), - ] - - func testStringPDF() throws { - let document = Document(margins: 15) - let page1 = Page("Page from direct HTML
") - document.pages = [page1] - let data = try document.generatePDF() - // Cop-out test, just ensuring that the returned data is something - XCTAssert(data.count > 50) - // Visual test - let fm = DataFile(workDir: "/tmp/vapor-wkhtmltopdf") - try fm.write(data, to: "/tmp/vapor-wkhtmltopdf/testOutput.pdf") - print("Test output PDF can be viewed at /tmp/vapor-wkhtmltopdf/testOutput.pdf") - } + + var app: Application! + + override func setUp() { + super.setUp() + + app = try! Application(config: .default(), environment: .testing, services: .default()) + } + + func testStringPDF() throws { + let document = Document(margins: 15) + let page1 = Page("Page from direct HTML
") + document.pages = [page1] + let data = try document.generatePDF(on: app).wait() + // Cop-out test, just ensuring that the returned data is something + XCTAssert(data.count > 50) + // Visual test + + FileManager.default.createFile(atPath: "/tmp/vapor-wkhtmltopdf/testOutput.pdf", contents: data, attributes: nil) + + print("Test output PDF can be viewed at /tmp/vapor-wkhtmltopdf/testOutput.pdf") + } + + static var allTests = [ + ("testStringPDF", testStringPDF), + ] }