PaperKitのPaperMarkupを画像変換でサムネイル表示

ベースはWWDC2025の「PaperKitの紹介」
https://developer.apple.com/jp/videos/play/wwdc2025/285

最終目的はPaperMarkupを画像化したものと、View情報でPDFエクスポートをしたい。
画面表示しない場合、asyncメソッドの取り扱いが課題になる。
下記のコードは、まず表示で画像化を確認したもの。

import SwiftUI
import PaperKit
import CoreGraphics
import UIKit

struct ThumbnailView: View {
    let markupModel: PaperMarkup?
    let size: CGSize
    
    @State private var thumbnail: CGImage? = nil
    
    var body: some View {
        Group {
            if let thumbnail {
                Image(decorative: thumbnail, scale: Device.screenScale(), orientation: .up)
                    .resizable()
                    .scaledToFit()
            } else {
                ZStack {
                    RoundedRectangle(cornerRadius: 12)
                        .fill(Color.gray.opacity(0.1))
                    Text("No thumbnail")
                        .foregroundStyle(.secondary)
                }
                .frame(width: 200, height: 200)
            }
        }
        .background(.gray)
        .task {
            do {
                if let markup = self.markupModel {
                    try await self.updateThumbnail(markup)
                }
            } catch {
                // You can log or handle the error here
                print("Failed to update thumbnail: \(error)")
            }
        }
    }
    
    @MainActor func updateThumbnail(_ markupModel: PaperMarkup) async throws {
        let thumbnailSize = CGSize(width: self.size.width, height: self.size.height)
        let scale = Device.screenScale()
        guard let context = makeCGContext(size: thumbnailSize, scale: scale) else {
            throw NSError(domain: "ThumbnailView", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to create CGContext"]) 
        }

        // 背景を指定する場合、指定なしで透過
//        context.setFillColor(gray: 1, alpha: 1)
//        context.fill(CGRect(origin: .zero, size: CGSize(width: thumbnailSize.width * scale, height: thumbnailSize.height * scale)))

        context.saveGState()

        // UIKit(上が0)とCoreGraphics(下が0)の座標系差を吸収
        context.translateBy(x: 0, y: thumbnailSize.height * scale)
        context.scaleBy(x: 1, y: -1)
        
        context.scaleBy(x: scale, y: scale)
        await markupModel.draw(in: context, frame: CGRect(origin: .zero, size: thumbnailSize))
        context.restoreGState()

        if let image = context.makeImage() {
            await MainActor.run { self.thumbnail = image }
        }
    }
    
    private func makeCGContext(size: CGSize, scale: CGFloat) -> CGContext? {
        let width = Int(size.width * scale)
        let height = Int(size.height * scale)
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue
        return CGContext(data: nil,
                         width: width,
                         height: height,
                         bitsPerComponent: 8,
                         bytesPerRow: 0,
                         space: colorSpace,
                         bitmapInfo: bitmapInfo)
    }
}

SwiftSwift,SwiftUI

Posted by shi-n