Printing HTML String in WKWebView Fails to Print Large PDFs - swift

Swift 4, Xcode 9.2, iOS 11
I generate an HTML string that represents a bunch of data displayed in a WKWebView like this:
//I include this so I can reference some images
let bundle = Bundle.main.bundlePath
//Load the HTML
webView.loadHTMLString(htmlString, baseURL: URL(fileURLWithPath: bundle))
Inside my WKWebView didFinish navigation delegate, I build out the PDF like so:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// - 1 - Grab the webView's print context
let fmt = webView.viewPrintFormatter()
fmt.perPageContentInsets = UIEdgeInsetsMake(10, 10, 10, 10) //Page margins
// - 2 - Assign print formatter to UIPrintPageRenderer
let render = UIPrintPageRenderer()
render.addPrintFormatter(fmt, startingAtPageAt: 0)
// - 3 - Assign paperRect and printableRect
let page = CGRect(x: 0, y: 0, width: 841.85, height: 595.22) //Page size
let printable = page.insetBy(dx: 0, dy: 0)
render.setValue(NSValue(cgRect: page), forKey: "paperRect")
render.setValue(NSValue(cgRect: printable), forKey: "printableRect")
// - 4 - Create PDF context and draw
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, page, nil) //Set page size to landscape
for i in 0...render.numberOfPages {
UIGraphicsBeginPDFPage()
render.drawPage(at: i, in: UIGraphicsGetPDFContextBounds())
}
UIGraphicsEndPDFContext()
// - 5 - Save the PDF file
let pdfURL = documentsURL.appendingPathComponent("PDF")
let path = pdfURL.appendingPathComponent("MyFile.pdf").path
pdfData.write(toFile: path, atomically: true)
}
This works nicely up to ~30 page PDFs. But when I give it a really large web page (40+ pages), then I just get a blank PDF with a single page in the end (nothing renders). My suspicion is that the PDF renderer is crashing or running out of memory, but there's no indication of that in the logs.
The htmlString getting passed in is always complete when I log it. Just the printing fails.
Is there a more efficient or more reliable way to pull this off? Or some way to better troubleshoot what's going on? I'd be so grateful for some help. :)

Related

Full Page screenshot of WKWebView for macOS

This is a macOS app, I'm trying to take a full page screenshot of a webview, but i'm unable to get the screenshot of the full page.
Screenshot function.
func takescreenshot(
_ webView: WKWebView,
didFinish navigation: WKNavigation!) {
let configuration = WKSnapshotConfiguration()
configuration.afterScreenUpdates = true
webView.takeSnapshot(with: configuration) { (image, error) in
if let image = image {
//Save Image
}
}
}
from the answers I've seen here the solution seems to be setting the webview scrollview offset, but this is only available for ios. This is the error i get:
Value of type "WKWebView" has no member "scrollView"
I guess the problem is caused by an internal scroll view that optimizes drawing for scrolling - by redrawing only parts that are soon to be visible.
To overcome this, you can temporarily resize the web view to it's full content extent. I know it's not an ideal solution, but you may derive better one based on this idea.
webView.evaluateJavaScript("[document.body.scrollWidth, document.body.scrollHeight];") { result, error in
if let result = result as? NSArray {
let orig = webView.frame
webView.frame = NSRect(x: 0, y: 0, width: (result[0] as! NSNumber).intValue, height: (result[1] as! NSNumber).intValue)
let configuration = WKSnapshotConfiguration()
webView.takeSnapshot(with: configuration) { image, error in
if let image = image {
NSPasteboard.general.clearContents()
NSPasteboard.general.setData(image.tiffRepresentation, forType: .tiff)
}
}
webView.frame = orig
}
}

Swift: Loss of resolution when redrawing PDF

