macOS CI / build-and-test (push) Skipped
TargetRasterLoader decodes printtarg TIFFs via ImageIO and re-tags into DeviceGray/DeviceRGB/DeviceCMYK around the decoder's own dataProvider — no resample, no bit-depth change (D10). Skip-only alpha padding (noneSkipLast/First, which ImageIO emits for opaque RGB TIFFs) is accepted; real coverage alpha, indexed, float and non-1/3/4-component layouts are rejected as unsupportedLayout. DPI metadata drives pointSize (px/dpi*72) with a 72 dpi fallback + warn; manifestDrift reports >0.5 mm disagreement with the printtarg manifest (D9). TargetPageCanvasView is the headless flipped canvas NSPrintOperation paginates: rectForPage stacks pages top-down, destinationRect anchors 1:1 at the page's top-left snapped to 0.001 pt, and draw() forces interpolation/antialias/font-smoothing off per page with a drawProbe seam for tests. PDF harness (NativeSpoolPDFTests, skippable via ICCERY_SKIP_PRINT_PDF=1) ran on this host: run() -> true, 2 pages at A4 media box, and the fixture's 72 pt patch verified 1:1 at the painted top-left (printer unprintable-margin offset tolerated per R10).
127 lines
4.6 KiB
Swift
127 lines
4.6 KiB
Swift
import CoreGraphics
|
|
import Foundation
|
|
import ImageIO
|
|
import UniformTypeIdentifiers
|
|
|
|
/// Shared TIFF fixture builder for the #201 raster / canvas / PDF
|
|
/// harness tests. Writes real TIFFs via `CGImageDestination` so the
|
|
/// loader exercises the same decode path as production.
|
|
enum TargetTestFixtures {
|
|
|
|
enum FixtureError: Error {
|
|
case imageNotCreated
|
|
case destinationNotCreated
|
|
case finalizeFailed
|
|
}
|
|
|
|
/// Fresh per-test directory under the temp root.
|
|
static func makeTempDirectory() throws -> URL {
|
|
let url = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("iccery-raster-\(UUID().uuidString)")
|
|
try FileManager.default.createDirectory(
|
|
at: url, withIntermediateDirectories: true)
|
|
return url
|
|
}
|
|
|
|
/// A solid-fill (or repeating-pattern) `CGImage` with a device
|
|
/// colour space for 1/3/4 components at 8 or 16 bpc. `alpha: true`
|
|
/// appends one alpha sample per pixel (`last`). `pixelBytes` is the
|
|
/// repeating per-pixel pattern (big-endian for 16 bpc).
|
|
static func makeImage(
|
|
px: CGSize,
|
|
components: Int,
|
|
bitsPerComponent: Int,
|
|
alpha: Bool = false,
|
|
pixelBytes: [UInt8]? = nil
|
|
) -> CGImage? {
|
|
let width = Int(px.width)
|
|
let height = Int(px.height)
|
|
let bytesPerComponent = bitsPerComponent / 8
|
|
let samples = components + (alpha ? 1 : 0)
|
|
let bytesPerPixel = samples * bytesPerComponent
|
|
let bytesPerRow = width * bytesPerPixel
|
|
let pattern = pixelBytes
|
|
?? Array(0..<bytesPerPixel).map { UInt8(($0 * 37 + 11) & 0xff) }
|
|
var row = [UInt8](repeating: 0, count: bytesPerRow)
|
|
for offset in stride(from: 0, to: bytesPerRow, by: pattern.count) {
|
|
for (index, byte) in pattern.enumerated()
|
|
where offset + index < bytesPerRow {
|
|
row[offset + index] = byte
|
|
}
|
|
}
|
|
var bytes = [UInt8]()
|
|
bytes.reserveCapacity(bytesPerRow * height)
|
|
for _ in 0..<height {
|
|
bytes.append(contentsOf: row)
|
|
}
|
|
let data = Data(bytes)
|
|
|
|
let space: CGColorSpace
|
|
switch components {
|
|
case 1: space = CGColorSpaceCreateDeviceGray()
|
|
case 3: space = CGColorSpaceCreateDeviceRGB()
|
|
case 4: space = CGColorSpaceCreateDeviceCMYK()
|
|
default: return nil
|
|
}
|
|
var info = CGBitmapInfo()
|
|
if bitsPerComponent == 16 {
|
|
info.insert(.byteOrder16Big)
|
|
}
|
|
if alpha {
|
|
info = CGBitmapInfo(
|
|
rawValue: info.rawValue | CGImageAlphaInfo.last.rawValue)
|
|
}
|
|
guard let provider = CGDataProvider(data: data as CFData)
|
|
else { return nil }
|
|
return CGImage(
|
|
width: width, height: height,
|
|
bitsPerComponent: bitsPerComponent,
|
|
bitsPerPixel: bitsPerComponent * samples,
|
|
bytesPerRow: bytesPerRow,
|
|
space: space,
|
|
bitmapInfo: info,
|
|
provider: provider,
|
|
decode: nil,
|
|
shouldInterpolate: false,
|
|
intent: .defaultIntent)
|
|
}
|
|
|
|
/// Writes `makeImage` output to a single-page TIFF. `dpi` nil
|
|
/// produces a TIFF with no resolution tags (the 72-fallback path).
|
|
static func makeTIFF(
|
|
px: CGSize,
|
|
dpi: Double?,
|
|
components: Int = 3,
|
|
bitsPerComponent: Int = 8,
|
|
alpha: Bool = false,
|
|
pixelBytes: [UInt8]? = nil,
|
|
in directory: URL
|
|
) throws -> URL {
|
|
guard let image = makeImage(
|
|
px: px, components: components,
|
|
bitsPerComponent: bitsPerComponent,
|
|
alpha: alpha, pixelBytes: pixelBytes)
|
|
else { throw FixtureError.imageNotCreated }
|
|
let url = directory.appendingPathComponent(
|
|
"fixture-\(UUID().uuidString).tiff")
|
|
guard let destination = CGImageDestinationCreateWithURL(
|
|
url as CFURL, UTType.tiff.identifier as CFString, 1, nil)
|
|
else { throw FixtureError.destinationNotCreated }
|
|
var properties: [CFString: Any] = [:]
|
|
if let dpi {
|
|
properties[kCGImagePropertyDPIWidth] = dpi
|
|
properties[kCGImagePropertyDPIHeight] = dpi
|
|
properties[kCGImagePropertyTIFFDictionary] = [
|
|
kCGImagePropertyTIFFXResolution: dpi,
|
|
kCGImagePropertyTIFFYResolution: dpi,
|
|
kCGImagePropertyTIFFResolutionUnit: 2, // inches
|
|
]
|
|
}
|
|
CGImageDestinationAddImage(
|
|
destination, image, properties as CFDictionary)
|
|
guard CGImageDestinationFinalize(destination)
|
|
else { throw FixtureError.finalizeFailed }
|
|
return url
|
|
}
|
|
}
|