Compare commits
9
Commits
v0.0.1-test2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b06d47abd1 | ||
|
|
842caaec04 | ||
|
|
8ee9747156 | ||
|
|
474888541b | ||
|
|
8eb4bd408d | ||
|
|
e319e5b106 | ||
|
|
e0b1719921 | ||
|
|
fe68c5c01d | ||
|
|
1c53e698aa |
@@ -112,6 +112,8 @@ jobs:
|
|||||||
-scheme TargetPrint \
|
-scheme TargetPrint \
|
||||||
-destination "platform=macOS" \
|
-destination "platform=macOS" \
|
||||||
-only-testing:TargetPrintTests \
|
-only-testing:TargetPrintTests \
|
||||||
|
-jobs "$(sysctl -n hw.ncpu 2>/dev/null || echo 4)" \
|
||||||
|
-parallelizeTargets \
|
||||||
CODE_SIGNING_ALLOWED=NO \
|
CODE_SIGNING_ALLOWED=NO \
|
||||||
ONLY_ACTIVE_ARCH=YES
|
ONLY_ACTIVE_ARCH=YES
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,13 @@
|
|||||||
|
|
||||||
#import <cups/cups.h>
|
#import <cups/cups.h>
|
||||||
#import <cups/ppd.h>
|
#import <cups/ppd.h>
|
||||||
|
|
||||||
|
// The Swift CUPS overlay marks cupsGetPPD unavailable ("use cupsCopyDestInfo").
|
||||||
|
// SPEC §10 still requires the PPD file for AirPrint detection and vendor
|
||||||
|
// colour-bypass keys, so we call the C symbol through this wrapper.
|
||||||
|
static inline const char *TPCupsGetPPD(const char *name) {
|
||||||
|
#pragma clang diagnostic push
|
||||||
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||||
|
return cupsGetPPD(name);
|
||||||
|
#pragma clang diagnostic pop
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ This directory is the **native AppKit implementation** of [`SPEC.md`](SPEC.md).
|
|||||||
|
|
||||||
| Item | Value |
|
| Item | Value |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| macOS | 10.15 Catalina → 26+ Tahoe |
|
| macOS | Intel **10.15** Catalina · Apple Silicon **11.0** Big Sur → 26+ Tahoe |
|
||||||
| Architecture | Universal 2 (Intel + Apple Silicon) |
|
| Architecture | Universal 2 (Intel + Apple Silicon) |
|
||||||
| Xcode | Command Line Tools / full Xcode (Swift 5) |
|
| Xcode | Command Line Tools / full Xcode (Swift 5) |
|
||||||
| Sandbox | Hardened Runtime **on**, App Sandbox **off** |
|
| Sandbox | Hardened Runtime **on**, App Sandbox **off** |
|
||||||
|
|||||||
Regular → Executable
+9
-3
@@ -31,7 +31,11 @@ if xcodebuild_works; then
|
|||||||
SCHEME="TargetPrint"
|
SCHEME="TargetPrint"
|
||||||
OUT_DIR="$ROOT/build/apps/$ARCH"
|
OUT_DIR="$ROOT/build/apps/$ARCH"
|
||||||
DD_DIR="$ROOT/build/derived-$ARCH"
|
DD_DIR="$ROOT/build/derived-$ARCH"
|
||||||
echo "==> Building TargetPrint ($CONFIGURATION, $ARCH) with $XCODEBUILD"
|
# shellcheck source=lib.sh
|
||||||
|
source "$SCRIPTS/lib.sh"
|
||||||
|
JOBS="$(cpu_jobs)"
|
||||||
|
MIN_OS="$(min_os_for_arch "$ARCH")"
|
||||||
|
echo "==> Building TargetPrint ($CONFIGURATION, $ARCH, min $MIN_OS) with $XCODEBUILD (-jobs $JOBS)"
|
||||||
rm -rf "$OUT_DIR"
|
rm -rf "$OUT_DIR"
|
||||||
mkdir -p "$OUT_DIR" "$DD_DIR"
|
mkdir -p "$OUT_DIR" "$DD_DIR"
|
||||||
"$XCODEBUILD" \
|
"$XCODEBUILD" \
|
||||||
@@ -39,19 +43,21 @@ if xcodebuild_works; then
|
|||||||
-scheme "$SCHEME" \
|
-scheme "$SCHEME" \
|
||||||
-configuration "$CONFIGURATION" \
|
-configuration "$CONFIGURATION" \
|
||||||
-arch "$ARCH" \
|
-arch "$ARCH" \
|
||||||
|
-jobs "$JOBS" \
|
||||||
|
-parallelizeTargets \
|
||||||
-derivedDataPath "$DD_DIR" \
|
-derivedDataPath "$DD_DIR" \
|
||||||
CONFIGURATION_BUILD_DIR="$OUT_DIR" \
|
CONFIGURATION_BUILD_DIR="$OUT_DIR" \
|
||||||
ONLY_ACTIVE_ARCH=NO \
|
ONLY_ACTIVE_ARCH=NO \
|
||||||
|
MACOSX_DEPLOYMENT_TARGET="$MIN_OS" \
|
||||||
CODE_SIGN_IDENTITY="${MACOS_CODESIGN_IDENTITY:--}" \
|
CODE_SIGN_IDENTITY="${MACOS_CODESIGN_IDENTITY:--}" \
|
||||||
CODE_SIGNING_ALLOWED=YES \
|
CODE_SIGNING_ALLOWED=YES \
|
||||||
ENABLE_HARDENED_RUNTIME=YES \
|
ENABLE_HARDENED_RUNTIME=YES \
|
||||||
|
SWIFT_USE_PARALLEL_WHOLE_MODULE_OPTIMIZATION=YES \
|
||||||
build
|
build
|
||||||
APP="$OUT_DIR/TargetPrint.app"
|
APP="$OUT_DIR/TargetPrint.app"
|
||||||
test -d "$APP"
|
test -d "$APP"
|
||||||
BIN="$APP/Contents/MacOS/TargetPrint"
|
BIN="$APP/Contents/MacOS/TargetPrint"
|
||||||
test -x "$BIN"
|
test -x "$BIN"
|
||||||
# shellcheck source=lib.sh
|
|
||||||
source "$SCRIPTS/lib.sh"
|
|
||||||
sign_app "$APP" "$ROOT/TargetPrint.entitlements"
|
sign_app "$APP" "$ROOT/TargetPrint.entitlements"
|
||||||
echo "==> lipo -info"
|
echo "==> lipo -info"
|
||||||
lipo -info "$BIN"
|
lipo -info "$BIN"
|
||||||
|
|||||||
Regular → Executable
+15
-2
@@ -1,13 +1,26 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# SPEC §12 — build x86_64, arm64, and Universal 2 TargetPrint.app
|
# SPEC §12 — build x86_64, arm64, and Universal 2 TargetPrint.app
|
||||||
|
# x86_64 and arm64 compile concurrently; then lipo.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
CONFIGURATION="${1:-Release}"
|
CONFIGURATION="${1:-Release}"
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
SCRIPTS="$(dirname "$0")"
|
SCRIPTS="$(dirname "$0")"
|
||||||
|
|
||||||
"$SCRIPTS/build_macos.sh" x86_64 "$CONFIGURATION"
|
echo "==> Compiling x86_64 and arm64 in parallel"
|
||||||
"$SCRIPTS/build_macos.sh" arm64 "$CONFIGURATION"
|
"$SCRIPTS/build_macos.sh" x86_64 "$CONFIGURATION" &
|
||||||
|
pid_x86=$!
|
||||||
|
"$SCRIPTS/build_macos.sh" arm64 "$CONFIGURATION" &
|
||||||
|
pid_arm=$!
|
||||||
|
|
||||||
|
status=0
|
||||||
|
wait "$pid_x86" || status=1
|
||||||
|
wait "$pid_arm" || status=1
|
||||||
|
if [ "$status" -ne 0 ]; then
|
||||||
|
echo "error: one or both architecture builds failed" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
"$SCRIPTS/make_universal.sh"
|
"$SCRIPTS/make_universal.sh"
|
||||||
|
|
||||||
echo "==> All architectures:"
|
echo "==> All architectures:"
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
+17
@@ -31,6 +31,23 @@ sign_app() {
|
|||||||
codesign --verify --verbose=2 "$app" || true
|
codesign --verify --verbose=2 "$app" || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cpu_jobs() {
|
||||||
|
if [ -n "${JOBS:-}" ]; then
|
||||||
|
echo "$JOBS"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sysctl -n hw.ncpu 2>/dev/null || nproc 2>/dev/null || echo 4
|
||||||
|
}
|
||||||
|
|
||||||
|
# Intel: Catalina 10.15. Apple Silicon did not exist until Big Sur 11.0.
|
||||||
|
min_os_for_arch() {
|
||||||
|
case "$1" in
|
||||||
|
x86_64) echo "${MACOSX_DEPLOYMENT_TARGET_X86_64:-10.15}" ;;
|
||||||
|
arm64) echo "${MACOSX_DEPLOYMENT_TARGET_ARM64:-11.0}" ;;
|
||||||
|
*) echo "10.15" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
zip_app() {
|
zip_app() {
|
||||||
local app="$1"
|
local app="$1"
|
||||||
local dest="$2"
|
local dest="$2"
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
+14
-10
@@ -20,13 +20,17 @@ BIN="$MACOS_DIR/TargetPrint"
|
|||||||
|
|
||||||
SDKROOT="${SDKROOT:-$(xcrun --sdk macosx --show-sdk-path)}"
|
SDKROOT="${SDKROOT:-$(xcrun --sdk macosx --show-sdk-path)}"
|
||||||
SWIFT="${SWIFT:-$(xcrun --find swiftc)}"
|
SWIFT="${SWIFT:-$(xcrun --find swiftc)}"
|
||||||
TARGET="${ARCH}-apple-macos10.15"
|
|
||||||
MIN_OS="10.15"
|
|
||||||
|
|
||||||
echo "==> swiftc build ($CONFIGURATION, $ARCH)"
|
# shellcheck source=lib.sh
|
||||||
|
source "$(dirname "$0")/lib.sh"
|
||||||
|
JOBS="$(cpu_jobs)"
|
||||||
|
MIN_OS="$(min_os_for_arch "$ARCH")"
|
||||||
|
TARGET="${ARCH}-apple-macos${MIN_OS}"
|
||||||
|
|
||||||
|
echo "==> swiftc build ($CONFIGURATION, $ARCH, $JOBS threads)"
|
||||||
echo " SDKROOT=$SDKROOT"
|
echo " SDKROOT=$SDKROOT"
|
||||||
echo " SWIFT=$SWIFT"
|
echo " SWIFT=$SWIFT"
|
||||||
echo " TARGET=$TARGET"
|
echo " TARGET=$TARGET (min $MIN_OS)"
|
||||||
|
|
||||||
rm -rf "$APP"
|
rm -rf "$APP"
|
||||||
mkdir -p "$MACOS_DIR" "$RES_DIR"
|
mkdir -p "$MACOS_DIR" "$RES_DIR"
|
||||||
@@ -40,7 +44,7 @@ if [ "${#SOURCES[@]}" -eq 0 ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
OPT_FLAGS=(-O)
|
OPT_FLAGS=(-O -whole-module-optimization)
|
||||||
if [ "$CONFIGURATION" != "Release" ]; then
|
if [ "$CONFIGURATION" != "Release" ]; then
|
||||||
OPT_FLAGS=(-Onone -g)
|
OPT_FLAGS=(-Onone -g)
|
||||||
fi
|
fi
|
||||||
@@ -50,7 +54,8 @@ fi
|
|||||||
-target "$TARGET" \
|
-target "$TARGET" \
|
||||||
-swift-version 5 \
|
-swift-version 5 \
|
||||||
"${OPT_FLAGS[@]}" \
|
"${OPT_FLAGS[@]}" \
|
||||||
-whole-module-optimization \
|
-num-threads "$JOBS" \
|
||||||
|
-j "$JOBS" \
|
||||||
-module-name TargetPrint \
|
-module-name TargetPrint \
|
||||||
-import-objc-header "$ROOT/Bridging-Header.h" \
|
-import-objc-header "$ROOT/Bridging-Header.h" \
|
||||||
-Xcc "-I${SDKROOT}/usr/include" \
|
-Xcc "-I${SDKROOT}/usr/include" \
|
||||||
@@ -83,9 +88,10 @@ if [ -f "$ICON_SRC" ] && command -v sips >/dev/null 2>&1; then
|
|||||||
for pair in 16:icon_16x16 32:icon_32x32 128:icon_128x128 256:icon_256x256 512:icon_512x512; do
|
for pair in 16:icon_16x16 32:icon_32x32 128:icon_128x128 256:icon_256x256 512:icon_512x512; do
|
||||||
size="${pair%%:*}"
|
size="${pair%%:*}"
|
||||||
name="${pair##*:}"
|
name="${pair##*:}"
|
||||||
sips -z "$size" "$size" "$ICON_SRC" --out "$ICONSET/AppIcon.iconset/${name}.png" >/dev/null
|
sips -z "$size" "$size" "$ICON_SRC" --out "$ICONSET/AppIcon.iconset/${name}.png" >/dev/null &
|
||||||
sips -z $((size * 2)) $((size * 2)) "$ICON_SRC" --out "$ICONSET/AppIcon.iconset/${name}@2x.png" >/dev/null
|
sips -z $((size * 2)) $((size * 2)) "$ICON_SRC" --out "$ICONSET/AppIcon.iconset/${name}@2x.png" >/dev/null &
|
||||||
done
|
done
|
||||||
|
wait
|
||||||
if command -v iconutil >/dev/null 2>&1 && iconutil -c icns "$ICONSET/AppIcon.iconset" -o "$RES_DIR/AppIcon.icns" 2>/dev/null; then
|
if command -v iconutil >/dev/null 2>&1 && iconutil -c icns "$ICONSET/AppIcon.iconset" -o "$RES_DIR/AppIcon.icns" 2>/dev/null; then
|
||||||
echo " AppIcon.icns"
|
echo " AppIcon.icns"
|
||||||
else
|
else
|
||||||
@@ -94,8 +100,6 @@ if [ -f "$ICON_SRC" ] && command -v sips >/dev/null 2>&1; then
|
|||||||
rm -rf "$ICONSET"
|
rm -rf "$ICONSET"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# shellcheck source=lib.sh
|
|
||||||
source "$(dirname "$0")/lib.sh"
|
|
||||||
sign_app "$APP" "$ROOT/TargetPrint.entitlements"
|
sign_app "$APP" "$ROOT/TargetPrint.entitlements"
|
||||||
|
|
||||||
echo "==> lipo -info"
|
echo "==> lipo -info"
|
||||||
|
|||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Standalone test runner for Command Line Tools (no Xcode.app required)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
SDKROOT="${SDKROOT:-$(xcrun --sdk macosx --show-sdk-path)}"
|
||||||
|
SWIFT="${SWIFT:-$(xcrun --find swiftc)}"
|
||||||
|
ARCH="$(uname -m)"
|
||||||
|
|
||||||
|
SOURCES=()
|
||||||
|
while IFS= read -r src; do
|
||||||
|
if [[ "$(basename "$src")" != "main.swift" ]]; then
|
||||||
|
SOURCES+=("$src")
|
||||||
|
fi
|
||||||
|
done < <(find "$ROOT/Sources" -name '*.swift' | LC_ALL=C sort)
|
||||||
|
|
||||||
|
SOURCES+=("$ROOT/Tests/TestRunnerMain.swift")
|
||||||
|
|
||||||
|
BIN="$(mktemp -t tp-test-XXXXXX)"
|
||||||
|
trap 'rm -f "$BIN"' EXIT
|
||||||
|
|
||||||
|
"$SWIFT" \
|
||||||
|
-sdk "$SDKROOT" \
|
||||||
|
-target "${ARCH}-apple-macos10.15" \
|
||||||
|
-swift-version 5 \
|
||||||
|
-Onone -g \
|
||||||
|
-import-objc-header "$ROOT/Bridging-Header.h" \
|
||||||
|
-Xcc "-I${SDKROOT}/usr/include" \
|
||||||
|
-framework AppKit \
|
||||||
|
-framework Foundation \
|
||||||
|
-framework CoreGraphics \
|
||||||
|
-framework ImageIO \
|
||||||
|
-framework ApplicationServices \
|
||||||
|
-lcups \
|
||||||
|
-o "$BIN" \
|
||||||
|
"${SOURCES[@]}"
|
||||||
|
|
||||||
|
"$BIN"
|
||||||
Regular → Executable
@@ -19,7 +19,7 @@ enum AppError: Error, CustomStringConvertible {
|
|||||||
switch self {
|
switch self {
|
||||||
case .invalidJob: return .invalidJob
|
case .invalidJob: return .invalidJob
|
||||||
case .unreadableTarget: return .unreadableTarget
|
case .unreadableTarget: return .unreadableTarget
|
||||||
case .printer: return .printer
|
case .printer: return .printerError
|
||||||
case .internalFatal: return .internalFatal
|
case .internalFatal: return .internalFatal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import AppKit
|
import AppKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
/// SPEC §9 — three-pane window: thumbnails | canvas | inspector.
|
/// SPEC §9 — three-pane window: thumbnails | canvas | inspector.
|
||||||
|
|
||||||
@@ -102,6 +103,7 @@ final class PreviewWindowController: NSWindowController {
|
|||||||
engine.mediaSize = parsed.printSettings.mediaSize
|
engine.mediaSize = parsed.printSettings.mediaSize
|
||||||
engine.mediaType = parsed.printSettings.mediaType
|
engine.mediaType = parsed.printSettings.mediaType
|
||||||
engine.paperSource = parsed.printSettings.paperSource
|
engine.paperSource = parsed.printSettings.paperSource
|
||||||
|
engine.printQuality = parsed.printSettings.printQuality
|
||||||
engine.resolution = parsed.printSettings.resolution
|
engine.resolution = parsed.printSettings.resolution
|
||||||
engine.scaling = parsed.printSettings.scaling
|
engine.scaling = parsed.printSettings.scaling
|
||||||
engine.centered = parsed.printSettings.centered
|
engine.centered = parsed.printSettings.centered
|
||||||
@@ -211,7 +213,15 @@ final class PreviewWindowController: NSWindowController {
|
|||||||
let panel = NSOpenPanel()
|
let panel = NSOpenPanel()
|
||||||
panel.allowsMultipleSelection = true
|
panel.allowsMultipleSelection = true
|
||||||
panel.canChooseDirectories = false
|
panel.canChooseDirectories = false
|
||||||
|
if #available(macOS 12.0, *) {
|
||||||
|
var types: [UTType] = [.tiff, .json]
|
||||||
|
if let jobType = UTType(filenameExtension: "targetjob") {
|
||||||
|
types.append(jobType)
|
||||||
|
}
|
||||||
|
panel.allowedContentTypes = types
|
||||||
|
} else {
|
||||||
panel.allowedFileTypes = ["tif", "tiff", "targetjob", "json"]
|
panel.allowedFileTypes = ["tif", "tiff", "targetjob", "json"]
|
||||||
|
}
|
||||||
panel.begin { result in
|
panel.begin { result in
|
||||||
if result == .OK { completion(panel.urls) }
|
if result == .OK { completion(panel.urls) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,32 @@ struct PrintSettings: Equatable {
|
|||||||
var mediaType: String?
|
var mediaType: String?
|
||||||
var paperSource: String?
|
var paperSource: String?
|
||||||
var resolution: String?
|
var resolution: String?
|
||||||
|
var printQuality: String?
|
||||||
var scaling: Double
|
var scaling: Double
|
||||||
var centered: Bool
|
var centered: Bool
|
||||||
var forceUnmanagedColor: Bool
|
var forceUnmanagedColor: Bool
|
||||||
|
|
||||||
|
init(
|
||||||
|
printerName: String,
|
||||||
|
mediaSize: String,
|
||||||
|
mediaType: String? = nil,
|
||||||
|
paperSource: String? = nil,
|
||||||
|
resolution: String? = nil,
|
||||||
|
printQuality: String? = nil,
|
||||||
|
scaling: Double,
|
||||||
|
centered: Bool,
|
||||||
|
forceUnmanagedColor: Bool
|
||||||
|
) {
|
||||||
|
self.printerName = printerName
|
||||||
|
self.mediaSize = mediaSize
|
||||||
|
self.mediaType = mediaType
|
||||||
|
self.paperSource = paperSource
|
||||||
|
self.resolution = resolution
|
||||||
|
self.printQuality = printQuality
|
||||||
|
self.scaling = scaling
|
||||||
|
self.centered = centered
|
||||||
|
self.forceUnmanagedColor = forceUnmanagedColor
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UIPolicy: Equatable {
|
struct UIPolicy: Equatable {
|
||||||
@@ -117,6 +140,7 @@ enum TargetJobParser {
|
|||||||
mediaType: stringValue(obj["mediaType"]),
|
mediaType: stringValue(obj["mediaType"]),
|
||||||
paperSource: stringValue(obj["paperSource"]),
|
paperSource: stringValue(obj["paperSource"]),
|
||||||
resolution: stringValue(obj["resolution"]),
|
resolution: stringValue(obj["resolution"]),
|
||||||
|
printQuality: stringValue(obj["printQuality"]),
|
||||||
scaling: scaling,
|
scaling: scaling,
|
||||||
centered: centered,
|
centered: centered,
|
||||||
forceUnmanagedColor: force
|
forceUnmanagedColor: force
|
||||||
@@ -144,6 +168,7 @@ enum TargetJobParser {
|
|||||||
if let v = job.printSettings.mediaType { ps["mediaType"] = v }
|
if let v = job.printSettings.mediaType { ps["mediaType"] = v }
|
||||||
if let v = job.printSettings.paperSource { ps["paperSource"] = v }
|
if let v = job.printSettings.paperSource { ps["paperSource"] = v }
|
||||||
if let v = job.printSettings.resolution { ps["resolution"] = v }
|
if let v = job.printSettings.resolution { ps["resolution"] = v }
|
||||||
|
if let v = job.printSettings.printQuality { ps["printQuality"] = v }
|
||||||
let obj: [String: Any] = [
|
let obj: [String: Any] = [
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"jobTitle": job.jobTitle,
|
"jobTitle": job.jobTitle,
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import Darwin
|
|||||||
|
|
||||||
/// SPEC §10 — libcups queue inspection, AirPrint detection, vendor PPD injection.
|
/// SPEC §10 — libcups queue inspection, AirPrint detection, vendor PPD injection.
|
||||||
|
|
||||||
|
struct PPDChoice: Equatable, Hashable {
|
||||||
|
var name: String // Computer-readable identifier (e.g. "13", "2", "Plain")
|
||||||
|
var title: String // Human-readable display label (e.g. "Epson Premium Glossy", "Cassette 1")
|
||||||
|
}
|
||||||
|
|
||||||
struct PrinterQueue: Equatable {
|
struct PrinterQueue: Equatable {
|
||||||
var name: String
|
var name: String
|
||||||
var instance: String?
|
var instance: String?
|
||||||
@@ -14,9 +19,58 @@ struct PrinterQueue: Equatable {
|
|||||||
var isAirPrint: Bool
|
var isAirPrint: Bool
|
||||||
var mediaSizes: [String]
|
var mediaSizes: [String]
|
||||||
var mediaTypes: [String]
|
var mediaTypes: [String]
|
||||||
|
var mediaTypeChoices: [PPDChoice]
|
||||||
|
var mediaTypeKeyword: String?
|
||||||
var trays: [String]
|
var trays: [String]
|
||||||
|
var trayChoices: [PPDChoice]
|
||||||
|
var trayKeyword: String?
|
||||||
|
var qualityChoices: [PPDChoice]
|
||||||
|
var qualityKeyword: String?
|
||||||
var resolutions: [String]
|
var resolutions: [String]
|
||||||
|
var resolutionChoices: [PPDChoice]
|
||||||
var ppdText: String
|
var ppdText: String
|
||||||
|
|
||||||
|
init(
|
||||||
|
name: String,
|
||||||
|
instance: String? = nil,
|
||||||
|
displayName: String,
|
||||||
|
uri: String,
|
||||||
|
make: String,
|
||||||
|
model: String,
|
||||||
|
isAirPrint: Bool,
|
||||||
|
mediaSizes: [String],
|
||||||
|
mediaTypes: [String],
|
||||||
|
mediaTypeChoices: [PPDChoice] = [],
|
||||||
|
mediaTypeKeyword: String? = nil,
|
||||||
|
trays: [String],
|
||||||
|
trayChoices: [PPDChoice] = [],
|
||||||
|
trayKeyword: String? = nil,
|
||||||
|
qualityChoices: [PPDChoice] = [],
|
||||||
|
qualityKeyword: String? = nil,
|
||||||
|
resolutions: [String],
|
||||||
|
resolutionChoices: [PPDChoice] = [],
|
||||||
|
ppdText: String
|
||||||
|
) {
|
||||||
|
self.name = name
|
||||||
|
self.instance = instance
|
||||||
|
self.displayName = displayName
|
||||||
|
self.uri = uri
|
||||||
|
self.make = make
|
||||||
|
self.model = model
|
||||||
|
self.isAirPrint = isAirPrint
|
||||||
|
self.mediaSizes = mediaSizes
|
||||||
|
self.mediaTypes = mediaTypes
|
||||||
|
self.mediaTypeChoices = mediaTypeChoices
|
||||||
|
self.mediaTypeKeyword = mediaTypeKeyword
|
||||||
|
self.trays = trays
|
||||||
|
self.trayChoices = trayChoices
|
||||||
|
self.trayKeyword = trayKeyword
|
||||||
|
self.qualityChoices = qualityChoices
|
||||||
|
self.qualityKeyword = qualityKeyword
|
||||||
|
self.resolutions = resolutions
|
||||||
|
self.resolutionChoices = resolutionChoices
|
||||||
|
self.ppdText = ppdText
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum CUPSManager {
|
enum CUPSManager {
|
||||||
@@ -62,8 +116,15 @@ enum CUPSManager {
|
|||||||
isAirPrint: air,
|
isAirPrint: air,
|
||||||
mediaSizes: options.pageSizes,
|
mediaSizes: options.pageSizes,
|
||||||
mediaTypes: options.mediaTypes,
|
mediaTypes: options.mediaTypes,
|
||||||
|
mediaTypeChoices: options.mediaTypeChoices,
|
||||||
|
mediaTypeKeyword: options.mediaTypeKeyword,
|
||||||
trays: options.trays,
|
trays: options.trays,
|
||||||
|
trayChoices: options.trayChoices,
|
||||||
|
trayKeyword: options.trayKeyword,
|
||||||
|
qualityChoices: options.qualityChoices,
|
||||||
|
qualityKeyword: options.qualityKeyword,
|
||||||
resolutions: options.resolutions,
|
resolutions: options.resolutions,
|
||||||
|
resolutionChoices: options.resolutionChoices,
|
||||||
ppdText: ppdText
|
ppdText: ppdText
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -77,9 +138,11 @@ enum CUPSManager {
|
|||||||
// MARK: - PPD
|
// MARK: - PPD
|
||||||
|
|
||||||
static func ppdPath(forQueue name: String) -> String? {
|
static func ppdPath(forQueue name: String) -> String? {
|
||||||
guard let cPath = cupsGetPPD(name) else { return nil }
|
name.withCString { cName in
|
||||||
|
guard let cPath = TPCupsGetPPD(cName) else { return nil }
|
||||||
return String(cString: cPath)
|
return String(cString: cPath)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static func ppdContents(forQueue name: String) -> String? {
|
static func ppdContents(forQueue name: String) -> String? {
|
||||||
guard let path = ppdPath(forQueue: name) else { return nil }
|
guard let path = ppdPath(forQueue: name) else { return nil }
|
||||||
@@ -90,17 +153,50 @@ enum CUPSManager {
|
|||||||
struct PPDOptions {
|
struct PPDOptions {
|
||||||
var pageSizes: [String] = []
|
var pageSizes: [String] = []
|
||||||
var mediaTypes: [String] = []
|
var mediaTypes: [String] = []
|
||||||
|
var mediaTypeChoices: [PPDChoice] = []
|
||||||
|
var mediaTypeKeyword: String? = nil
|
||||||
var trays: [String] = []
|
var trays: [String] = []
|
||||||
|
var trayChoices: [PPDChoice] = []
|
||||||
|
var trayKeyword: String? = nil
|
||||||
|
var qualityChoices: [PPDChoice] = []
|
||||||
|
var qualityKeyword: String? = nil
|
||||||
var resolutions: [String] = []
|
var resolutions: [String] = []
|
||||||
|
var resolutionChoices: [PPDChoice] = []
|
||||||
var colorChoices: [(keyword: String, choice: String)] = []
|
var colorChoices: [(keyword: String, choice: String)] = []
|
||||||
}
|
}
|
||||||
|
|
||||||
static func discoverPPDOptions(_ ppdText: String) -> PPDOptions {
|
static func discoverPPDOptions(_ ppdText: String) -> PPDOptions {
|
||||||
var out = PPDOptions()
|
var out = PPDOptions()
|
||||||
out.pageSizes = optionChoices(in: ppdText, keyword: "PageSize")
|
out.pageSizes = optionChoices(in: ppdText, keyword: "PageSize")
|
||||||
out.mediaTypes = optionChoices(in: ppdText, keyword: "MediaType")
|
|
||||||
out.trays = optionChoices(in: ppdText, keyword: "InputSlot")
|
let media = discoverOptionChoices(
|
||||||
out.resolutions = optionChoices(in: ppdText, keyword: "Resolution")
|
in: ppdText,
|
||||||
|
candidateKeywords: ["MediaType", "CNIJMediaType"],
|
||||||
|
openUITargets: ["media type", "mediatype"]
|
||||||
|
)
|
||||||
|
out.mediaTypeKeyword = media.keyword
|
||||||
|
out.mediaTypeChoices = media.choices
|
||||||
|
out.mediaTypes = media.choices.map { $0.title }
|
||||||
|
|
||||||
|
let trays = discoverOptionChoices(
|
||||||
|
in: ppdText,
|
||||||
|
candidateKeywords: ["InputSlot", "EPIJ_FdSo", "CNIJMediaSupply"],
|
||||||
|
openUITargets: ["paper source", "media source", "input slot", "paper feed", "feed source"]
|
||||||
|
)
|
||||||
|
out.trayKeyword = trays.keyword
|
||||||
|
out.trayChoices = trays.choices
|
||||||
|
out.trays = trays.choices.map { $0.title }
|
||||||
|
|
||||||
|
let quality = discoverOptionChoices(
|
||||||
|
in: ppdText,
|
||||||
|
candidateKeywords: ["CNIJPrintQuality", "EPIJ_Qual", "cupsPrintQuality", "PrintQuality", "CNIJPrintMode2", "Quality", "StpQuality"],
|
||||||
|
openUITargets: ["print quality", "quality"]
|
||||||
|
)
|
||||||
|
out.qualityKeyword = quality.keyword
|
||||||
|
out.qualityChoices = quality.choices
|
||||||
|
|
||||||
|
out.resolutionChoices = optionDetails(in: ppdText, keyword: "Resolution")
|
||||||
|
out.resolutions = out.resolutionChoices.map { $0.name }
|
||||||
// Walk every *OpenUI for Colour/Color keys (SPEC §10.3 generic).
|
// Walk every *OpenUI for Colour/Color keys (SPEC §10.3 generic).
|
||||||
let lines = ppdText.split(whereSeparator: \.isNewline)
|
let lines = ppdText.split(whereSeparator: \.isNewline)
|
||||||
var currentKeyword: String?
|
var currentKeyword: String?
|
||||||
@@ -174,22 +270,127 @@ enum CUPSManager {
|
|||||||
return ("", makeModel)
|
return ("", makeModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func optionChoices(in ppdText: String, keyword: String) -> [String] {
|
/// Decodes Adobe PPD hex escape sequences (e.g. `<2F>` -> `/`, `<2E>` -> `.`, `<3A>` -> `:`).
|
||||||
var choices: [String] = []
|
static func decodePPDString(_ s: String) -> String {
|
||||||
|
var result = ""
|
||||||
|
var idx = s.startIndex
|
||||||
|
while idx < s.endIndex {
|
||||||
|
if s[idx] == "<" {
|
||||||
|
if let closeIdx = s[idx...].firstIndex(of: ">") {
|
||||||
|
let hexContent = s[s.index(after: idx)..<closeIdx]
|
||||||
|
let cleanHex = hexContent.filter { $0.isHexDigit }
|
||||||
|
if !cleanHex.isEmpty && cleanHex.count % 2 == 0 {
|
||||||
|
var bytes: [UInt8] = []
|
||||||
|
var hexIdx = cleanHex.startIndex
|
||||||
|
var valid = true
|
||||||
|
while hexIdx < cleanHex.endIndex {
|
||||||
|
let nextHexIdx = cleanHex.index(hexIdx, offsetBy: 2)
|
||||||
|
let byteStr = cleanHex[hexIdx..<nextHexIdx]
|
||||||
|
if let b = UInt8(byteStr, radix: 16) {
|
||||||
|
bytes.append(b)
|
||||||
|
} else {
|
||||||
|
valid = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
hexIdx = nextHexIdx
|
||||||
|
}
|
||||||
|
if valid, let decoded = String(bytes: bytes, encoding: .utf8) ?? String(bytes: bytes, encoding: .isoLatin1) {
|
||||||
|
result.append(decoded)
|
||||||
|
idx = s.index(after: closeIdx)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.append(s[idx])
|
||||||
|
idx = s.index(after: idx)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses option lines for a keyword, extracting both internal choice code (`name`) and human-readable label (`title`).
|
||||||
|
static func optionDetails(in ppdText: String, keyword: String) -> [PPDChoice] {
|
||||||
|
var choices: [PPDChoice] = []
|
||||||
let prefix = "*\(keyword) "
|
let prefix = "*\(keyword) "
|
||||||
for raw in ppdText.split(whereSeparator: \.isNewline) {
|
for raw in ppdText.split(whereSeparator: \.isNewline) {
|
||||||
let line = String(raw)
|
let line = String(raw).trimmingCharacters(in: .whitespaces)
|
||||||
guard line.hasPrefix(prefix) else { continue }
|
guard line.hasPrefix(prefix) else { continue }
|
||||||
let rest = String(line.dropFirst(prefix.count))
|
let rest = String(line.dropFirst(prefix.count)).trimmingCharacters(in: .whitespaces)
|
||||||
let token = rest.split(whereSeparator: { $0 == "/" || $0 == ":" || $0 == " " }).first
|
guard let colonIdx = rest.firstIndex(of: ":") else { continue }
|
||||||
if let token = token {
|
let choicePart = rest[..<colonIdx].trimmingCharacters(in: .whitespaces)
|
||||||
let name = String(token)
|
let name: String
|
||||||
if !choices.contains(name) { choices.append(name) }
|
let title: String
|
||||||
|
if let slashIdx = choicePart.firstIndex(of: "/") {
|
||||||
|
name = String(choicePart[..<slashIdx]).trimmingCharacters(in: .whitespaces)
|
||||||
|
var rawTitle = String(choicePart[choicePart.index(after: slashIdx)...]).trimmingCharacters(in: .whitespaces)
|
||||||
|
if rawTitle.hasPrefix("\"") && rawTitle.hasSuffix("\"") && rawTitle.count >= 2 {
|
||||||
|
rawTitle = String(rawTitle.dropFirst().dropLast()).trimmingCharacters(in: .whitespaces)
|
||||||
|
}
|
||||||
|
let decoded = decodePPDString(rawTitle).trimmingCharacters(in: .whitespaces)
|
||||||
|
title = decoded.isEmpty ? name : decoded
|
||||||
|
} else {
|
||||||
|
name = choicePart
|
||||||
|
title = choicePart
|
||||||
|
}
|
||||||
|
guard !name.isEmpty else { continue }
|
||||||
|
if !choices.contains(where: { $0.name == name }) {
|
||||||
|
choices.append(PPDChoice(name: name, title: title))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return choices
|
return choices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func optionChoices(in ppdText: String, keyword: String) -> [String] {
|
||||||
|
optionDetails(in: ppdText, keyword: keyword).map { $0.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func discoverOptionChoices(
|
||||||
|
in ppdText: String,
|
||||||
|
candidateKeywords: [String],
|
||||||
|
openUITargets: [String] = []
|
||||||
|
) -> (keyword: String?, choices: [PPDChoice]) {
|
||||||
|
for kw in candidateKeywords {
|
||||||
|
let choices = optionDetails(in: ppdText, keyword: kw)
|
||||||
|
if !choices.isEmpty {
|
||||||
|
return (kw, choices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If not found in candidate keywords, scan *OpenUI for matching translations
|
||||||
|
let lines = ppdText.split(whereSeparator: \.isNewline)
|
||||||
|
for raw in lines {
|
||||||
|
let s = String(raw).trimmingCharacters(in: .whitespaces)
|
||||||
|
guard s.hasPrefix("*OpenUI") else { continue }
|
||||||
|
guard let (kw, trans) = openUIKeywordAndTranslation(s) else { continue }
|
||||||
|
let lowerTrans = trans.lowercased()
|
||||||
|
let lowerKw = kw.lowercased()
|
||||||
|
if openUITargets.contains(where: { lowerTrans.contains($0) || lowerKw.contains($0) }) {
|
||||||
|
let choices = optionDetails(in: ppdText, keyword: kw)
|
||||||
|
if !choices.isEmpty {
|
||||||
|
return (kw, choices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (nil, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func openUIKeywordAndTranslation(_ line: String) -> (keyword: String, translation: String)? {
|
||||||
|
// e.g. *OpenUI *PageSize/Media Size: PickOne
|
||||||
|
guard let star = line.firstIndex(of: "*") else { return nil }
|
||||||
|
let afterFirstStar = line[star...].dropFirst()
|
||||||
|
guard let secondStar = afterFirstStar.firstIndex(of: "*") else { return nil }
|
||||||
|
let rest = afterFirstStar[secondStar...].dropFirst()
|
||||||
|
guard let colonIdx = rest.firstIndex(of: ":") else { return nil }
|
||||||
|
let target = rest[..<colonIdx].trimmingCharacters(in: .whitespaces)
|
||||||
|
if let slashIdx = target.firstIndex(of: "/") {
|
||||||
|
let kw = String(target[..<slashIdx]).trimmingCharacters(in: .whitespaces)
|
||||||
|
let trans = decodePPDString(String(target[target.index(after: slashIdx)...]).trimmingCharacters(in: .whitespaces))
|
||||||
|
return (kw, trans)
|
||||||
|
} else {
|
||||||
|
let kw = String(target)
|
||||||
|
return (kw, kw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static func openUIKeyword(_ line: String) -> String? {
|
private static func openUIKeyword(_ line: String) -> String? {
|
||||||
// *OpenUI *PageSize/Media Size: PickOne
|
// *OpenUI *PageSize/Media Size: PickOne
|
||||||
guard let star = line.firstIndex(of: "*") else { return nil }
|
guard let star = line.firstIndex(of: "*") else { return nil }
|
||||||
|
|||||||
@@ -80,14 +80,19 @@ enum ColorMatching {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func stripColorMatchingAccessories(from panel: NSPrintPanel) {
|
static func stripColorMatchingAccessories(from panel: NSPrintPanel) {
|
||||||
let accessories = panel.accessoryControllers()
|
let accessories = panel.accessoryControllers
|
||||||
for ac in accessories {
|
for ac in accessories {
|
||||||
let title = (ac.title ?? "") + " " + (ac.nibName ?? "")
|
let cls = NSStringFromClass(type(of: ac))
|
||||||
|
let title = (ac.title ?? "") + " " + (ac.nibName ?? "") + " " + cls
|
||||||
let hay = title.lowercased()
|
let hay = title.lowercased()
|
||||||
if hay.contains("color matching") || hay.contains("colour matching") || hay.contains("colormatch") {
|
if hay.contains("color matching") || hay.contains("colour matching") || hay.contains("colormatch") {
|
||||||
panel.removeAccessoryController(ac)
|
// SDK types this as NSViewController; removeAccessoryController
|
||||||
|
// requires NSPrintPanelAccessorizing (SPEC §7.2).
|
||||||
|
if let accessor = ac as? (NSViewController & NSPrintPanelAccessorizing) {
|
||||||
|
panel.removeAccessoryController(accessor)
|
||||||
TPLog.info("Removed print-panel accessory: \(title)")
|
TPLog.info("Removed print-panel accessory: \(title)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ final class PrintEngine {
|
|||||||
var mediaSize: String
|
var mediaSize: String
|
||||||
var mediaType: String?
|
var mediaType: String?
|
||||||
var paperSource: String?
|
var paperSource: String?
|
||||||
|
var printQuality: String?
|
||||||
var resolution: String?
|
var resolution: String?
|
||||||
var scaling: Double
|
var scaling: Double
|
||||||
var centered: Bool
|
var centered: Bool
|
||||||
@@ -24,6 +25,7 @@ final class PrintEngine {
|
|||||||
mediaSize = job.printSettings.mediaSize
|
mediaSize = job.printSettings.mediaSize
|
||||||
mediaType = job.printSettings.mediaType
|
mediaType = job.printSettings.mediaType
|
||||||
paperSource = job.printSettings.paperSource
|
paperSource = job.printSettings.paperSource
|
||||||
|
printQuality = job.printSettings.printQuality
|
||||||
resolution = job.printSettings.resolution
|
resolution = job.printSettings.resolution
|
||||||
scaling = job.printSettings.scaling
|
scaling = job.printSettings.scaling
|
||||||
centered = job.printSettings.centered
|
centered = job.printSettings.centered
|
||||||
@@ -39,6 +41,7 @@ final class PrintEngine {
|
|||||||
mediaSize = "A4"
|
mediaSize = "A4"
|
||||||
mediaType = nil
|
mediaType = nil
|
||||||
paperSource = nil
|
paperSource = nil
|
||||||
|
printQuality = nil
|
||||||
resolution = nil
|
resolution = nil
|
||||||
scaling = 1.0
|
scaling = 1.0
|
||||||
centered = true
|
centered = true
|
||||||
@@ -48,6 +51,7 @@ final class PrintEngine {
|
|||||||
allowBasicDriverChanges = true
|
allowBasicDriverChanges = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configures `NSPrintInfo` for `NSPrintOperation` (SPEC §4.4, §10.3).
|
||||||
func makePrintInfo() throws -> NSPrintInfo {
|
func makePrintInfo() throws -> NSPrintInfo {
|
||||||
let info = NSPrintInfo.shared.copy() as! NSPrintInfo
|
let info = NSPrintInfo.shared.copy() as! NSPrintInfo
|
||||||
info.jobDisposition = .spool
|
info.jobDisposition = .spool
|
||||||
@@ -72,8 +76,10 @@ final class PrintEngine {
|
|||||||
|
|
||||||
ColorMatching.apply(colorMode, to: info)
|
ColorMatching.apply(colorMode, to: info)
|
||||||
|
|
||||||
|
var resolvedQueue: PrinterQueue? = nil
|
||||||
do {
|
do {
|
||||||
if let queue = try CUPSManager.namedQueue(printerName) ?? try CUPSManager.listQueues().first(where: { $0.name == printerName }) {
|
if let queue = try CUPSManager.namedQueue(printerName) {
|
||||||
|
resolvedQueue = queue
|
||||||
let bypass = CUPSManager.vendorColorBypass(make: queue.make, model: queue.model, ppdText: queue.ppdText)
|
let bypass = CUPSManager.vendorColorBypass(make: queue.make, model: queue.model, ppdText: queue.ppdText)
|
||||||
CUPSManager.applyVendorBypass(bypass, to: info)
|
CUPSManager.applyVendorBypass(bypass, to: info)
|
||||||
}
|
}
|
||||||
@@ -82,14 +88,66 @@ final class PrintEngine {
|
|||||||
throw AppError.printer(String(describing: error))
|
throw AppError.printer(String(describing: error))
|
||||||
}
|
}
|
||||||
|
|
||||||
applyOptionalPPDKeys(to: info)
|
applyOptionalPPDKeys(to: info, queue: resolvedQueue)
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applyOptionalPPDKeys(to info: NSPrintInfo) {
|
func applyOptionalPPDKeys(to info: NSPrintInfo, queue: PrinterQueue?) {
|
||||||
var extras: [String: String] = [:]
|
var extras: [String: String] = [:]
|
||||||
if let mediaType = mediaType { extras["MediaType"] = mediaType }
|
if let mediaType = mediaType {
|
||||||
if let paperSource = paperSource { extras["InputSlot"] = paperSource }
|
let key = queue?.mediaTypeKeyword ?? "MediaType"
|
||||||
|
if let match = queue?.mediaTypeChoices.first(where: { $0.name == mediaType || $0.title.caseInsensitiveCompare(mediaType) == .orderedSame }) {
|
||||||
|
extras[key] = match.name
|
||||||
|
} else {
|
||||||
|
extras[key] = mediaType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let paperSource = paperSource {
|
||||||
|
let key = queue?.trayKeyword ?? "InputSlot"
|
||||||
|
if let match = queue?.trayChoices.first(where: { $0.name == paperSource || $0.title.caseInsensitiveCompare(paperSource) == .orderedSame }) {
|
||||||
|
extras[key] = match.name
|
||||||
|
} else if paperSource.lowercased() != "auto" || key == "InputSlot" {
|
||||||
|
extras[key] = paperSource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let printQuality = printQuality {
|
||||||
|
let key = queue?.qualityKeyword ?? "PrintQuality"
|
||||||
|
let resolvedCode: String
|
||||||
|
if let match = queue?.qualityChoices.first(where: { $0.name == printQuality || $0.title.caseInsensitiveCompare(printQuality) == .orderedSame }) {
|
||||||
|
resolvedCode = match.name
|
||||||
|
} else {
|
||||||
|
resolvedCode = printQuality
|
||||||
|
}
|
||||||
|
|
||||||
|
if key == "CNIJPrintQuality" {
|
||||||
|
switch resolvedCode {
|
||||||
|
case "0": // Super Fine (notch 5 — Custom mode)
|
||||||
|
extras["CNIJPrintQuality"] = "0"
|
||||||
|
extras["CNIJPrintMode2"] = "5"
|
||||||
|
extras["CNIJPQualitySlider"] = "5"
|
||||||
|
case "5": // Fine (notch 4 — High preset)
|
||||||
|
extras["CNIJPrintQuality"] = "5"
|
||||||
|
extras["CNIJPrintMode2"] = "1"
|
||||||
|
extras["CNIJPQualitySlider"] = "4"
|
||||||
|
case "10": // Normal(Fine) (notch 3 — Standard preset)
|
||||||
|
extras["CNIJPrintQuality"] = "10"
|
||||||
|
extras["CNIJPrintMode2"] = "2"
|
||||||
|
extras["CNIJPQualitySlider"] = "3"
|
||||||
|
case "15": // Normal(Fast) (notch 2)
|
||||||
|
extras["CNIJPrintQuality"] = "15"
|
||||||
|
extras["CNIJPrintMode2"] = "5"
|
||||||
|
extras["CNIJPQualitySlider"] = "2"
|
||||||
|
case "20": // Fast (notch 1 — Fast preset)
|
||||||
|
extras["CNIJPrintQuality"] = "20"
|
||||||
|
extras["CNIJPrintMode2"] = "3"
|
||||||
|
extras["CNIJPQualitySlider"] = "1"
|
||||||
|
default:
|
||||||
|
extras["CNIJPrintQuality"] = resolvedCode
|
||||||
|
}
|
||||||
|
} else if resolvedCode.lowercased() != "auto" || key == "cupsPrintQuality" {
|
||||||
|
extras[key] = resolvedCode
|
||||||
|
}
|
||||||
|
}
|
||||||
if let resolution = resolution { extras["Resolution"] = resolution }
|
if let resolution = resolution { extras["Resolution"] = resolution }
|
||||||
if extras.isEmpty { return }
|
if extras.isEmpty { return }
|
||||||
CUPSManager.applyVendorBypass(extras, to: info)
|
CUPSManager.applyVendorBypass(extras, to: info)
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ final class InspectorView: NSView {
|
|||||||
let mediaPop = NSPopUpButton()
|
let mediaPop = NSPopUpButton()
|
||||||
let typePop = NSPopUpButton()
|
let typePop = NSPopUpButton()
|
||||||
let trayPop = NSPopUpButton()
|
let trayPop = NSPopUpButton()
|
||||||
|
let qualityPop = NSPopUpButton()
|
||||||
let resolutionPop = NSPopUpButton()
|
let resolutionPop = NSPopUpButton()
|
||||||
let scaleSlider = NSSlider()
|
let scaleSlider = NSSlider()
|
||||||
let scaleLabel = NSTextField(labelWithString: "100%")
|
let scaleLabel = NSTextField(labelWithString: "100%")
|
||||||
@@ -46,6 +47,7 @@ final class InspectorView: NSView {
|
|||||||
mediaPop.target = self; mediaPop.action = #selector(changed)
|
mediaPop.target = self; mediaPop.action = #selector(changed)
|
||||||
typePop.target = self; typePop.action = #selector(changed)
|
typePop.target = self; typePop.action = #selector(changed)
|
||||||
trayPop.target = self; trayPop.action = #selector(changed)
|
trayPop.target = self; trayPop.action = #selector(changed)
|
||||||
|
qualityPop.target = self; qualityPop.action = #selector(changed)
|
||||||
resolutionPop.target = self; resolutionPop.action = #selector(changed)
|
resolutionPop.target = self; resolutionPop.action = #selector(changed)
|
||||||
scaleSlider.minValue = 0.5
|
scaleSlider.minValue = 0.5
|
||||||
scaleSlider.maxValue = 1.5
|
scaleSlider.maxValue = 1.5
|
||||||
@@ -79,6 +81,7 @@ final class InspectorView: NSView {
|
|||||||
section("Media size", mediaPop),
|
section("Media size", mediaPop),
|
||||||
section("Media type", typePop),
|
section("Media type", typePop),
|
||||||
section("Tray", trayPop),
|
section("Tray", trayPop),
|
||||||
|
section("Print quality", qualityPop),
|
||||||
section("Resolution", resolutionPop),
|
section("Resolution", resolutionPop),
|
||||||
section("Scale", scaleRow()),
|
section("Scale", scaleRow()),
|
||||||
centeredCheck,
|
centeredCheck,
|
||||||
@@ -101,6 +104,7 @@ final class InspectorView: NSView {
|
|||||||
mediaPop.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
mediaPop.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
||||||
typePop.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
typePop.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
||||||
trayPop.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
trayPop.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
||||||
|
qualityPop.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
||||||
resolutionPop.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
resolutionPop.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
||||||
colorSeg.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
colorSeg.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
||||||
])
|
])
|
||||||
@@ -124,8 +128,9 @@ final class InspectorView: NSView {
|
|||||||
}
|
}
|
||||||
refreshDependent()
|
refreshDependent()
|
||||||
select(mediaPop, engine.mediaSize)
|
select(mediaPop, engine.mediaSize)
|
||||||
if let t = engine.mediaType { select(typePop, t) }
|
if let t = engine.mediaType { selectChoice(typePop, t) }
|
||||||
if let t = engine.paperSource { select(trayPop, t) }
|
if let t = engine.paperSource { selectChoice(trayPop, t) }
|
||||||
|
if let q = engine.printQuality { selectChoice(qualityPop, q) }
|
||||||
if let r = engine.resolution { select(resolutionPop, r) }
|
if let r = engine.resolution { select(resolutionPop, r) }
|
||||||
scaleSlider.doubleValue = engine.scaling
|
scaleSlider.doubleValue = engine.scaling
|
||||||
scaleLabel.stringValue = String(format: "%.0f%%", engine.scaling * 100)
|
scaleLabel.stringValue = String(format: "%.0f%%", engine.scaling * 100)
|
||||||
@@ -143,8 +148,9 @@ final class InspectorView: NSView {
|
|||||||
func push(into engine: PrintEngine) {
|
func push(into engine: PrintEngine) {
|
||||||
if let q = selectedQueue { engine.printerName = q.name }
|
if let q = selectedQueue { engine.printerName = q.name }
|
||||||
engine.mediaSize = mediaPop.titleOfSelectedItem ?? engine.mediaSize
|
engine.mediaSize = mediaPop.titleOfSelectedItem ?? engine.mediaSize
|
||||||
engine.mediaType = emptyToNil(typePop.titleOfSelectedItem)
|
engine.mediaType = (typePop.selectedItem?.representedObject as? String) ?? emptyToNil(typePop.titleOfSelectedItem)
|
||||||
engine.paperSource = emptyToNil(trayPop.titleOfSelectedItem)
|
engine.paperSource = (trayPop.selectedItem?.representedObject as? String) ?? emptyToNil(trayPop.titleOfSelectedItem)
|
||||||
|
engine.printQuality = (qualityPop.selectedItem?.representedObject as? String) ?? emptyToNil(qualityPop.titleOfSelectedItem)
|
||||||
engine.resolution = emptyToNil(resolutionPop.titleOfSelectedItem)
|
engine.resolution = emptyToNil(resolutionPop.titleOfSelectedItem)
|
||||||
engine.scaling = scaleSlider.doubleValue
|
engine.scaling = scaleSlider.doubleValue
|
||||||
engine.centered = centeredCheck.state == .on
|
engine.centered = centeredCheck.state == .on
|
||||||
@@ -161,6 +167,7 @@ final class InspectorView: NSView {
|
|||||||
mediaPop.isEnabled = mediaEnabled
|
mediaPop.isEnabled = mediaEnabled
|
||||||
typePop.isEnabled = mediaEnabled
|
typePop.isEnabled = mediaEnabled
|
||||||
trayPop.isEnabled = mediaEnabled
|
trayPop.isEnabled = mediaEnabled
|
||||||
|
qualityPop.isEnabled = mediaEnabled && (selectedQueue?.qualityChoices.isEmpty == false)
|
||||||
resolutionPop.isEnabled = !jobLocked
|
resolutionPop.isEnabled = !jobLocked
|
||||||
landscapeCheck.isEnabled = mediaEnabled
|
landscapeCheck.isEnabled = mediaEnabled
|
||||||
scaleSlider.isEnabled = !jobLocked
|
scaleSlider.isEnabled = !jobLocked
|
||||||
@@ -184,8 +191,13 @@ final class InspectorView: NSView {
|
|||||||
private func refreshDependent() {
|
private func refreshDependent() {
|
||||||
let q = selectedQueue
|
let q = selectedQueue
|
||||||
refill(mediaPop, q?.mediaSizes.isEmpty == false ? q!.mediaSizes : Geometry.papers.map { $0.name })
|
refill(mediaPop, q?.mediaSizes.isEmpty == false ? q!.mediaSizes : Geometry.papers.map { $0.name })
|
||||||
refill(typePop, q?.mediaTypes ?? ["Plain"])
|
let mediaChoices = (q?.mediaTypeChoices.isEmpty == false) ? q!.mediaTypeChoices : [PPDChoice(name: "Plain", title: "Plain")]
|
||||||
refill(trayPop, q?.trays ?? ["Auto"])
|
refillChoices(typePop, mediaChoices)
|
||||||
|
let trayChoices = (q?.trayChoices.isEmpty == false) ? q!.trayChoices : [PPDChoice(name: "Auto", title: "Auto")]
|
||||||
|
refillChoices(trayPop, trayChoices)
|
||||||
|
let qualityChoices = (q?.qualityChoices.isEmpty == false) ? q!.qualityChoices : [PPDChoice(name: "Auto", title: "Auto")]
|
||||||
|
refillChoices(qualityPop, qualityChoices)
|
||||||
|
qualityPop.isEnabled = allowBasicDriverChanges && !jobLocked && (q?.qualityChoices.isEmpty == false)
|
||||||
refill(resolutionPop, q?.resolutions ?? [])
|
refill(resolutionPop, q?.resolutions ?? [])
|
||||||
updateAirPrint()
|
updateAirPrint()
|
||||||
}
|
}
|
||||||
@@ -206,6 +218,33 @@ final class InspectorView: NSView {
|
|||||||
if let current = current { pop.selectItem(withTitle: current) }
|
if let current = current { pop.selectItem(withTitle: current) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func refillChoices(_ pop: NSPopUpButton, _ choices: [PPDChoice]) {
|
||||||
|
let currentChoice = (pop.selectedItem?.representedObject as? String) ?? pop.titleOfSelectedItem
|
||||||
|
pop.removeAllItems()
|
||||||
|
for choice in choices {
|
||||||
|
pop.addItem(withTitle: choice.title)
|
||||||
|
pop.lastItem?.representedObject = choice.name
|
||||||
|
}
|
||||||
|
if let current = currentChoice,
|
||||||
|
choices.contains(where: { $0.name == current || $0.title == current }) {
|
||||||
|
selectChoice(pop, current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func selectChoice(_ pop: NSPopUpButton, _ value: String) {
|
||||||
|
if let item = pop.itemArray.first(where: { ($0.representedObject as? String) == value }) {
|
||||||
|
pop.select(item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let item = pop.itemArray.first(where: { $0.title == value }) {
|
||||||
|
pop.select(item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pop.addItem(withTitle: value)
|
||||||
|
pop.lastItem?.representedObject = value
|
||||||
|
pop.selectItem(withTitle: value)
|
||||||
|
}
|
||||||
|
|
||||||
private func select(_ pop: NSPopUpButton, _ title: String) {
|
private func select(_ pop: NSPopUpButton, _ title: String) {
|
||||||
pop.selectItem(withTitle: title)
|
pop.selectItem(withTitle: title)
|
||||||
if pop.indexOfSelectedItem < 0 {
|
if pop.indexOfSelectedItem < 0 {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
6CC9ACBD3E08666338305F82 /* TargetJobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 240466A1B8F60F23F51D0A75 /* TargetJobTests.swift */; };
|
6CC9ACBD3E08666338305F82 /* TargetJobTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 240466A1B8F60F23F51D0A75 /* TargetJobTests.swift */; };
|
||||||
37A78C4C83D7DB7ED2278F1E /* GeometryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79A6DBFC8F9430081786B20C /* GeometryTests.swift */; };
|
37A78C4C83D7DB7ED2278F1E /* GeometryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79A6DBFC8F9430081786B20C /* GeometryTests.swift */; };
|
||||||
C723596BBFFD3559C97683FA /* AirPrintTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4F405305D559131407E062C /* AirPrintTests.swift */; };
|
C723596BBFFD3559C97683FA /* AirPrintTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4F405305D559131407E062C /* AirPrintTests.swift */; };
|
||||||
|
F812A34B2CA7679A4143984A /* PPDOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E812A34F2CA7679A4143984B /* PPDOptionsTests.swift */; };
|
||||||
627BBC9C62B0C159B656E415 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9CCD11486CB5CFFBAC839C56 /* Assets.xcassets */; };
|
627BBC9C62B0C159B656E415 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9CCD11486CB5CFFBAC839C56 /* Assets.xcassets */; };
|
||||||
177F2AAF9AFD18C5EAF84162 /* ICCery-logo.svg in Resources */ = {isa = PBXBuildFile; fileRef = FEF2A34F2CA7679A4143984A /* ICCery-logo.svg */; };
|
177F2AAF9AFD18C5EAF84162 /* ICCery-logo.svg in Resources */ = {isa = PBXBuildFile; fileRef = FEF2A34F2CA7679A4143984A /* ICCery-logo.svg */; };
|
||||||
FC7AF16B3A0F19874BD56B43 /* app-icon.svg in Resources */ = {isa = PBXBuildFile; fileRef = 0E068D0260049363CDFEBCFD /* app-icon.svg */; };
|
FC7AF16B3A0F19874BD56B43 /* app-icon.svg in Resources */ = {isa = PBXBuildFile; fileRef = 0E068D0260049363CDFEBCFD /* app-icon.svg */; };
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
240466A1B8F60F23F51D0A75 /* TargetJobTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetJobTests.swift; sourceTree = "<group>"; };
|
240466A1B8F60F23F51D0A75 /* TargetJobTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetJobTests.swift; sourceTree = "<group>"; };
|
||||||
79A6DBFC8F9430081786B20C /* GeometryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometryTests.swift; sourceTree = "<group>"; };
|
79A6DBFC8F9430081786B20C /* GeometryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometryTests.swift; sourceTree = "<group>"; };
|
||||||
A4F405305D559131407E062C /* AirPrintTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirPrintTests.swift; sourceTree = "<group>"; };
|
A4F405305D559131407E062C /* AirPrintTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirPrintTests.swift; sourceTree = "<group>"; };
|
||||||
|
E812A34F2CA7679A4143984B /* PPDOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPDOptionsTests.swift; sourceTree = "<group>"; };
|
||||||
1507F021100E808DBE2FAA00 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
1507F021100E808DBE2FAA00 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
67629214EEC199752DF68F2F /* TargetPrint.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TargetPrint.entitlements; sourceTree = "<group>"; };
|
67629214EEC199752DF68F2F /* TargetPrint.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TargetPrint.entitlements; sourceTree = "<group>"; };
|
||||||
096EF18F1A2CF6F3694EDF95 /* Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; fileEncoding = 4; path = Bridging-Header.h; sourceTree = "<group>"; };
|
096EF18F1A2CF6F3694EDF95 /* Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; fileEncoding = 4; path = Bridging-Header.h; sourceTree = "<group>"; };
|
||||||
@@ -159,6 +161,7 @@
|
|||||||
240466A1B8F60F23F51D0A75 /* TargetJobTests.swift */,
|
240466A1B8F60F23F51D0A75 /* TargetJobTests.swift */,
|
||||||
79A6DBFC8F9430081786B20C /* GeometryTests.swift */,
|
79A6DBFC8F9430081786B20C /* GeometryTests.swift */,
|
||||||
A4F405305D559131407E062C /* AirPrintTests.swift */,
|
A4F405305D559131407E062C /* AirPrintTests.swift */,
|
||||||
|
E812A34F2CA7679A4143984B /* PPDOptionsTests.swift */,
|
||||||
);
|
);
|
||||||
path = Tests;
|
path = Tests;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -325,6 +328,7 @@
|
|||||||
6CC9ACBD3E08666338305F82 /* TargetJobTests.swift in Sources */,
|
6CC9ACBD3E08666338305F82 /* TargetJobTests.swift in Sources */,
|
||||||
37A78C4C83D7DB7ED2278F1E /* GeometryTests.swift in Sources */,
|
37A78C4C83D7DB7ED2278F1E /* GeometryTests.swift in Sources */,
|
||||||
C723596BBFFD3559C97683FA /* AirPrintTests.swift in Sources */,
|
C723596BBFFD3559C97683FA /* AirPrintTests.swift in Sources */,
|
||||||
|
F812A34B2CA7679A4143984A /* PPDOptionsTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,325 @@
|
|||||||
|
import Foundation
|
||||||
|
#if canImport(XCTest)
|
||||||
|
import XCTest
|
||||||
|
#endif
|
||||||
|
@testable import TargetPrint
|
||||||
|
|
||||||
|
#if canImport(XCTest)
|
||||||
|
final class PPDOptionsTests: XCTestCase {
|
||||||
|
|
||||||
|
func testDecodePPDString() {
|
||||||
|
XCTAssertEqual(CUPSManager.decodePPDString("plain"), "plain")
|
||||||
|
XCTAssertEqual(CUPSManager.decodePPDString("CD<2F>DVD"), "CD/DVD")
|
||||||
|
XCTAssertEqual(CUPSManager.decodePPDString("Photo<20>Paper<2E> Glossy"), "Photo Paper. Glossy")
|
||||||
|
XCTAssertEqual(CUPSManager.decodePPDString("Colon<3A>Test"), "Colon:Test")
|
||||||
|
XCTAssertEqual(CUPSManager.decodePPDString("Unclosed<2F"), "Unclosed<2F")
|
||||||
|
XCTAssertEqual(CUPSManager.decodePPDString("Invalid<ZZ>"), "Invalid<ZZ>")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStandardPPDMediaTypeAndTray() {
|
||||||
|
let ppd = """
|
||||||
|
*OpenUI *PageSize/Media Size: PickOne
|
||||||
|
*PageSize A4/A4: ""
|
||||||
|
*PageSize Letter/US Letter: ""
|
||||||
|
*CloseUI: *PageSize
|
||||||
|
*OpenUI *MediaType/Media Type: PickOne
|
||||||
|
*MediaType Plain/Plain Paper: ""
|
||||||
|
*MediaType Glossy/Photo Glossy: ""
|
||||||
|
*CloseUI: *MediaType
|
||||||
|
*OpenUI *InputSlot/Paper Source: PickOne
|
||||||
|
*InputSlot Auto/Automatic Selection: ""
|
||||||
|
*InputSlot Upper/Upper Cassette: ""
|
||||||
|
*CloseUI: *InputSlot
|
||||||
|
"""
|
||||||
|
let options = CUPSManager.discoverPPDOptions(ppd)
|
||||||
|
XCTAssertEqual(options.pageSizes, ["A4", "Letter"])
|
||||||
|
XCTAssertEqual(options.mediaTypeKeyword, "MediaType")
|
||||||
|
XCTAssertEqual(options.mediaTypeChoices, [
|
||||||
|
PPDChoice(name: "Plain", title: "Plain Paper"),
|
||||||
|
PPDChoice(name: "Glossy", title: "Photo Glossy")
|
||||||
|
])
|
||||||
|
XCTAssertEqual(options.mediaTypes, ["Plain Paper", "Photo Glossy"])
|
||||||
|
|
||||||
|
XCTAssertEqual(options.trayKeyword, "InputSlot")
|
||||||
|
XCTAssertEqual(options.trayChoices, [
|
||||||
|
PPDChoice(name: "Auto", title: "Automatic Selection"),
|
||||||
|
PPDChoice(name: "Upper", title: "Upper Cassette")
|
||||||
|
])
|
||||||
|
XCTAssertEqual(options.trays, ["Automatic Selection", "Upper Cassette"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEpsonPPDMediaTypeAndTray() {
|
||||||
|
let ppd = """
|
||||||
|
*OpenUI *MediaType/Media Type: PickOne
|
||||||
|
*MediaType 0/plain papers: ""
|
||||||
|
*MediaType 13/Epson Premium Glossy: ""
|
||||||
|
*MediaType 26/CD<2F>DVD: ""
|
||||||
|
*CloseUI: *MediaType
|
||||||
|
*OpenUI *EPIJ_FdSo/Paper Source: PickOne
|
||||||
|
*EPIJ_FdSo 2/Cassette 1: ""
|
||||||
|
*EPIJ_FdSo 3/Cassette 2: ""
|
||||||
|
*EPIJ_FdSo 12/Rear Paper Feed Slot: ""
|
||||||
|
*CloseUI: *EPIJ_FdSo
|
||||||
|
"""
|
||||||
|
let options = CUPSManager.discoverPPDOptions(ppd)
|
||||||
|
XCTAssertEqual(options.mediaTypeKeyword, "MediaType")
|
||||||
|
XCTAssertEqual(options.mediaTypeChoices, [
|
||||||
|
PPDChoice(name: "0", title: "plain papers"),
|
||||||
|
PPDChoice(name: "13", title: "Epson Premium Glossy"),
|
||||||
|
PPDChoice(name: "26", title: "CD/DVD")
|
||||||
|
])
|
||||||
|
XCTAssertEqual(options.mediaTypes, ["plain papers", "Epson Premium Glossy", "CD/DVD"])
|
||||||
|
|
||||||
|
XCTAssertEqual(options.trayKeyword, "EPIJ_FdSo")
|
||||||
|
XCTAssertEqual(options.trayChoices, [
|
||||||
|
PPDChoice(name: "2", title: "Cassette 1"),
|
||||||
|
PPDChoice(name: "3", title: "Cassette 2"),
|
||||||
|
PPDChoice(name: "12", title: "Rear Paper Feed Slot")
|
||||||
|
])
|
||||||
|
XCTAssertEqual(options.trays, ["Cassette 1", "Cassette 2", "Rear Paper Feed Slot"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCanonPPDMediaTypeAndTray() {
|
||||||
|
let ppd = """
|
||||||
|
*OpenUI *CNIJMediaType/Media Type: PickOne
|
||||||
|
*CNIJMediaType 0/Plain Paper: ""
|
||||||
|
*CNIJMediaType 92/Photo Paper Plus Glossy II: ""
|
||||||
|
*CloseUI: *CNIJMediaType
|
||||||
|
*OpenUI *CNIJMediaSupply/Paper Source: PickOne
|
||||||
|
*CNIJMediaSupply 7/Rear Tray: ""
|
||||||
|
*CNIJMediaSupply 33/Manual Feed: ""
|
||||||
|
*CloseUI: *CNIJMediaSupply
|
||||||
|
"""
|
||||||
|
let options = CUPSManager.discoverPPDOptions(ppd)
|
||||||
|
XCTAssertEqual(options.mediaTypeKeyword, "CNIJMediaType")
|
||||||
|
XCTAssertEqual(options.mediaTypeChoices, [
|
||||||
|
PPDChoice(name: "0", title: "Plain Paper"),
|
||||||
|
PPDChoice(name: "92", title: "Photo Paper Plus Glossy II")
|
||||||
|
])
|
||||||
|
XCTAssertEqual(options.mediaTypes, ["Plain Paper", "Photo Paper Plus Glossy II"])
|
||||||
|
|
||||||
|
XCTAssertEqual(options.trayKeyword, "CNIJMediaSupply")
|
||||||
|
XCTAssertEqual(options.trayChoices, [
|
||||||
|
PPDChoice(name: "7", title: "Rear Tray"),
|
||||||
|
PPDChoice(name: "33", title: "Manual Feed")
|
||||||
|
])
|
||||||
|
XCTAssertEqual(options.trays, ["Rear Tray", "Manual Feed"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEmptyPPDYieldsNilKeywordsAndEmptyChoices() {
|
||||||
|
let options = CUPSManager.discoverPPDOptions("")
|
||||||
|
XCTAssertNil(options.mediaTypeKeyword)
|
||||||
|
XCTAssertTrue(options.mediaTypeChoices.isEmpty)
|
||||||
|
XCTAssertTrue(options.mediaTypes.isEmpty)
|
||||||
|
XCTAssertNil(options.trayKeyword)
|
||||||
|
XCTAssertTrue(options.trayChoices.isEmpty)
|
||||||
|
XCTAssertTrue(options.trays.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testQuotesStrippedFromTranslation() {
|
||||||
|
let ppd = """
|
||||||
|
*OpenUI *MediaType/Media Type: PickOne
|
||||||
|
*MediaType Custom/"My Custom Paper": ""
|
||||||
|
*CloseUI: *MediaType
|
||||||
|
"""
|
||||||
|
let options = CUPSManager.discoverPPDOptions(ppd)
|
||||||
|
XCTAssertEqual(options.mediaTypeChoices, [
|
||||||
|
PPDChoice(name: "Custom", title: "My Custom Paper")
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testChoiceWithoutTranslationDefaultsTitleToName() {
|
||||||
|
let ppd = """
|
||||||
|
*MediaType Plain: ""
|
||||||
|
"""
|
||||||
|
let choices = CUPSManager.optionDetails(in: ppd, keyword: "MediaType")
|
||||||
|
XCTAssertEqual(choices, [
|
||||||
|
PPDChoice(name: "Plain", title: "Plain")
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPrintEngineDynamicPPDKeysAndResolution() {
|
||||||
|
let epsonPPD = """
|
||||||
|
*OpenUI *MediaType/Media Type: PickOne
|
||||||
|
*MediaType 0/plain papers: ""
|
||||||
|
*MediaType 13/Epson Premium Glossy: ""
|
||||||
|
*CloseUI: *MediaType
|
||||||
|
*OpenUI *EPIJ_FdSo/Paper Source: PickOne
|
||||||
|
*EPIJ_FdSo 2/Cassette 1: ""
|
||||||
|
*EPIJ_FdSo 12/Rear Paper Feed Slot: ""
|
||||||
|
*CloseUI: *EPIJ_FdSo
|
||||||
|
"""
|
||||||
|
let options = CUPSManager.discoverPPDOptions(epsonPPD)
|
||||||
|
let queue = PrinterQueue(
|
||||||
|
name: "TestEpson",
|
||||||
|
displayName: "Test Epson",
|
||||||
|
uri: "usb://epson",
|
||||||
|
make: "Epson",
|
||||||
|
model: "XP-55",
|
||||||
|
isAirPrint: false,
|
||||||
|
mediaSizes: ["A4"],
|
||||||
|
mediaTypes: options.mediaTypes,
|
||||||
|
mediaTypeChoices: options.mediaTypeChoices,
|
||||||
|
mediaTypeKeyword: options.mediaTypeKeyword,
|
||||||
|
trays: options.trays,
|
||||||
|
trayChoices: options.trayChoices,
|
||||||
|
trayKeyword: options.trayKeyword,
|
||||||
|
resolutions: [],
|
||||||
|
ppdText: epsonPPD
|
||||||
|
)
|
||||||
|
|
||||||
|
let engine = PrintEngine()
|
||||||
|
engine.mediaType = "Epson Premium Glossy" // passed by human display title
|
||||||
|
engine.paperSource = "Cassette 1" // passed by human display title
|
||||||
|
|
||||||
|
let printInfo = NSPrintInfo()
|
||||||
|
engine.applyOptionalPPDKeys(to: printInfo, queue: queue)
|
||||||
|
|
||||||
|
let dict = printInfo.dictionary()
|
||||||
|
let settingsKey = NSPrintInfo.AttributeKey(rawValue: "com.apple.print.printSettings")
|
||||||
|
let settings = dict[settingsKey] as? NSDictionary
|
||||||
|
|
||||||
|
XCTAssertEqual(settings?["MediaType"] as? String, "13")
|
||||||
|
XCTAssertEqual(settings?["EPIJ_FdSo"] as? String, "2")
|
||||||
|
|
||||||
|
// Canon test
|
||||||
|
let canonPPD = """
|
||||||
|
*OpenUI *CNIJMediaType/Media Type: PickOne
|
||||||
|
*CNIJMediaType 92/Photo Paper Plus Glossy II: ""
|
||||||
|
*CloseUI: *CNIJMediaType
|
||||||
|
*OpenUI *CNIJMediaSupply/Paper Source: PickOne
|
||||||
|
*CNIJMediaSupply 7/Rear Tray: ""
|
||||||
|
*CloseUI: *CNIJMediaSupply
|
||||||
|
"""
|
||||||
|
let canonOpts = CUPSManager.discoverPPDOptions(canonPPD)
|
||||||
|
let canonQueue = PrinterQueue(
|
||||||
|
name: "TestCanon",
|
||||||
|
displayName: "Test Canon",
|
||||||
|
uri: "usb://canon",
|
||||||
|
make: "Canon",
|
||||||
|
model: "Pro9500",
|
||||||
|
isAirPrint: false,
|
||||||
|
mediaSizes: ["A4"],
|
||||||
|
mediaTypes: canonOpts.mediaTypes,
|
||||||
|
mediaTypeChoices: canonOpts.mediaTypeChoices,
|
||||||
|
mediaTypeKeyword: canonOpts.mediaTypeKeyword,
|
||||||
|
trays: canonOpts.trays,
|
||||||
|
trayChoices: canonOpts.trayChoices,
|
||||||
|
trayKeyword: canonOpts.trayKeyword,
|
||||||
|
resolutions: [],
|
||||||
|
ppdText: canonPPD
|
||||||
|
)
|
||||||
|
|
||||||
|
let canonEngine = PrintEngine()
|
||||||
|
canonEngine.mediaType = "Photo Paper Plus Glossy II"
|
||||||
|
canonEngine.paperSource = "Rear Tray"
|
||||||
|
|
||||||
|
let canonPrintInfo = NSPrintInfo()
|
||||||
|
canonEngine.applyOptionalPPDKeys(to: canonPrintInfo, queue: canonQueue)
|
||||||
|
|
||||||
|
let canonSettings = canonPrintInfo.dictionary()[settingsKey] as? NSDictionary
|
||||||
|
XCTAssertEqual(canonSettings?["CNIJMediaType"] as? String, "92")
|
||||||
|
XCTAssertEqual(canonSettings?["CNIJMediaSupply"] as? String, "7")
|
||||||
|
|
||||||
|
// Canon 5-notch Custom quality tests
|
||||||
|
let canonQualityPPD = """
|
||||||
|
*OpenUI *CNIJPrintQuality/Print Quality: PickOne
|
||||||
|
*CNIJPrintQuality 0/Super Fine: ""
|
||||||
|
*CNIJPrintQuality 5/Fine: ""
|
||||||
|
*CNIJPrintQuality 10/Normal(Fine): ""
|
||||||
|
*CNIJPrintQuality 15/Normal(Fast): ""
|
||||||
|
*CNIJPrintQuality 20/Fast: ""
|
||||||
|
*CloseUI: *CNIJPrintQuality
|
||||||
|
"""
|
||||||
|
let canonQOpts = CUPSManager.discoverPPDOptions(canonQualityPPD)
|
||||||
|
XCTAssertEqual(canonQOpts.qualityKeyword, "CNIJPrintQuality")
|
||||||
|
XCTAssertEqual(canonQOpts.qualityChoices, [
|
||||||
|
PPDChoice(name: "0", title: "Super Fine"),
|
||||||
|
PPDChoice(name: "5", title: "Fine"),
|
||||||
|
PPDChoice(name: "10", title: "Normal(Fine)"),
|
||||||
|
PPDChoice(name: "15", title: "Normal(Fast)"),
|
||||||
|
PPDChoice(name: "20", title: "Fast")
|
||||||
|
])
|
||||||
|
|
||||||
|
let canonQQueue = PrinterQueue(
|
||||||
|
name: "TestCanonQ",
|
||||||
|
displayName: "Test Canon Quality",
|
||||||
|
uri: "usb://canon",
|
||||||
|
make: "Canon",
|
||||||
|
model: "Pro9500",
|
||||||
|
isAirPrint: false,
|
||||||
|
mediaSizes: ["A4"],
|
||||||
|
mediaTypes: [],
|
||||||
|
mediaTypeChoices: [],
|
||||||
|
mediaTypeKeyword: nil,
|
||||||
|
trays: [],
|
||||||
|
trayChoices: [],
|
||||||
|
trayKeyword: nil,
|
||||||
|
qualityChoices: canonQOpts.qualityChoices,
|
||||||
|
qualityKeyword: canonQOpts.qualityKeyword,
|
||||||
|
resolutions: [],
|
||||||
|
ppdText: canonQualityPPD
|
||||||
|
)
|
||||||
|
|
||||||
|
// Super Fine: custom mode 5, slider notch 5, quality 0
|
||||||
|
let superFineEngine = PrintEngine()
|
||||||
|
superFineEngine.printQuality = "Super Fine"
|
||||||
|
let superFineInfo = NSPrintInfo()
|
||||||
|
superFineEngine.applyOptionalPPDKeys(to: superFineInfo, queue: canonQQueue)
|
||||||
|
let superFineSettings = superFineInfo.dictionary()[settingsKey] as? NSDictionary
|
||||||
|
XCTAssertEqual(superFineSettings?["CNIJPrintQuality"] as? String, "0")
|
||||||
|
XCTAssertEqual(superFineSettings?["CNIJPrintMode2"] as? String, "5")
|
||||||
|
XCTAssertEqual(superFineSettings?["CNIJPQualitySlider"] as? String, "5")
|
||||||
|
|
||||||
|
// Fine: preset mode 1, slider notch 4, quality 5
|
||||||
|
let fineEngine = PrintEngine()
|
||||||
|
fineEngine.printQuality = "Fine"
|
||||||
|
let fineInfo = NSPrintInfo()
|
||||||
|
fineEngine.applyOptionalPPDKeys(to: fineInfo, queue: canonQQueue)
|
||||||
|
let fineSettings = fineInfo.dictionary()[settingsKey] as? NSDictionary
|
||||||
|
XCTAssertEqual(fineSettings?["CNIJPrintQuality"] as? String, "5")
|
||||||
|
XCTAssertEqual(fineSettings?["CNIJPrintMode2"] as? String, "1")
|
||||||
|
XCTAssertEqual(fineSettings?["CNIJPQualitySlider"] as? String, "4")
|
||||||
|
|
||||||
|
// Epson quality test
|
||||||
|
let epsonQualityPPD = """
|
||||||
|
*OpenUI *EPIJ_Qual/Quality: PickOne
|
||||||
|
*EPIJ_Qual 307/Best Quality: ""
|
||||||
|
*EPIJ_Qual 305/Quality: ""
|
||||||
|
*EPIJ_Qual 304/Fine: ""
|
||||||
|
*CloseUI: *EPIJ_Qual
|
||||||
|
"""
|
||||||
|
let epsonQOpts = CUPSManager.discoverPPDOptions(epsonQualityPPD)
|
||||||
|
XCTAssertEqual(epsonQOpts.qualityKeyword, "EPIJ_Qual")
|
||||||
|
XCTAssertEqual(epsonQOpts.qualityChoices.count, 3)
|
||||||
|
XCTAssertEqual(epsonQOpts.qualityChoices.first?.name, "307")
|
||||||
|
XCTAssertEqual(epsonQOpts.qualityChoices.first?.title, "Best Quality")
|
||||||
|
|
||||||
|
let epsonQQueue = PrinterQueue(
|
||||||
|
name: "TestEpsonQ",
|
||||||
|
displayName: "Test Epson Quality",
|
||||||
|
uri: "usb://epson",
|
||||||
|
make: "Epson",
|
||||||
|
model: "XP-55",
|
||||||
|
isAirPrint: false,
|
||||||
|
mediaSizes: ["A4"],
|
||||||
|
mediaTypes: [],
|
||||||
|
mediaTypeChoices: [],
|
||||||
|
mediaTypeKeyword: nil,
|
||||||
|
trays: [],
|
||||||
|
trayChoices: [],
|
||||||
|
trayKeyword: nil,
|
||||||
|
qualityChoices: epsonQOpts.qualityChoices,
|
||||||
|
qualityKeyword: epsonQOpts.qualityKeyword,
|
||||||
|
resolutions: [],
|
||||||
|
ppdText: epsonQualityPPD
|
||||||
|
)
|
||||||
|
let epsonQEngine = PrintEngine()
|
||||||
|
epsonQEngine.printQuality = "Best Quality"
|
||||||
|
let epsonQInfo = NSPrintInfo()
|
||||||
|
epsonQEngine.applyOptionalPPDKeys(to: epsonQInfo, queue: epsonQQueue)
|
||||||
|
let epsonQSettings = epsonQInfo.dictionary()[settingsKey] as? NSDictionary
|
||||||
|
XCTAssertEqual(epsonQSettings?["EPIJ_Qual"] as? String, "307")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
import Foundation
|
||||||
|
import AppKit
|
||||||
|
|
||||||
|
// Standalone CLI test runner for environments without full Xcode / XCTest.framework (e.g. Command Line Tools).
|
||||||
|
|
||||||
|
func expect(_ condition: @autoclosure () -> Bool, _ message: String = "", file: StaticString = #file, line: UInt = #line) {
|
||||||
|
if !condition() {
|
||||||
|
fputs("❌ FAIL: \(file):\(line): condition was false. \(message)\n", stderr)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectEqual<T: Equatable>(_ actual: T, _ expected: T, _ message: String = "", file: StaticString = #file, line: UInt = #line) {
|
||||||
|
if actual != expected {
|
||||||
|
fputs("❌ FAIL: \(file):\(line): expected '\(expected)', got '\(actual)'. \(message)\n", stderr)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func expectNil<T>(_ actual: T?, _ message: String = "", file: StaticString = #file, line: UInt = #line) {
|
||||||
|
if let actual = actual {
|
||||||
|
fputs("❌ FAIL: \(file):\(line): expected nil, got '\(actual)'. \(message)\n", stderr)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct TestRunner {
|
||||||
|
static func main() {
|
||||||
|
print("==> Running TargetPrint test suite...")
|
||||||
|
|
||||||
|
// MARK: - 1. PPD Options & Choice Tests (Issues #1 and #2)
|
||||||
|
print("--> PPDOptionsTests")
|
||||||
|
// Hex decoding
|
||||||
|
expectEqual(CUPSManager.decodePPDString("plain"), "plain")
|
||||||
|
expectEqual(CUPSManager.decodePPDString("CD<2F>DVD"), "CD/DVD")
|
||||||
|
expectEqual(CUPSManager.decodePPDString("Photo<20>Paper<2E> Glossy"), "Photo Paper. Glossy")
|
||||||
|
expectEqual(CUPSManager.decodePPDString("Colon<3A>Test"), "Colon:Test")
|
||||||
|
expectEqual(CUPSManager.decodePPDString("Unclosed<2F"), "Unclosed<2F")
|
||||||
|
expectEqual(CUPSManager.decodePPDString("Invalid<ZZ>"), "Invalid<ZZ>")
|
||||||
|
|
||||||
|
// Standard PPD
|
||||||
|
let stdPPD = """
|
||||||
|
*OpenUI *PageSize/Media Size: PickOne
|
||||||
|
*PageSize A4/A4: ""
|
||||||
|
*PageSize Letter/US Letter: ""
|
||||||
|
*CloseUI: *PageSize
|
||||||
|
*OpenUI *MediaType/Media Type: PickOne
|
||||||
|
*MediaType Plain/Plain Paper: ""
|
||||||
|
*MediaType Glossy/Photo Glossy: ""
|
||||||
|
*CloseUI: *MediaType
|
||||||
|
*OpenUI *InputSlot/Paper Source: PickOne
|
||||||
|
*InputSlot Auto/Automatic Selection: ""
|
||||||
|
*InputSlot Upper/Upper Cassette: ""
|
||||||
|
*CloseUI: *InputSlot
|
||||||
|
"""
|
||||||
|
let stdOpts = CUPSManager.discoverPPDOptions(stdPPD)
|
||||||
|
expectEqual(stdOpts.pageSizes, ["A4", "Letter"])
|
||||||
|
expectEqual(stdOpts.mediaTypeKeyword, "MediaType")
|
||||||
|
expectEqual(stdOpts.mediaTypeChoices, [
|
||||||
|
PPDChoice(name: "Plain", title: "Plain Paper"),
|
||||||
|
PPDChoice(name: "Glossy", title: "Photo Glossy")
|
||||||
|
])
|
||||||
|
expectEqual(stdOpts.mediaTypes, ["Plain Paper", "Photo Glossy"])
|
||||||
|
expectEqual(stdOpts.trayKeyword, "InputSlot")
|
||||||
|
expectEqual(stdOpts.trayChoices, [
|
||||||
|
PPDChoice(name: "Auto", title: "Automatic Selection"),
|
||||||
|
PPDChoice(name: "Upper", title: "Upper Cassette")
|
||||||
|
])
|
||||||
|
expectEqual(stdOpts.trays, ["Automatic Selection", "Upper Cassette"])
|
||||||
|
|
||||||
|
// Epson PPD
|
||||||
|
let epsonPPD = """
|
||||||
|
*OpenUI *MediaType/Media Type: PickOne
|
||||||
|
*MediaType 0/plain papers: ""
|
||||||
|
*MediaType 13/Epson Premium Glossy: ""
|
||||||
|
*MediaType 26/CD<2F>DVD: ""
|
||||||
|
*CloseUI: *MediaType
|
||||||
|
*OpenUI *EPIJ_FdSo/Paper Source: PickOne
|
||||||
|
*EPIJ_FdSo 2/Cassette 1: ""
|
||||||
|
*EPIJ_FdSo 3/Cassette 2: ""
|
||||||
|
*EPIJ_FdSo 12/Rear Paper Feed Slot: ""
|
||||||
|
*CloseUI: *EPIJ_FdSo
|
||||||
|
"""
|
||||||
|
let epsonOpts = CUPSManager.discoverPPDOptions(epsonPPD)
|
||||||
|
expectEqual(epsonOpts.mediaTypeKeyword, "MediaType")
|
||||||
|
expectEqual(epsonOpts.mediaTypeChoices, [
|
||||||
|
PPDChoice(name: "0", title: "plain papers"),
|
||||||
|
PPDChoice(name: "13", title: "Epson Premium Glossy"),
|
||||||
|
PPDChoice(name: "26", title: "CD/DVD")
|
||||||
|
])
|
||||||
|
expectEqual(epsonOpts.mediaTypes, ["plain papers", "Epson Premium Glossy", "CD/DVD"])
|
||||||
|
expectEqual(epsonOpts.trayKeyword, "EPIJ_FdSo")
|
||||||
|
expectEqual(epsonOpts.trayChoices, [
|
||||||
|
PPDChoice(name: "2", title: "Cassette 1"),
|
||||||
|
PPDChoice(name: "3", title: "Cassette 2"),
|
||||||
|
PPDChoice(name: "12", title: "Rear Paper Feed Slot")
|
||||||
|
])
|
||||||
|
expectEqual(epsonOpts.trays, ["Cassette 1", "Cassette 2", "Rear Paper Feed Slot"])
|
||||||
|
|
||||||
|
// Canon PPD
|
||||||
|
let canonPPD = """
|
||||||
|
*OpenUI *CNIJMediaType/Media Type: PickOne
|
||||||
|
*CNIJMediaType 0/Plain Paper: ""
|
||||||
|
*CNIJMediaType 92/Photo Paper Plus Glossy II: ""
|
||||||
|
*CloseUI: *CNIJMediaType
|
||||||
|
*OpenUI *CNIJMediaSupply/Paper Source: PickOne
|
||||||
|
*CNIJMediaSupply 7/Rear Tray: ""
|
||||||
|
*CNIJMediaSupply 33/Manual Feed: ""
|
||||||
|
*CloseUI: *CNIJMediaSupply
|
||||||
|
"""
|
||||||
|
let canonOpts = CUPSManager.discoverPPDOptions(canonPPD)
|
||||||
|
expectEqual(canonOpts.mediaTypeKeyword, "CNIJMediaType")
|
||||||
|
expectEqual(canonOpts.mediaTypeChoices, [
|
||||||
|
PPDChoice(name: "0", title: "Plain Paper"),
|
||||||
|
PPDChoice(name: "92", title: "Photo Paper Plus Glossy II")
|
||||||
|
])
|
||||||
|
expectEqual(canonOpts.mediaTypes, ["Plain Paper", "Photo Paper Plus Glossy II"])
|
||||||
|
expectEqual(canonOpts.trayKeyword, "CNIJMediaSupply")
|
||||||
|
expectEqual(canonOpts.trayChoices, [
|
||||||
|
PPDChoice(name: "7", title: "Rear Tray"),
|
||||||
|
PPDChoice(name: "33", title: "Manual Feed")
|
||||||
|
])
|
||||||
|
expectEqual(canonOpts.trays, ["Rear Tray", "Manual Feed"])
|
||||||
|
|
||||||
|
// Empty PPD
|
||||||
|
let emptyOpts = CUPSManager.discoverPPDOptions("")
|
||||||
|
expectNil(emptyOpts.mediaTypeKeyword)
|
||||||
|
expect(emptyOpts.mediaTypeChoices.isEmpty)
|
||||||
|
expect(emptyOpts.mediaTypes.isEmpty)
|
||||||
|
expectNil(emptyOpts.trayKeyword)
|
||||||
|
expect(emptyOpts.trayChoices.isEmpty)
|
||||||
|
expect(emptyOpts.trays.isEmpty)
|
||||||
|
|
||||||
|
// Strip quotes & fallback
|
||||||
|
let quotePPD = """
|
||||||
|
*OpenUI *MediaType/Media Type: PickOne
|
||||||
|
*MediaType Custom/"My Custom Paper": ""
|
||||||
|
*MediaType Fallback: ""
|
||||||
|
*CloseUI: *MediaType
|
||||||
|
"""
|
||||||
|
let quoteOpts = CUPSManager.discoverPPDOptions(quotePPD)
|
||||||
|
expectEqual(quoteOpts.mediaTypeChoices, [
|
||||||
|
PPDChoice(name: "Custom", title: "My Custom Paper"),
|
||||||
|
PPDChoice(name: "Fallback", title: "Fallback")
|
||||||
|
])
|
||||||
|
|
||||||
|
// PrintEngine dynamic resolution and vendor keys
|
||||||
|
let epsonQueue = PrinterQueue(
|
||||||
|
name: "TestEpson",
|
||||||
|
displayName: "Test Epson",
|
||||||
|
uri: "usb://epson",
|
||||||
|
make: "Epson",
|
||||||
|
model: "XP-55",
|
||||||
|
isAirPrint: false,
|
||||||
|
mediaSizes: ["A4"],
|
||||||
|
mediaTypes: epsonOpts.mediaTypes,
|
||||||
|
mediaTypeChoices: epsonOpts.mediaTypeChoices,
|
||||||
|
mediaTypeKeyword: epsonOpts.mediaTypeKeyword,
|
||||||
|
trays: epsonOpts.trays,
|
||||||
|
trayChoices: epsonOpts.trayChoices,
|
||||||
|
trayKeyword: epsonOpts.trayKeyword,
|
||||||
|
resolutions: [],
|
||||||
|
ppdText: epsonPPD
|
||||||
|
)
|
||||||
|
let engine = PrintEngine()
|
||||||
|
engine.mediaType = "Epson Premium Glossy"
|
||||||
|
engine.paperSource = "Cassette 1"
|
||||||
|
let printInfo = NSPrintInfo()
|
||||||
|
engine.applyOptionalPPDKeys(to: printInfo, queue: epsonQueue)
|
||||||
|
let settingsKey = NSPrintInfo.AttributeKey(rawValue: "com.apple.print.printSettings")
|
||||||
|
let settings = printInfo.dictionary()[settingsKey] as? NSDictionary
|
||||||
|
expectEqual(settings?["MediaType"] as? String, Optional("13"))
|
||||||
|
expectEqual(settings?["EPIJ_FdSo"] as? String, Optional("2"))
|
||||||
|
|
||||||
|
let canonQueue = PrinterQueue(
|
||||||
|
name: "TestCanon",
|
||||||
|
displayName: "Test Canon",
|
||||||
|
uri: "usb://canon",
|
||||||
|
make: "Canon",
|
||||||
|
model: "Pro9500",
|
||||||
|
isAirPrint: false,
|
||||||
|
mediaSizes: ["A4"],
|
||||||
|
mediaTypes: canonOpts.mediaTypes,
|
||||||
|
mediaTypeChoices: canonOpts.mediaTypeChoices,
|
||||||
|
mediaTypeKeyword: canonOpts.mediaTypeKeyword,
|
||||||
|
trays: canonOpts.trays,
|
||||||
|
trayChoices: canonOpts.trayChoices,
|
||||||
|
trayKeyword: canonOpts.trayKeyword,
|
||||||
|
resolutions: [],
|
||||||
|
ppdText: canonPPD
|
||||||
|
)
|
||||||
|
let canonEngine = PrintEngine()
|
||||||
|
canonEngine.mediaType = "Photo Paper Plus Glossy II"
|
||||||
|
canonEngine.paperSource = "Rear Tray"
|
||||||
|
let canonPrintInfo = NSPrintInfo()
|
||||||
|
canonEngine.applyOptionalPPDKeys(to: canonPrintInfo, queue: canonQueue)
|
||||||
|
let canonSettings = canonPrintInfo.dictionary()[settingsKey] as? NSDictionary
|
||||||
|
expectEqual(canonSettings?["CNIJMediaType"] as? String, Optional("92"))
|
||||||
|
expectEqual(canonSettings?["CNIJMediaSupply"] as? String, Optional("7"))
|
||||||
|
|
||||||
|
// Canon 5-notch Custom quality tests
|
||||||
|
let canonQualityPPD = """
|
||||||
|
*OpenUI *CNIJPrintQuality/Print Quality: PickOne
|
||||||
|
*CNIJPrintQuality 0/Super Fine: ""
|
||||||
|
*CNIJPrintQuality 5/Fine: ""
|
||||||
|
*CNIJPrintQuality 10/Normal(Fine): ""
|
||||||
|
*CNIJPrintQuality 15/Normal(Fast): ""
|
||||||
|
*CNIJPrintQuality 20/Fast: ""
|
||||||
|
*CloseUI: *CNIJPrintQuality
|
||||||
|
"""
|
||||||
|
let canonQOpts = CUPSManager.discoverPPDOptions(canonQualityPPD)
|
||||||
|
expectEqual(canonQOpts.qualityKeyword, "CNIJPrintQuality")
|
||||||
|
expectEqual(canonQOpts.qualityChoices, [
|
||||||
|
PPDChoice(name: "0", title: "Super Fine"),
|
||||||
|
PPDChoice(name: "5", title: "Fine"),
|
||||||
|
PPDChoice(name: "10", title: "Normal(Fine)"),
|
||||||
|
PPDChoice(name: "15", title: "Normal(Fast)"),
|
||||||
|
PPDChoice(name: "20", title: "Fast")
|
||||||
|
])
|
||||||
|
|
||||||
|
let canonQQueue = PrinterQueue(
|
||||||
|
name: "TestCanonQ",
|
||||||
|
displayName: "Test Canon Quality",
|
||||||
|
uri: "usb://canon",
|
||||||
|
make: "Canon",
|
||||||
|
model: "Pro9500",
|
||||||
|
isAirPrint: false,
|
||||||
|
mediaSizes: ["A4"],
|
||||||
|
mediaTypes: [],
|
||||||
|
mediaTypeChoices: [],
|
||||||
|
mediaTypeKeyword: nil,
|
||||||
|
trays: [],
|
||||||
|
trayChoices: [],
|
||||||
|
trayKeyword: nil,
|
||||||
|
qualityChoices: canonQOpts.qualityChoices,
|
||||||
|
qualityKeyword: canonQOpts.qualityKeyword,
|
||||||
|
resolutions: [],
|
||||||
|
ppdText: canonQualityPPD
|
||||||
|
)
|
||||||
|
|
||||||
|
// Super Fine: custom mode 5, slider notch 5, quality 0
|
||||||
|
let superFineEngine = PrintEngine()
|
||||||
|
superFineEngine.printQuality = "Super Fine"
|
||||||
|
let superFineInfo = NSPrintInfo()
|
||||||
|
superFineEngine.applyOptionalPPDKeys(to: superFineInfo, queue: canonQQueue)
|
||||||
|
let superFineSettings = superFineInfo.dictionary()[settingsKey] as? NSDictionary
|
||||||
|
expectEqual(superFineSettings?["CNIJPrintQuality"] as? String, Optional("0"))
|
||||||
|
expectEqual(superFineSettings?["CNIJPrintMode2"] as? String, Optional("5"))
|
||||||
|
expectEqual(superFineSettings?["CNIJPQualitySlider"] as? String, Optional("5"))
|
||||||
|
|
||||||
|
// Fine: preset mode 1, slider notch 4, quality 5
|
||||||
|
let fineEngine = PrintEngine()
|
||||||
|
fineEngine.printQuality = "Fine"
|
||||||
|
let fineInfo = NSPrintInfo()
|
||||||
|
fineEngine.applyOptionalPPDKeys(to: fineInfo, queue: canonQQueue)
|
||||||
|
let fineSettings = fineInfo.dictionary()[settingsKey] as? NSDictionary
|
||||||
|
expectEqual(fineSettings?["CNIJPrintQuality"] as? String, Optional("5"))
|
||||||
|
expectEqual(fineSettings?["CNIJPrintMode2"] as? String, Optional("1"))
|
||||||
|
expectEqual(fineSettings?["CNIJPQualitySlider"] as? String, Optional("4"))
|
||||||
|
|
||||||
|
// Epson quality test
|
||||||
|
let epsonQualityPPD = """
|
||||||
|
*OpenUI *EPIJ_Qual/Quality: PickOne
|
||||||
|
*EPIJ_Qual 307/Best Quality: ""
|
||||||
|
*EPIJ_Qual 305/Quality: ""
|
||||||
|
*EPIJ_Qual 304/Fine: ""
|
||||||
|
*CloseUI: *EPIJ_Qual
|
||||||
|
"""
|
||||||
|
let epsonQOpts = CUPSManager.discoverPPDOptions(epsonQualityPPD)
|
||||||
|
expectEqual(epsonQOpts.qualityKeyword, "EPIJ_Qual")
|
||||||
|
expectEqual(epsonQOpts.qualityChoices.count, 3)
|
||||||
|
expectEqual(epsonQOpts.qualityChoices.first?.name, "307")
|
||||||
|
expectEqual(epsonQOpts.qualityChoices.first?.title, "Best Quality")
|
||||||
|
|
||||||
|
let epsonQQueue = PrinterQueue(
|
||||||
|
name: "TestEpsonQ",
|
||||||
|
displayName: "Test Epson Quality",
|
||||||
|
uri: "usb://epson",
|
||||||
|
make: "Epson",
|
||||||
|
model: "XP-55",
|
||||||
|
isAirPrint: false,
|
||||||
|
mediaSizes: ["A4"],
|
||||||
|
mediaTypes: [],
|
||||||
|
mediaTypeChoices: [],
|
||||||
|
mediaTypeKeyword: nil,
|
||||||
|
trays: [],
|
||||||
|
trayChoices: [],
|
||||||
|
trayKeyword: nil,
|
||||||
|
qualityChoices: epsonQOpts.qualityChoices,
|
||||||
|
qualityKeyword: epsonQOpts.qualityKeyword,
|
||||||
|
resolutions: [],
|
||||||
|
ppdText: epsonQualityPPD
|
||||||
|
)
|
||||||
|
let epsonQEngine = PrintEngine()
|
||||||
|
epsonQEngine.printQuality = "Best Quality"
|
||||||
|
let epsonQInfo = NSPrintInfo()
|
||||||
|
epsonQEngine.applyOptionalPPDKeys(to: epsonQInfo, queue: epsonQQueue)
|
||||||
|
let epsonQSettings = epsonQInfo.dictionary()[settingsKey] as? NSDictionary
|
||||||
|
expectEqual(epsonQSettings?["EPIJ_Qual"] as? String, Optional("307"))
|
||||||
|
|
||||||
|
print(" [PASS] PPDOptionsTests")
|
||||||
|
|
||||||
|
// MARK: - 2. AirPrintTests
|
||||||
|
print("--> AirPrintTests")
|
||||||
|
expect(AirPrintDetector.isAirPrint(uri: "apple-airprint://Brother%20HL._ipp._tcp.local/", ppdText: "", make: "Brother", model: "HL-L3270CDW"))
|
||||||
|
expect(AirPrintDetector.isAirPrint(uri: "ipps://living-room.local/ipp/print", ppdText: "*Manufacturer: Apple\n*APAirPrint: True\n", make: "Apple", model: "AirPrint Printer"))
|
||||||
|
expect(AirPrintDetector.isAirPrint(uri: "dnssd://Foo._ipp._tcp.local/", ppdText: "", make: "Apple", model: "Living Room AirPrint"))
|
||||||
|
expect(!AirPrintDetector.isAirPrint(uri: "usb://EPSON/XP-55%20Series?serial=X5R001", ppdText: "*Manufacturer: Epson\n*EPSONColorControls: Off\n", make: "Epson", model: "XP-55"))
|
||||||
|
expect(!AirPrintDetector.isAirPrint(uri: "ipps://office-printer.local/ipp/print", ppdText: "*Manufacturer: HP\n*HPColorControl: Off\n", make: "HP", model: "OfficeJet Pro 9010"))
|
||||||
|
|
||||||
|
let epsonBypass = CUPSManager.vendorColorBypass(make: "Epson", model: "XP-55", ppdText: "")
|
||||||
|
expectEqual(epsonBypass["ColorModel"], "RGB")
|
||||||
|
expectEqual(epsonBypass["EPSONColorControls"], "Off")
|
||||||
|
|
||||||
|
let canonBypass = CUPSManager.vendorColorBypass(make: "Canon", model: "PRO-100", ppdText: "")
|
||||||
|
expectEqual(canonBypass["CNColorMatching"], "None")
|
||||||
|
|
||||||
|
let hpBypass = CUPSManager.vendorColorBypass(make: "HP", model: "9010", ppdText: "")
|
||||||
|
expectEqual(hpBypass["HPColorControl"], "Off")
|
||||||
|
|
||||||
|
print(" [PASS] AirPrintTests")
|
||||||
|
|
||||||
|
// MARK: - 3. GeometryTests
|
||||||
|
print("--> GeometryTests")
|
||||||
|
let w = Geometry.physicalInches(pixels: 2400, dpi: 300)
|
||||||
|
let h = Geometry.physicalInches(pixels: 3000, dpi: 300)
|
||||||
|
expectEqual(w, 8.0)
|
||||||
|
expectEqual(h, 10.0)
|
||||||
|
expect(Geometry.geometricAccuracyPass(expectedWidthIn: 8, expectedHeightIn: 10, actualWidthIn: w, actualHeightIn: h))
|
||||||
|
expectEqual(Geometry.physicalInches(pixels: 720, dpi: 0), 10.0)
|
||||||
|
expectEqual(Geometry.physicalInches(pixels: 720, dpi: -3), 10.0)
|
||||||
|
expectEqual(Geometry.physicalInches(pixels: 72, dpi: 72), 1.0)
|
||||||
|
let letter = Geometry.paper(named: "Letter")
|
||||||
|
expect(abs(letter.widthPt - 612.0) < 0.01)
|
||||||
|
expect(abs(letter.heightPt - 792.0) < 0.01)
|
||||||
|
print(" [PASS] GeometryTests")
|
||||||
|
|
||||||
|
// MARK: - 4. TargetJobTests
|
||||||
|
print("--> TargetJobTests")
|
||||||
|
let validJSON = """
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"jobTitle": "Epson_XP55_IlfordLustre_Target_P1-2",
|
||||||
|
"files": ["/tmp/a.tif", "/tmp/b.tif"],
|
||||||
|
"printSettings": {
|
||||||
|
"printerName": "EPSON_XP_55_Series",
|
||||||
|
"mediaSize": "A4",
|
||||||
|
"mediaType": "PremiumGlossy",
|
||||||
|
"paperSource": "Auto",
|
||||||
|
"resolution": "5760x1440dpi",
|
||||||
|
"printQuality": "Super Fine",
|
||||||
|
"scaling": 1.0,
|
||||||
|
"centered": true,
|
||||||
|
"forceUnmanagedColor": true
|
||||||
|
},
|
||||||
|
"uiPolicy": {
|
||||||
|
"lockColorManagement": true,
|
||||||
|
"allowBasicDriverChanges": true
|
||||||
|
},
|
||||||
|
"unknownKey": "ignored"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
do {
|
||||||
|
let parsed = try TargetJobParser.parse(text: validJSON)
|
||||||
|
expectEqual(parsed.version, 1)
|
||||||
|
expectEqual(parsed.jobTitle, "Epson_XP55_IlfordLustre_Target_P1-2")
|
||||||
|
expectEqual(parsed.files.count, 2)
|
||||||
|
expectEqual(parsed.printSettings.mediaType, "PremiumGlossy")
|
||||||
|
expectEqual(parsed.printSettings.printQuality, "Super Fine")
|
||||||
|
let serialized = try TargetJobParser.serialize(parsed)
|
||||||
|
let again = try TargetJobParser.parse(data: serialized)
|
||||||
|
expectEqual(parsed, again)
|
||||||
|
expectEqual(again.printSettings.printQuality, "Super Fine")
|
||||||
|
} catch {
|
||||||
|
fputs("TargetJob parsing error: \(error)\n", stderr)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
print(" [PASS] TargetJobTests")
|
||||||
|
|
||||||
|
print("==> All test suites passed successfully! 🎉")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user