25 KiB
ICCery-CPU — Specification
ICCery Colour Print Utility
Binary name: TargetPrint
Repository: ICCery-CPU
Version: 0.0.1 (initial)
Status: Specification / Implementation Ready
This document is the authoritative technical specification for the TargetPrint native macOS utility. It is intended to be placed at the root of a new empty repository named ICCery-CPU and used as the single source of truth for implementation.
Table of Contents
- Purpose & Scope
- Architecture Overview
- Target Environment & Constraints
- Invocation Contract & IPC
- JSON Job Schema (
TargetJob) - High-Fidelity Rendering Engine
- Colour Management Bypass
- Multi-Page Pagination
- User Interface (Standalone Mode)
- CUPS / PPD Integration
- Project Layout
- Build System
- Error Handling, Logging & Diagnostics
- Security & Entitlements
- Verification & Acceptance Tests
- Tauri / Rust Integration Contract
- Future Extensions (Non-Goals for v1)
1. Purpose & Scope
TargetPrint is a high-precision, compiled, native macOS utility written in Swift + AppKit (with Objective-C/Carbon bridging where required). It serves as an independent fallback companion for ICCery, a Rust/Tauri colour-profiling desktop application.
Mission
Print multi-page raster colour profiling targets (.tif / .tiff) with absolute geometric and pixel-level fidelity, while completely disabling or strictly controlling:
- Operating-system ColourSync transformations
- Driver-level colour adjustments
- Any Quartz 2D interpolation or anti-aliasing during rendering
Out of Scope (v1)
- PDF or vector target printing
- Windows / Linux support
- Network print queue management beyond the local CUPS system
- Colour management profile creation or editing
- Automatic printer driver installation
2. Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ ICCery (Tauri) │
│ (Rust side serialises TargetJob JSON → temp file) │
└──────────────────────────┬──────────────────────────────────┘
│ std::process::Command
│ --job /tmp/iccery_job_UUID.json
▼
┌─────────────────────────────────────────────────────────────┐
│ TargetPrint.app │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ main.swift │→ │ AppDelegate │→ │ PreviewWindow │ │
│ │ (CLI parse) │ │ │ │ Controller │ │
│ └─────────────┘ └──────────────┘ └─────────┬─────────┘ │
│ │ │
│ ┌────────────────────────────────────────────▼──────────┐ │
│ │ TargetCanvasView (NSView) │ │
│ │ • ImageIO decode (zero interpolation) │ │
│ │ • Exact DPI geometry in PostScript points │ │
│ │ • knowsPageRange / rectForPage pagination │ │
│ └────────────────────────────────────────────┬──────────┘ │
│ │ │
│ ┌────────────────────────────────────────────▼──────────┐ │
│ │ PrintEngine │ │
│ │ • NSPrintOperation + NSPrintInfo │ │
│ │ • Carbon CorePrinting dictionary injection │ │
│ │ • UI policy (lock Color Matching pane) │ │
│ └────────────────────────────────────────────┬──────────┘ │
│ │ │
│ ┌────────────────────────────────────────────▼──────────┐ │
│ │ CUPSManager │ │
│ │ • libcups (cupsGetNamedDest / cupsGetPPD) │ │
│ │ • AirPrint detection │ │
│ │ • Vendor-specific PPD option injection │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
The process is always a full AppKit GUI application (NSApplication). It never runs as a pure command-line tool; even when launched with --job it owns its own run loop and presents UI.
3. Target Environment & Constraints
| Item | Requirement |
|---|---|
| Deployment Target | macOS 10.15 Catalina → macOS 26+ Tahoe |
| Architectures | Universal 2 (x86_64 + arm64) |
| Language / Runtime | Swift 5.x (compatible with the oldest supported Xcode that still targets 10.15) |
| UI Framework | AppKit only (no SwiftUI for v1) |
| Third-party dependencies | None. Link exclusively against system frameworks |
| Frameworks | AppKit, CoreGraphics, ImageIO, ApplicationServices (CorePrinting), Foundation, libcups (via bridging header) |
| Sandbox | Hardened Runtime enabled, App Sandbox disabled |
| Code Signing | Required for distribution; development builds may use ad-hoc signing |
| Minimum Xcode | Xcode 10+ (for 10.15 support); recommended modern Xcode with dual-arch support |
Bridging Header
// Bridging-Header.h
#import <cups/cups.h>
#import <cups/ppd.h>
4. Invocation Contract & IPC
Two mutually exclusive modes.
Mode A — Structured Job Mode (ICCery → TargetPrint)
/Applications/TargetPrint.app/Contents/MacOS/TargetPrint --job /tmp/iccery_job_<uuid>.json
Alternative: file-association open of a .targetjob file (UTI to be registered in Info.plist).
Lifecycle Requirements
- Process launches as a normal AppKit application.
- Immediately parses the job file (or fails with a clear alert + non-zero exit).
- Creates the preview window and loads the target pages.
- Configures
NSPrintInfofrom the job. - Presents
NSPrintPanelas a modal sheet on the preview window without requiring further user action to reach the print dialog. - The calling Tauri process must not block waiting for the print dialog; TargetPrint owns its run loop.
Exit codes:
| Code | Meaning |
|---|---|
| 0 | Normal termination (user cancelled or print completed) |
| 1 | Invalid / missing job file |
| 2 | One or more target files unreadable |
| 3 | Printer / CUPS error |
| 4 | Internal fatal error |
Mode B — Standalone Interactive Mode
Launched with no arguments or by double-clicking the app icon / dropping files onto it.
Behaviour:
- Empty drop-zone canvas.
- Accepts multi-file drag-and-drop of
.tif/.tiff. - Standard File → Open… (multi-select).
- Page thumbnail sidebar.
- Inspector for printer, media, tray, scale, colour-management mode.
- Explicit Print… button that shows
NSPrintPanel.
5. JSON Job Schema (TargetJob)
{
"$schema": "https://iccery.local/schemas/targetjob.v1.json",
"version": 1,
"jobTitle": "Epson_XP55_IlfordLustre_Target_P1-2",
"files": [
"/absolute/path/to/target_page_1.tif",
"/absolute/path/to/target_page_2.tif"
],
"printSettings": {
"printerName": "EPSON_XP_55_Series",
"mediaSize": "A4",
"mediaType": "PremiumGlossy",
"paperSource": "Auto",
"resolution": "5760x1440dpi",
"scaling": 1.0,
"centered": true,
"forceUnmanagedColor": true
},
"uiPolicy": {
"lockColorManagement": true,
"allowBasicDriverChanges": true
}
}
Field Semantics
| Field | Type | Required | Notes |
|---|---|---|---|
version |
int | yes | Currently only 1 is accepted |
jobTitle |
string | yes | Used as window title and CUPS job name |
files |
[string] | yes | Absolute paths; order defines page order |
printSettings.printerName |
string | yes | Exact CUPS queue name (lpstat -a) |
printSettings.mediaSize |
string | yes | e.g. A4, Letter, 4x6, or PPD media name |
printSettings.mediaType |
string | no | PPD MediaType value |
printSettings.paperSource |
string | no | PPD InputSlot value (Auto, Tray1, …) |
printSettings.resolution |
string | no | Informational / PPD Resolution if present |
printSettings.scaling |
number | yes | 1.0 = 100 %. Only 1.0 is recommended for profiling |
printSettings.centered |
bool | yes | Centre image within imageable area |
printSettings.forceUnmanagedColor |
bool | yes | Master switch for colour bypass |
uiPolicy.lockColorManagement |
bool | yes | Hide / disable Color Matching pane |
uiPolicy.allowBasicDriverChanges |
bool | yes | Whether paper size / orientation remain editable |
Validation must be strict. Unknown keys are ignored; missing required keys cause an immediate fatal error with a user-visible alert.
6. High-Fidelity Rendering Engine
6.1 Image Loading (ImageIO)
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
let image = CGImageSourceCreateImageAtIndex(source, 0, [
kCGImageSourceShouldCache: true,
kCGImageSourceShouldAllowFloat: false
] as CFDictionary) else { /* error */ }
Extract DPI:
let props = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any]
let dpiX = props?[kCGImagePropertyDPIWidth] as? CGFloat ?? 72.0
let dpiY = props?[kCGImagePropertyDPIHeight] as? CGFloat ?? 72.0
let pixelWidth = CGFloat(image.width)
let pixelHeight = CGFloat(image.height)
6.2 Geometry (PostScript Points)
macOS printing uses 72 points = 1 inch.
physicalWidthInches = pixelWidth / dpiX
physicalHeightInches = pixelHeight / dpiY
pointWidth = physicalWidthInches * 72.0
pointHeight = physicalHeightInches * 72.0
All layout calculations must use these point values. Never multiply by backingScaleFactor.
6.3 Colour-Space Handling
When forceUnmanagedColor == true:
- Create a device RGB colour space:
CGColorSpaceCreateDeviceRGB() - Draw the
CGImageinto a bitmap context that uses this colour space, or draw the image directly while ensuring no source profile is applied. - Do not call
CGImageCreateCopyWithColorSpacewith a calibrated space.
Goal: the pixel values that leave the process and enter the CUPS raster filter are identical (within quantisation) to the values stored in the TIFF.
6.4 Drawing Implementation (TargetCanvasView)
override func draw(_ dirtyRect: NSRect) {
guard let context = NSGraphicsContext.current?.cgContext else { return }
context.interpolationQuality = .none
context.setShouldAntialias(false)
context.setAllowsAntialiasing(false)
// Calculate destination rect in points (1:1 with physical size)
let destRect = calculateDestinationRect(for: currentPage,
paperSize: printInfo.paperSize,
imageable: printInfo.imageablePageBounds,
centered: settings.centered,
scaling: settings.scaling)
context.draw(currentCGImage, in: destRect)
}
calculateDestinationRect must never introduce fractional pixel offsets that would cause the print pipeline to resample.
7. Colour Management Bypass
This is the most critical section. Colour translation must be prevented at two independent layers.
7.1 Quartz / ColorSync Layer (NSPrintInfo + CorePrinting)
let printInfo = NSPrintInfo.shared
var dict = printInfo.dictionary()
// Force custom (i.e. “no”) colour matching
dict[NSPrintInfo.AttributeKey(rawValue: "PMColorMatchingMode")] = "APCustomColorMatching"
dict[NSPrintInfo.AttributeKey(rawValue: "PMCustomColorMatchingProfile")] = ""
// Optional legacy keys still observed by some drivers
dict[NSPrintInfo.AttributeKey(rawValue: "com.apple.print.PrintSettings.PMColorMatchingMode")] = "APCustomColorMatching"
printInfo.dictionary().setDictionary(dict as! [AnyHashable: Any])
When the user (in standalone mode) explicitly requests ColorSync or Driver Managed:
| Mode | PMColorMatchingMode value |
|---|---|
| Unmanaged (default for profiling) | "APCustomColorMatching" |
| ColorSync | "APColorSync" |
| Driver / Vendor | "APPrinterExtension" |
7.2 Print Panel UI Policy
When uiPolicy.lockColorManagement == true:
let panel = printOperation.printPanel
panel.options = [
.showsCopies,
.showsPageRange,
.showsPaperSize,
.showsOrientation,
.showsScaling,
.showsPreview
]
// Explicitly omit any colour-matching accessory
Further, implement NSPrintPanelAccessorizing (or inspect the panel’s accessory controllers after presentation) and remove any view controller whose title contains “Color Matching” or “Colour Matching”.
7.3 Driver / PPD Layer
See Section 10.
8. Multi-Page Pagination
TargetCanvasView (or a dedicated NSDocument / print operation data source) must implement:
override func knowsPageRange(_ range: NSRangePointer) -> Bool {
range.pointee = NSRange(location: 1, length: pages.count)
return true
}
override func rectForPage(_ pageNumber: Int) -> NSRect {
// pageNumber is 1-based
let page = pages[pageNumber - 1]
return calculatePageBounds(for: page, on: currentPrintInfo)
}
Each page is rendered in isolation. The graphics context origin must be reset so that page N never inherits a transform from page N-1.
Page order is exactly the order of the files array in the job (or the order files were added in standalone mode).
9. User Interface (Standalone Mode)
Window Layout
┌──────────────────────────────────────────────────────────────┐
│ TargetPrint — <jobTitle or “Untitled”> │
├────────────┬───────────────────────────────────┬─────────────┤
│ Thumbnail │ │ Inspector │
│ Sidebar │ TargetCanvasView │ - Printer │
│ │ (drop zone when empty) │ - Media │
│ [page 1] │ │ - Tray │
│ [page 2] │ │ - Scale │
│ … │ │ - Colour │
│ │ │ Mode │
│ │ │ │
│ │ │ [Print…] │
└────────────┴───────────────────────────────────┴─────────────┘
- Drop zone accepts multiple TIFF files.
- Thumbnails are generated with
NSImageat low resolution for speed; the canvas always uses the full-resolutionCGImage. - Colour-mode segmented control: No Colour Management | ColorSync | Driver Managed.
- When a job is loaded via
--job, the inspector is read-only for locked fields.
10. CUPS / PPD Integration
10.1 Queue Inspection
// via Bridging-Header
cups_dest_t *dest = cupsGetNamedDest(CUPS_HTTP_DEFAULT, printerName, NULL);
const char *ppdFile = cupsGetPPD(printerName);
ppd_file_t *ppd = ppdOpenFile(ppdFile);
10.2 AirPrint Detection
A queue is considered AirPrint (and therefore unsuitable for reliable unmanaged colour) when any of the following is true:
- Device URI begins with
apple-airprint://oripps://with AirPrint markers - PPD contains
*APAirPrint: True(or equivalent) - PPD manufacturer is “Apple” and model contains “AirPrint”
In this case the preview window must show a persistent non-modal warning badge:
Warning: Selected printer uses an AirPrint / driverless queue. Hardware colour management cannot be reliably disabled. Install the official OEM driver (Epson, Canon, etc.) for accurate profiling.
10.3 Vendor-Specific PPD Options
Inject into NSPrintInfo.printSettings (or the low-level dictionary) before the print operation runs:
| Vendor | Keys to set for “off” |
|---|---|
| Epson | ColorModel=RGB, EPSONColorControls=Off (or NoColorAdjustment) |
| Canon | CNColorMatching=None |
| HP | ColorModel=RGB, HPColorControl=Off (where available) |
| Generic | Prefer any option whose name contains Color / Colour and whose choice is None / Off / No |
Discovery is performed by walking ppd->options and matching known patterns. Unknown printers receive only the CorePrinting colour-matching bypass.
11. Project Layout
ICCery-CPU/
├── README.md # Short project overview + build instructions
├── SPEC.md # This document
├── LICENSE # (to be chosen)
├── TargetPrint.xcodeproj/
├── Info.plist
├── TargetPrint.entitlements
├── Bridging-Header.h
├── Sources/
│ ├── main.swift
│ ├── AppDelegate.swift
│ ├── Models/
│ │ ├── TargetJob.swift # Codable + validation
│ │ └── TargetPage.swift # ImageIO + geometry
│ ├── Views/
│ │ ├── TargetCanvasView.swift
│ │ ├── DropZoneView.swift
│ │ ├── ThumbnailSidebarView.swift
│ │ └── InspectorView.swift
│ ├── Controllers/
│ │ └── PreviewWindowController.swift
│ └── Printing/
│ ├── PrintEngine.swift
│ └── CUPSManager.swift
├── Resources/
│ ├── Assets.xcassets
│ └── Base.lproj/
└── Scripts/
├── build_universal.sh
└── package_app.sh
12. Build System
Universal Binary Script (Scripts/build_universal.sh)
#!/bin/bash
set -euo pipefail
CONFIGURATION=${1:-Release}
SCHEME="TargetPrint"
PROJECT="TargetPrint.xcodeproj"
BUILD_DIR="build"
# Build each architecture
xcodebuild -project "$PROJECT" -scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-arch x86_64 \
-derivedDataPath "$BUILD_DIR/x86_64" \
BUILD_DIR="$BUILD_DIR/x86_64" \
ONLY_ACTIVE_ARCH=NO
xcodebuild -project "$PROJECT" -scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-arch arm64 \
-derivedDataPath "$BUILD_DIR/arm64" \
BUILD_DIR="$BUILD_DIR/arm64" \
ONLY_ACTIVE_ARCH=NO
# Lipo
mkdir -p "$BUILD_DIR/universal"
lipo -create \
"$BUILD_DIR/x86_64/Build/Products/$CONFIGURATION/TargetPrint.app/Contents/MacOS/TargetPrint" \
"$BUILD_DIR/arm64/Build/Products/$CONFIGURATION/TargetPrint.app/Contents/MacOS/TargetPrint" \
-output "$BUILD_DIR/universal/TargetPrint"
# Re-assemble .app bundle
# … (copy resources, Info.plist, frameworks, then replace binary)
The final artefact is a Universal 2 .app bundle.
13. Error Handling, Logging & Diagnostics
- All user-facing errors surface via
NSAlert. - Internal diagnostics are written to:
os_log(subsystemcom.iccery.TargetPrint)- Optionally a rotating log file under
~/Library/Logs/TargetPrint/when launched with--verbose.
- Job JSON is never deleted by TargetPrint; the caller (ICCery) is responsible for cleanup. Leaving the file in
/tmpprovides an audit trail. - On fatal parse errors the process exits with the codes defined in §4 after showing an alert.
14. Security & Entitlements
TargetPrint.entitlements:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Hardened Runtime -->
<key>com.apple.security.cs.allow-jit</key>
<false/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<false/>
<key>com.apple.security.cs.disable-library-validation</key>
<false/>
<!-- Explicitly no App Sandbox -->
<!-- (absence of com.apple.security.app-sandbox is required) -->
</dict>
</plist>
Because the binary must call cupsGetPPD and inspect local print queues, the App Sandbox cannot be enabled. Hardened Runtime remains mandatory for notarisation.
15. Verification & Acceptance Tests
| ID | Test | Pass Criterion |
|---|---|---|
| G1 | Geometric accuracy | 300 DPI image of 2400 × 3000 px prints at exactly 8.00 × 10.00 in (± 0.1 mm) measured on the physical print or by examining the CUPS raster bounding box |
| P1 | Pixel integrity | Adjacent pure patches (RGB 255,0,0 next to 0,255,0) show zero anti-aliased transition pixels in the CUPS raster (inspect with cupsfilter or by capturing the raw raster) |
| C1 | Colour bypass | With forceUnmanagedColor: true, the printed patches match the source TIFF values within printer gamut limits (verified by spectrophotometer or known reference) |
| I1 | IPC compliance | TargetPrint --job valid.json opens the preview and immediately presents a correctly pre-configured NSPrintPanel; Color Matching controls are absent or disabled |
| A1 | AirPrint warning | Selecting an AirPrint queue displays the permanent warning badge |
| M1 | Multi-page | Two-page job prints page 1 then page 2 with correct geometry and no coordinate bleed |
Automated unit tests should cover:
- JSON decoding + validation
- DPI extraction edge cases (missing tags → 72 dpi)
- Geometry calculation pure functions
- AirPrint detection heuristics
UI and print-pipeline tests remain manual / semi-automated.
16. Tauri / Rust Integration Contract
On the ICCery (Rust) side the only required interaction is:
use std::env;
use std::fs;
use std::process::Command;
// 1. Serialise TargetJob
let job = TargetJob { /* … */ };
let json = serde_json::to_string_pretty(&job)?;
// 2. Write to temp file
let mut path = env::temp_dir();
path.push(format!("iccery_job_{}.json", uuid::Uuid::new_v4()));
fs::write(&path, json)?;
// 3. Launch asynchronously – do not wait
Command::new("/Applications/TargetPrint.app/Contents/MacOS/TargetPrint")
.arg("--job")
.arg(&path)
.spawn()?; // fire-and-forget
TargetPrint never signals completion back to Tauri; the user interacts solely with the native print dialog.
17. Future Extensions (Non-Goals for v1)
- Soft-proof overlay on the canvas
- Built-in target generator
- Direct CUPS job submission without
NSPrintPanel(headless mode) - Support for
.png/ 16-bit TIFF - Per-page media / tray overrides
- Notarised Sparkle auto-update channel
These items are deliberately excluded from the initial implementation so that the core fidelity and colour-bypass guarantees can be stabilised first.
End of Specification
This document should be treated as the contract for all subsequent implementation work in the ICCery-CPU repository.