luvix 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
luvix/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Luvix HDR image processing toolkit."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ try:
6
+ __version__ = version("luvix")
7
+ except PackageNotFoundError:
8
+ __version__ = "0+unknown"
luvix/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Run Luvix with ``python -m luvix``."""
2
+
3
+ from luvix.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
File without changes
File without changes
@@ -0,0 +1,43 @@
1
+ """Compile and execute the macOS ImageIO backend on demand."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import platform
7
+ import subprocess
8
+ from importlib.resources import files
9
+ from pathlib import Path
10
+
11
+
12
+ class BackendError(RuntimeError):
13
+ """Native backend failed."""
14
+
15
+
16
+ def backend_binary() -> Path:
17
+ if platform.system() != "Darwin":
18
+ raise BackendError("Apple HDR backend requires macOS; no portable backend yet")
19
+ source = Path(str(files("luvix.backends.apple").joinpath("imageio.swift")))
20
+ digest = hashlib.sha256(source.read_bytes()).hexdigest()[:16]
21
+ target = Path.home() / ".cache" / "luvix" / f"imageio-{digest}"
22
+ if not target.exists():
23
+ target.parent.mkdir(parents=True, exist_ok=True)
24
+ result = subprocess.run(
25
+ ["swiftc", "-O", str(source), "-o", str(target)],
26
+ capture_output=True,
27
+ text=True,
28
+ check=False,
29
+ )
30
+ if result.returncode:
31
+ raise BackendError(f"Swift compilation failed:\n{result.stderr}")
32
+ return target
33
+
34
+
35
+ def run_backend(*args: str) -> str:
36
+ result = subprocess.run(
37
+ [str(backend_binary()), *map(str, args)], capture_output=True, text=True, check=False
38
+ )
39
+ if result.returncode:
40
+ raise BackendError(
41
+ result.stderr.strip() or result.stdout.strip() or "Native backend failed"
42
+ )
43
+ return result.stdout.strip()
@@ -0,0 +1,39 @@
1
+ """Build and run the isolated macOS HDR decode diagnostic."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import platform
7
+ import subprocess
8
+ from importlib.resources import files
9
+ from pathlib import Path
10
+
11
+ from luvix.backends.apple.bridge import BackendError
12
+
13
+
14
+ def run_hdr_probe(source: Path, samples: int) -> str:
15
+ """Return JSON from the native HDR-aware decode probe."""
16
+ if platform.system() != "Darwin":
17
+ raise BackendError("HDR-aware ImageIO decoding requires macOS")
18
+ swift = Path(str(files("luvix.backends.apple").joinpath("hdr_probe.swift")))
19
+ digest = hashlib.sha256(swift.read_bytes()).hexdigest()[:16]
20
+ binary = Path.home() / ".cache" / "luvix" / f"hdr-probe-{digest}"
21
+ if not binary.exists():
22
+ binary.parent.mkdir(parents=True, exist_ok=True)
23
+ result = subprocess.run(
24
+ ["swiftc", "-O", str(swift), "-o", str(binary)],
25
+ capture_output=True,
26
+ text=True,
27
+ check=False,
28
+ )
29
+ if result.returncode:
30
+ raise BackendError(f"Swift compilation failed:\n{result.stderr}")
31
+ result = subprocess.run(
32
+ [str(binary), "inspect-hdr", str(source), str(samples)],
33
+ capture_output=True,
34
+ text=True,
35
+ check=False,
36
+ )
37
+ if result.returncode:
38
+ raise BackendError(result.stderr.strip() or "HDR probe failed")
39
+ return result.stdout.strip()
@@ -0,0 +1,121 @@
1
+ import Foundation
2
+ import CoreGraphics
3
+ import CoreImage
4
+ import ImageIO
5
+
6
+ func fail(_ message: String) -> Never {
7
+ fputs("Error: \(message)\n", stderr)
8
+ exit(1)
9
+ }
10
+
11
+ let args = CommandLine.arguments
12
+ guard args.count == 4, let count = Int(args[3]), (1...64).contains(count) else {
13
+ fail("Usage: hdr_probe inspect-hdr SOURCE.HEIC SAMPLES(1..64)")
14
+ }
15
+ let url = URL(fileURLWithPath: args[2])
16
+ guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else {
17
+ fail("Cannot open source")
18
+ }
19
+ guard CGImageSourceCopyAuxiliaryDataInfoAtIndex(
20
+ source, 0, kCGImageAuxiliaryDataTypeHDRGainMap
21
+ ) != nil else { fail("No Apple HDR gain map found") }
22
+
23
+ guard let linearP3 = CGColorSpace(name: CGColorSpace.extendedLinearDisplayP3) else {
24
+ fail("Extended linear Display P3 unavailable")
25
+ }
26
+ let context = CIContext(options: [
27
+ .workingColorSpace: linearP3,
28
+ .outputColorSpace: linearP3,
29
+ .useSoftwareRenderer: false
30
+ ])
31
+
32
+ struct Result {
33
+ let report: [String: Any]
34
+ let sampleValues: [Float]
35
+ }
36
+
37
+ func measure(_ hdr: Bool) -> Result {
38
+ // ImageIO requires the decode *request* key, with HDR/SDR as its value.
39
+ // Using kCGImageSourceDecodeToHDR as the dictionary key silently ignored
40
+ // the request and produced identical results in both branches.
41
+ let decodeMode = hdr ? kCGImageSourceDecodeToHDR : kCGImageSourceDecodeToSDR
42
+ let options: [CFString: Any] = [kCGImageSourceDecodeRequest: decodeMode]
43
+ guard let decoded = CGImageSourceCreateImageAtIndex(source, 0, options as CFDictionary) else {
44
+ fail("ImageIO decode failed (HDR=\(hdr))")
45
+ }
46
+ let width = decoded.width
47
+ let height = decoded.height
48
+ let ci = CIImage(cgImage: decoded)
49
+ let rowBytes = width * 4 * MemoryLayout<Float>.size
50
+ var pixels = [Float](repeating: 0, count: width * height * 4)
51
+ pixels.withUnsafeMutableBytes { bytes in
52
+ guard let address = bytes.baseAddress else { fail("No bitmap storage") }
53
+ context.render(
54
+ ci,
55
+ toBitmap: address,
56
+ rowBytes: rowBytes,
57
+ bounds: CGRect(x: 0, y: 0, width: width, height: height),
58
+ format: .RGBAf,
59
+ colorSpace: linearP3
60
+ )
61
+ }
62
+ var minima = [Double](repeating: .infinity, count: 3)
63
+ var maxima = [Double](repeating: -.infinity, count: 3)
64
+ var aboveOne = 0
65
+ var sampled = 0
66
+ var invalid = 0
67
+ let stridePixels = max(1, width * height / 100_000)
68
+ for i in Swift.stride(from: 0, to: width * height, by: stridePixels) {
69
+ let offset = i * 4
70
+ let rgb = (0..<3).map { Double(pixels[offset + $0]) }
71
+ if !rgb.allSatisfy({ $0.isFinite }) { invalid += 1; continue }
72
+ for c in 0..<3 {
73
+ minima[c] = min(minima[c], rgb[c])
74
+ maxima[c] = max(maxima[c], rgb[c])
75
+ }
76
+ if rgb.contains(where: { $0 > 1 }) { aboveOne += 1 }
77
+ sampled += 1
78
+ }
79
+ var points: [[String: Any]] = []
80
+ var sampleValues: [Float] = []
81
+ for i in 0..<count {
82
+ let x = (2 * i + 1) * width / (2 * count)
83
+ let y = height / 2
84
+ let offset = (y * width + x) * 4
85
+ let rgb = (0..<3).map { pixels[offset + $0] }
86
+ sampleValues.append(contentsOf: rgb)
87
+ points.append(["x": x, "y_raw": y, "rgb_linear_p3": rgb.map(Double.init)])
88
+ }
89
+ let report: [String: Any] = [
90
+ "decoded_bits_per_component": decoded.bitsPerComponent,
91
+ "rendered_format": "RGBAf_float32",
92
+ "min_rgb": sampled > 0 ? minima : [0, 0, 0],
93
+ "max_rgb": sampled > 0 ? maxima : [0, 0, 0],
94
+ "pixels_above_one": aboveOne,
95
+ "sampled_pixels": sampled,
96
+ "invalid_pixels": invalid,
97
+ "samples": points
98
+ ]
99
+ return Result(report: report, sampleValues: sampleValues)
100
+ }
101
+
102
+ let sdr = measure(false)
103
+ let hdr = measure(true)
104
+ let maxSampleDifference = zip(sdr.sampleValues, hdr.sampleValues)
105
+ .map { abs(Double($0) - Double($1)) }.max() ?? 0
106
+ let output: [String: Any] = [
107
+ "source": url.lastPathComponent,
108
+ "working_space": "extendedLinearDisplayP3",
109
+ "decode_option": "kCGImageSourceDecodeRequest",
110
+ "sdr": sdr.report,
111
+ "hdr": hdr.report,
112
+ "max_sample_difference": maxSampleDifference,
113
+ "hdr_decode_distinct_at_sample_points": maxSampleDifference > 1e-6,
114
+ "note": "Explicit SDR/HDR ImageIO decode requests; compare measured values before accepting reconstruction."
115
+ ]
116
+ guard JSONSerialization.isValidJSONObject(output),
117
+ let data = try? JSONSerialization.data(withJSONObject: output, options: [.sortedKeys]),
118
+ let json = String(data: data, encoding: .utf8) else {
119
+ fail("JSON serialization failed")
120
+ }
121
+ print(json)
@@ -0,0 +1,37 @@
1
+ """Compile and invoke Apple's full-resolution float HDR raster renderer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import platform
7
+ import subprocess
8
+ from importlib.resources import files
9
+ from pathlib import Path
10
+
11
+ from luvix.backends.apple.bridge import BackendError
12
+
13
+
14
+ def render_hdr_raster(source: Path, prefix: Path) -> None:
15
+ if platform.system() != "Darwin":
16
+ raise BackendError("Apple HDR raster export requires macOS")
17
+ swift = Path(str(files("luvix.backends.apple").joinpath("hdr_raster.swift")))
18
+ digest = hashlib.sha256(swift.read_bytes()).hexdigest()[:16]
19
+ binary = Path.home() / ".cache" / "luvix" / f"hdr-raster-{digest}"
20
+ if not binary.exists():
21
+ binary.parent.mkdir(parents=True, exist_ok=True)
22
+ compiled = subprocess.run(
23
+ ["swiftc", "-O", str(swift), "-o", str(binary)],
24
+ capture_output=True,
25
+ text=True,
26
+ check=False,
27
+ )
28
+ if compiled.returncode:
29
+ raise BackendError(f"Swift compilation failed:\n{compiled.stderr}")
30
+ result = subprocess.run(
31
+ [str(binary), str(source), str(prefix)],
32
+ capture_output=True,
33
+ text=True,
34
+ check=False,
35
+ )
36
+ if result.returncode:
37
+ raise BackendError(result.stderr.strip() or "HDR raster export failed")
@@ -0,0 +1,99 @@
1
+ import Foundation
2
+ import CoreGraphics
3
+ import CoreImage
4
+ import ImageIO
5
+
6
+ func fail(_ message: String) -> Never {
7
+ fputs("Error: \(message)\n", stderr)
8
+ exit(1)
9
+ }
10
+
11
+ // Write raw interleaved Float32 RGBA in native little-endian order and a JSON sidecar.
12
+ // Raw pixel orientation is retained. Source HEIC is never modified.
13
+ let args = CommandLine.arguments
14
+ guard args.count == 3 else { fail("Usage: hdr_raster SOURCE.HEIC OUTPUT_PREFIX") }
15
+ let sourceURL = URL(fileURLWithPath: args[1])
16
+ let prefix = URL(fileURLWithPath: args[2])
17
+ guard !FileManager.default.fileExists(atPath: prefix.path + ".rgba32f"),
18
+ !FileManager.default.fileExists(atPath: prefix.path + ".json") else {
19
+ fail("Refusing to overwrite existing intermediate")
20
+ }
21
+ guard let source = CGImageSourceCreateWithURL(sourceURL as CFURL, nil) else {
22
+ fail("Cannot open source")
23
+ }
24
+ guard CGImageSourceCopyAuxiliaryDataInfoAtIndex(
25
+ source, 0, kCGImageAuxiliaryDataTypeHDRGainMap
26
+ ) != nil else { fail("No Apple HDR gain map found") }
27
+ let options: [CFString: Any] = [kCGImageSourceDecodeRequest: kCGImageSourceDecodeToHDR]
28
+ guard let decoded = CGImageSourceCreateImageAtIndex(source, 0, options as CFDictionary) else {
29
+ fail("HDR decode failed")
30
+ }
31
+ // Only use a linear working space with the same primaries as the input.
32
+ // Reject unsupported profiles rather than silently converting to Display P3.
33
+ let baseOptions: [CFString: Any] = [kCGImageSourceDecodeRequest: kCGImageSourceDecodeToSDR]
34
+ guard let base = CGImageSourceCreateImageAtIndex(source, 0, baseOptions as CFDictionary),
35
+ let sourceSpace = base.colorSpace else { fail("Cannot determine source color space") }
36
+ let sourceName = (sourceSpace.name as String?) ?? ""
37
+ let linearName: CFString
38
+ let sourcePrimaries: String
39
+ if sourceName == (CGColorSpace.displayP3 as String) {
40
+ linearName = CGColorSpace.extendedLinearDisplayP3
41
+ sourcePrimaries = "Display P3"
42
+ } else if sourceName == (CGColorSpace.sRGB as String) {
43
+ linearName = CGColorSpace.extendedLinearSRGB
44
+ sourcePrimaries = "sRGB"
45
+ } else {
46
+ fail("Unsupported source color space: \(sourceName). No implicit gamut conversion permitted")
47
+ }
48
+ guard let linearP3 = CGColorSpace(name: linearName) else {
49
+ fail("Requested linear working color space unavailable")
50
+ }
51
+ let width = decoded.width
52
+ let height = decoded.height
53
+ let rowBytes = width * 4 * MemoryLayout<Float>.size
54
+ let context = CIContext(options: [
55
+ .workingColorSpace: linearP3,
56
+ .outputColorSpace: linearP3
57
+ ])
58
+ let image = CIImage(cgImage: decoded)
59
+ var pixels = [Float](repeating: 0, count: width * height * 4)
60
+ pixels.withUnsafeMutableBytes { bytes in
61
+ guard let address = bytes.baseAddress else { fail("Allocation failed") }
62
+ context.render(
63
+ image, toBitmap: address, rowBytes: rowBytes,
64
+ bounds: CGRect(x: 0, y: 0, width: width, height: height),
65
+ format: .RGBAf, colorSpace: linearP3
66
+ )
67
+ }
68
+ let finite = pixels.allSatisfy { $0.isFinite }
69
+ guard finite else { fail("Non-finite HDR pixel detected") }
70
+ let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil)
71
+ as? [String: Any]
72
+
73
+ let orientation = properties?[kCGImagePropertyOrientation as String]
74
+ as? Int ?? 1
75
+ let rawURL = URL(fileURLWithPath: prefix.path + ".rgba32f")
76
+ let jsonURL = URL(fileURLWithPath: prefix.path + ".json")
77
+ let payload: [String: Any] = [
78
+ "width": width, "height": height, "channels": 4,
79
+ "dtype": "<f4", "layout": "RGBA", "orientation": orientation,
80
+ "color_space": sourcePrimaries == "Display P3" ? "extendedLinearDisplayP3" : "extendedLinearSRGB",
81
+ "source_primaries": sourcePrimaries,
82
+ "decode_request": "kCGImageSourceDecodeToHDR",
83
+ "reference_white": "relative SDR white = 1.0",
84
+ "source": sourceURL.lastPathComponent
85
+ ]
86
+ guard let metadata = try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]) else {
87
+ fail("JSON encoding failed")
88
+ }
89
+ do {
90
+ try FileManager.default.createDirectory(at: prefix.deletingLastPathComponent(), withIntermediateDirectories: true)
91
+ let rawData = pixels.withUnsafeBytes { Data($0) }
92
+ try rawData.write(to: rawURL, options: .atomic)
93
+ try metadata.write(to: jsonURL, options: .atomic)
94
+ print(String(data: metadata, encoding: .utf8) ?? "{}")
95
+ } catch {
96
+ try? FileManager.default.removeItem(at: rawURL)
97
+ try? FileManager.default.removeItem(at: jsonURL)
98
+ fail("Writing HDR intermediate failed: \(error)")
99
+ }
@@ -0,0 +1,78 @@
1
+ """Get an Apple-supplied extended-linear Display P3 ICC profile."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import platform
7
+ import subprocess
8
+ from importlib.resources import files
9
+ from pathlib import Path
10
+ from tempfile import TemporaryDirectory
11
+
12
+ from luvix.backends.apple.bridge import BackendError
13
+
14
+
15
+ def linear_display_p3_icc() -> bytes:
16
+ if platform.system() != "Darwin":
17
+ raise BackendError("Linear Display P3 ICC extraction requires macOS")
18
+ swift = Path(str(files("luvix.backends.apple").joinpath("icc_profile.swift")))
19
+ digest = hashlib.sha256(swift.read_bytes()).hexdigest()[:16]
20
+ binary = Path.home() / ".cache" / "luvix" / f"icc-profile-{digest}"
21
+ if not binary.exists():
22
+ binary.parent.mkdir(parents=True, exist_ok=True)
23
+ result = subprocess.run(
24
+ ["swiftc", "-O", str(swift), "-o", str(binary)],
25
+ capture_output=True,
26
+ text=True,
27
+ check=False,
28
+ )
29
+ if result.returncode:
30
+ raise BackendError(f"ICC Swift compilation failed:\n{result.stderr}")
31
+ with TemporaryDirectory(prefix="luvix-icc-") as directory:
32
+ destination = Path(directory) / "linear-display-p3.icc"
33
+ result = subprocess.run(
34
+ [str(binary), str(destination)],
35
+ capture_output=True,
36
+ text=True,
37
+ check=False,
38
+ )
39
+ if result.returncode:
40
+ raise BackendError(result.stderr.strip() or "ICC extraction failed")
41
+ return destination.read_bytes()
42
+
43
+
44
+ def source_linear_icc(working_space: str) -> bytes:
45
+ """Return the matching Apple linear ICC; unsupported spaces fail closed."""
46
+ if working_space not in {"extendedLinearDisplayP3", "extendedLinearSRGB"}:
47
+ raise BackendError(f"Unsupported working space: {working_space}")
48
+ return (
49
+ linear_display_p3_icc()
50
+ if working_space == "extendedLinearDisplayP3"
51
+ else _linear_srgb_icc()
52
+ )
53
+
54
+
55
+ def _linear_srgb_icc() -> bytes:
56
+ import hashlib
57
+ import subprocess
58
+ from importlib.resources import files
59
+ from tempfile import TemporaryDirectory
60
+
61
+ swift = Path(str(files("luvix.backends.apple").joinpath("icc_profile.swift")))
62
+ digest = hashlib.sha256(swift.read_bytes()).hexdigest()[:16]
63
+ binary = Path.home() / ".cache" / "luvix" / f"icc-profile-{digest}"
64
+ if not binary.exists():
65
+ binary.parent.mkdir(parents=True, exist_ok=True)
66
+ result = subprocess.run(
67
+ ["swiftc", "-O", str(swift), "-o", str(binary)], capture_output=True, text=True
68
+ )
69
+ if result.returncode:
70
+ raise BackendError(result.stderr)
71
+ with TemporaryDirectory(prefix="luvix-icc-") as directory:
72
+ destination = Path(directory) / "linear-srgb.icc"
73
+ result = subprocess.run(
74
+ [str(binary), str(destination), "srgb"], capture_output=True, text=True
75
+ )
76
+ if result.returncode:
77
+ raise BackendError(result.stderr.strip())
78
+ return destination.read_bytes()
@@ -0,0 +1,31 @@
1
+ import Foundation
2
+ import CoreGraphics
3
+
4
+ func fail(_ message: String) -> Never {
5
+ fputs("Error: \(message)\n", stderr)
6
+ exit(1)
7
+ }
8
+
9
+ guard (2...3).contains(CommandLine.arguments.count) else {
10
+ fail("Usage: icc_profile OUTPUT.icc")
11
+ }
12
+ let profileName = CommandLine.arguments.count == 3 ? CommandLine.arguments[2] : "p3"
13
+ let spaceName: CFString = profileName == "srgb"
14
+ ? CGColorSpace.extendedLinearSRGB : CGColorSpace.extendedLinearDisplayP3
15
+ guard let space = CGColorSpace(name: spaceName) else {
16
+ fail("Extended linear Display P3 is unavailable")
17
+ }
18
+ guard let profile = space.copyICCData() else {
19
+ fail("No ICC profile data available for extended linear Display P3")
20
+ }
21
+ let data = profile as Data
22
+ guard data.count >= 128 else { fail("ICC data is unexpectedly short") }
23
+ let declaredSize = data.prefix(4).reduce(UInt32(0)) { ($0 << 8) | UInt32($1) }
24
+ guard declaredSize == data.count else { fail("ICC header length mismatch") }
25
+ guard data.subdata(in: 36..<40) == Data("acsp".utf8) else {
26
+ fail("ICC signature missing")
27
+ }
28
+ let output = URL(fileURLWithPath: CommandLine.arguments[1])
29
+ do { try data.write(to: output, options: .atomic) }
30
+ catch { fail("Cannot write ICC profile: \(error)") }
31
+ print("Exported extended linear Display P3 ICC (\(data.count) bytes)")
@@ -0,0 +1,143 @@
1
+ import Foundation
2
+ import CoreGraphics
3
+ import ImageIO
4
+ import UniformTypeIdentifiers
5
+
6
+ func die(_ message: String) -> Never { fputs("Error: \(message)\n", stderr); exit(1) }
7
+ func source(_ url: URL) -> CGImageSource {
8
+ guard let s = CGImageSourceCreateWithURL(url as CFURL, nil) else { die("Cannot open \(url.path)") }
9
+ return s
10
+ }
11
+ func auxiliary(_ s: CGImageSource) -> [String: Any] {
12
+ guard let d = CGImageSourceCopyAuxiliaryDataInfoAtIndex(s, 0, kCGImageAuxiliaryDataTypeHDRGainMap) as? [String: Any] else { die("Missing HDR gain map") }
13
+ return d
14
+ }
15
+ struct MapInfo { let width: Int; let height: Int; let stride: Int; let bytes: Data }
16
+ func mapInfo(_ aux: [String: Any]) -> MapInfo {
17
+ guard let desc = aux[kCGImageAuxiliaryDataInfoDataDescription as String] as? [String: Any],
18
+ let w = desc["Width"] as? Int, let h = desc["Height"] as? Int,
19
+ let stride = desc["BytesPerRow"] as? Int,
20
+ let format = desc["PixelFormat"] as? Int,
21
+ let bytes = aux[kCGImageAuxiliaryDataInfoData as String] as? Data,
22
+ format == 1278226488, w > 0, h > 0, stride >= w, bytes.count == stride*h else {
23
+ die("Expected Apple L008 grayscale gain map; unsupported layout")
24
+ }
25
+ return MapInfo(width: w, height: h, stride: stride, bytes: bytes)
26
+ }
27
+ func grayImage(_ m: MapInfo) -> CGImage {
28
+ guard let provider = CGDataProvider(data: m.bytes as CFData),
29
+ let img = CGImage(width: m.width, height: m.height, bitsPerComponent: 8,
30
+ bitsPerPixel: 8, bytesPerRow: m.stride,
31
+ space: CGColorSpaceCreateDeviceGray(),
32
+ bitmapInfo: CGBitmapInfo(rawValue: 0), provider: provider,
33
+ decode: nil, shouldInterpolate: false, intent: .defaultIntent) else { die("Could not decode gain map") }
34
+ return img
35
+ }
36
+ func writePNG(_ image: CGImage, _ url: URL) {
37
+ guard let d = CGImageDestinationCreateWithURL(url as CFURL, UTType.png.identifier as CFString, 1, nil) else { die("Cannot create PNG") }
38
+ CGImageDestinationAddImage(d, image, nil)
39
+ guard CGImageDestinationFinalize(d) else { die("Could not write PNG") }
40
+ }
41
+ func readEditedMap(_ url: URL, original: MapInfo) -> Data {
42
+ let s = source(url)
43
+ guard let image = CGImageSourceCreateImageAtIndex(s, 0, nil), image.width == original.width, image.height == original.height else { die("Edited map must have dimensions \(original.width)x\(original.height)") }
44
+ // Render as 8-bit DeviceGray; preserve original row padding in output buffer.
45
+ var bytes = [UInt8](original.bytes)
46
+ let success = bytes.withUnsafeMutableBytes { buffer -> Bool in
47
+ guard let ctx = CGContext(data: buffer.baseAddress, width: original.width,
48
+ height: original.height, bitsPerComponent: 8,
49
+ bytesPerRow: original.stride,
50
+ space: CGColorSpaceCreateDeviceGray(),
51
+ bitmapInfo: CGImageAlphaInfo.none.rawValue) else { return false }
52
+ ctx.interpolationQuality = .none
53
+ ctx.draw(image, in: CGRect(x: 0, y: 0, width: original.width, height: original.height))
54
+ return true
55
+ }
56
+ guard success else { die("Failed rendering edited gain map") }
57
+ return Data(bytes)
58
+ }
59
+ func writeHEIC(_ base: CGImage, _ original: CGImageSource, _ aux: [String: Any], _ url: URL) {
60
+ guard let d = CGImageDestinationCreateWithURL(url as CFURL, UTType.heic.identifier as CFString, 1, nil) else { die("Cannot create HEIC") }
61
+ let props = CGImageSourceCopyPropertiesAtIndex(original, 0, nil)
62
+ CGImageDestinationAddImage(d, base, props)
63
+ CGImageDestinationAddAuxiliaryDataInfo(d, kCGImageAuxiliaryDataTypeHDRGainMap, aux as CFDictionary)
64
+ guard CGImageDestinationFinalize(d) else { die("Could not finalize HEIC") }
65
+ let check = source(url)
66
+ _ = mapInfo(auxiliary(check))
67
+ // ImageIO can mark output files hidden; explicitly clear the flag.
68
+ let p = Process(); p.executableURL = URL(fileURLWithPath: "/usr/bin/chflags")
69
+ p.arguments = ["nohidden", url.path]
70
+ do { try p.run(); p.waitUntilExit() } catch { die("chflags failed: \(error)") }
71
+ guard p.terminationStatus == 0 else { die("Could not clear hidden flag") }
72
+ }
73
+
74
+ func details(_ s: CGImageSource) -> [String: Any] {
75
+ let image = CGImageSourceCreateImageAtIndex(s, 0, nil)!
76
+ let m = mapInfo(auxiliary(s))
77
+ let props = CGImageSourceCopyPropertiesAtIndex(s, 0, nil) as? [String: Any] ?? [:]
78
+ let meta = auxiliary(s)[kCGImageAuxiliaryDataInfoMetadata as String]
79
+ return ["base_width": image.width, "base_height": image.height,
80
+ "gain_width": m.width, "gain_height": m.height,
81
+ "gain_stride": m.stride, "gain_format": "L008",
82
+ "color_space": image.colorSpace?.name as String? ?? "unknown",
83
+ "orientation": props[kCGImagePropertyOrientation as String] ?? 1,
84
+ "has_gainmap_metadata": meta != nil]
85
+ }
86
+ func emitJSON(_ d: [String: Any]) {
87
+ guard let data = try? JSONSerialization.data(withJSONObject: d, options: [.sortedKeys]),
88
+ let text = String(data: data, encoding: .utf8) else { die("JSON error") }
89
+ print(text)
90
+ }
91
+ let args = CommandLine.arguments
92
+ if args.count < 2 { die("Usage: apple_backend inspect|extract|rebuild|validate ...") }
93
+ switch args[1] {
94
+ case "inspect":
95
+ guard args.count == 3 else { die("inspect source.HEIC") }
96
+ emitJSON(details(source(URL(fileURLWithPath: args[2]))))
97
+ case "extract":
98
+ guard args.count == 4 else { die("extract source.HEIC directory") }
99
+ let input = URL(fileURLWithPath: args[2]); let s = source(input)
100
+ let dir = URL(fileURLWithPath: args[3], isDirectory: true)
101
+ do { try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) } catch { die("Cannot create directory") }
102
+ for name in ["base.png", "gainmap.png", "manifest.json"] {
103
+ guard !FileManager.default.fileExists(atPath: dir.appendingPathComponent(name).path) else { die("Refusing to overwrite " + name) }
104
+ }
105
+ let base = CGImageSourceCreateImageAtIndex(s, 0, nil)!
106
+ writePNG(base, dir.appendingPathComponent("base.png"))
107
+ writePNG(grayImage(mapInfo(auxiliary(s))), dir.appendingPathComponent("gainmap.png"))
108
+ var report = details(s)
109
+ report["source_name"] = input.lastPathComponent
110
+ let data = try! JSONSerialization.data(withJSONObject: report, options: [.prettyPrinted, .sortedKeys])
111
+ try! data.write(to: dir.appendingPathComponent("manifest.json"))
112
+ print("Extracted base.png, gainmap.png, manifest.json")
113
+ case "rebuild":
114
+ guard args.count == 6 else { die("rebuild source.HEIC base.png|- gainmap.png|- output.HEIC") }
115
+ let original = source(URL(fileURLWithPath: args[2]))
116
+ let origBase = CGImageSourceCreateImageAtIndex(original, 0, nil)!
117
+ let base: CGImage
118
+ if args[3] == "-" { base = origBase }
119
+ else {
120
+ let bs = source(URL(fileURLWithPath: args[3]))
121
+ guard let edited = CGImageSourceCreateImageAtIndex(bs, 0, nil) else { die("Invalid edited base") }
122
+ base = edited
123
+ }
124
+ guard base.width == origBase.width, base.height == origBase.height else { die("Base dimensions mismatch") }
125
+ guard base.colorSpace?.name == origBase.colorSpace?.name else { die("Base color profile mismatch") }
126
+ var aux = auxiliary(original)
127
+ if args[4] != "-" {
128
+ aux[kCGImageAuxiliaryDataInfoData as String] = readEditedMap(URL(fileURLWithPath: args[4]), original: mapInfo(aux))
129
+ }
130
+ let output = URL(fileURLWithPath: args[5])
131
+ guard !FileManager.default.fileExists(atPath: output.path) else { die("Refusing to overwrite existing output") }
132
+ writeHEIC(base, original, aux, output)
133
+ print("Rebuilt: \(output.path)")
134
+ case "validate":
135
+ guard args.count == 4 else { die("validate source.HEIC output.HEIC") }
136
+ let a = details(source(URL(fileURLWithPath: args[2])))
137
+ let b = details(source(URL(fileURLWithPath: args[3])))
138
+ let keys = ["base_width", "base_height", "gain_width", "gain_height", "gain_format", "color_space", "orientation"]
139
+ let differences = keys.filter { String(describing: a[$0]) != String(describing: b[$0]) }
140
+ emitJSON(["valid": differences.isEmpty, "mismatched_fields": differences])
141
+ if !differences.isEmpty { exit(2) }
142
+ default: die("Unknown command")
143
+ }