I'm trying to figure out how to save a WebView to a PDF and totally stuck, would really appreciate some help?
I'm doing this in Cocoa & Swift on OSX, here's my code so far:
import Cocoa
import WebKit
class ViewController: NSViewController {
override func loadView() {
super.loadView()
}
override func viewDidLoad() {
super.viewDidLoad()
loadHTMLString()
}
func loadHTMLString() {
let webView = WKWebView(frame: self.view.frame)
webView.loadHTMLString("<html><body><p>Hello, World!</p></body></html>", baseURL: nil)
self.view.addSubview(webView)
createPDFFromView(webView, saveToDocumentWithFileName: "test.pdf")
}
func createPDFFromView(view: NSView, saveToDocumentWithFileName fileName: String) {
let pdfData = view.dataWithPDFInsideRect(view.bounds)
if let documentDirectories = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first {
let documentsFileName = documentDirectories + "/" + fileName
debugPrint(documentsFileName)
pdfData.writeToFile(documentsFileName, atomically: false)
}
}
}
It's pretty simple, what I'm doing is creating a WebView and writing some basic html content to it which renders this:
And then takes the view and saves it to a PDF file but that comes out blank:
I've tried grabbing the contents from the webView and View but no joy.
I've found a similar problem here How to take a screenshot when a webview finished rending regarding saving the webview to an image, but so far no luck with an OSX Solution.
Could it be something to do with the document dimensions?
or that the contents is in a subview?
maybe if you capture the View you can't capture the SubView?
Any ideas?
iOS 11.0 and above, Apple has provided following API to capture snapshot of WKWebView.
#available(iOS 11.0, *)
open func takeSnapshot(with snapshotConfiguration: WKSnapshotConfiguration?, completionHandler: #escaping (UIImage?, Error?) -> Swift.Void)
Sample usage:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
if #available(iOS 11.0, *) {
webView.takeSnapshot(with: nil) { (image, error) in
//Do your stuff with image
}
}
}
iOS 10 and below, UIWebView has to be used to capture snapshot. Following method can be used to achieve that.
func webViewDidFinishLoad(_ webView: UIWebView) {
let image = captureScreen(webView: webView)
//Do your stuff with image
}
func captureScreen(webView: UIWebView) -> UIImage {
UIGraphicsBeginImageContext(webView.bounds.size)
webView.layer.render(in: UIGraphicsGetCurrentContext()!)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
Here's another relevant answer
So I kind of figured out how to solve it, it turns out you can't (especially on OSX) access and print a webview from a WKWebView.
You have to use a WebView and NOT a WKWebView (I originally started with WKWebView because a few of the articles I read said to use that).
A WebView object is pretty much similar to a WKWebView object, which is fun as hell :-)
But it gives you access to .mainFrame & .frameView which you'll need to print it's content.
Here's my code:
let webView = WebView(frame: self.view.frame)
let localfilePath = NSBundle.mainBundle().URLForResource(fileName, withExtension: "html");
let req = NSURLRequest(URL: localfilePath!);
webView.mainFrame.loadRequest(req)
self.view.addSubview(webView)
Once it's rendered I then added a 1 second delay just to make sure the content has rendered before I print it,
// needs 1 second delay
let delay = 1 * Double(NSEC_PER_SEC)
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue()) {
// works!
let data = webView.dataWithPDFInsideRect(webView.frame)
let doc = PDFDocument.init(data: data)
doc.writeToFile("/Users/john/Desktop/test.pdf")
// works!
let printInfo = NSPrintInfo.sharedPrintInfo()
let printOperation = NSPrintOperation(view: webView.mainFrame.frameView, printInfo: printInfo)
printOperation.runOperation()
}
Here I'm printing it and saving it as a PDF, just so I'm doubly sure it works in all circumstances.
I'm sure it can be improved, I hate the delay hack, should replace that with some kind of callback or delegate to run when the content has fully loaded.
Related
I am using WKWebView to render several (around 100) web pages that I then need to render to PDF. I am using the createPDF method of WKWebView to accomplish this. The reason I'm doing each individual page in its own web view is because createPDF doesn't respect page breaks in the HTML (as far as I know).
So I have a class where I start the loop to render each page:
class PrintVC: ViewController, WKNavigationDelegate {
var pages = [Page]()
func start(){
//A "page" is a struct that has the string content to load each web view
for page in pages{
let webView = WKWebView()
webView.navigationDelegate = self
webView.loadHTMLString(page.content, baseURL: Bundle.main.bundleURL)
}
}
}
I know the page is ready to be saved to PDF in the didFinish navigation delegate method:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
let config = WKPDFConfiguration()
config.rect = CGRect(x: 0, y: 0, width: 792, height: 612)
//Create the PDF
webView.createPDF(configuration: config){ result in
switch result{
case .success(let data):
do{
try data.write(to: URL(fileURLWithPath: "file-???.pdf"))
}catch let error{
print(error)
}
case .failure(let error):
print(error)
}
}
}
}
The trouble I'm having is I don't know when each individual page is done rendering. I also don't know how to pass each page's name to be used in the file path to save it.
How can I start a bunch of WKWebView loads and know when they are all done? Or better still, how can I reuse the same WKWebView and load each individual page in the same way? I assume using the same web view would be a better use of memory.
How can I start a bunch of WKWebView loads and know when they are all done?
Well, you'd need to identify which web view caused the delegate method to be called. It is for this reason that the first parameter - webView: WKWebView - exists.
One way is to put each (web view, pair) into a dictionary ([WKWebView: Page]). Then start the loading:
// assume you have declared a property "self.webViewDict"
for page in pages{
let webView = WKWebView()
webView.navigationDelegate = self
self.webViewDict[webView] = page
webView.loadHTMLString(page.content, baseURL: Bundle.main.bundleURL)
}
When one finishes loading, you can identify the page by doing webViewDict[webView]. You should then remove the web view from the dictionary:
webViewDict[webView] = nil
if webViewDict.isEmpty {
// everything is loaded!
}
how can I reuse the same WKWebView and load each individual page in the same way?
Note that if you use the same WKWebView, you'll have to load the pages sequentially. The same web view can't load multiple things at the same time.
You can just removed the loaded pages from pages. If you don't want to do that, you can copy pages to another var first.
In start, load the first page:
if let firstPage = pages.first {
webView.loadHTMLString(firstPage.content, baseURL: Bundle.main.bundleURL)
}
When you successfully load a page, do the same thing again:
case .success(let data):
pages.removeFirst()
if let firstPage = pages.first {
webView.loadHTMLString(firstPage.content, baseURL: Bundle.main.bundleURL)
} else {
// we are done!
}
This questions pertains to macOS, not iOS.
Note that this question is being reported as a duplicate of this question. The answers on that page pertain either to iOS (irrelevant) or uses the deprecated WebView as a solution, which is exactly what my question is about in the first place.
So Apple has deprecated WebView in favor of WKWebView, but I'm not seeing a working solution for being able to export (or print) a PDF from the new view type. I've tried the following (and more) from within the delegate method webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)
1.
let pdfData = webView.dataWithPDF(inside: webView.frame)
try? pdfData.write(to: URL(fileURLWithPath: "/filepath/to/test.pdf"))
This resulted in a literal blank pdf file.
2.
webView.takeSnapshot(with: nil) { (image, error) in
if let error = error {
print("Error: \(error)")
return
}
guard let image = image else {
print("No image")
return
}
try? image.tiffRepresentation?.write(to: URL(fileURLWithPath: "/path/to/test.tif"))
}
And while this got actual content in the image, it was a (giant) rendered bitmap image having lost all of its textual/string data, which also only showed what was visible on screen, nothing beyond the edges of the window.
Meanwhile, following the numerous examples available on the web, using WebView works well and as expected. Am I to believe that Apple released a half baked replacement for a deprecated framework, or am I doing something wrong?
This is now possible with WKWebView's createPDF method: https://developer.apple.com/documentation/webkit/wkwebview/3650490-createpdf
Here's an example:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
let config = WKPDFConfiguration()
//Set the page size (this can match the html{} size in your CSS
config.rect = CGRect(x: 0, y: 0, width: 792, height: 612)
//Render the PDF
webView.createPDF(configuration: config){ result in
switch result{
case .success(let data):
try! data.write(to: URL(fileURLWithPath: "/path/file.pdf"))
case .failure(let error):
print(error)
}
}
}
I have it working on both macOS and iOS. Hopefully this helps.
I'm using Xcode 11.5 and loading a web page in a WKWebView. I have file access set to read only and this is my apps capability list.
This is how my loadView look like
override func loadView() {
//Inject JS string to read console.logs for debugging
let configuration = WKWebViewConfiguration()
let action = "var originalCL = console.log; console.log = function(msg){ originalCL(msg); window.webkit.messageHandlers.iosListener.postMessage(msg); }" //Run original console.log function + print it in Xcode console
let script = WKUserScript(source: action, injectionTime: .atDocumentStart, forMainFrameOnly: false) //Inject script at the start of the document
configuration.userContentController.addUserScript(script)
configuration.userContentController.add(self, name: "iosListener")
//Initialize WKWebView
webView = WebView(frame: (NSScreen.main?.frame)!, configuration: configuration)
//Set delegates and load view in the window
webView.navigationDelegate = self
webView.uiDelegate = self
view = webView
}
When I use Safari on iOS and I tab the input type=file the dialog opens up but using my macOS app using a webview it does not work? Do I need to set more capabilities on my app? I found some old issues with file inputs but they seem to be resolved?
Implemented the UIDelegate like this.
func webView(_ webView: WKWebView, runOpenPanelWith parameters: WKOpenPanelParameters, initiatedByFrame frame: WKFrameInfo, completionHandler: #escaping ([URL]?) -> Void) {
let openPanel = NSOpenPanel()
openPanel.canChooseFiles = true
openPanel.begin { (result) in
if result == NSApplication.ModalResponse.OK {
if let url = openPanel.url {
completionHandler([url])
}
} else if result == NSApplication.ModalResponse.cancel {
completionHandler(nil)
}
}
}
I have an array of Google News article urls. Google News article urls redirect immediately to real urls, ie: CNBC.com/.... I am trying to pull out the real, redirected url. I thought I could loop through the list and load the Google News link in a WebView, then call webView.url in a DispatchQueue after 1 second to get the real url, but this doesn't work.
How could you fetch a list of redirected urls quickly?
Here's my code you could use to reproduce the problem:
let webView = WKWebView()
let myList = [URL(string: "https://news.google.com/articles/CAIiEDthIxbgofssGWTpXgeJXzwqGQgEKhAIACoHCAow2Nb3CjDivdcCMJ_d7gU?hl=en-US&gl=US&ceid=US%3Aen"), URL(string: "https://news.google.com/articles/CAIiEP5m1nAOPt-LIA4IWMOdB3MqGQgEKhAIACoHCAowocv1CjCSptoCMPrTpgU?hl=en-US&gl=US&ceid=US%3Aen")]
for url in myList {
guard let link = url else {continue}
self.webView.loadUrl(string: link.absoluteString)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
let redirectedLink = self.webView.url
print("HERE redirected url: ", redirectedLink) // this does not work
}
}
There are two problems with your attempt:
1) You're using one and the same web view in the loop and since nothing inside the loop blocks until the web view has finished loading, you just end up cancelling the previous request with every loop pass.
2) Even if you did block inside the loop, accessing the URL after a second won't work reliably since the navigation could easily take longer than that.
What I would recommend doing is to continue using a single web view (to save resources) but to use its navigation delegate interface for resolving the URLs one by one.
This is a crude example to give you a basic idea:
import UIKit
import WebKit
#objc class RedirectResolver: NSObject, WKNavigationDelegate {
private var urls: [URL]
private var resolvedURLs = [URL]()
private let completion: ([URL]) -> Void
private let webView = WKWebView()
init(urls: [URL], completion: #escaping ([URL]) -> Void) {
self.urls = urls
self.completion = completion
super.init()
webView.navigationDelegate = self
}
func start() {
resolveNext()
}
private func resolveNext() {
guard let url = urls.popLast() else {
completion(resolvedURLs)
return
}
let request = URLRequest(url: url)
webView.load(request)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
resolvedURLs.append(webView.url!)
resolveNext()
}
}
class ViewController: UIViewController {
private var resolver: RedirectResolver!
override func viewDidLoad() {
super.viewDidLoad()
resolver = RedirectResolver(
urls: [URL(string: "https://news.google.com/articles/CAIiEDthIxbgofssGWTpXgeJXzwqGQgEKhAIACoHCAow2Nb3CjDivdcCMJ_d7gU?hl=en-US&gl=US&ceid=US%3Aen")!, URL(string: "https://news.google.com/articles/CAIiEP5m1nAOPt-LIA4IWMOdB3MqGQgEKhAIACoHCAowocv1CjCSptoCMPrTpgU?hl=en-US&gl=US&ceid=US%3Aen")!],
completion: { urls in
print(urls)
})
resolver.start()
}
}
This outputs the following resolved URLs:
[https://amp.cnn.com/cnn/2020/04/09/politics/trump-coronavirus-tests/index.html, https://www.cnbc.com/amp/2020/04/10/asia-markets-coronavirus-china-inflation-data-currencies-in-focus.html]
One other thing to note is that the redirection of those URLs in particular seems to rely on JavaScript which means you indeed need a web view. Otherwise kicking off URLRequests manually and observing the responses would have been enough.
I am able to implement the new WebKit in 7.1 Deployment. I can use it without error on the devices running in iOS8 up. However, when the device falls below iOS8, my WKWebView becomes nil even after the initialization, my suspect was even if you silence webkit and successfully add it on your project and the deployment was 7.1, if the OS actually fall below iOS8 this WebKit becomes unvalable.
I want to confirm this error so I can proceed. Since this webkit was introduced as of the release of swift and iOS8. Thanks
Here is a simple example, where I create a new protocol and extend both UIWebView and WKWebView from the same protocol. With this, it makes a easy to keep track of both these views inside my view controller and both of these use common method to load from url, it makes easy for abstraction.
protocol MyWebView{
func loadRequestFromUrl(url: NSURL!)
}
extension UIWebView:MyWebView{
func loadRequestFromUrl(url: NSURL!){
let urlRequest = NSURLRequest(URL: url)
loadRequest(urlRequest)
}
}
extension WKWebView:MyWebView{
func loadRequestFromUrl(url: NSURL!){
let urlRequest = NSURLRequest(URL: url)
loadRequest(urlRequest)
}
}
// This is a simple closure, which takes the compared system version, the comparison test success block and failure block
let SYSTEM_VERSION_GREATER_THAN_OR_EQUAL: (String, () -> (), () -> ()) -> Void = {
(var passedVersion: String, onTestPass: () -> (), onTestFail: () -> ()) in
let device = UIDevice.currentDevice()
let version = device.systemVersion
let comparisonOptions = version.compare(passedVersion, options: NSStringCompareOptions.NumericSearch, range: Range(start: version.startIndex, end: version.endIndex), locale: nil)
if comparisonOptions == NSComparisonResult.OrderedAscending || comparisonOptions == NSComparisonResult.OrderedSame{
onTestPass()
}else{
onTestFail()
}
}
class ViewController: UIViewController{
var webView: MyWebView!
override func viewDidLoad() {
super.viewDidLoad()
SYSTEM_VERSION_GREATER_THAN_OR_EQUAL("8.0",
{
let theWebView = WKWebView(frame: self.view.bounds)
self.view.addSubview(theWebView)
self.webView = theWebView
},
{
let theWebView = UIWebView(frame: self.view.bounds)
self.view.addSubview(theWebView)
self.webView = theWebView
})
webView.loadRequestFromUrl(NSURL(string: "http://google.com"))
}
}