接口开始5%

This commit is contained in:
rain
2026-07-22 17:31:33 +08:00
parent eced3d1e6e
commit 9b0ad62df4
426 changed files with 20111 additions and 28472 deletions
@@ -0,0 +1,132 @@
"""按 schema v3 清单确定性生成不透明长背景与 G01 暖金边框。"""
import argparse
import json
from pathlib import Path
from typing import Any
from PIL import Image, PngImagePlugin
def _resolve_inside(workspace: Path, relative_path: str) -> Path:
"""解析仓库相对路径,并在任何读写发生前拒绝目录逃逸。"""
absolute = (workspace / relative_path).resolve()
try:
absolute.relative_to(workspace.resolve())
except ValueError as error:
raise ValueError(f"path escapes workspace: {relative_path}") from error
return absolute
def build_opaque_resize(source: Image.Image, output_size: tuple[int, int]) -> Image.Image:
"""把同宽高比母版按 LANCZOS 直接缩放为无透明通道的 RGB 正式图。"""
image = source.convert("RGB")
if image.size != output_size:
image = image.resize(output_size, Image.Resampling.LANCZOS)
return image
def build_opaque_cover_crop(source: Image.Image, output_size: tuple[int, int]) -> Image.Image:
"""等比 cover 后从中心裁切;算法与旧五模块构建器的可见像素完全一致。"""
width, height = output_size
image = source.convert("RGB")
scale = max(width / image.width, height / image.height)
resized = image.resize(
(round(image.width * scale), round(image.height * scale)),
Image.Resampling.LANCZOS,
)
left = max(0, (resized.width - width) // 2)
top = max(0, (resized.height - height) // 2)
return resized.crop((left, top, left + width, top + height))
def extract_warm_gold_frame(source: Image.Image, config: dict[str, Any]) -> Image.Image:
"""复刻旧 Sharp 暖金边框阈值,同时清零全透明像素的隐藏 RGB。"""
image = source.convert("RGBA")
width, height = image.size
border_band = int(config["borderBand"])
output = Image.new("RGBA", image.size, (0, 0, 0, 0))
result: list[tuple[int, int, int, int]] = []
for index, (red, green, blue, source_alpha) in enumerate(image.get_flattened_data()):
x = index % width
y = index // width
distance_to_edge = min(x, y, width - 1 - x, height - 1 - y)
warm_gold = (
red > green > blue
and red - green >= int(config["redGreenMin"])
and green - blue >= int(config["greenBlueMin"])
and red - blue >= int(config["redBlueMin"])
and red < int(config["redMaxExclusive"])
and blue < int(config["blueMaxExclusive"])
)
if distance_to_edge >= border_band or not warm_gold:
result.append((0, 0, 0, 0))
continue
edge_alpha = max(
0,
min(
255,
(red - blue - int(config["alphaOffset"])) * int(config["alphaScale"]),
),
)
alpha = min(source_alpha, edge_alpha)
result.append((red, green, blue, alpha) if alpha else (0, 0, 0, 0))
output.putdata(result)
return output
def save_png(image: Image.Image, path: Path) -> None:
"""以固定压缩参数和标准 sRGB chunk 保存,保证同输入得到同字节。"""
path.parent.mkdir(parents=True, exist_ok=True)
png_info = PngImagePlugin.PngInfo()
png_info.add(b"sRGB", b"\x00")
image.save(path, format="PNG", optimize=True, compress_level=9, pnginfo=png_info)
def build_manifest(manifest_path: Path, workspace: Path) -> None:
"""执行已经由 Node 严格校验的清单,并再次核对真实母版像素尺寸。"""
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
for asset in manifest["assets"]:
source_path = _resolve_inside(workspace, asset["source"])
output_path = _resolve_inside(workspace, asset["output"])
expected_source = asset["sourcePixels"]
output_pixels = asset["outputPixels"]
output_size = (int(output_pixels["width"]), int(output_pixels["height"]))
with Image.open(source_path) as source:
expected_size = (int(expected_source["width"]), int(expected_source["height"]))
if source.size != expected_size:
raise ValueError(
f"{asset['id']} source expected {expected_size[0]}x{expected_size[1]}, "
f"got {source.width}x{source.height}"
)
mode = asset["processing"]["mode"]
if mode == "opaque-resize":
output = build_opaque_resize(source, output_size)
elif mode == "opaque-cover-crop":
output = build_opaque_cover_crop(source, output_size)
elif mode == "warm-gold-frame-extract":
output = extract_warm_gold_frame(source, asset["processing"])
else:
raise ValueError(f"unsupported raster processing mode: {mode}")
if output.size != output_size:
raise ValueError(f"{asset['id']} produced unexpected output size: {output.size}")
save_png(output, output_path)
print(f"BUILT {asset['id']} -> {asset['output']}")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("manifest", type=Path)
parser.add_argument("--workspace", type=Path, required=True)
args = parser.parse_args()
build_manifest(args.manifest.resolve(), args.workspace.resolve())
if __name__ == "__main__":
main()