iOS WebView生成長截圖的第三種解決方案

前言

因爲項目須要,新近實現了一個長截圖庫 SnapshotKit。其中,須要支持 UIWebViewWKWebView 組件生成長截圖。爲了實現這個特性,查閱了不少資料,同時也作了不一樣的新奇思路嘗試,最終實現了一個新的、取巧的技術方案。git

如下主要總結了在「WebView生成長截圖」需求方面,「網上已有方案」和「個人全新方案」的各自實現要點和優缺點。github

WebView生成長截圖的已有方案

根據 Google 所搜索到的資料,目前iOS WebView生成長截圖的方案主要有2種:web

  • 方案一:修改Frame,截圖組件
  • 方案二:分頁截圖組件內容,合成長圖

下面將會簡述方案一和方案二的具體實現。swift

方案一:修改Frame,截圖組件

方案一的實現要點在於:修改 webView.scrollViewframeSizecontentSize,而後對整個 webView.scrollView 進行截圖。api

不過,這個方案只適用 UIWebView 組件,由於其是一次性加載網頁全部的內容。而 WKWebView 組件,爲了節省內存,加載網頁內容時,只加載可視部分——這一點相似 UITableView 組件。在修改webView.scrollViewframeSize 後,當即執行了截圖操做, 這時候,WKWebView因爲還沒把網頁的內容加載出來,致使生成的長截圖是空白的。async

方案一核心代碼以下:ide

extension UIScrollView {
   public func takeSnapshotOfFullContent() -> UIImage? {
        let originalFrame = self.frame
        let originalOffset = self.contentOffset

        self.frame = CGRect.init(origin: originalFrame.origin, size: self.contentSize)
        self.contentOffset = .zero

        let backgroundColor = self.backgroundColor ?? UIColor.white

        UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0)

        guard let context = UIGraphicsGetCurrentContext() else {
            return nil
        }
        context.setFillColor(backgroundColor.cgColor)
        context.setStrokeColor(backgroundColor.cgColor)

        self.drawHierarchy(in: self.bounds, afterScreenUpdates: true)
        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        self.frame = originalFrame
        self.contentOffset = originalOffset

        return image
    }
}
複製代碼

測試代碼:性能

// example code
 private func takeSnapshotOfUIWebView() {
    let image = self.webView.scrollView.takeSnapshotOfFullContent()
   // 處理image
}    
複製代碼

方案二:分頁截圖組件內容,合成長圖

方案二的實現要點在於:分頁滾動WebView組件的內容,而後生成分頁截圖,最後把全部分頁截圖合成一張長圖。測試

這個方案適用於 UIWebView 組件和 WKWebView 組件。優化

方案二核心代碼以下:

extension UIScrollView {
    public func takeScreenshotOfFullContent(_ completion: @escaping ((UIImage?) -> Void)) {
        // 分頁繪製內容到ImageContext
        let originalOffset = self.contentOffset

        // 當contentSize.height<bounds.height時,保證至少有1頁的內容繪製
        var pageNum = 1
        if self.contentSize.height > self.bounds.height {
            pageNum = Int(floorf(Float(self.contentSize.height / self.bounds.height)))
        }

        let backgroundColor = self.backgroundColor ?? UIColor.white

        UIGraphicsBeginImageContextWithOptions(self.contentSize, true, 0)

        guard let context = UIGraphicsGetCurrentContext() else {
            completion(nil)
            return
        }
        context.setFillColor(backgroundColor.cgColor)
        context.setStrokeColor(backgroundColor.cgColor)

        self.drawScreenshotOfPageContent(0, maxIndex: pageNum) {
            let image = UIGraphicsGetImageFromCurrentImageContext()
            UIGraphicsEndImageContext()
            self.contentOffset = originalOffset
            completion(image)
        }
    }

    fileprivate func drawScreenshotOfPageContent(_ index: Int, maxIndex: Int, completion: @escaping () -> Void) {

        self.setContentOffset(CGPoint(x: 0, y: CGFloat(index) * self.frame.size.height), animated: false)
        let pageFrame = CGRect(x: 0, y: CGFloat(index) * self.frame.size.height, width: self.bounds.size.width, height: self.bounds.size.height)

        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
            self.drawHierarchy(in: pageFrame, afterScreenUpdates: true)

            if index < maxIndex {
                self.drawScreenshotOfPageContent(index + 1, maxIndex: maxIndex, completion: completion)
            }else{
                completion()
            }
        }
    }
}
複製代碼

測試代碼:

// example code
private func takeSnapshotOfUIWebView() {
    self.uiWebView.scrollView.takeScreenshotOfFullContent { (image) in
        // 處理image
    }
}

private func takeSnapshotOfWKWebView() {
    self.wkWebView.scrollView.takeScreenshotOfFullContent { (image) in
        // 處理image
    }
}
複製代碼

WebView生成長截圖的新方案

除了方案一和方案二,還有新方案嗎?

答案是確定加肯定以及必定的。

這個新方案的要點在於:iOS系統的WebView打印功能。

iOS系統支持把WebView的內容打印到PDF文件上,藉助這個特性,新方案的設計以下:

  1. 把 WebView組件的內容所有打印到一頁PDF上

  2. 把PDF轉換成圖片

新方案的核心代碼以下:

import UIKit
import WebKit

/// WebViewPrintPageRenderer: use to print the full content of webview into one image
internal final class WebViewPrintPageRenderer: UIPrintPageRenderer {

