Files
ICCery-CPU/Sources/Models/Geometry.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

176 lines
6.4 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import CoreGraphics
/// SPEC §6.2 — macOS printing uses 72 PostScript points = 1 inch.
/// All layout uses these values. Never multiply by `backingScaleFactor`.
enum Geometry {
static let pointsPerInch: CGFloat = 72
static let mmPerInch: CGFloat = 25.4
/// Typical hardware margin used when `imageablePageBounds` is unavailable.
static let defaultMarginMM: CGFloat = 12.7
/// G1 pass band: ±0.1 mm on the physical print.
static let geometricToleranceMM: CGFloat = 0.1
struct Size {
var width: CGFloat
var height: CGFloat
}
struct Paper {
var name: String
var widthPt: CGFloat
var heightPt: CGFloat
var size: CGSize { CGSize(width: widthPt, height: heightPt) }
func oriented(landscape: Bool) -> Paper {
if !landscape { return self }
return Paper(name: name + " landscape", widthPt: heightPt, heightPt: widthPt)
}
}
struct Report: Equatable {
var physicalWidthIn: CGFloat
var physicalHeightIn: CGFloat
var pointWidth: CGFloat
var pointHeight: CGFloat
var dest: CGRect
var paper: CGRect
var imageable: CGRect
var overflow: Bool
}
static let papers: [Paper] = [
Paper(name: "A4", widthPt: mmToPoints(210), heightPt: mmToPoints(297)),
Paper(name: "A3", widthPt: mmToPoints(297), heightPt: mmToPoints(420)),
Paper(name: "A5", widthPt: mmToPoints(148), heightPt: mmToPoints(210)),
Paper(name: "Letter", widthPt: inchesToPoints(8.5), heightPt: inchesToPoints(11)),
Paper(name: "Legal", widthPt: inchesToPoints(8.5), heightPt: inchesToPoints(14)),
Paper(name: "Tabloid", widthPt: inchesToPoints(11), heightPt: inchesToPoints(17)),
Paper(name: "4x6", widthPt: inchesToPoints(4), heightPt: inchesToPoints(6)),
Paper(name: "5x7", widthPt: inchesToPoints(5), heightPt: inchesToPoints(7)),
]
static func paper(named name: String) -> Paper {
let key = name.trimmingCharacters(in: .whitespacesAndNewlines)
if let match = papers.first(where: { $0.name.caseInsensitiveCompare(key) == .orderedSame }) {
return match
}
// PPD PageSize names often look like "A4.Borderless" or "Letter"
if let match = papers.first(where: { key.localizedCaseInsensitiveContains($0.name) }) {
return match
}
return papers[0]
}
static func inchesToPoints(_ inches: CGFloat) -> CGFloat { inches * pointsPerInch }
static func pointsToInches(_ pt: CGFloat) -> CGFloat { pt / pointsPerInch }
static func mmToPoints(_ mm: CGFloat) -> CGFloat { (mm / mmPerInch) * pointsPerInch }
static func pointsToMm(_ pt: CGFloat) -> CGFloat { (pt / pointsPerInch) * mmPerInch }
static func mmToInches(_ mm: CGFloat) -> CGFloat { mm / mmPerInch }
static func physicalInches(pixels: CGFloat, dpi: CGFloat) -> CGFloat {
let safe = dpi > 0 ? dpi : 72
return pixels / safe
}
static func pointSize(pixels: CGFloat, dpi: CGFloat, scaling: CGFloat) -> CGFloat {
physicalInches(pixels: pixels, dpi: dpi) * pointsPerInch * scaling
}
static func imageableBounds(paper: Paper, marginMM: CGFloat = defaultMarginMM) -> CGRect {
let m = mmToPoints(marginMM)
return CGRect(
x: m,
y: m,
width: max(0, paper.widthPt - m * 2),
height: max(0, paper.heightPt - m * 2)
)
}
/// Destination rectangle in PostScript points on the page.
/// Size is the exact physical size (pixels/dpi × 72 × scaling). Origin is
/// centred in the imageable rect when `centered` is true, otherwise top-left
/// of the imageable area (view is flipped: y grows down).
static func calculateDestinationRect(
pixelWidth: CGFloat,
pixelHeight: CGFloat,
dpiX: CGFloat,
dpiY: CGFloat,
paper: Paper,
imageable: CGRect,
centered: Bool,
scaling: CGFloat
) -> CGRect {
let w = pointSize(pixels: pixelWidth, dpi: dpiX, scaling: scaling)
let h = pointSize(pixels: pixelHeight, dpi: dpiY, scaling: scaling)
var x: CGFloat
var y: CGFloat
if centered {
x = imageable.midX - w / 2
y = imageable.midY - h / 2
} else {
x = imageable.minX
y = imageable.minY
}
// Keep exact size. Round origin to 1/1000 pt so the print pipeline is
// not asked to sample on a fractional device pixel, without violating
// the G1 ±0.1 mm band (0.1 mm ≈ 0.28 pt).
x = (x * 1000).rounded() / 1000
y = (y * 1000).rounded() / 1000
return CGRect(x: x, y: y, width: w, height: h)
}
static func report(
pixelWidth: CGFloat,
pixelHeight: CGFloat,
dpiX: CGFloat,
dpiY: CGFloat,
paper: Paper,
imageable: CGRect,
centered: Bool,
scaling: CGFloat
) -> Report {
let physW = physicalInches(pixels: pixelWidth, dpi: dpiX)
let physH = physicalInches(pixels: pixelHeight, dpi: dpiY)
let dest = calculateDestinationRect(
pixelWidth: pixelWidth,
pixelHeight: pixelHeight,
dpiX: dpiX,
dpiY: dpiY,
paper: paper,
imageable: imageable,
centered: centered,
scaling: scaling
)
let paperRect = CGRect(x: 0, y: 0, width: paper.widthPt, height: paper.heightPt)
let overflow = dest.minX < imageable.minX - 0.01
|| dest.minY < imageable.minY - 0.01
|| dest.maxX > imageable.maxX + 0.01
|| dest.maxY > imageable.maxY + 0.01
return Report(
physicalWidthIn: physW,
physicalHeightIn: physH,
pointWidth: dest.width,
pointHeight: dest.height,
dest: dest,
paper: paperRect,
imageable: imageable,
overflow: overflow
)
}
/// G1: expected physical size matches actual within ±0.1 mm.
static func geometricAccuracyPass(
expectedWidthIn: CGFloat,
expectedHeightIn: CGFloat,
actualWidthIn: CGFloat,
actualHeightIn: CGFloat
) -> Bool {
let tol = mmToInches(geometricToleranceMM)
return abs(actualWidthIn - expectedWidthIn) <= tol
&& abs(actualHeightIn - expectedHeightIn) <= tol
}
}