NSView 和 PDF 不同的输出

Posted

技术标签:

【中文标题】NSView 和 PDF 不同的输出【英文标题】:NSView and PDF different output 【发布时间】:2020-08-24 16:45:57 【问题描述】:

适用于 macOS 和 Swift 5.3

我正在使用 Quartz 生成 PDF,通过使用屏幕外 CALayers 并通过我找到的 CALayer 扩展将组合层转换为 NSImage here。

但是,cgpath 行的屏幕质量与 PDF 质量相差很大。

如您所见,左侧的图像(均以 100% 缩放)(屏幕输出)对于我的 cgpath 有窄线,而右侧的图像(pdf 输出)路径更大且模糊。此外,这两种情况下的文本都是模糊的。什么会导致这种情况发生,我该如何解决它以使 cgpath 线条清晰(并且像屏幕上一样小)并且文本清晰?如果我减小路径的 lineWidth 不会使它们在 PDF 中变小,它会使它们更加不透明。我正在使用系统字体 Helvetica 作为文本。

支持层是一个单独的 CALayer,我添加了一些 CAShapeLayers、CATextLayers 和 CALayers 来制作一个视图,然后按照上述方法将组合层转换为 NSImage,并将其保存为 PDF 文档。

这是我用来生成 PDF 的代码:

NSBezierPath 扩展:

extension NSBezierPath 
    // Credit - Henrick - 9/18/2016
    // https://***.com/questions/1815568/how-can-i-convert-nsbezierpath-to-cgpath
    public var cgPath: CGPath 
        let path = CGMutablePath()
        var points = [CGPoint](repeating: .zero, count: 3)
        for i in 0 ..< self.elementCount 
            let type = self.element(at: i, associatedPoints: &points)
            switch type 
            case .moveTo:
                path.move(to: points[0])
            case .lineTo:
                path.addLine(to: points[0])
            case .curveTo:
                path.addCurve(to: points[2], control1: points[0], control2: points[1])
            case .closePath:
                path.closeSubpath()
            @unknown default:
                print("Error occurred in NSBezierPath extension.")
            
        
        return path
    

CALayer 扩展:

extension CALayer 

    /*
     Credit - Robert Myran - 12/29/2016
     https://***.com/questions/41386423/get-image-from-calayer-or-nsview-swift-3
     */
    /// Get `NSImage` representation of the layer.
    ///
    /// - Returns: `NSImage` of the layer.

    func image() -> NSImage 
        let width = 612
        let height = 792
        let imageRepresentation = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: width, pixelsHigh: height, bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSColorSpaceName.deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)!
        imageRepresentation.size = CGSize(width: width, height: height)

        let context = NSGraphicsContext(bitmapImageRep: imageRepresentation)!

        render(in: context.cgContext)

        return NSImage(cgImage: imageRepresentation.cgImage!, size: bounds.size)
    


NSImage 扩展:

extension NSImage 

    /*
     Credit - Xue Yu - 7/9/2017
     https://gist.github.com/KrisYu/83d7d97cae35a0b10fd238e5c86d288f
     */

    var toCGImage: CGImage 
        var imageRect = NSRect(x: 0, y: 0, width: size.width, height: size.height)
        guard let image =  cgImage(forProposedRect: &imageRect, context: nil, hints: nil) else 
            abort()
        
        return image
    

网格绘制方法:

func insertGrid() -> CALayer 

    /*
     Draws a single table grid of 25 boxes (5 high by 5 wide)
     centered on a letter sized page
     */

    // Create a new shape layer for the grid
    let gridLayer = CAShapeLayer()
    // Assign the grid fill and stroke colors
    gridLayer.strokeColor = NSColor.black.cgColor
    gridLayer.fillColor = NSColor.clear.cgColor

    // Create the path
    let gridPath = NSBezierPath()
    gridPath.lineWidth = 0.25

    // Draw the paths for the grid
    // Create the outside box
    gridPath.move(to: CGPoint(x: col1X, y: bottomY)) // Bottom left corner
    gridPath.line(to: CGPoint(x: col1X, y: topY)) // Column 1, left line
    gridPath.line(to: CGPoint(x: rightX, y: topY)) // Top line of row
    gridPath.line(to: CGPoint(x: rightX, y: bottomY)) // Column 3 right line
    gridPath.line(to: CGPoint(x: col1X, y: bottomY)) // Bottom line of row
        
    // Add in column lines
    gridPath.move(to: CGPoint(x: col2X, y: topY)) // Between columns 1 & 2
    gridPath.line(to: CGPoint(x: col2X, y: bottomY)) // Line between columns 1 & 2
    gridPath.move(to: CGPoint(x: col3X, y: topY)) // Between columns 2 & 3
    gridPath.line(to: CGPoint(x: col3X, y: bottomY)) // Line between columns 2 & 3

    // Close the path
    gridPath.close()
    // Add grid to layer (note the use of the cgPath extension)
    gridLayer.path = gridPath.cgPath

    return gridLayer

创建PDF方法:

func createPDF(image: NSImage) -> NSData 

    /*
     Credit - Xue Yu - 7/9/2017
     https://gist.github.com/KrisYu/83d7d97cae35a0b10fd238e5c86d288f
     */

    let pdfData = NSMutableData()

    let pdfConsumer = CGDataConsumer(data: pdfData as CFMutableData)!
        
    var mediaBox = NSRect.init(x: 0, y: 0, width: image.size.width, height: image.size.height)
        
    let pdfContext = CGContext(consumer: pdfConsumer, mediaBox: &mediaBox, nil)!

    pdfContext.beginPage(mediaBox: &mediaBox)
    pdfContext.draw(image.toCGImage , in: mediaBox)
    pdfContext.endPage()
        
    return pdfData

我这样称呼它:

createPDF(image: pdfBaseLayer.image())?.write(to: pdfFileURL, atomically: true)

有没有办法将其构建为矢量图像,还是没有必要?

这就是我的工作:

    将 pdfBaseLayer 创建为 CALayer 将 gridLayer 创建为 CAShapeLayer,然后执行 pdfBaseLayer.addSublayer(gridLayer) 将 textLayer 创建为三个单独的 CATextLayer 子层和 然后是 pdfBaseLayer.addSubLayer(textLayer)。

如果您需要更多代码,请告诉我。

【问题讨论】:

稍微想一想,在开始生成 PDF 之前,我就知道它有多少页。为了获得清晰的线条和更清晰的文本,我是否应该只创建一个具有count 空白页长的 PDF 文档,然后将路径和文本直接绘制到 PDF 中? 【参考方案1】:

问题已解决。我将 CALayer 作为 CGContext 插入到 pdf 中。本质上,据我所知,它是将 CGContext 转换为图像,然后将其放入 pdf 中。从那以后,我改变了我的方法,放弃创建 CALayer 直接写入 CGContext,然后将其写入 pdf。这种方式让一切都超级清晰。

【讨论】:

以上是关于NSView 和 PDF 不同的输出的主要内容,如果未能解决你的问题,请参考以下文章

从具有相同名称但不同扩展名的文件(*.pdf 和 *.rtf)中,如果 *.rtf 较新,则在控制台上输出消息

MacOS-MacAPP的NSView改变背景颜色

特立独行的问题:在 NSView 上添加子视图时

不使用 NSView 打印

使用层支持的 NSView 作为 NSDockTile contentView

PDFBox:拆分 pdf 和输出前缀