Bring in plaza props/prefabs, ZhaoYun source GLB, design originals, and Hunyuan/Blender tooling used to produce them. Co-authored-by: Cursor <cursoragent@cursor.com>
89 lines
2.9 KiB
Python
89 lines
2.9 KiB
Python
"""检查混元 GLB 内嵌贴图能否解码。损坏 PNG 在 Blender 里是品红,烘进 color 就会整灯发紫。"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import struct
|
|
from pathlib import Path
|
|
from typing import List, Tuple
|
|
|
|
|
|
def _glb_chunks(data: bytes) -> Tuple[dict, bytes]:
|
|
if len(data) < 12 or data[:4] != b"glTF":
|
|
raise ValueError("不是 glTF GLB")
|
|
offset = 12
|
|
gltf = None
|
|
blob = b""
|
|
while offset + 8 <= len(data):
|
|
chunk_len, chunk_type = struct.unpack_from("<I4s", data, offset)
|
|
offset += 8
|
|
chunk = data[offset : offset + chunk_len]
|
|
offset += chunk_len
|
|
offset = (offset + 3) & ~3
|
|
if chunk_type == b"JSON":
|
|
gltf = json.loads(chunk.decode("utf-8"))
|
|
elif chunk_type == b"BIN\x00":
|
|
blob = chunk
|
|
if gltf is None:
|
|
raise ValueError("GLB 没有 JSON chunk")
|
|
return gltf, blob
|
|
|
|
|
|
def _image_blobs(gltf: dict, blob: bytes) -> List[bytes]:
|
|
views = gltf.get("bufferViews") or []
|
|
out: List[bytes] = []
|
|
for image in gltf.get("images") or []:
|
|
uri = image.get("uri")
|
|
if isinstance(uri, str) and uri.startswith("data:"):
|
|
continue
|
|
index = image.get("bufferView")
|
|
if index is None:
|
|
continue
|
|
view = views[int(index)]
|
|
start = int(view.get("byteOffset") or 0)
|
|
length = int(view.get("byteLength") or 0)
|
|
out.append(blob[start : start + length])
|
|
return out
|
|
|
|
|
|
def glb_textures_ok(path: Path) -> Tuple[bool, str]:
|
|
"""内嵌贴图都能解码则 (True, '')。无贴图或损坏返回 False 与原因。"""
|
|
path = Path(path)
|
|
if not path.is_file() or path.stat().st_size < 12:
|
|
return False, "文件不存在或过短"
|
|
data = path.read_bytes()
|
|
if data[:4] != b"glTF":
|
|
return False, "文件头不是 glTF"
|
|
try:
|
|
gltf, blob = _glb_chunks(data)
|
|
blobs = _image_blobs(gltf, blob)
|
|
except Exception as exc: # noqa: BLE001
|
|
return False, str(exc)
|
|
if not blobs:
|
|
return False, "没有内嵌贴图"
|
|
try:
|
|
from PIL import Image
|
|
except ImportError:
|
|
for raw in blobs:
|
|
if not (raw.startswith(b"\x89PNG") or raw[:2] == b"\xff\xd8"):
|
|
return False, "内嵌贴图不是 PNG/JPEG"
|
|
return True, ""
|
|
import io
|
|
|
|
for i, raw in enumerate(blobs):
|
|
try:
|
|
img = Image.open(io.BytesIO(raw))
|
|
img.load()
|
|
if img.size[0] < 8 or img.size[1] < 8:
|
|
return False, f"贴图 {i} 尺寸无效 {img.size}"
|
|
except Exception as exc: # noqa: BLE001
|
|
return False, f"贴图 {i} 无法解码: {exc}"
|
|
return True, ""
|
|
|
|
|
|
def assert_glb_textures_ok(path: Path) -> None:
|
|
ok, reason = glb_textures_ok(path)
|
|
if not ok:
|
|
raise RuntimeError(
|
|
f"{path} 内嵌贴图不可用({reason})。Blender 会显示品红并烘进 color。"
|
|
)
|