I'm trying to use a PDF file (contains a form with table for several 'records') as a template.
My idea is to read in the original file as a template, and then use Swifts drawing features to insert the text at the relevant positions before saving as a new pdf file and printing.
My problem is that i'm seeing a small loss of resolution (fonts are slightly wooly, gridlines are no longer crisp) when re-saving the output.
I've tried two approaches, the first with my 'template' as a file in the project, and the second with it as an Asset (Scales: Single Scale, Resizing: Preserve Vector Data).
Here is my code:
func createCompletedForm(records: [MyDataObject]) -> URL? {
let directoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = URL(fileURLWithPath: "pdfToPrint", relativeTo: directoryURL).appendingPathExtension("pdf")
guard let templateUrl = Bundle.main.url(forResource: "MyPdfTemplate", withExtension: "pdf") else { return nil }
guard let document = CGPDFDocument(templateUrl as CFURL) else { return nil }
guard let page = document.page(at: 1) else { return nil }
let pageTemplate = page.getBoxRect(.mediaBox)
UIGraphicsBeginPDFContextToFile(fileURL.path, pageTemplate, nil)
guard let pdfContext = UIGraphicsGetCurrentContext() else {
print("Unable to access PDF Context.")
return nil
}
// Mark the beginning of the page.
pdfContext.beginPDFPage(nil)
// Save the context state to restore after we are done drawing the image.
pdfContext.saveGState()
// Change the PDF context to match the UIKit coordinate system.
pdfContext.translateBy(x: 0, y: StandardPageDimensions.ISO216_A4.height)
pdfContext.scaleBy(x: 1, y: -1)
// Option 1: Draw PDF from a file added to my project
DrawingHelper.drawPDFfromCGPDF(page: page, drawingArea: pageTemplate)
// Option 2: Draw PDF from Assets
//let baseTemplate = UIImage(named: "MyPdfTemplate")
//baseTemplate?.draw(at: CGPoint(x: 0, y: 0))
// Draw the records over the template - NOT the source of the problem, happens even when commented out
//addRecordsToTemplate(records: records)
// Restoring the context back to its original state.
pdfContext.restoreGState()
// Mark the end of the current page.
pdfContext.endPDFPage()
UIGraphicsEndPDFContext()
// Useful to find and open the file produced on the simulator
print("pdf created at : \(fileURL.path)")
return fileURL
}
// And the drawing function from my helper class
static func drawPDFfromCGPDF(page: CGPDFPage, drawingArea: CGRect) {
let renderer = UIGraphicsImageRenderer(size: drawingArea.size)
let img = renderer.image { ctx in
UIColor.white.set()
ctx.fill(drawingArea)
ctx.cgContext.translateBy(x: 0.0, y: drawingArea.size.height)
ctx.cgContext.scaleBy(x: 1.0, y: -1.0)
ctx.cgContext.drawPDFPage(page)
}
img.draw(at: CGPoint(x: 0, y: 0))
}

How to Save PDF (and print) from UITableView (or cell) easy way?

I read and search a lot on internet and stack, but I have a problem
I've used this extension
extension UITableView {
// Export pdf from UITableView and save pdf in drectory and return pdf file path
func exportAsPdfFromTable() -> String {
self.showsVerticalScrollIndicator = false
let originalBounds = self.bounds
self.bounds = CGRect(x:originalBounds.origin.x, y: originalBounds.origin.y, width: self.contentSize.width, height: self.contentSize.height)
let pdfPageFrame = CGRect(x: 0, y: 0, width: self.bounds.size.width, height: self.contentSize.height)
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, pdfPageFrame, nil)
UIGraphicsBeginPDFPageWithInfo(pdfPageFrame, nil)
guard let pdfContext = UIGraphicsGetCurrentContext() else { return "" }
self.layer.render(in: pdfContext)
UIGraphicsEndPDFContext()
self.bounds = originalBounds
// Save pdf data
return self.saveTablePdf(data: pdfData)
}
// Save pdf file in document directory
func saveTablePdf(data: NSMutableData) -> String {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let docDirectoryPath = paths[0]
let pdfPath = docDirectoryPath.appendingPathComponent("myPDF.pdf")
if data.write(to: pdfPath, atomically: true) {
return pdfPath.path
} else {
return ""
}
}
}
Then i save the path in this way
myPdfPath = self.tableView.exportAsPdfFromTable()
I tried several ways to show the pdf:
- UIActivityViewController (with this work but not well)
- WKWebView
- SFSafariViewController (only http / https, path file not work)
- UIDocumentInteractionController (not show preview and not work)
The extension used to save the pdf does not work well (not fill print preview page). Generate a 125MB pdf with a title I don't want.
Is there an easy way to do this?
1) TableView (generate pdf on a white background (always even if the tableview is red for example)
2) Print the generated pdf.
Thanks

IMessage MSSticker view created from UIView incorrect sizing

