创建包含多页的PDF文件

14

我需要实现一个创建包含多页文本的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类准备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
    }
}

实际输出

enter image description here

期望的输出

整个文本将出现在 PDF 中,具有新的 PDF 页面而不会减小字体大小。

非常感谢您的任何帮助。 谢谢。

3个回答

10

输出

输入图像描述

已使用CTFramesetterCreateFrameCFAttributedStringGetLength进行修复


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类准备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
    }
}

在类似Adobe Acrobat Pro的软件中,这个PDF文件的所有文本都可以编辑吗? - Christian W
@pratol sodha,另外,您如何控制此内容在第一页的垂直位置开始打印? - Christian W

4
与Pratik Sodha的答案类似,但使用TextKit而不是CoreText。这样做的主要优点是TextKit完全支持NSAttributedString,而不是CFAttributedString。这意味着我现在可以在文本中放置NSTextAttachment,而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)
        }
    }
}

我已经测试了这段代码,在iOS14.X上可以运行,但在iOS15.X上无法运行。 - wlixcc
@wlixcc 感谢您通知我。我在iOS 15尝试了一些方法,现在应该已经修复了。 - Sweeper
非常抱歉,它正在工作。我不得不将文本类型从'''NSMutableAttributedString'''更改为'''NSAttributedString'''。 - Christian W
这种方法唯一的问题是,像Adobe Acrobat Pro这样的程序无法对文本进行编辑。 - Christian W
我测试了这段代码,它很好地工作了。我希望我能给它点赞100次。 - Ismail Osunlana

3

这个答案很启发人。但我还需要用不同的样式渲染文本,为了重复使用你的代码,我将 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
}

and addText(_:context) to 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),在此之前,我们需要“重置”上下文。

// 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)

我正在努力让这个工作起来。我看到了上面的其他例子,可以让主要答案工作,但我更愿意应用你的答案,但是我得到了一堆错误.. 这是完整的代码吗?有什么遗漏的吗? - JerseyDevel
2
这里有一个在playground中编写的可工作示例 - Duke Fredrick

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接