一、问题背景
在运用 WKWebView
的 createPDF
方法把一个网页的内容生成为 PDF 的时候,发现通常生成的 PDF 都是只要一页,但当网页满足长时,生成的 PDF 会被分为多页。
例如,运用 这个很长的网页 进行测试,会发现生成的 PDF 被分为了 6 页,前 5 页的分辨率为 390 14400,在 72dpi 下,1 厘米 ≈ 28.346 像素,所以对应 13.758 厘米 508.000 厘米,也便是一页 PDF 被限制到了这么高。
下文就对这个 input.pdf 进行操作。(原本想上传pdf文件的,但如同现在还不支撑?)
二、处理历程
1、首先判断是否是 iOS 体系生成 PDF 时存在天然限制
那么我就测验生成一个长于 14400 的 pdf 文件,发现是可行的。
let renderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: 390, height: 20000))
let data = renderer.pdfData { context in
}
try? data.write(to: URL(fileURLWithPath: "/Users/macbookpro/Desktop/test.pdf"))
2、把 PDF 的每页制作到同一个单页 PDF 上
既然能够生生长的 PDF ,那我把被分页了的 PDF 的每一页都画到一个 context 上,终究不就拿到了一个单页的 PDF 吗?
我想起来之前用 Core Graphics 画图片的时候,大概是这样写的:
let text = "text"
text.draw(at: CGPoint(x: 10, y: 30))
let image = UIImage(named: "test_image")
image?.draw(in: CGRect(x: 0, y: 0, width: 100, height: 100))
那么只要我手动把 PDF 的每个 page 画在同一个 page 的特定当地就行了。
可是却发现 PDFPage 只要下面这个 draw 方法,并没有让我们自定义制作的方位。
func draw(with box: PDFDisplayBox, to context: CGContext)
PDFDisplayBox 是一个枚举类型,期望制作 PDF 的整个页面就用 .mediaBox。
这儿要注意的一点是,PDFDocument 有两个,别离是 PDFDocument 和 CGPDFDocument,后者是 Core Graphics 里原生的表示一个 PDF 文件的类,前者是 PDFKit 中的类,适当于是封装了一层。它们的方法不太一样:
let path = "/Users/macbookpro/Desktop/input.pdf"
let renderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: 390, height: 20000))
// 运用 PDFDocument
let data = renderer.pdfData { context in
context.beginPage()
// PDFDocument 的 page 是从 0 开端的
let pdf = PDFDocument(url: URL(fileURLWithPath: path))!
for i in 0..<pdf.pageCount {
let page = pdf.page(at: i)!
page.draw(with: .mediaBox, to: context.cgContext)
}
}
// 运用 CGPDFDocument
let data = renderer.pdfData { context in
context.beginPage()
let cgPdf = CGPDFDocument(URL(fileURLWithPath: path) as CFURL)!
// CGPDFDocument 的 page 是从 1 开端的
for i in 1...cgPdf.numberOfPages {
let page = cgPdf.page(at: i)!
context.cgContext.drawPDFPage(page)
}
}
我就先这样写试了下:
let path = "/Users/macbookpro/Desktop/input.pdf"
let pdf = PDFDocument(url: URL(fileURLWithPath: path))!
var width: CGFloat = 0
var totalHeight: CGFloat = 0
var allPages: [PDFPage] = []
for i in 0..<pdf.pageCount {
let page = pdf.page(at: i)!
width = page.bounds(for: .mediaBox).width
totalHeight += page.bounds(for: .mediaBox).height
allPages.append(page)
}
let renderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: width, height: totalHeight))
let data = renderer.pdfData { context in
context.beginPage()
// 转化坐标系
context.cgContext.translateBy(x: 0, y: totalHeight)
context.cgContext.scaleBy(x: 1.0, y: -1.0)
for page in allPages {
page.draw(with: .mediaBox, to: context.cgContext)
}
}
try? data.write(to: URL(fileURLWithPath: "/Users/macbookpro/Desktop/output.pdf"))
(这儿忘记截图了)
公然,直接 draw 的话,后面的 page 会覆盖掉之前的 page。能够看到 output.pdf 中第 6 页因为短,只盖住了第 5 页的下面一小部分。
3、把 PDF 的每页别离制作成图片,然后再制作图片到 PDF 上的特定方位
在 Google 搜了一些相关的,看到一篇将 PDF 转为图片的文章,就想到了这个思路,写了个小 demo 测验。
let path = "/Users/macbookpro/Desktop/input.pdf"
let pdf = PDFDocument(url: URL(fileURLWithPath: path))!
var width: CGFloat = 0
var totalHeight: CGFloat = 0
// 把每页生成为图片
var allImages: [UIImage] = []
for i in 0..<pdf.pageCount {
let page = pdf.page(at: i)!
width = page.bounds(for: .mediaBox).width
totalHeight += page.bounds(for: .mediaBox).height
let renderer = UIGraphicsImageRenderer(size: page.bounds(for: .mediaBox).size)
let image = renderer.image { context in
page.draw(with: .mediaBox, to: context.cgContext)
}
allImages.append(image)
}
// 把所有图片画成一个单页 PDF
let renderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: width, height: totalHeight))
let data = renderer.pdfData { context in
context.beginPage()
var offset: CGFloat = 0
// 转化坐标系
context.cgContext.translateBy(x: 0, y: totalHeight)
context.cgContext.scaleBy(x: 1.0, y: -1.0)
for image in allImages {
offset += image.size.height
image.draw(in: CGRect(x: 0, y: totalHeight - offset, width: image.size.width, height: image.size.height))
}
}
try? data.write(to: URL(fileURLWithPath: "/Users/macbookpro/Desktop/output.pdf"))
发现成品根本能满足要求,缺陷也是很明显的:
- 增加了耗时,画了一遍图片,再画一遍 PDF。
- PDF 文件变大了,且烘托图片更为消耗功能,我在电脑上用 WPS 翻开它,一卡一卡的。
- PDF 是没有灵性的。首先不能再像本来一样挑选 PDF 里的文字了,下图一是本来的 PDF;其次,扩大到一定程度能够看到下图二下图三的比照。
回顾这个思路,我终究要的是 PDF,开始的原料也是 PDF,我把它转成图片再转回来,这根本就不合理啊,还是再看看怎么在画 PDF 时能操控画的方位吧。
4、经过改变坐标系,来操控即将制作的方位
上面的代码里能够看到这样两句:
// 转化坐标系
context.cgContext.translateBy(x: 0, y: totalHeight)
context.cgContext.scaleBy(x: 1.0, y: -1.0)
这是因为在 Quartz 2D 中默许的坐标体系是:原点(0,0)位于左下角,沿着 x 轴从左到右坐标值逐步增大;沿着 y 轴从下到上坐标值逐步增大。这和 UIView 或 PDFDocument 的坐标系是不同的,所以需求转化坐标系后再 draw。
那么是不是就能够经过在画每个 PDFPage 之前对坐标系进行一定的转化,就能操控 PDFPage 所制作的方位了呢?经过几番测验,总算成功了。
let path = "/Users/macbookpro/Desktop/input.pdf"
let pdf = PDFDocument(url: URL(fileURLWithPath: path))!
var width: CGFloat = 0
var totalHeight: CGFloat = 0
var allPages: [PDFPage] = []
for i in 0..<pdf.pageCount {
let page = pdf.page(at: i)!
width = page.bounds(for: .mediaBox).width
totalHeight += page.bounds(for: .mediaBox).height
allPages.append(page)
}
let renderer = UIGraphicsPDFRenderer(bounds: CGRect(x: 0, y: 0, width: width, height: totalHeight))
let data = renderer.pdfData { context in
context.beginPage()
var offset: CGFloat = 0
for page in allPages {
let pageBounds = page.bounds(for: .mediaBox)
context.cgContext.translateBy(x: 0, y: offset + pageBounds.height)
context.cgContext.scaleBy(x: 1.0, y: -1.0)
page.draw(with: .mediaBox, to: context.cgContext)
context.cgContext.translateBy(x: 0, y: offset + pageBounds.height)
context.cgContext.scaleBy(x: 1.0, y: -1.0)
offset += pageBounds.height
}
}
try? data.write(to: URL(fileURLWithPath: "/Users/macbookpro/Desktop/output.pdf"))
终究的作用是很符合预期的。
三、终究作用
左为处理之前,右为处理之后。
拼接处细节:
封装好的函数:
funcconvertMultiPageToSinglePage(dataoldPdfData:Data)->Data?{
guardletoldPdf=PDFDocument(data:oldPdfData)else{returnnil}
ifoldPdf.pageCount==0{returnnil}
ifoldPdf.pageCount==1{returnoldPdfData}
varallPages:[PDFPage]=[]
vartotalHeight:CGFloat=0
varwidth:CGFloat=0
foriin0..<oldPdf.pageCount{
guardletpage=oldPdf.page(at:i)else{continue}
letbounds=page.bounds(for:.mediaBox)
width=bounds.width
totalHeight+=bounds.height
allPages.append(page)
}
letpdfBounds=CGRect(x:0,y:0,width:width,height:totalHeight)
letrenderer=UIGraphicsPDFRenderer(bounds:pdfBounds)
letdata=renderer.pdfData{contextin
context.beginPage()
varoffset:CGFloat=0
forpageinallPages{
letpageBounds=page.bounds(for:.mediaBox)
context.cgContext.translateBy(x:0,y:offset+pageBounds.height)
context.cgContext.scaleBy(x:1.0,y:-1.0)
page.draw(with:.mediaBox,to:context.cgContext)
context.cgContext.translateBy(x:0,y:offset+pageBounds.height)
context.cgContext.scaleBy(x:1.0,y:-1.0)
offset+=pageBounds.height
}
}
returndata
}
四、拓展:PDF 分页
相同基于这个思路,很简单完成 PDF 的分页。
// ratioWidth 和 ratioHeight 的数值大小无所谓,传这两个是为了知道想要得到的 PDF 页的宽高比例
func convertSinglePageToMultiPage(data singlePagePdfData: Data, ratioWidth: CGFloat, ratioHeight: CGFloat) -> Data? {
guard let singlePagePdf = PDFDocument(data: singlePagePdfData) else { return nil }
guard let oldPage = singlePagePdf.page(at: 0) else { return nil }
let oldPdfWidth = oldPage.bounds(for: .mediaBox).width
let oldPdfHeight = oldPage.bounds(for: .mediaBox).height
let pageHeight = oldPdfWidth * ratioHeight / ratioWidth
let pageNum = Int(ceil(oldPdfHeight / pageHeight))
let newPdfBounds = CGRect(x: 0, y: 0, width: oldPdfWidth, height: pageHeight)
let renderer = UIGraphicsPDFRenderer(bounds: newPdfBounds)
let data = renderer.pdfData { context in
for i in 0..<pageNum {
let offset = pageHeight * CGFloat(i)
context.beginPage()
context.cgContext.translateBy(x: 0, y: oldPdfHeight - offset)
context.cgContext.scaleBy(x: 1.0, y: -1.0)
oldPage.draw(with: .mediaBox, to: context.cgContext)
}
}
return data
}
注
本文实践写于 2021 年 12 月,运用 iOS 15.0,若因时效性原因导致文中内容有所疏忽,敬请谅解,也欢迎批评指正。