Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa9a2042bf |
@@ -27,5 +27,4 @@ ICCery.xcodeproj/
|
||||
Release/
|
||||
notarization/
|
||||
build/
|
||||
docs/megaplans/*
|
||||
docs/megaplans
|
||||
|
||||
@@ -56,19 +56,23 @@ public enum PrinttargArgs {
|
||||
}
|
||||
args.append(contentsOf: ["-R", "\(config.customSeed)"])
|
||||
case .raster:
|
||||
args.append(contentsOf: ArgsBuilder.flag("-r", when: true))
|
||||
args.append("-r")
|
||||
}
|
||||
|
||||
args.append(contentsOf: ArgsBuilder.optionIfNonEmpty("-d", config.label))
|
||||
if let label = config.label?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!label.isEmpty {
|
||||
args.append(contentsOf: ["-d", label])
|
||||
}
|
||||
|
||||
guard (72...600).contains(config.dpi) else {
|
||||
throw PrinttargArgError.invalidDPI(config.dpi)
|
||||
}
|
||||
args.append(contentsOf: [config.bitDepth.flag, "\(config.dpi)"])
|
||||
|
||||
if !CalibrationIdentity.isCalibration(cleanBasename) {
|
||||
args.append(contentsOf: ArgsBuilder.optionIfNonEmpty(
|
||||
config.calibrationEmbedOnly ? "-I" : "-K", config.calibrationFile))
|
||||
if !CalibrationIdentity.isCalibration(cleanBasename),
|
||||
let cal = config.calibrationFile?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!cal.isEmpty {
|
||||
args.append(contentsOf: [config.calibrationEmbedOnly ? "-I" : "-K", cal])
|
||||
}
|
||||
|
||||
args.append(cleanBasename)
|
||||
|
||||
@@ -70,12 +70,18 @@ public enum TargenArgs {
|
||||
if let n = config.neutralSteps, n > 0 {
|
||||
args.append(contentsOf: ["-n", "\(n)"])
|
||||
}
|
||||
args.append(contentsOf: ArgsBuilder.optionUnlessApprox("-N", config.neutralConcentration, skip: 0.50))
|
||||
args.append(contentsOf: ArgsBuilder.optionIfNonEmpty("-c", config.preconditioningProfile))
|
||||
args.append(contentsOf: ArgsBuilder.flag("-G", when: config.ofpsHighQuality == true))
|
||||
args.append(contentsOf: ArgsBuilder.option("-A", config.ofpsAdaptation.map {
|
||||
String(format: "%.2f", locale: Locale(identifier: "en_US_POSIX"), $0)
|
||||
}))
|
||||
if let nConc = config.neutralConcentration, abs(nConc - 0.50) >= 0.001 {
|
||||
args.append(contentsOf: ["-N", String(format: "%.2f", locale: Locale(identifier: "en_US_POSIX"), nConc)])
|
||||
}
|
||||
if let c = config.preconditioningProfile?.trimmingCharacters(in: .whitespacesAndNewlines), !c.isEmpty {
|
||||
args.append(contentsOf: ["-c", c])
|
||||
}
|
||||
if config.ofpsHighQuality == true {
|
||||
args.append("-G")
|
||||
}
|
||||
if let a = config.ofpsAdaptation {
|
||||
args.append(contentsOf: ["-A", String(format: "%.2f", locale: Locale(identifier: "en_US_POSIX"), a)])
|
||||
}
|
||||
if let algFlag = config.fullSpreadAlgorithm?.flag {
|
||||
args.append(algFlag)
|
||||
}
|
||||
@@ -85,9 +91,11 @@ public enum TargenArgs {
|
||||
}
|
||||
args.append(contentsOf: ["-l", "\(inkLimit)"])
|
||||
}
|
||||
args.append(contentsOf: ArgsBuilder.optionUnlessApprox("-V", config.darkEmphasis, skip: 1.0))
|
||||
if let p = config.devicePower, p > 0 {
|
||||
args.append(contentsOf: ArgsBuilder.optionUnlessApprox("-p", p, skip: 1.0))
|
||||
if let v = config.darkEmphasis, abs(v - 1.0) >= 0.001 {
|
||||
args.append(contentsOf: ["-V", String(format: "%.2f", locale: Locale(identifier: "en_US_POSIX"), v)])
|
||||
}
|
||||
if let p = config.devicePower, p > 0, abs(p - 1.0) >= 0.001 {
|
||||
args.append(contentsOf: ["-p", String(format: "%.2f", locale: Locale(identifier: "en_US_POSIX"), p)])
|
||||
}
|
||||
|
||||
args.append(cleanBasename)
|
||||
|
||||
@@ -79,9 +79,16 @@ public enum PrintcalArgs {
|
||||
|
||||
var args: [String] = ["-v", "-e"]
|
||||
|
||||
args.append(contentsOf: ArgsBuilder.flag("-I", when: config.noInkLimit))
|
||||
args.append(contentsOf: ArgsBuilder.flag("-z", when: config.verify))
|
||||
args.append(contentsOf: ArgsBuilder.optionIfNonEmpty("-a", config.previousCalPath))
|
||||
if config.noInkLimit {
|
||||
args.append("-I")
|
||||
}
|
||||
if config.verify {
|
||||
args.append("-z")
|
||||
}
|
||||
if let previous = config.previousCalPath?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!previous.isEmpty {
|
||||
args.append(contentsOf: ["-a", previous])
|
||||
}
|
||||
if let tac = config.totalInkLimit, tac > 0 {
|
||||
args.append(contentsOf: ["-m", String(format: "%.1f", tac)])
|
||||
} else if let tac = config.totalInkLimit {
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import Testing
|
||||
import Foundation
|
||||
@testable import ICCeryCore
|
||||
|
||||
@Suite("ArgsBuilder")
|
||||
struct ArgsBuilderTests {
|
||||
|
||||
// MARK: - option
|
||||
|
||||
@Test("option: nil emits nothing")
|
||||
func optionNil() {
|
||||
#expect(ArgsBuilder.option("-f", nil) == [])
|
||||
}
|
||||
|
||||
@Test("option: present value emits flag and value verbatim")
|
||||
func optionPresent() {
|
||||
#expect(ArgsBuilder.option("-f", "abc") == ["-f", "abc"])
|
||||
#expect(ArgsBuilder.option("-f", "") == ["-f", ""])
|
||||
#expect(ArgsBuilder.option("-f", " padded ") == ["-f", " padded "])
|
||||
}
|
||||
|
||||
// MARK: - optionIfNonEmpty
|
||||
|
||||
@Test("optionIfNonEmpty: nil and empty emit nothing")
|
||||
func optionIfNonEmptyNilEmpty() {
|
||||
#expect(ArgsBuilder.optionIfNonEmpty("-d", nil) == [])
|
||||
#expect(ArgsBuilder.optionIfNonEmpty("-d", "") == [])
|
||||
}
|
||||
|
||||
@Test("optionIfNonEmpty: whitespace-only emits nothing")
|
||||
func optionIfNonEmptyWhitespace() {
|
||||
#expect(ArgsBuilder.optionIfNonEmpty("-d", " ") == [])
|
||||
#expect(ArgsBuilder.optionIfNonEmpty("-d", " \t\n ") == [])
|
||||
}
|
||||
|
||||
@Test("optionIfNonEmpty: trims surrounding whitespace")
|
||||
func optionIfNonEmptyTrims() {
|
||||
#expect(ArgsBuilder.optionIfNonEmpty("-d", " label ") == ["-d", "label"])
|
||||
#expect(ArgsBuilder.optionIfNonEmpty("-d", "\tcal.cal\n") == ["-d", "cal.cal"])
|
||||
}
|
||||
|
||||
// MARK: - optionUnlessApprox
|
||||
|
||||
@Test("optionUnlessApprox: nil emits nothing")
|
||||
func optionUnlessApproxNil() {
|
||||
#expect(ArgsBuilder.optionUnlessApprox("-N", nil, skip: 0.50) == [])
|
||||
}
|
||||
|
||||
@Test("optionUnlessApprox: exact skip value emits nothing")
|
||||
func optionUnlessApproxExactSkip() {
|
||||
#expect(ArgsBuilder.optionUnlessApprox("-N", 0.50, skip: 0.50) == [])
|
||||
#expect(ArgsBuilder.optionUnlessApprox("-V", 1.0, skip: 1.0) == [])
|
||||
}
|
||||
|
||||
@Test("optionUnlessApprox: within epsilon emits nothing")
|
||||
func optionUnlessApproxWithinEpsilon() {
|
||||
#expect(ArgsBuilder.optionUnlessApprox("-N", 0.5005, skip: 0.50) == [])
|
||||
#expect(ArgsBuilder.optionUnlessApprox("-V", 0.9995, skip: 1.0) == [])
|
||||
}
|
||||
|
||||
@Test("optionUnlessApprox: outside epsilon emits flag")
|
||||
func optionUnlessApproxOutsideEpsilon() {
|
||||
#expect(ArgsBuilder.optionUnlessApprox("-N", 0.75, skip: 0.50) == ["-N", "0.75"])
|
||||
#expect(ArgsBuilder.optionUnlessApprox("-V", 1.50, skip: 1.0) == ["-V", "1.50"])
|
||||
#expect(ArgsBuilder.optionUnlessApprox("-N", 0.498, skip: 0.50) == ["-N", "0.50"])
|
||||
}
|
||||
|
||||
@Test("optionUnlessApprox: POSIX formatting is locale-stable")
|
||||
func optionUnlessApproxPOSIX() {
|
||||
// 1234.5 must never produce a grouping separator or comma decimal.
|
||||
#expect(ArgsBuilder.optionUnlessApprox("-p", 1234.5, skip: 1.0) == ["-p", "1234.50"])
|
||||
#expect(ArgsBuilder.optionUnlessApprox("-p", 2.0, skip: 1.0) == ["-p", "2.00"])
|
||||
}
|
||||
|
||||
@Test("optionUnlessApprox: custom epsilon and format honoured")
|
||||
func optionUnlessApproxCustom() {
|
||||
#expect(ArgsBuilder.optionUnlessApprox("-x", 1.005, skip: 1.0, epsilon: 0.01) == [])
|
||||
#expect(ArgsBuilder.optionUnlessApprox("-x", 1.5, skip: 1.0, format: "%.1f") == ["-x", "1.5"])
|
||||
}
|
||||
|
||||
// MARK: - flag
|
||||
|
||||
@Test("flag: true emits the bare flag")
|
||||
func flagTrue() {
|
||||
#expect(ArgsBuilder.flag("-G", when: true) == ["-G"])
|
||||
#expect(ArgsBuilder.flag("-r", when: true) == ["-r"])
|
||||
}
|
||||
|
||||
@Test("flag: false emits nothing")
|
||||
func flagFalse() {
|
||||
#expect(ArgsBuilder.flag("-G", when: false) == [])
|
||||
#expect(ArgsBuilder.flag("-r", when: false) == [])
|
||||
}
|
||||
}
|
||||
@@ -159,51 +159,6 @@ struct ChartreadRowTests {
|
||||
#expect(row.patchCount == 1)
|
||||
#expect(row.patches[0].measured.lab?.l == 51)
|
||||
}
|
||||
|
||||
@Test("Decodes a row carrying both XYZ and Lab arrays")
|
||||
func decodeXYZAndLab() throws {
|
||||
let json = """
|
||||
{"event": "row_complete", "row_id": "B", "row_index": 1, "total_rows": 2,
|
||||
"patch_count": 1, "patches": [
|
||||
{"id": "7", "loc": "B7", "is_pad": false, "device": [10, 20, 30, 40],
|
||||
"measured": {"XYZ": [30.5, 32.1, 25.9], "Lab": [63.4, 2.5, -8.2]}}
|
||||
]}
|
||||
"""
|
||||
let row = try JSONDecoder().decode(ChartreadRow.self, from: Data(json.utf8))
|
||||
let measured = row.patches[0].measured
|
||||
#expect(measured.xyz == CIEXYZ(x: 30.5, y: 32.1, z: 25.9))
|
||||
#expect(measured.lab == CIELab(l: 63.4, a: 2.5, b: -8.2))
|
||||
}
|
||||
|
||||
@Test("XYZColor/CIEXYZ encode as an unkeyed three-number array")
|
||||
func xyzWireEncoding() throws {
|
||||
for color in [XYZColor(x: 1.5, y: 2.5, z: 3.5), CIEXYZ(x: 1.5, y: 2.5, z: 3.5)] {
|
||||
let value = try JSONSerialization.jsonObject(
|
||||
with: JSONEncoder().encode(color))
|
||||
#expect(value as? [Double] == [1.5, 2.5, 3.5])
|
||||
}
|
||||
}
|
||||
|
||||
@Test("LabColor/CIELab encode as an unkeyed three-number array")
|
||||
func labWireEncoding() throws {
|
||||
for color in [LabColor(l: 50, a: -1, b: 2), CIELab(l: 50, a: -1, b: 2)] {
|
||||
let value = try JSONSerialization.jsonObject(
|
||||
with: JSONEncoder().encode(color))
|
||||
#expect(value as? [Double] == [50, -1, 2])
|
||||
}
|
||||
}
|
||||
|
||||
@Test("PatchColor keeps the XYZ and Lab keys over unkeyed arrays")
|
||||
func patchColorKeys() throws {
|
||||
let color = PatchColor(
|
||||
xyz: CIEXYZ(x: 10, y: 20, z: 30),
|
||||
lab: CIELab(l: 55, a: 1, b: -2))
|
||||
let object = try JSONSerialization.jsonObject(
|
||||
with: JSONEncoder().encode(color)) as? [String: Any]
|
||||
#expect(object?["XYZ"] as? [Double] == [10, 20, 30])
|
||||
#expect(object?["Lab"] as? [Double] == [55, 1, -2])
|
||||
#expect(object?["spectral"] == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@Suite("ColourMath")
|
||||
|
||||
@@ -44,29 +44,6 @@ struct PrintcalArgsTests {
|
||||
])
|
||||
}
|
||||
|
||||
@Test("Whitespace-only previous calibration path emits no -a")
|
||||
func whitespacePreviousCal() throws {
|
||||
let config = PrintcalConfig(
|
||||
ti3Basename: "demo",
|
||||
outputURL: tmp,
|
||||
previousCalPath: " \n\t "
|
||||
)
|
||||
let args = try PrintcalArgs.build(config: config)
|
||||
#expect(!args.contains("-a"))
|
||||
#expect(args == ["-v", "-e", "-o", "/tmp/out.cal", "CAL_demo"])
|
||||
}
|
||||
|
||||
@Test("Previous calibration path is trimmed before emission")
|
||||
func previousCalTrimmed() throws {
|
||||
let config = PrintcalConfig(
|
||||
ti3Basename: "demo",
|
||||
outputURL: tmp,
|
||||
previousCalPath: " /tmp/old.cal "
|
||||
)
|
||||
let args = try PrintcalArgs.build(config: config)
|
||||
#expect(args[args.firstIndex(of: "-a")! + 1] == "/tmp/old.cal")
|
||||
}
|
||||
|
||||
@Test("Rejects invalid per-channel limit")
|
||||
func rejectsBadChannelLimit() {
|
||||
let config = PrintcalConfig(
|
||||
|
||||
@@ -131,23 +131,6 @@ struct PrinttargArgsTests {
|
||||
#expect(!args.contains("-I"))
|
||||
}
|
||||
|
||||
@Test("Whitespace-only label emits no -d; whitespace-only calibration emits no -K/-I")
|
||||
func whitespaceOptions() throws {
|
||||
let args = try PrinttargArgs.build(
|
||||
config: config(label: " \n ", calFile: " \t "))
|
||||
#expect(!args.contains("-d"))
|
||||
#expect(!args.contains("-K"))
|
||||
#expect(!args.contains("-I"))
|
||||
}
|
||||
|
||||
@Test("Label and calibration values are trimmed before emission")
|
||||
func trimmedOptions() throws {
|
||||
let args = try PrinttargArgs.build(
|
||||
config: config(label: " My Label ", calFile: " /tmp/a.cal "))
|
||||
#expect(args[args.firstIndex(of: "-d")! + 1] == "My Label")
|
||||
#expect(args[args.firstIndex(of: "-K")! + 1] == "/tmp/a.cal")
|
||||
}
|
||||
|
||||
@Test("Unsafe basename throws")
|
||||
func unsafeBasename() {
|
||||
#expect(throws: PathSecurity.Error.self) {
|
||||
|
||||
@@ -162,34 +162,6 @@ struct TargenArgsTests {
|
||||
#expect(!args.contains("-p"))
|
||||
}
|
||||
|
||||
@Test("Whitespace-only preconditioning profile emits no -c")
|
||||
func whitespacePreconditioner() throws {
|
||||
let config = TargenConfig(
|
||||
colourSpace: .rgb,
|
||||
patchCount: 800,
|
||||
whitePatches: 4,
|
||||
blackPatches: 4,
|
||||
preconditioningProfile: " \n\t ",
|
||||
basename: "ws_pre"
|
||||
)
|
||||
let args = try TargenArgs.build(config: config)
|
||||
#expect(!args.contains("-c"))
|
||||
}
|
||||
|
||||
@Test("Preconditioning profile is trimmed before emission")
|
||||
func preconditionerTrimmed() throws {
|
||||
let config = TargenConfig(
|
||||
colourSpace: .rgb,
|
||||
patchCount: 800,
|
||||
whitePatches: 4,
|
||||
blackPatches: 4,
|
||||
preconditioningProfile: " /path/to/profile.icc ",
|
||||
basename: "trim_pre"
|
||||
)
|
||||
let args = try TargenArgs.build(config: config)
|
||||
#expect(args[args.firstIndex(of: "-c")! + 1] == "/path/to/profile.icc")
|
||||
}
|
||||
|
||||
@Test("Invalid basename throws")
|
||||
func invalidBasenameThrows() {
|
||||
let config = TargenConfig(
|
||||
|
||||
Reference in New Issue
Block a user