Release PPT workflow 0.7.1 with portable installation and production components

This commit is contained in:
2026-10-12 18:45:54 +08:00
parent 4e3271ff62
commit f932b1970d
40 changed files with 2166 additions and 28 deletions
+38 -10
View File
@@ -14,18 +14,45 @@ import template_ref as ref
from native_export import boundary_file, regular
def paint(manifest_path: Path, theme_id: str, role: str, output: Path):
def paint(manifest_path: Path, theme_id: str, role: str | None, output: Path,
material_path: Path | None = None):
manifest_path = regular(manifest_path, "manifest")
manifest = ref.load_manifest(manifest_path)
ref.require(manifest["schema_version"] == 2, "Paint application requires manifest schema 2")
ref.require(theme_id in manifest["themes"], "Unknown theme")
_, theme = ref.load_theme(manifest_path.parent, manifest["themes"][theme_id], theme_id)
asset = theme["assets"].get(role)
ref.require(asset is not None and "paint_contract" in asset, "Asset needs an explicit SVG paint contract")
root_path = manifest_path.parent
if material_path is None:
asset = theme["assets"].get(role)
ref.require(asset is not None and "paint_contract" in asset, "Asset needs an explicit SVG paint contract")
root_path = manifest_path.parent
bindings = [target for binding in theme["asset_bindings"] if binding["asset"] == role
for target in binding["targets"]]
else:
ref.require(role is None, "Choose a template asset or a local material, not both")
material_path = regular(material_path, "material descriptor")
asset = ref.read_json(material_path, (1,))
ref.require(set(asset) == {"schema_version", "id", "path", "paint_contract", "bindings"},
"Material descriptor requires exactly schema_version/id/path/paint_contract/bindings")
role = ref.identifier(asset["id"], "material ID")
root_path = material_path.parent
bindings = asset["bindings"]
ref.require(isinstance(bindings, list), "Material bindings must be an array")
_, contract = ref.load_paint_contract(root_path, asset, role)
source = boundary_file(root_path, asset["path"], "SVG asset")
contract_path = boundary_file(root_path, asset["paint_contract"], "paint contract")
contract = ref.read_json(contract_path, (1,))
boundary_file(root_path, asset["paint_contract"], "paint contract")
allowed = {(rule["selector"], rule["channel"]) for rule in contract["rules"] if rule["policy"] == "palette"}
targets = {}
for target in bindings:
ref.require(isinstance(target, dict) and set(target) == {"selector", "channel", "palette_index"},
"Binding requires exactly selector/channel/palette_index")
key = (ref.string(target["selector"], "selector"), ref.string(target["channel"], "channel"))
ref.require(key in allowed and key not in targets, "Unknown, fixed, structural or duplicate paint binding")
index = ref.string(target["palette_index"], "palette index")
ref.require(index in theme["palette"], "Unknown palette index")
targets[key] = index
ref.require(set(targets) == allowed, "Palette bindings must cover exactly the permitted channels")
if material_path is not None:
ref.require(bool(allowed), "A new icon needs at least one adjustable palette channel")
text = source.read_text(encoding="utf-8")
ref.require(not re.search(r"<!DOCTYPE|<!ENTITY", text, re.I), "SVG declarations/entities forbidden")
root = ET.fromstring(text)
@@ -36,8 +63,6 @@ def paint(manifest_path: Path, theme_id: str, role: str, output: Path):
if node.get("id"):
ref.require(node.get("id") not in by_id, "SVG IDs must be unique")
by_id[node.get("id")] = node
targets = {(target["selector"], target["channel"]): target["palette_index"]
for binding in theme["asset_bindings"] if binding["asset"] == role for target in binding["targets"]}
changes = []
for rule in contract["rules"]:
selector, channel = rule["selector"], rule["channel"]
@@ -61,6 +86,7 @@ def paint(manifest_path: Path, theme_id: str, role: str, output: Path):
with output.open("xb") as stream:
ET.ElementTree(root).write(stream, encoding="utf-8", xml_declaration=True)
return {"asset": role, "theme": theme_id, "output": str(output), "paint_changes": changes,
"material": str(material_path) if material_path else None,
"verification_scope": "explicit SVG channel transformation; rendered material appearance unverified"}
@@ -68,11 +94,13 @@ def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--manifest", type=Path, required=True)
parser.add_argument("--theme", required=True)
parser.add_argument("--asset", required=True)
selection = parser.add_mutually_exclusive_group(required=True)
selection.add_argument("--asset")
selection.add_argument("--material", type=Path)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
try:
print(json.dumps(paint(args.manifest, args.theme, args.asset, args.output), ensure_ascii=False))
print(json.dumps(paint(args.manifest, args.theme, args.asset, args.output, args.material), ensure_ascii=False))
return 0
except (ref.InputError, OSError, ValueError, KeyError, TypeError, ET.ParseError) as error:
print(json.dumps({"error": str(error)}, ensure_ascii=False), file=sys.stderr)