    private var formatter: UIPrintFormatter

    private var contentSize: CGSize

    /// 生成PrintPageRenderer實例
    ///
    /// - Parameters:
    /// - formatter: WebView的viewPrintFormatter
    /// - contentSize: WebView的ContentSize
    required init(formatter: UIPrintFormatter, contentSize: CGSize) {
        self.formatter = formatter
        self.contentSize = contentSize
        super.init()
        self.addPrintFormatter(formatter, startingAtPageAt: 0)
    }

    override var paperRect: CGRect {
        return CGRect.init(origin: .zero, size: contentSize)
    }

    override var printableRect: CGRect {
        return CGRect.init(origin: .zero, size: contentSize)
    }

    private func printContentToPDFPage() -> CGPDFPage? {
        let data = NSMutableData()
        UIGraphicsBeginPDFContextToData(data, self.paperRect, nil)
        self.prepare(forDrawingPages: NSMakeRange(0, 1))
        let bounds = UIGraphicsGetPDFContextBounds()
        UIGraphicsBeginPDFPage()
        self.drawPage(at: 0, in: bounds)
        UIGraphicsEndPDFContext()

        let cfData = data as CFData
        guard let provider = CGDataProvider.init(data: cfData) else {
            return nil
        }
        let pdfDocument = CGPDFDocument.init(provider)
        let pdfPage = pdfDocument?.page(at: 1)

        return pdfPage
    }

    private func covertPDFPageToImage(_ pdfPage: CGPDFPage) -> UIImage? {
        let pageRect = pdfPage.getBoxRect(.trimBox)
        let contentSize = CGSize.init(width: floor(pageRect.size.width), height: floor(pageRect.size.height))

        // usually you want UIGraphicsBeginImageContextWithOptions last parameter to be 0.0 as this will us the device's scale
        UIGraphicsBeginImageContextWithOptions(contentSize, true, 2.0)
        guard let context = UIGraphicsGetCurrentContext() else {
            return nil
        }

        context.setFillColor(UIColor.white.cgColor)
        context.setStrokeColor(UIColor.white.cgColor)
        context.fill(pageRect)

        context.saveGState()
        context.translateBy(x: 0, y: contentSize.height)
        context.scaleBy(x: 1.0, y: -1.0)

        context.interpolationQuality = .low
        context.setRenderingIntent(.defaultIntent)
        context.drawPDFPage(pdfPage)
        context.restoreGState()

        let image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return image
    }

    /// print the full content of webview into one image
    ///
    /// - Important: if the size of content is very large, then the size of image will be also very large
    /// - Returns: UIImage?
    internal func printContentToImage() -> UIImage? {
        guard let pdfPage = self.printContentToPDFPage() else {
            return nil
        }

        let image = self.covertPDFPageToImage(pdfPage)
        return image
    }
}

extension UIWebView {
    public func takeScreenshotOfFullContent(_ completion: @escaping ((UIImage?) -> Void)) {
        self.scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
            let renderer = WebViewPrintPageRenderer.init(formatter: self.viewPrintFormatter(), contentSize: self.scrollView.contentSize)
            let image = renderer.printContentToImage()
            completion(image)
        }
    }
}

extension WKWebView {
    public func takeScreenshotOfFullContent(_ completion: @escaping ((UIImage?) -> Void)) {
        self.scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
            let renderer = WebViewPrintPageRenderer.init(formatter: self.viewPrintFormatter(), contentSize: self.scrollView.contentSize)
            let image = renderer.printContentToImage()
            completion(image)
        }
    }
}
複製代碼

WebViewPrintPageRenderer 是該方案的核心類,負責把 WebView組件內容打印到PDF,而後把PDF轉換爲圖片。

UIWebViewWKWebView 則實現對應的擴展。

測試代碼:

// example code
private func takeSnapshotOfUIWebView() {
    self.uiWebView.scrollView.takeScreenshotOfFullContent { (image) in
        // 處理image
    }
}

private func takeSnapshotOfWKWebView() {
    self.wkWebView.scrollView.takeScreenshotOfFullContent { (image) in
        // 處理image
    }
}
複製代碼

三種技術方案優劣對比

那麼,這三種技術方案各自存在什麼優缺點呢,適用什麼場景呢?

  • 方案一:只適用 UIWebView;若網頁內容不少,生成長截圖時,會佔用過多內存。 因此,該方案只適合不須要支持 WKWebView, 且網頁內容不會太多的場景。
  • 方案二:適用 UIWebViewWKWebView,且特別適合 WKWebView。因爲採用分頁生成截圖機制,有效減小內存佔用。不過,這個方案存在一個問題:若網頁存在 position: fixed 的元素(如網頁頭部固定的導航欄),該元素會重複出如今生成的長圖上。
  • 方案三:適用 UIWebViewWKWebView。其中最重要的一步——「把WebView內容打印到PDF」 是由iOS系統實現,因此該方案的性能在理論上是能夠獲得保障的。不過,這個方案存在一個問題:在把網頁內容打印到PDF時,iOS系統獲取的 contentSize 比WebView的實際contentSize 要大,從而致使生成的圖片在靠近底部的內容部分和實際存在一點差別。具體能夠下載運行個人長截圖庫 SnapshotKit 的 Demo,經過其中的 UIWebViewWKWebView 截圖示例查看具體截圖效果。

以上三個方案,總的來講,解決了部分場景的需求,但都不夠完美,仍需作進一步的優化。

相關文章
相關標籤/搜索