欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS使用WebView生成长截图的第3种解决方案

程序员文章站 2024-01-07 11:32:04
前言 webview就是一个内嵌浏览器控件,在ios中主要有两种webview:uiwebview和wkwebview,uiwebview是ios2之后开始使用,wk...

前言

webview就是一个内嵌浏览器控件,在ios中主要有两种webview:uiwebview和wkwebview,uiwebview是ios2之后开始使用,wkwebview是在ios8开始使用,wkwebview将逐步取代笨重的uiwebview。

由于项目需要,新近实现了一个长截图库 snapshotkit。其中,需要支持 uiwebview、wkwebview 组件生成长截图。为了实现这个特性,查阅了很多资料,同时也做了不同的新奇思路尝试,最终实现了一个新的、取巧的技术方案。

以下主要总结了在“webview生成长截图”需求方面,“网上已有方案”和“我的全新方案”的各自实现要点和优缺点。

webview生成长截图的已有方案

根据 google 所搜索到的资料,目前ios webview生成长截图的方案主要有2种:

  • 方案一:修改frame,截图组件
  • 方案二:分页截图组件内容,合成长图

下面将会简述方案一和方案二的具体实现。

方案一:修改frame,截图组件

方案一的实现要点在于:修改 webview.scrollview 的 framesize  为 contentsize,然后对整个 webview.scrollview 进行截图。

不过,这个方案只适用 uiwebview 组件,因为其是一次性加载网页所有的内容。而 wkwebview 组件,为了节省内存,加载网页内容时,只加载可视部分——这一点类似 uitableview 组件。在修改webview.scrollview 的 framesize 后,立即执行了截图操作, 这时候,wkwebview由于还没把网页的内容加载出来,导致生成的长截图是空白的。

方案一核心代码如下:

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文件上,借助这个特性,新方案的设计如下:

  • 把 webview组件的内容全部打印到一页pdf上
  • 把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转换为图片。
uiwebview 和 wkwebview 则实现对应的扩展。

测试代码:

// example code
private func takesnapshotofuiwebview() {
 self.uiwebview.scrollview.takescreenshotoffullcontent { (image) in
  // 处理image
 }
}

private func takesnapshotofwkwebview() {
 self.wkwebview.scrollview.takescreenshotoffullcontent { (image) in
  // 处理image
 }
}

三种技术方案优劣对比

那么,这三种技术方案各自存在什么优缺点呢,适用什么场景呢?

方案一:只适用 uiwebview;若网页内容很多,生成长截图时,会占用过多内存。 所以,该方案只适合不需要支持 wkwebview, 且网页内容不会太多的场景。

方案二:适用 uiwebview 和 wkwebview,且特别适合 wkwebview。由于采用分页生成截图机制,有效减少内存占用。不过,这个方案存在一个问题:若网页存在 position: fixed 的元素(如网页头部固定的导航栏),该元素会重复出现在生成的长图上。

方案三:适用 uiwebview 和 wkwebview。其中最重要的一步——“把webview内容打印到pdf” 是由ios系统实现,所以该方案的性能在理论上是可以得到保障的。不过,这个方案存在一个问题:在把网页内容打印到pdf时,ios系统获取的 contentsize 比webview的实际contentsize 要大,从而导致生成的图片在靠近底部的内容部分和实际存在一点差异。具体可以下载运行我的长截图库 snapshotkit 的 demo,通过其中的 uiwebview 和 wkwebview 截图示例查看具体截图效果。

以上三个方案,总的来说,解决了部分场景的需求,但都不够完美,仍需做进一步的优化。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。

上一篇:

下一篇: