从 UILabel 文本掩码获取 PDF

Get PDF from a UILabel text mask

我正在尝试使用 UILabel 文本掩码从 UIView 获取 PDF。

  override func viewDidLoad() {
        super.viewDidLoad()
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 200 ))
        label.text = "Label Text"
        label.font = UIFont.systemFont(ofSize: 25)
        label.textAlignment = .center
        label.textColor = UIColor.white

        let overlayView = UIImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 200 ))
        overlayView.image = UIImage(named: "erasemask.png")
        
        label.mask = overlayView
        view_process.addSubview(label)
        
    }

   func exportAsPdfFromView(){

        let pdfPageFrame = CGRect(x: 0, y: 0, width: view_process.bounds.size.width, height: view_process.bounds.size.height)

        let pdfData = NSMutableData()
        UIGraphicsBeginPDFContextToData(pdfData, pdfPageFrame, nil)
        UIGraphicsBeginPDFPageWithInfo(pdfPageFrame, nil)

        guard let pdfContext = UIGraphicsGetCurrentContext() else { return "" }
       
        view_process.layer.render(in: pdfContext)
        UIGraphicsEndPDFContext()
          
       let path = self.saveViewPdf(data: pdfData)
        print(path)

      }

    func saveViewPdf(data: NSMutableData) -> String {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let docDirectoryPath = paths[0]
        let pdfPath = docDirectoryPath.appendingPathComponent("viewPdf.pdf")
        if data.write(to: pdfPath, atomically: true) {
            return pdfPath.path
        } else {
            return ""
        }
      }

但我没有得到带遮罩的 PDF。我不想将 UIView 转换为 UImage,然后再将 UImage 转换为 PDF。 我想要可编辑的 PDF,所以不想转换成 UIImage。

谁能帮助我如何将 Masked UILabel 转换为 PDF?

此处erasemask.png

您必须自己在 pdf 中绘制文本才能使其可编辑。

我稍微修改了你的代码,它工作了。您可以简单地 copy/paste 在新的应用程序视图控制器中检查结果。

这是在 AffinityDesigner 中打开的文档的屏幕截图。蒙版正确应用为图层蒙版,文本可编辑。

它需要一些调整以符合精确的布局。

import UIKit

class ViewController: UIViewController {
    
    var labelFrame: CGRect { return CGRect(x: 0, y: 0, width: 200, height: 200 )}
    
    lazy var label: UILabel = {
        let label = UILabel(frame: labelFrame)
        label.text = "Label Text"
        label.font = UIFont.systemFont(ofSize: 25)
        label.textAlignment = .center
        label.textColor = UIColor.black
        return label
    }()
    
    var maskImage = UIImage(named: "erasemask.png")
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(label)
        let overlayView = UIImageView(frame: labelFrame)
        overlayView.image = maskImage
        
        label.mask = overlayView

        exportAsPdfFromView()
    }
    
    func exportAsPdfFromView() {
        
        let pdfPageFrame = CGRect(x: 0, y: 0, width: view.bounds.size.width, height: view.bounds.size.height)
        
        let pdfData = NSMutableData()
        UIGraphicsBeginPDFContextToData(pdfData, pdfPageFrame, nil)
        UIGraphicsBeginPDFPageWithInfo(pdfPageFrame, nil)
        guard let context = UIGraphicsGetCurrentContext() else { return }
                
        // Clip context
        if let overlayImage = maskImage?.cgImage {
            context.clip(to: labelFrame, mask: overlayImage)
        }
        
        // Draw String
        let string: String = label.text ?? ""
        let attributes: [NSAttributedString.Key: Any] = [
            NSAttributedString.Key.foregroundColor: label.textColor ?? .black,
            NSAttributedString.Key.font: label.font ?? UIFont.systemFont(ofSize: 25)
        ]
        let stringRectSize = string.size(withAttributes: attributes)
        let x = (labelFrame.width - stringRectSize.width) / 2
        let y = (labelFrame.height - stringRectSize.height) / 2
        let stringRect = CGRect(origin: CGPoint(x: x, y: y), size: stringRectSize)
        string.draw(in: stringRect, withAttributes: attributes)

        UIGraphicsEndPDFContext()
        
        saveViewPdf(data: pdfData)
    }
    
    func saveViewPdf(data: NSMutableData) {
        guard let docDirectoryPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
        let pdfPath = docDirectoryPath.appendingPathComponent("viewPdf.pdf")
        print(pdfPath)
        data.write(to: pdfPath, atomically: true)
    }
}

已编辑

作为比较,我使用 Apple view.render 函数添加了结果,使用问题中的代码(蓝色背景,以便我们可以看到白色文本)。它清楚地表明该功能不支持可编辑文本和遮罩。 它将文档导出为一堆平面图像和额外的 - 无用的 - 组。 所以我想保留 pdf 实体类型的唯一解决方案是通过渲染每个对象来组成文档。

如果你需要导出复杂的表单,做一个在视图层次中爬行并渲染每个对象内容的助手一定不会太难。

如果您还需要使用交互式按钮呈现 pdf,您可能需要使用注释(在 PDFKit 中可用)。顺便说一下,您还可以创建带有注释的可编辑字段,但我不确定它是否支持屏蔽。

最后编辑:

由于您使用外部框架来呈现 pdf ( PDFGenerator ),因此有所不同。只能覆盖框架使用的view.layer.render函数,使用context.clip函数。

要将ui 组件导出为可编辑的,我认为它们必须没有父视图。一旦存在视图层次结构,就会创建一个支持位图,并且所有渲染调用都使用此位图作为参数进行,而不是用于调用第一个渲染的 PDFContext。

这就是 PDFGenerator 的工作原理,它在视图层次结构中爬行,通过从超级视图中删除它们来呈现视图,呈现到上下文,然后将它们移回层次结构。

这样做的最大缺点是当视图在层次结构中移回时会导致错误。可能是因为约束丢失,或者某些视图(如 UIStackView)的行为不同。在他们的 github.

上有许多未解决的问题

我还不太明白为什么下面的标签即使在视图层次结构中也呈现为可编辑。我猜这是由于 Apple 如何完成 render 功能。现在不能再进一步了..

自定义字段示例:

可能需要做更多的工作才能尊重精确的字段布局,但这是呈现为可编辑文本的视图的基础。它给出的结果与答案的第一部分完全相同。

  • 它适用于任何蒙版视图,而不仅仅是 UIImage。
  • 即使在层次结构中也呈现为可编辑。

所以如果你在这个问题的初始代码中使用这个标签而不是 UILabel,它就可以了。

// Extension to get mask view as an image
extension UIView {
    var cgImage: CGImage? {
        UIGraphicsBeginImageContextWithOptions(bounds.size, isOpaque, contentScaleFactor);
        guard let context = UIGraphicsGetCurrentContext() else { return nil }
        layer.render(in: context)
        let image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return image?.cgImage
    }
}

class PMLayer: CALayer {
    // Needed to access text style
    weak var label: UILabel?
    
    override init() { super.init() }
    required init?(coder: NSCoder) { super.init(coder: coder) }
    
    override init(layer: Any) {
        super.init(layer: layer)
        if let pm = layer as? PMLayer {
            label = pm.label
        }
    }
    
    override func render(in ctx: CGContext) {
        guard let label = label else { return }
        
        // Clip context
        if let mask = label.mask?.cgImage {
            ctx.clip(to: label.frame, mask: mask)
        }
        
        // Draw String
        let string: String = label.text ?? ""
        let attributes: [NSAttributedString.Key: Any] = [
            NSAttributedString.Key.foregroundColor: label.textColor ?? .black,
            NSAttributedString.Key.font: label.font ?? UIFont.systemFont(ofSize: 25)
        ]
        let stringRectSize = string.size(withAttributes: attributes)
        let x = (label.frame.width - stringRectSize.width) / 2
        let y = (label.frame.height - stringRectSize.height) / 2
        let stringRect = CGRect(origin: CGPoint(x: x, y: y), size: stringRectSize)
        string.draw(in: stringRect, withAttributes: attributes)
    }
}

class PMLabel: UILabel {
    override class var layerClass: AnyClass { return PMLayer.self }

    // Be sure the label is correctly linked to the layer when we access it
    override var layer: CALayer {
        (super.layer as? PMLayer)?.label = self
        return super.layer
    }
}

这是我的决定

import UIKit
import PDFKit

class ViewController: UIViewController {
    
    @IBOutlet weak var view_process: UIView!
    @IBOutlet weak var pdf_view: UIView!
    let documentInteractionController = UIDocumentInteractionController()
    @IBAction func bClick(_ sender: Any) {
        exportAsPdfFromView()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        documentInteractionController.delegate = self
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 200 ))
        label.text = "Label Text"
        label.font = UIFont.systemFont(ofSize: 25)
        label.textAlignment = .center
        label.textColor = UIColor.blue
        
        let overlayView = UIImageView(frame: CGRect(x: 0, y: 0, width: 200, height: 200 ))
        overlayView.image = UIImage(named: "erasemask.png")
        
        label.mask = overlayView
        view_process.addSubview(label)
        
    }
    
    func exportAsPdfFromView(){
        
        let pdfPageFrame = view_process.bounds//.CGRect(x: 0, y: 0, width: view_process.bounds.size.width, height: view_process.bounds.size.height)
        
        let pdfData = NSMutableData()
        UIGraphicsBeginPDFContextToData(pdfData, pdfPageFrame, nil)
        UIGraphicsBeginPDFPageWithInfo(pdfPageFrame, nil)
        
        let pdfContext = UIGraphicsGetCurrentContext()!
        
        view_process.layer.render(in: pdfContext)
        UIGraphicsEndPDFContext()
        
        let path = self.saveViewPdf(data: pdfData)
        print(path)
        self.share(url: URL(string: "file://\(path)")!)
    }
    
    func saveViewPdf(data: NSMutableData) -> String {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        let docDirectoryPath = paths[0]
        let pdfPath = docDirectoryPath.appendingPathComponent("viewPdf.pdf")
        if data.write(to: pdfPath, atomically: true) {
            return pdfPath.path
        } else {
            return ""
        }
    }
    
    func openPdf(path: String) {
        let pdfView = PDFView(frame: self.view.bounds)
        pdfView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        pdf_view.addSubview(pdfView)
        
        // Fit content in PDFView.
        pdfView.autoScales = true
        
        // Load Sample.pdf file from app bundle.
        let fileURL = URL(string: path)
        pdfView.document = PDFDocument(url: fileURL!)
    }
    
    
    func share(url: URL) {
        documentInteractionController.url = url
        //        documentInteractionController.uti = url.typeIdentifier ?? "public.data, public.content"
        //        documentInteractionController.name = url.localizedName ?? url.lastPathComponent
        documentInteractionController.presentPreview(animated: true)
    }
}

extension ViewController: UIDocumentInteractionControllerDelegate {
    func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
        guard let navVC = self.navigationController else {
            return self
        }
        return navVC
    }
}