一、问题背景

在运用 WKWebViewcreatePDF 方法把一个网页的内容生成为 PDF 的时候,发现通常生成的 PDF 都是只要一页,但当网页满足长时,生成的 PDF 会被分为多页。

例如,运用 这个很长的网页 进行测试,会发现生成的 PDF 被分为了 6 页,前 5 页的分辨率为 390 14400,在 72dpi 下,1 厘米 ≈ 28.346 像素,所以对应 13.758 厘米 508.000 厘米,也便是一页 PDF 被限制到了这么高。

iOS实现PDF多页合并&分页
iOS实现PDF多页合并&分页
iOS实现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"))

iOS实现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;其次,扩大到一定程度能够看到下图二下图三的比照。

iOS实现PDF多页合并&分页

iOS实现PDF多页合并&分页
iOS实现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"))

终究的作用是很符合预期的。

三、终究作用

左为处理之前,右为处理之后。

iOS实现PDF多页合并&分页

拼接处细节:

iOS实现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,若因时效性原因导致文中内容有所疏忽,敬请谅解,也欢迎批评指正。