Files
ICCery-CPU/Sources/Models/TargetPage.swift
T
Local Admin 43bff18101
Build macOS Packages / Build macOS (Intel) (push) Failing after 10s
Build macOS Packages / Build macOS (Apple Silicon) (push) Failing after 10s
Build macOS Packages / Build macOS (Universal) (push) Skipped
removed cruft and restructured.
2026-09-07 05:21:44 -07:00

155 lines
5.8 KiB
Swift

import Foundation
import ImageIO
import CoreGraphics
import AppKit
/// One raster page. Loaded with ImageIO (SPEC §6.1) and held as a Device-RGB
/// `CGImage` whose pixel values match the TIFF (SPEC §6.3).
final class TargetPage {
let url: URL
let name: String
let pixelWidth: Int
let pixelHeight: Int
let dpiX: CGFloat
let dpiY: CGFloat
/// Device RGB, `shouldInterpolate = false`. Used for both preview and print.
let cgImage: CGImage
/// Low-resolution thumbnail for the sidebar (SPEC §9).
let thumbnail: NSImage
var physicalWidthIn: CGFloat {
Geometry.physicalInches(pixels: CGFloat(pixelWidth), dpi: dpiX)
}
var physicalHeightIn: CGFloat {
Geometry.physicalInches(pixels: CGFloat(pixelHeight), dpi: dpiY)
}
init(url: URL, name: String, pixelWidth: Int, pixelHeight: Int,
dpiX: CGFloat, dpiY: CGFloat, cgImage: CGImage, thumbnail: NSImage) {
self.url = url
self.name = name
self.pixelWidth = pixelWidth
self.pixelHeight = pixelHeight
self.dpiX = dpiX
self.dpiY = dpiY
self.cgImage = cgImage
self.thumbnail = thumbnail
}
static func load(url: URL) throws -> [TargetPage] {
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else {
throw AppError.unreadableTarget(url.path)
}
let count = CGImageSourceGetCount(source)
guard count > 0 else { throw AppError.unreadableTarget(url.path) }
var pages: [TargetPage] = []
for index in 0..<count {
pages.append(try load(source: source, url: url, index: index, total: count))
}
return pages
}
private static func load(source: CGImageSource, url: URL, index: Int, total: Int) throws -> TargetPage {
let options: [CFString: Any] = [
kCGImageSourceShouldCache: true,
kCGImageSourceShouldAllowFloat: false,
kCGImageSourceCreateThumbnailFromImageIfAbsent: false,
]
guard let image = CGImageSourceCreateImageAtIndex(source, index, options as CFDictionary) else {
throw AppError.unreadableTarget("\(url.path) frame \(index)")
}
var dpiX: CGFloat = 72
var dpiY: CGFloat = 72
if let props = CGImageSourceCopyPropertiesAtIndex(source, index, nil) as? [CFString: Any] {
if let x = props[kCGImagePropertyDPIWidth] as? CGFloat, x > 0 { dpiX = x }
else if let x = props[kCGImagePropertyDPIWidth] as? NSNumber, x.doubleValue > 0 { dpiX = CGFloat(x.doubleValue) }
if let y = props[kCGImagePropertyDPIHeight] as? CGFloat, y > 0 { dpiY = y }
else if let y = props[kCGImagePropertyDPIHeight] as? NSNumber, y.doubleValue > 0 { dpiY = CGFloat(y.doubleValue) }
}
guard let unmanaged = makeUnmanagedDeviceRGB(image) else {
throw AppError.unreadableTarget("unable to wrap \(url.lastPathComponent) as Device RGB")
}
let suffix = total > 1 ? " [\(index + 1)]" : ""
let name = url.lastPathComponent + suffix
let thumb = makeThumbnail(unmanaged)
return TargetPage(
url: url,
name: name,
pixelWidth: unmanaged.width,
pixelHeight: unmanaged.height,
dpiX: dpiX,
dpiY: dpiY,
cgImage: unmanaged,
thumbnail: thumb
)
}
/// Reinterpret the source bitmap as Device RGB without a ColorSync transform.
/// SPEC §6.3: do **not** call `CGImageCreateCopyWithColorSpace` with a calibrated space.
static func makeUnmanagedDeviceRGB(_ image: CGImage) -> CGImage? {
let width = image.width
let height = image.height
let bpc = 8
let spp = 4
let bpr = width * spp
let device = CGColorSpaceCreateDeviceRGB()
guard let buffer = NSMutableData(length: bpr * height) else { return nil }
let bitmapInfo = CGBitmapInfo.byteOrder32Big.rawValue | CGImageAlphaInfo.noneSkipLast.rawValue
guard let ctx = CGContext(
data: buffer.mutableBytes,
width: width,
height: height,
bitsPerComponent: bpc,
bytesPerRow: bpr,
space: image.colorSpace ?? device,
bitmapInfo: bitmapInfo
) else { return nil }
ctx.interpolationQuality = .none
ctx.setShouldAntialias(false)
ctx.setAllowsAntialiasing(false)
ctx.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
guard let provider = CGDataProvider(data: buffer) else { return nil }
return CGImage(
width: width,
height: height,
bitsPerComponent: bpc,
bitsPerPixel: 32,
bytesPerRow: bpr,
space: device,
bitmapInfo: CGBitmapInfo(rawValue: bitmapInfo),
provider: provider,
decode: nil,
shouldInterpolate: false,
intent: .defaultIntent
)
}
private static func makeThumbnail(_ image: CGImage) -> NSImage {
let maxEdge: CGFloat = 128
let w = CGFloat(image.width)
let h = CGFloat(image.height)
let scale = min(maxEdge / max(w, 1), maxEdge / max(h, 1), 1)
let tw = max(1, (w * scale).rounded())
let th = max(1, (h * scale).rounded())
let img = NSImage(size: NSSize(width: tw, height: th))
img.lockFocus()
if let gc = NSGraphicsContext.current {
gc.imageInterpolation = .medium
gc.shouldAntialias = true
}
let ns = NSImage(cgImage: image, size: NSSize(width: w, height: h))
ns.draw(in: NSRect(x: 0, y: 0, width: tw, height: th),
from: NSRect(x: 0, y: 0, width: w, height: h),
operation: .copy,
fraction: 1)
img.unlockFocus()
return img
}
}