"""Reproduce the PhotoCraft Guide Patchy 1.08 PSD fixture check. Requires Python 3, psd-tools==1.24.0 and Pillow. Download the seven *-input.psd files beside this script. Outputs go to a new folder. """ import argparse import hashlib import json import os from pathlib import Path import subprocess from PIL import Image, ImageChops, ImageStat from psd_tools import PSDImage INPUT_HASHES = {'2layers': '406ddf7cbf5a1065b992b9c0d2a59be6290a371b90f23b9a04c0e6bbc94beb0c', 'text': 'eb40ec54a5f270913463bcc847be65672ce6fff0d0d56288725f58e299e00f1c', 'mask': 'a6928c0e306022d474876e61f4b1dc5b7c1ed1e1cb422e6e72511da0a6aae90b', 'layer_effects': '6ab41ce6e17b3287c703dc4cf4d898506ffc1bd9babf20d0f1de5ef6df1d2e1e', 'placedLayer': '69ea01bf88cb85c48d3a78c3bb9e06ae141c9c9fc6a88267eae95c315e16180e', '16bit5x5': 'c78689ea7b576f23bbd8b6b4f4993a365266c3ffc9aaa02cc20bef4a36a9d7a0', '32bit5x5': '66a4260e6bb8a60deff94031a66fa502c74870d75a723b4180aef02c35a97d0e'} def inspect(path): doc = PSDImage.open(path) layers = [{"name": layer.name, "kind": layer.kind, "mask": layer.has_mask(), "effects": len(layer.effects), "text": layer.text if layer.kind == "type" else None} for layer in doc.descendants()] return {"width": doc.width, "height": doc.height, "depth": doc.depth, "layers": layers}, doc def structure(record): return [{key: value for key, value in layer.items() if key != "text"} for layer in record["layers"]] def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--patchy", required=True, type=Path) parser.add_argument("--inputs", type=Path, default=Path(".")) parser.add_argument("--output", required=True, type=Path) args = parser.parse_args() for name, expected in INPUT_HASHES.items(): file = args.inputs / (name + "-input.psd") if hashlib.sha256(file.read_bytes()).hexdigest() != expected: raise ValueError("Input hash mismatch: " + str(file)) args.output.mkdir(parents=True, exist_ok=False) env = dict(os.environ, PATCHY_SETTINGS_DIR=str((args.output / "settings").resolve())) records = [] for name in INPUT_HASHES: source = (args.inputs / (name + "-input.psd")).resolve() for extension in ("png", "psd"): target = (args.output / (name + "-output." + extension)).resolve() command = [str(args.patchy.resolve()), "--headless", "--export", str(target)] if name == "text" and extension == "psd": command += ["--append-text", " + PhotoCraft Guide"] command.append(str(source)) run = subprocess.run(command, capture_output=True, text=True, env=env, timeout=60) (args.output / (name + "-" + extension + "-log.txt")).write_text("\n".join(line.rstrip() for line in (run.stdout + run.stderr).splitlines()) + "\n") run.check_returncode() before, original = inspect(source) after, _ = inspect(args.output / (name + "-output.psd")) reference = original.topil().convert("RGB") render = Image.open(args.output / (name + "-output.png")).convert("RGB") difference = ImageChops.difference(reference, render) records.append({"id": name, "input": before, "output": after, "structureRetained": structure(before) == structure(after), "depthRetained": before["depth"] == after["depth"], "textEditVerified": name == "text" and any( " + PhotoCraft Guide" in (layer["text"] or "") for layer in after["layers"]), "renderMeanAbsoluteDifference": round(sum(ImageStat.Stat(difference).mean) / 3, 4)}) (args.output / "results.json").write_text(json.dumps(records, ensure_ascii=False, indent=2) + "\n") print(json.dumps(records, ensure_ascii=False, indent=2)) if __name__ == "__main__": main()