Hey I have been struggling with this for a couple of days now and can't seem to find any documentation out side of the standard grid views for MSStickerView sizes
I am working on an app that creates MSStickerViews dynamically - it does this via converting a UIView into an UIImage saving this to disk then passing the URL to MSSticker before creating the MSStickerView the frame of this is then set to the size of the original view.
The problem I have is that when I drag the MSStickerView into the messages window, the MSStickerView shrinks while being dragged - then when dropped in the messages window, changes to a larger size. I have no idea how to control the size when dragged or the final image size
Heres my code to create an image from a view
extension UIView {
func imageFromView() -> UIImage? {
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.isOpaque, 0.0)
defer { UIGraphicsEndImageContext() }
if let context = UIGraphicsGetCurrentContext() {
self.layer.render(in: context)
let image = UIGraphicsGetImageFromCurrentImageContext()
return image
}
return nil
}
}
And here's the code to save this to disk
extension UIImage {
func savedPath(name: String) -> URL{
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let filePath = "\(paths[0])/name.png"
let url = URL(fileURLWithPath: filePath)
// Save image.
if let data = self.pngData() {
do {
try data.write(to: url)
} catch let error as NSError {
}
}
return url
}
}
finally here is the code that converts the data path to a Sticker
if let stickerImage = backgroundBox.imageFromView() {
let url = stickerImage.savedPath(name: textBox.text ?? "StickerMCSticker")
if let msSticker = try? MSSticker(contentsOfFileURL: url, localizedDescription: "") {
var newFrame = self.backgroundBox.frame
newFrame.size.width = newFrame.size.width
newFrame.size.height = newFrame.size.height
let stickerView = MSStickerView(frame: newFrame, sticker: msSticker)
self.view.addSubview(stickerView)
print("** sticker frame \(stickerView.frame)")
self.sticker = stickerView
}
}
I wondered first off if there was something I need to do regarding retina sizes, but adding #2x in the file just breaks the image - so am stuck on this - the WWDC sessions seem to show stickers being created from file paths and not altering in size in the transition between drag and drop - any help would be appreciated!
I fixed this issue eventually by getting the frame from the view I was copying's frame then calling sizeToFit()-
init(sticker: MSSticker, size: CGSize) {
let stickerFrame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
self.sticker = MSStickerView(frame: stickerFrame, sticker: sticker)
self.sticker.sizeToFit()
super.init(nibName: nil, bund
as the StickerView was not setting the correct size. Essentially the experience I was seeing was that the sticker size on my view was not accurate with the size of the MSSticker - so the moment the drag was initialized, the real sticker size was implemented (which was different to the frame size / autoLayout I was applying in my view)

Loading Images into UITextview/WKWebView from HTML

Trying to load HTML content that contains images into either a UITextView or a WKWebView (I just need it rendered!)
However, when I load HTML into WKWebView, the images do not load and the view doesn't reload and respect the Autolayout constraints from before the content loads:
Relevant code is here:
webView.navigationDelegate = self
webView.loadHTMLString(body, baseURL: nil)
and
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
if webView.isLoading == false {
webView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { [weak self] (result, error) in
guard let safeSelf = self else {return}
if let height = result as? CGFloat {
print("height of webview: \(height)")
webView.frame.size.height += height
//Layout subviews?
}
})
}
}
However, with UITextView, according to the accepted answer found here I can do the following:
extension String {
var htmlToAttributedString: NSMutableAttributedString? {
guard let data = data(using: .unicode) else { return NSMutableAttributedString() }
do {
return try NSMutableAttributedString(data: data, options: [.documentType: NSMutableAttributedString.DocumentType.html], documentAttributes: nil)
} catch {
return NSMutableAttributedString()
}
}
and set it as such:
textView.attributedText = htmlString.htmlToAttributedString
This works well in terms of the Autolayout... for just texts, but it is not preserving images(doesn't even show an indication of where images would go), only text. Not event preserving fonts or font sizes used by the html.
I have directly used the accepted answer from that post and it does not preserve images, even though the OP asked for preserving images.
What is either a) the correct way to load content into a WKWebView and load the images (my Transport Security is set to allow arbitrary loads for this demo) or b) preserve images in UITextView's parsing of the HTML? I am given the html directly and not the URL.
EDIT: For testing purposes, I have hardcoded a small sample html in my code for use in UIWebView / WKWebView to mirror a portion of what I get from the API call.
let body = "<div class=\"media-attachment\"><img src=\"https://testimages.weareher.com/feed/0/2018-08/t9Qc549F7uCtZdbdXx1ERLbXJN2EXIXVacouJDlX.png\"/><p class=\"media-caption\">via Latinxontherise</p></div></div>"
i suggest u should UIWebView to load HTMLString. Remember drag delegate for webView and u only call: your_webview.loadHTMLString(yourHTMLString, baseURL: nil)
your_webview.scalesPageToFit = true
So, the images were not loading because they were webP images, and Apple does not support webP (because it is a google thing).
As for the auto layout stuff, here is the basic solution (for the WKWebView):
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
//A delay here is needed because didFinish does not guarantee content has finished displaying
//Height can only be retrieve after display finishes - hence the delay.
var height: CGFloat = 0.0
dispatch_after(0.1, block: {
height = webView.scrollView.contentSize.height
webView.snp.makeConstraints({make in //I used SnapKit here, because that's how my repo is setup
make.height.equalTo(height)
})
// If the the webview is in another view, update that view's frame with the height + the existing height of that view's frame. For me, I had the webview as the header of a uitableview (the cells are user comments, where the header is the "post"):
guard var headerFrame = self.tableView.tableHeaderView?.frame else {return}
headerFrame.size.height = headerFrame.size.height + height
self.header.frame = headerFrame
self.tableView.tableHeaderView = self.header
})
The above code is working very reliably for me. The only only things that i needed was to use WKPreferences to set a minimum font, since the WKWebview doesn't respect the font existing in the html, and disabled scrolling, because that is the point of resizing it to match the content.