创建包含多个页面的 PDF
Create PDF with multiple pages
我需要实现创建包含多页文本的 pdf 的功能。
class PDFCreator {
func prepareData() -> Data {
//1
let pdfMetaData = [
kCGPDFContextCreator: "PDF Creator",
kCGPDFContextAuthor: "Pratik Sodha",
kCGPDFContextTitle: "My PDF"
]
//2
let format = UIGraphicsPDFRendererFormat()
format.documentInfo = pdfMetaData as [String: Any]
//3
let pageWidth = 8.5 * 72.0
let pageHeight = 11 * 72.0
let pageRect = CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
//4
let renderer = UIGraphicsPDFRenderer(bounds: pageRect, format: format)
//5
let data = renderer.pdfData { (context) in
//6
context.beginPage()
self.addText("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", pageRect: pageRect)
}
return data
}
@discardableResult
func addText(_ text : String, pageRect: CGRect) -> CGFloat {
// 1
let textFont = UIFont.systemFont(ofSize: 60.0, weight: .regular)
// 2
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .natural
paragraphStyle.lineBreakMode = .byWordWrapping
// 3
let textAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: textFont
]
let attributedText = NSAttributedString(string: text, attributes: textAttributes)
let textSize = attributedText.boundingRect(with: pageRect.size, options: [.usesFontLeading, .usesLineFragmentOrigin], context: nil)
// 4
let textRect = CGRect(x: 10,
y: 10,
width: pageRect.width - 20,
height: textSize.height)
attributedText.draw(in: textRect)
return textRect.origin.y + textRect.size.height
}
}
使用PDFCreator
class准备pdf数据并使用PDFView显示。
import UIKit
import PDFKit
class PDFPreviewViewController: UIViewController {
//1
@IBOutlet weak private var pdfView : PDFView!
override func viewDidLoad() {
super.viewDidLoad()
//2
let pdfData = PDFCreator().prepareData()
//3
pdfView.document = PDFDocument(data: pdfData)
pdfView.autoScales = true
}
}
实际输出
异常输出
Whole text will be in PDF with new PDF page without decreasing font size.
非常感谢任何帮助。
谢谢你。
输出
Fixed using CTFramesetterCreateFrame
and CFAttributedStringGetLength
class PDFCreator {
lazy var pageWidth : CGFloat = {
return 8.5 * 72.0
}()
lazy var pageHeight : CGFloat = {
return 11 * 72.0
}()
lazy var pageRect : CGRect = {
CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
}()
lazy var marginPoint : CGPoint = {
return CGPoint(x: 10, y: 10)
}()
lazy var marginSize : CGSize = {
return CGSize(width: self.marginPoint.x * 2 , height: self.marginPoint.y * 2)
}()
func prepareData() -> Data {
//1
let pdfMetaData = [
kCGPDFContextCreator: "PDF Creator",
kCGPDFContextAuthor: "Pratik Sodha",
kCGPDFContextTitle: "My PDF"
]
//2
let format = UIGraphicsPDFRendererFormat()
format.documentInfo = pdfMetaData as [String: Any]
//3
let renderer = UIGraphicsPDFRenderer(bounds: pageRect, format: format)
//5
let data = renderer.pdfData { (context) in
//6
self.addText("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", context: context)
}
return data
}
@discardableResult
func addText(_ text : String, context : UIGraphicsPDFRendererContext) -> CGFloat {
// 1
let textFont = UIFont.systemFont(ofSize: 60.0, weight: .regular)
// 2
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .natural
paragraphStyle.lineBreakMode = .byWordWrapping
// 3
let textAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: textFont
]
//4
let currentText = CFAttributedStringCreate(nil,
text as CFString,
textAttributes as CFDictionary)
//5
let framesetter = CTFramesetterCreateWithAttributedString(currentText!)
//6
var currentRange = CFRangeMake(0, 0)
var currentPage = 0
var done = false
repeat {
//7
/* Mark the beginning of a new page.*/
context.beginPage()
//8
/*Draw a page number at the bottom of each page.*/
currentPage += 1
drawPageNumber(currentPage)
//9
/*Render the current page and update the current range to
point to the beginning of the next page. */
currentRange = renderPage(currentPage,
withTextRange: currentRange,
andFramesetter: framesetter)
//10
/* If we're at the end of the text, exit the loop. */
if currentRange.location == CFAttributedStringGetLength(currentText) {
done = true
}
} while !done
return CGFloat(currentRange.location + currentRange.length)
}
func renderPage(_ pageNum: Int, withTextRange currentRange: CFRange, andFramesetter framesetter: CTFramesetter?) -> CFRange {
var currentRange = currentRange
// Get the graphics context.
let currentContext = UIGraphicsGetCurrentContext()
// Put the text matrix into a known state. This ensures
// that no old scaling factors are left in place.
currentContext?.textMatrix = .identity
// Create a path object to enclose the text. Use 72 point
// margins all around the text.
let frameRect = CGRect(x: self.marginPoint.x, y: self.marginPoint.y, width: self.pageWidth - self.marginSize.width, height: self.pageHeight - self.marginSize.height)
let framePath = CGMutablePath()
framePath.addRect(frameRect, transform: .identity)
// Get the frame that will do the rendering.
// The currentRange variable specifies only the starting point. The framesetter
// lays out as much text as will fit into the frame.
let frameRef = CTFramesetterCreateFrame(framesetter!, currentRange, framePath, nil)
// Core Text draws from the bottom-left corner up, so flip
// the current transform prior to drawing.
currentContext?.translateBy(x: 0, y: self.pageHeight)
currentContext?.scaleBy(x: 1.0, y: -1.0)
// Draw the frame.
CTFrameDraw(frameRef, currentContext!)
// Update the current range based on what was drawn.
currentRange = CTFrameGetVisibleStringRange(frameRef)
currentRange.location += currentRange.length
currentRange.length = CFIndex(0)
return currentRange
}
func drawPageNumber(_ pageNum: Int) {
let theFont = UIFont.systemFont(ofSize: 20)
let pageString = NSMutableAttributedString(string: "Page \(pageNum)")
pageString.addAttribute(NSAttributedString.Key.font, value: theFont, range: NSRange(location: 0, length: pageString.length))
let pageStringSize = pageString.size()
let stringRect = CGRect(x: (pageRect.width - pageStringSize.width) / 2.0,
y: pageRect.height - (pageStringSize.height) / 2.0 - 15,
width: pageStringSize.width,
height: pageStringSize.height)
pageString.draw(in: stringRect)
}
}
使用 PDFCreator class 准备 pdf 数据并使用 PDFView 显示。
import UIKit
import PDFKit
class PDFPreviewViewController: UIViewController {
//1
@IBOutlet weak private var pdfView : PDFView!
override func viewDidLoad() {
super.viewDidLoad()
//2
let pdfData = PDFCreator().prepareData()
//3
pdfView.document = PDFDocument(data: pdfData)
pdfView.autoScales = true
}
}
这个答案很鼓舞人心。但我还需要渲染不同样式的文本,所以为了重用你的代码,我将 renderPage(_:withTextRange:andFramesetter)
更改为 renderPage(_:withTextRange:andFramesetter:from)
:
func renderPage(_ pageNum: Int, withTextRange currentRange: CFRange, andFramesetter framesetter: CTFramesetter?, from lastPosition: CGFloat = 0) -> CFRange {
var currentRange = currentRange
let currentContext = UIGraphicsGetCurrentContext()
currentContext?.textMatrix = .identity
// MARK: - Chanages1: Resize rect based on the last ending point
let frameRect = CGRect(
x: self.marginPoint.x,
y: self.marginPoint.y + lastPosition,
width: self.pageWidth - self.marginSize.width,
height: self.pageHeight - self.marginSize.height - lastPosition
)
let framePath = CGMutablePath()
framePath.addRect(frameRect)
let frameRef = CTFramesetterCreateFrame(framesetter!, currentRange, framePath, nil)
// MARK: - Changes2
if lastPosition == 0 {
currentContext?.translateBy(x: 0, y: self.pageHeight)
currentContext?.scaleBy(x: 1.0, y: -1.0)
} else {
// MARK: - Already in the context, no need to "mirror"
currentContext?.translateBy(x: 0, y: -lastPosition)
}
CTFrameDraw(frameRef, currentContext!)
currentRange = CTFrameGetVisibleStringRange(frameRef)
currentRange.location += currentRange.length
currentRange.length = CFIndex(0)
return currentRange
}
和addText(_:context)
到add(_:font:in:at:from)
func add(_ text: String,
font: UIFont,
in context: UIGraphicsPDFRendererContext,
at currentPage: Int,
from lastPosition: CGFloat = 0) -> (Int, CGFloat) {
// ...
var lastPosition = lastPosition
var newPosition: CGFloat = 0
repeat {
// ...
// MARK: - Changes1: text left needed to fill
let textRect = CTFramesetterSuggestFrameSizeWithConstraints(
framesetter, currentRange, nil,
.init(
width: self.pageWidth - self.marginSize.width,
height: self.pageHeight - self.marginSize.height - lastPosition
),
nil
)
currentRange = renderPage(2, withTextRange: currentRange, andFramesetter: framesetter, from: lastPosition)
// MARK: - Changes2: reset after first drawing
lastPosition = 0
// MARK: - save the newPosition
newPosition = textRect.height > newPosition ? textRect.height : newPosition
// ...
} while !done
return (currentPage, newPosition)
}
现在,在 prepareData
中,我可以像这样添加不同样式的文本:
let data = renderer.pdfData { (context) in
context.beginPage()
drawPageNumber(1)
var lastGroup: (Int, CGFloat)
lastGroup = add(body, font: .systemFont(ofSize: 23), in: context, at: 1)
lastGroup = add(body, font: .systemFont(ofSize: 33), in: context, at: lastGroup.0, from: lastGroup.1)
lastGroup = add(body, font: .systemFont(ofSize: 43), in: context, at: lastGroup.0, from: lastGroup.1)
lastGroup = add(body, font: .systemFont(ofSize: 53), in: context, at: lastGroup.0, from: lastGroup.1)
}
更新
旧版本仅在文本超过页面高度时才有效。绘制较短的文本会很乱。这次保留上次画完的位置
这是更新后的版本:
// MARK: - lazy vars
lazy var maxTextBounds: CGSize = {
return CGSize(width: pageWidth - marginSize.width,
height: pageHeight - marginSize.height)
}()
lazy var cgContext: CGContext = {
print("getting cgContext")
let context = UIGraphicsGetCurrentContext()!
context.textMatrix = .identity
// MARK: - flip context
context.translateBy(x: 0, y: pageHeight)
context.scaleBy(x: 1, y: -1)
return context
}()
// MARK: - Render page
func renderPage(_ pageNum: Int, withTextRange currentRange: CFRange, andFramesetter framesetter: CTFramesetter?, from lastPosition: CGFloat = 0) -> (CFRange, CGFloat) {
// MARK: - text height in current page
let textBounds = CTFramesetterSuggestFrameSizeWithConstraints(framesetter!,
currentRange,
nil,
.init(width: maxTextBounds.width, height: maxTextBounds.height - lastPosition),
nil)
if maxTextBounds.height == lastPosition {
// not enough space in this page
// MARK: - reset
return (currentRange, 0)
}
// MARK: - path where text drawn at
let framePath = CGMutablePath()
// MARK: - invisble rect surrounds the text, when drawing the rect will be move to marginPoint
framePath.addRect(CGRect(origin: .zero, size: textBounds))
// MARK: - text frame
let frameRef = CTFramesetterCreateFrame(framesetter!, currentRange, framePath, nil)
// MARK: - move up
print("move up by", pageHeight - (textBounds.height + lastPosition + marginPoint.y))
cgContext.translateBy(x: marginPoint.x, y: pageHeight - (textBounds.height + lastPosition + marginPoint.y))
// MARK: - draw
CTFrameDraw(frameRef, cgContext)
// MARK: - move back for next
cgContext.translateBy(x: -marginPoint.x, y: -pageHeight + (textBounds.height + lastPosition + marginPoint.y))
// MARK: - udpate current range
var currentRange = currentRange
currentRange = CTFrameGetVisibleStringRange(frameRef)
currentRange.location += currentRange.length
currentRange.length = CFIndex(0)
// MARK: - updating the succeeding position
var newPosition = textBounds.height + lastPosition
if newPosition >= pageHeight - marginSize.height {
newPosition = 0
}
return (currentRange, newPosition)
}
当文本太长放不下当前页面时,add(_:font:in:at:from)
会重复调用renderPage(_:withTextRange:andFramesetter:from)
,在此之前,我们需要"reset"上下文。
// in else block
context.beginPage()
currentPage += 1
drawPageNumber(currentPage)
lastPosition = 0
// MARK: - new Page, reset context for those texts not finished drawing
cgContext.textMatrix = .identity
cgContext.translateBy(x: 0, y: pageHeight)
cgContext.scaleBy(x: 1, y: -1)
类似于 Pratik Sodha 的回答,但使用 TextKit 而不是 CoreText。这样做的主要优点是 TextKit 完全支持 NSAttributedString
,而不是 CFAttributedString
。这意味着我现在可以在文本中放置 NSTextAttachment
s,CFAttributedString
不支持。
class AttributedStringToPDFConverter {
lazy var pageWidth : CGFloat = {
return 8.5 * 72.0
}()
lazy var pageHeight : CGFloat = {
return 11 * 72.0
}()
lazy var pageRect : CGRect = {
CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
}()
lazy var marginPoint : CGPoint = {
return CGPoint(x: 50, y: 50)
}()
lazy var marginSize : CGSize = {
return CGSize(width: self.marginPoint.x * 2 , height: self.marginPoint.y * 2)
}()
func pdfData(from attributedString: NSAttributedString) -> Data {
let pdfMetaData = [
kCGPDFContextCreator: "...",
kCGPDFContextTitle: "..."
]
let format = UIGraphicsPDFRendererFormat()
format.documentInfo = pdfMetaData as [String: Any]
let renderer = UIGraphicsPDFRenderer(bounds: pageRect, format: format)
let data = renderer.pdfData { (context) in
self.addText(attributedString, context: context)
}
return data
}
private func addText(_ text : NSAttributedString, context : UIGraphicsPDFRendererContext) {
let layoutManager = NSLayoutManager()
let textStorage = NSTextStorage()
textStorage.append(text)
textStorage.addLayoutManager(layoutManager)
let textContainerSize = CGSize(width: pageWidth - marginSize.width, height: pageHeight - marginSize.height)
var textContainer: NSTextContainer
var textViews = [UITextView]()
// keep adding text containers and text views...
repeat {
textContainer = NSTextContainer(size: textContainerSize)
layoutManager.addTextContainer(textContainer)
textViews.append(UITextView(frame: CGRect(origin: marginPoint, size: textContainerSize), textContainer: textContainer))
// while the last glyph is not in a text container
} while layoutManager.textContainer(forGlyphAt: layoutManager.numberOfGlyphs - 1, effectiveRange: nil) == nil
// draw each text view
for textView in textViews {
context.beginPage()
context.cgContext.translateBy(x: marginPoint.x, y: marginPoint.y)
textView.textContainerInset = .zero
textView.backgroundColor = .white
textView.layer.render(in: context.cgContext)
}
}
}
我需要实现创建包含多页文本的 pdf 的功能。
class PDFCreator {
func prepareData() -> Data {
//1
let pdfMetaData = [
kCGPDFContextCreator: "PDF Creator",
kCGPDFContextAuthor: "Pratik Sodha",
kCGPDFContextTitle: "My PDF"
]
//2
let format = UIGraphicsPDFRendererFormat()
format.documentInfo = pdfMetaData as [String: Any]
//3
let pageWidth = 8.5 * 72.0
let pageHeight = 11 * 72.0
let pageRect = CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
//4
let renderer = UIGraphicsPDFRenderer(bounds: pageRect, format: format)
//5
let data = renderer.pdfData { (context) in
//6
context.beginPage()
self.addText("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", pageRect: pageRect)
}
return data
}
@discardableResult
func addText(_ text : String, pageRect: CGRect) -> CGFloat {
// 1
let textFont = UIFont.systemFont(ofSize: 60.0, weight: .regular)
// 2
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .natural
paragraphStyle.lineBreakMode = .byWordWrapping
// 3
let textAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: textFont
]
let attributedText = NSAttributedString(string: text, attributes: textAttributes)
let textSize = attributedText.boundingRect(with: pageRect.size, options: [.usesFontLeading, .usesLineFragmentOrigin], context: nil)
// 4
let textRect = CGRect(x: 10,
y: 10,
width: pageRect.width - 20,
height: textSize.height)
attributedText.draw(in: textRect)
return textRect.origin.y + textRect.size.height
}
}
使用PDFCreator
class准备pdf数据并使用PDFView显示。
import UIKit
import PDFKit
class PDFPreviewViewController: UIViewController {
//1
@IBOutlet weak private var pdfView : PDFView!
override func viewDidLoad() {
super.viewDidLoad()
//2
let pdfData = PDFCreator().prepareData()
//3
pdfView.document = PDFDocument(data: pdfData)
pdfView.autoScales = true
}
}
实际输出
异常输出
Whole text will be in PDF with new PDF page without decreasing font size.
非常感谢任何帮助。 谢谢你。
输出
Fixed using
CTFramesetterCreateFrame
andCFAttributedStringGetLength
class PDFCreator {
lazy var pageWidth : CGFloat = {
return 8.5 * 72.0
}()
lazy var pageHeight : CGFloat = {
return 11 * 72.0
}()
lazy var pageRect : CGRect = {
CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
}()
lazy var marginPoint : CGPoint = {
return CGPoint(x: 10, y: 10)
}()
lazy var marginSize : CGSize = {
return CGSize(width: self.marginPoint.x * 2 , height: self.marginPoint.y * 2)
}()
func prepareData() -> Data {
//1
let pdfMetaData = [
kCGPDFContextCreator: "PDF Creator",
kCGPDFContextAuthor: "Pratik Sodha",
kCGPDFContextTitle: "My PDF"
]
//2
let format = UIGraphicsPDFRendererFormat()
format.documentInfo = pdfMetaData as [String: Any]
//3
let renderer = UIGraphicsPDFRenderer(bounds: pageRect, format: format)
//5
let data = renderer.pdfData { (context) in
//6
self.addText("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.", context: context)
}
return data
}
@discardableResult
func addText(_ text : String, context : UIGraphicsPDFRendererContext) -> CGFloat {
// 1
let textFont = UIFont.systemFont(ofSize: 60.0, weight: .regular)
// 2
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .natural
paragraphStyle.lineBreakMode = .byWordWrapping
// 3
let textAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: textFont
]
//4
let currentText = CFAttributedStringCreate(nil,
text as CFString,
textAttributes as CFDictionary)
//5
let framesetter = CTFramesetterCreateWithAttributedString(currentText!)
//6
var currentRange = CFRangeMake(0, 0)
var currentPage = 0
var done = false
repeat {
//7
/* Mark the beginning of a new page.*/
context.beginPage()
//8
/*Draw a page number at the bottom of each page.*/
currentPage += 1
drawPageNumber(currentPage)
//9
/*Render the current page and update the current range to
point to the beginning of the next page. */
currentRange = renderPage(currentPage,
withTextRange: currentRange,
andFramesetter: framesetter)
//10
/* If we're at the end of the text, exit the loop. */
if currentRange.location == CFAttributedStringGetLength(currentText) {
done = true
}
} while !done
return CGFloat(currentRange.location + currentRange.length)
}
func renderPage(_ pageNum: Int, withTextRange currentRange: CFRange, andFramesetter framesetter: CTFramesetter?) -> CFRange {
var currentRange = currentRange
// Get the graphics context.
let currentContext = UIGraphicsGetCurrentContext()
// Put the text matrix into a known state. This ensures
// that no old scaling factors are left in place.
currentContext?.textMatrix = .identity
// Create a path object to enclose the text. Use 72 point
// margins all around the text.
let frameRect = CGRect(x: self.marginPoint.x, y: self.marginPoint.y, width: self.pageWidth - self.marginSize.width, height: self.pageHeight - self.marginSize.height)
let framePath = CGMutablePath()
framePath.addRect(frameRect, transform: .identity)
// Get the frame that will do the rendering.
// The currentRange variable specifies only the starting point. The framesetter
// lays out as much text as will fit into the frame.
let frameRef = CTFramesetterCreateFrame(framesetter!, currentRange, framePath, nil)
// Core Text draws from the bottom-left corner up, so flip
// the current transform prior to drawing.
currentContext?.translateBy(x: 0, y: self.pageHeight)
currentContext?.scaleBy(x: 1.0, y: -1.0)
// Draw the frame.
CTFrameDraw(frameRef, currentContext!)
// Update the current range based on what was drawn.
currentRange = CTFrameGetVisibleStringRange(frameRef)
currentRange.location += currentRange.length
currentRange.length = CFIndex(0)
return currentRange
}
func drawPageNumber(_ pageNum: Int) {
let theFont = UIFont.systemFont(ofSize: 20)
let pageString = NSMutableAttributedString(string: "Page \(pageNum)")
pageString.addAttribute(NSAttributedString.Key.font, value: theFont, range: NSRange(location: 0, length: pageString.length))
let pageStringSize = pageString.size()
let stringRect = CGRect(x: (pageRect.width - pageStringSize.width) / 2.0,
y: pageRect.height - (pageStringSize.height) / 2.0 - 15,
width: pageStringSize.width,
height: pageStringSize.height)
pageString.draw(in: stringRect)
}
}
使用 PDFCreator class 准备 pdf 数据并使用 PDFView 显示。
import UIKit
import PDFKit
class PDFPreviewViewController: UIViewController {
//1
@IBOutlet weak private var pdfView : PDFView!
override func viewDidLoad() {
super.viewDidLoad()
//2
let pdfData = PDFCreator().prepareData()
//3
pdfView.document = PDFDocument(data: pdfData)
pdfView.autoScales = true
}
}
这个答案很鼓舞人心。但我还需要渲染不同样式的文本,所以为了重用你的代码,我将 renderPage(_:withTextRange:andFramesetter)
更改为 renderPage(_:withTextRange:andFramesetter:from)
:
func renderPage(_ pageNum: Int, withTextRange currentRange: CFRange, andFramesetter framesetter: CTFramesetter?, from lastPosition: CGFloat = 0) -> CFRange {
var currentRange = currentRange
let currentContext = UIGraphicsGetCurrentContext()
currentContext?.textMatrix = .identity
// MARK: - Chanages1: Resize rect based on the last ending point
let frameRect = CGRect(
x: self.marginPoint.x,
y: self.marginPoint.y + lastPosition,
width: self.pageWidth - self.marginSize.width,
height: self.pageHeight - self.marginSize.height - lastPosition
)
let framePath = CGMutablePath()
framePath.addRect(frameRect)
let frameRef = CTFramesetterCreateFrame(framesetter!, currentRange, framePath, nil)
// MARK: - Changes2
if lastPosition == 0 {
currentContext?.translateBy(x: 0, y: self.pageHeight)
currentContext?.scaleBy(x: 1.0, y: -1.0)
} else {
// MARK: - Already in the context, no need to "mirror"
currentContext?.translateBy(x: 0, y: -lastPosition)
}
CTFrameDraw(frameRef, currentContext!)
currentRange = CTFrameGetVisibleStringRange(frameRef)
currentRange.location += currentRange.length
currentRange.length = CFIndex(0)
return currentRange
}
和addText(_:context)
到add(_:font:in:at:from)
func add(_ text: String,
font: UIFont,
in context: UIGraphicsPDFRendererContext,
at currentPage: Int,
from lastPosition: CGFloat = 0) -> (Int, CGFloat) {
// ...
var lastPosition = lastPosition
var newPosition: CGFloat = 0
repeat {
// ...
// MARK: - Changes1: text left needed to fill
let textRect = CTFramesetterSuggestFrameSizeWithConstraints(
framesetter, currentRange, nil,
.init(
width: self.pageWidth - self.marginSize.width,
height: self.pageHeight - self.marginSize.height - lastPosition
),
nil
)
currentRange = renderPage(2, withTextRange: currentRange, andFramesetter: framesetter, from: lastPosition)
// MARK: - Changes2: reset after first drawing
lastPosition = 0
// MARK: - save the newPosition
newPosition = textRect.height > newPosition ? textRect.height : newPosition
// ...
} while !done
return (currentPage, newPosition)
}
现在,在 prepareData
中,我可以像这样添加不同样式的文本:
let data = renderer.pdfData { (context) in
context.beginPage()
drawPageNumber(1)
var lastGroup: (Int, CGFloat)
lastGroup = add(body, font: .systemFont(ofSize: 23), in: context, at: 1)
lastGroup = add(body, font: .systemFont(ofSize: 33), in: context, at: lastGroup.0, from: lastGroup.1)
lastGroup = add(body, font: .systemFont(ofSize: 43), in: context, at: lastGroup.0, from: lastGroup.1)
lastGroup = add(body, font: .systemFont(ofSize: 53), in: context, at: lastGroup.0, from: lastGroup.1)
}
更新
旧版本仅在文本超过页面高度时才有效。绘制较短的文本会很乱。这次保留上次画完的位置
这是更新后的版本:
// MARK: - lazy vars
lazy var maxTextBounds: CGSize = {
return CGSize(width: pageWidth - marginSize.width,
height: pageHeight - marginSize.height)
}()
lazy var cgContext: CGContext = {
print("getting cgContext")
let context = UIGraphicsGetCurrentContext()!
context.textMatrix = .identity
// MARK: - flip context
context.translateBy(x: 0, y: pageHeight)
context.scaleBy(x: 1, y: -1)
return context
}()
// MARK: - Render page
func renderPage(_ pageNum: Int, withTextRange currentRange: CFRange, andFramesetter framesetter: CTFramesetter?, from lastPosition: CGFloat = 0) -> (CFRange, CGFloat) {
// MARK: - text height in current page
let textBounds = CTFramesetterSuggestFrameSizeWithConstraints(framesetter!,
currentRange,
nil,
.init(width: maxTextBounds.width, height: maxTextBounds.height - lastPosition),
nil)
if maxTextBounds.height == lastPosition {
// not enough space in this page
// MARK: - reset
return (currentRange, 0)
}
// MARK: - path where text drawn at
let framePath = CGMutablePath()
// MARK: - invisble rect surrounds the text, when drawing the rect will be move to marginPoint
framePath.addRect(CGRect(origin: .zero, size: textBounds))
// MARK: - text frame
let frameRef = CTFramesetterCreateFrame(framesetter!, currentRange, framePath, nil)
// MARK: - move up
print("move up by", pageHeight - (textBounds.height + lastPosition + marginPoint.y))
cgContext.translateBy(x: marginPoint.x, y: pageHeight - (textBounds.height + lastPosition + marginPoint.y))
// MARK: - draw
CTFrameDraw(frameRef, cgContext)
// MARK: - move back for next
cgContext.translateBy(x: -marginPoint.x, y: -pageHeight + (textBounds.height + lastPosition + marginPoint.y))
// MARK: - udpate current range
var currentRange = currentRange
currentRange = CTFrameGetVisibleStringRange(frameRef)
currentRange.location += currentRange.length
currentRange.length = CFIndex(0)
// MARK: - updating the succeeding position
var newPosition = textBounds.height + lastPosition
if newPosition >= pageHeight - marginSize.height {
newPosition = 0
}
return (currentRange, newPosition)
}
当文本太长放不下当前页面时,add(_:font:in:at:from)
会重复调用renderPage(_:withTextRange:andFramesetter:from)
,在此之前,我们需要"reset"上下文。
// in else block
context.beginPage()
currentPage += 1
drawPageNumber(currentPage)
lastPosition = 0
// MARK: - new Page, reset context for those texts not finished drawing
cgContext.textMatrix = .identity
cgContext.translateBy(x: 0, y: pageHeight)
cgContext.scaleBy(x: 1, y: -1)
类似于 Pratik Sodha 的回答,但使用 TextKit 而不是 CoreText。这样做的主要优点是 TextKit 完全支持 NSAttributedString
,而不是 CFAttributedString
。这意味着我现在可以在文本中放置 NSTextAttachment
s,CFAttributedString
不支持。
class AttributedStringToPDFConverter {
lazy var pageWidth : CGFloat = {
return 8.5 * 72.0
}()
lazy var pageHeight : CGFloat = {
return 11 * 72.0
}()
lazy var pageRect : CGRect = {
CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
}()
lazy var marginPoint : CGPoint = {
return CGPoint(x: 50, y: 50)
}()
lazy var marginSize : CGSize = {
return CGSize(width: self.marginPoint.x * 2 , height: self.marginPoint.y * 2)
}()
func pdfData(from attributedString: NSAttributedString) -> Data {
let pdfMetaData = [
kCGPDFContextCreator: "...",
kCGPDFContextTitle: "..."
]
let format = UIGraphicsPDFRendererFormat()
format.documentInfo = pdfMetaData as [String: Any]
let renderer = UIGraphicsPDFRenderer(bounds: pageRect, format: format)
let data = renderer.pdfData { (context) in
self.addText(attributedString, context: context)
}
return data
}
private func addText(_ text : NSAttributedString, context : UIGraphicsPDFRendererContext) {
let layoutManager = NSLayoutManager()
let textStorage = NSTextStorage()
textStorage.append(text)
textStorage.addLayoutManager(layoutManager)
let textContainerSize = CGSize(width: pageWidth - marginSize.width, height: pageHeight - marginSize.height)
var textContainer: NSTextContainer
var textViews = [UITextView]()
// keep adding text containers and text views...
repeat {
textContainer = NSTextContainer(size: textContainerSize)
layoutManager.addTextContainer(textContainer)
textViews.append(UITextView(frame: CGRect(origin: marginPoint, size: textContainerSize), textContainer: textContainer))
// while the last glyph is not in a text container
} while layoutManager.textContainer(forGlyphAt: layoutManager.numberOfGlyphs - 1, effectiveRange: nil) == nil
// draw each text view
for textView in textViews {
context.beginPage()
context.cgContext.translateBy(x: marginPoint.x, y: marginPoint.y)
textView.textContainerInset = .zero
textView.backgroundColor = .white
textView.layer.render(in: context.cgContext)
}
}
}