Files
StarPilot/tools/color_cal.py
T
2026-04-12 01:39:01 -05:00

106 lines
3.4 KiB
Python

#!/usr/bin/env python3
import argparse
import binascii
import os
import struct
from pathlib import Path
IGNORED_TAIL = b"\x00\x00\x00\x00\x00"
VALUES_COUNT = 13
VALUES_FMT = f"<{VALUES_COUNT}e"
VALUES_SIZE = struct.calcsize(VALUES_FMT)
def read_color_cal(path: str) -> tuple[float, list[float], list[float], bytes]:
with open(path, "rb") as f:
data = f.read()
if len(data) < VALUES_SIZE:
raise ValueError(f"{path} is too short: expected at least {VALUES_SIZE} bytes, got {len(data)}")
values = struct.unpack(VALUES_FMT, data[:VALUES_SIZE])
gamma = float(values[0])
ccm = [float(v) for v in values[1:10]]
wb = [float(v) for v in values[10:13]]
return gamma, ccm, wb, data[VALUES_SIZE:]
def write_color_cal(path: str, gamma: float, ccm: list[float], wb: list[float], tail: bytes = IGNORED_TAIL) -> None:
if len(ccm) != 9:
raise ValueError("CCM must contain exactly 9 values")
if len(wb) != 3:
raise ValueError("WB gains must contain exactly 3 values")
payload = struct.pack(VALUES_FMT, gamma, *ccm, *wb)
data = payload + tail
out = Path(path)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_bytes(data)
def cmd_analyze(args: argparse.Namespace) -> int:
gamma, ccm, wb, tail = read_color_cal(args.path)
data = Path(args.path).read_bytes()
print(f"path: {args.path}")
print(f"size: {len(data)}")
print(f"hex: {binascii.hexlify(data).decode()}")
print(f"gamma: {gamma:.4f}")
print("ccm:")
print(f" [{ccm[0]:.4f}, {ccm[1]:.4f}, {ccm[2]:.4f}]")
print(f" [{ccm[3]:.4f}, {ccm[4]:.4f}, {ccm[5]:.4f}]")
print(f" [{ccm[6]:.4f}, {ccm[7]:.4f}, {ccm[8]:.4f}]")
print(f"wb: [R={wb[0]:.4f}, G={wb[1]:.4f}, B={wb[2]:.4f}]")
print(f"ignored_tail_hex: {tail.hex()}")
return 0
def cmd_generate(args: argparse.Namespace) -> int:
tail = binascii.unhexlify(args.tail_hex) if args.tail_hex else IGNORED_TAIL
write_color_cal(args.output, args.gamma, args.ccm, args.wb, tail=tail)
print(f"wrote {args.output}")
return 0
def cmd_from_hex(args: argparse.Namespace) -> int:
data = binascii.unhexlify(args.hex_string)
out = Path(args.output)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_bytes(data)
print(f"wrote {args.output}")
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Inspect and generate comma/Weston color_cal files.")
subparsers = parser.add_subparsers(dest="command", required=True)
analyze = subparsers.add_parser("analyze", help="Decode an existing color_cal file")
analyze.add_argument("path")
analyze.set_defaults(func=cmd_analyze)
generate = subparsers.add_parser("generate", help="Generate a color_cal file from gamma/CCM/WB values")
generate.add_argument("output")
generate.add_argument("--gamma", type=float, required=True)
generate.add_argument("--ccm", type=float, nargs=9, required=True)
generate.add_argument("--wb", type=float, nargs=3, required=True)
generate.add_argument("--tail-hex", default="", help="Optional trailing bytes as hex; Weston ignores these")
generate.set_defaults(func=cmd_generate)
from_hex = subparsers.add_parser("from-hex", help="Write a raw color_cal blob from a hex string")
from_hex.add_argument("output")
from_hex.add_argument("hex_string")
from_hex.set_defaults(func=cmd_from_hex)
return parser
def main() -> int:
parser = build_parser()
args = parser.parse_args()
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())