项目文件夹

文件
wehub-resource-sync 8ef9db2513
Deploy to GitHub Pages / deploy (push) Has been cancelled
Deploy to GitHub Pages / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:56:49 +08:00

365 行
12 KiB
Python

#!/usr/bin/env python3
"""
PPT Master - Preset Shape Data Loader
Load and validate the bundled DrawingML preset geometry catalog.
Usage:
Import load_preset_shape_definitions from pptx_shapes.loader.
Examples:
definitions = load_preset_shape_definitions()
Dependencies:
None (only uses standard library)
"""
from __future__ import annotations
import hashlib
from pathlib import Path
from xml.etree import ElementTree as ET
from .errors import PresetShapeDataError
from .models import (
AdjustHandleDefinition,
ConnectionSiteDefinition,
GuideDefinition,
PathCommandDefinition,
PointExpression,
PresetShapeDefinition,
ShapePathDefinition,
TextRectangleDefinition,
)
DRAWINGML_NS = "http://schemas.openxmlformats.org/drawingml/2006/main"
EXPECTED_SHAPE_COUNT = 187
BUNDLED_DEFINITIONS_SHA256 = (
"4a762444d8d85876881c02a5b1dedf6f73006fcd8acb7b4e393435615b37c780"
)
BUNDLED_SHAPE_TYPES_SHA256 = (
"f2c3bdcda8569b358ce3196cfeb183849e33bfc7955fac961dc85fceb6b3b587"
)
_PACKAGE_DIR = Path(__file__).resolve().parent
BUNDLED_DEFINITIONS_PATH = _PACKAGE_DIR / "data" / "presetShapeDefinitions.xml"
BUNDLED_SHAPE_TYPES_PATH = _PACKAGE_DIR / "data" / "shape_type_values.txt"
_EXPECTED_ROOT_TAG = "presetShapeDefinitons"
_PATH_COMMAND_ARITY = {
"moveTo": 1,
"lnTo": 1,
"quadBezTo": 2,
"cubicBezTo": 3,
"arcTo": 0,
"close": 0,
}
def load_shape_type_values(path: Path | None = None) -> tuple[str, ...]:
"""Load the independent Open XML ``ShapeTypeValues`` coverage list."""
source = path or BUNDLED_SHAPE_TYPES_PATH
try:
raw = source.read_bytes()
names = tuple(
line.strip()
for line in raw.decode("utf-8").splitlines()
if line.strip() and not line.lstrip().startswith("#")
)
except (OSError, UnicodeDecodeError) as exc:
raise PresetShapeDataError(
f"Cannot read preset shape type catalog {source}: {exc}"
) from exc
if path is None:
actual = hashlib.sha256(_normalized_lf_bytes(raw)).hexdigest()
if actual != BUNDLED_SHAPE_TYPES_SHA256:
raise PresetShapeDataError(
f"ShapeTypeValues checksum mismatch for {source}: "
f"expected {BUNDLED_SHAPE_TYPES_SHA256}, found {actual}"
)
if len(names) != len(set(names)):
raise PresetShapeDataError(f"Duplicate names in shape type catalog: {source}")
if len(names) != EXPECTED_SHAPE_COUNT:
raise PresetShapeDataError(
f"Expected {EXPECTED_SHAPE_COUNT} ShapeTypeValues, found {len(names)}"
)
return names
def load_preset_shape_definitions(
path: Path | None = None,
*,
expected_sha256: str | None = None,
expected_names: tuple[str, ...] | None = None,
) -> tuple[PresetShapeDefinition, ...]:
"""Load one preset XML catalog and enforce uniqueness and coverage.
The bundled catalog is hash-locked automatically. External catalogs are
validated structurally and can opt into a caller-provided hash.
"""
source = path or BUNDLED_DEFINITIONS_PATH
locked_hash = (
BUNDLED_DEFINITIONS_SHA256
if path is None and expected_sha256 is None
else expected_sha256
)
raw = _read_verified_bytes(source, locked_hash)
try:
root = ET.fromstring(raw)
except ET.ParseError as exc:
raise PresetShapeDataError(f"Invalid preset geometry XML {source}: {exc}") from exc
if _local_name(root.tag) != _EXPECTED_ROOT_TAG:
raise PresetShapeDataError(
f"Unexpected preset geometry root {_local_name(root.tag)!r} in {source}"
)
definitions = tuple(_parse_shape(element) for element in root)
names = tuple(definition.name for definition in definitions)
if len(names) != len(set(names)):
raise PresetShapeDataError(f"Duplicate preset geometry names in {source}")
catalog_names = expected_names
if catalog_names is None and path is None:
catalog_names = load_shape_type_values()
if catalog_names is not None:
missing = sorted(set(catalog_names) - set(names))
extra = sorted(set(names) - set(catalog_names))
if missing or extra:
raise PresetShapeDataError(
"Preset geometry coverage differs from ShapeTypeValues: "
f"missing={missing}, extra={extra}"
)
return definitions
def _read_verified_bytes(path: Path, expected_sha256: str | None) -> bytes:
try:
raw = path.read_bytes()
except OSError as exc:
raise PresetShapeDataError(f"Cannot read preset geometry data {path}: {exc}") from exc
if expected_sha256:
actual = hashlib.sha256(_normalized_lf_bytes(raw)).hexdigest()
if actual != expected_sha256:
raise PresetShapeDataError(
f"Preset geometry checksum mismatch for {path}: "
f"expected {expected_sha256}, found {actual}"
)
return raw
def _normalized_lf_bytes(raw: bytes) -> bytes:
"""Normalize checkout line endings before verifying text-resource hashes."""
return raw.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
def _parse_shape(element: ET.Element) -> PresetShapeDefinition:
name = _local_name(element.tag)
if not name:
raise PresetShapeDataError("Preset shape name must not be empty")
adjustments = _parse_guides(_find_child(element, "avLst"))
guides = _parse_guides(_find_child(element, "gdLst"))
handles = _parse_handles(_find_child(element, "ahLst"))
connections = _parse_connections(_find_child(element, "cxnLst"))
text_rectangle = _parse_text_rectangle(_find_child(element, "rect"))
paths = _parse_paths(_find_child(element, "pathLst"), name)
return PresetShapeDefinition(
name=name,
adjustments=adjustments,
guides=guides,
handles=handles,
connections=connections,
text_rectangle=text_rectangle,
paths=paths,
)
def _parse_guides(container: ET.Element | None) -> tuple[GuideDefinition, ...]:
if container is None:
return ()
guides = []
for element in container:
if _local_name(element.tag) != "gd":
raise PresetShapeDataError(
f"Unexpected {_local_name(element.tag)!r} in guide list"
)
name = _required_attribute(element, "name")
formula = _required_attribute(element, "fmla")
guides.append(GuideDefinition(name=name, formula=formula))
# Some normative preset definitions intentionally rebind an intermediate
# name later in the ordered guide list. Preserve that sequential behavior.
return tuple(guides)
def _parse_handles(
container: ET.Element | None,
) -> tuple[AdjustHandleDefinition, ...]:
if container is None:
return ()
handles = []
for element in container:
kind = _local_name(element.tag)
if kind not in {"ahXY", "ahPolar"}:
raise PresetShapeDataError(f"Unexpected adjustment handle: {kind!r}")
position = _parse_position(element)
handles.append(
AdjustHandleDefinition(
kind="xy" if kind == "ahXY" else "polar",
position=position,
x_reference=element.attrib.get("gdRefX"),
minimum_x=element.attrib.get("minX"),
maximum_x=element.attrib.get("maxX"),
y_reference=element.attrib.get("gdRefY"),
minimum_y=element.attrib.get("minY"),
maximum_y=element.attrib.get("maxY"),
angle_reference=element.attrib.get("gdRefAng"),
minimum_angle=element.attrib.get("minAng"),
maximum_angle=element.attrib.get("maxAng"),
radius_reference=element.attrib.get("gdRefR"),
minimum_radius=element.attrib.get("minR"),
maximum_radius=element.attrib.get("maxR"),
)
)
return tuple(handles)
def _parse_connections(
container: ET.Element | None,
) -> tuple[ConnectionSiteDefinition, ...]:
if container is None:
return ()
connections = []
for element in container:
if _local_name(element.tag) != "cxn":
raise PresetShapeDataError(
f"Unexpected connection-site element: {_local_name(element.tag)!r}"
)
connections.append(
ConnectionSiteDefinition(
angle=_required_attribute(element, "ang"),
position=_parse_position(element),
)
)
return tuple(connections)
def _parse_text_rectangle(
element: ET.Element | None,
) -> TextRectangleDefinition | None:
if element is None:
return None
return TextRectangleDefinition(
left=_required_attribute(element, "l"),
top=_required_attribute(element, "t"),
right=_required_attribute(element, "r"),
bottom=_required_attribute(element, "b"),
)
def _parse_paths(
container: ET.Element | None,
shape_name: str,
) -> tuple[ShapePathDefinition, ...]:
if container is None:
raise PresetShapeDataError(f"Preset {shape_name!r} has no path list")
paths = []
for element in container:
if _local_name(element.tag) != "path":
raise PresetShapeDataError(
f"Unexpected path-list element: {_local_name(element.tag)!r}"
)
paths.append(
ShapePathDefinition(
coordinate_width=element.attrib.get("w"),
coordinate_height=element.attrib.get("h"),
fill=element.attrib.get("fill", "norm"),
stroke=_parse_boolean(element.attrib.get("stroke"), default=True),
extrusion_ok=_parse_boolean(
element.attrib.get("extrusionOk"),
default=True,
),
commands=tuple(_parse_path_command(command) for command in element),
)
)
if not paths:
raise PresetShapeDataError(f"Preset {shape_name!r} has an empty path list")
return tuple(paths)
def _parse_path_command(element: ET.Element) -> PathCommandDefinition:
name = _local_name(element.tag)
expected_points = _PATH_COMMAND_ARITY.get(name)
if expected_points is None:
raise PresetShapeDataError(f"Unsupported preset path command: {name!r}")
if name == "arcTo":
parameters = tuple(
_required_attribute(element, attribute)
for attribute in ("wR", "hR", "stAng", "swAng")
)
return PathCommandDefinition(name=name, parameters=parameters)
points = tuple(
child for child in element if _local_name(child.tag) == "pt"
)
if len(points) != expected_points:
raise PresetShapeDataError(
f"Path command {name!r} expects {expected_points} points, "
f"found {len(points)}"
)
parameters = tuple(
coordinate
for point in points
for coordinate in (
_required_attribute(point, "x"),
_required_attribute(point, "y"),
)
)
return PathCommandDefinition(name=name, parameters=parameters)
def _parse_position(parent: ET.Element) -> PointExpression:
positions = [
child for child in parent if _local_name(child.tag) == "pos"
]
if len(positions) != 1:
raise PresetShapeDataError(
f"{_local_name(parent.tag)!r} must contain exactly one position"
)
return PointExpression(
x=_required_attribute(positions[0], "x"),
y=_required_attribute(positions[0], "y"),
)
def _find_child(parent: ET.Element, local_name: str) -> ET.Element | None:
matches = [child for child in parent if _local_name(child.tag) == local_name]
if len(matches) > 1:
raise PresetShapeDataError(
f"Preset contains duplicate {local_name!r} elements"
)
return matches[0] if matches else None
def _required_attribute(element: ET.Element, name: str) -> str:
value = element.attrib.get(name)
if value is None or not value.strip():
raise PresetShapeDataError(
f"Element {_local_name(element.tag)!r} requires attribute {name!r}"
)
return value.strip()
def _parse_boolean(value: str | None, *, default: bool) -> bool:
if value is None:
return default
if value in {"true", "1"}:
return True
if value in {"false", "0"}:
return False
raise PresetShapeDataError(f"Invalid DrawingML boolean value: {value!r}")
def _local_name(tag: str) -> str:
return tag.rsplit("}", 1)[-1]