On this page
Headless part-manifest export: the single source of truth that validates a project, builds the manifest, and renders deterministic per-part PNG strips.
#server.src.pixelweaver.part_export
#server.src.pixelweaver.part_export
Headless part-manifest export (G8) -- the single source of truth for the server/CLI export path.
Given a ProjectState (loaded from disk or held in memory), this module: - validates the part registry, part-sets, and per-part compositing preconditions (validate_part_export); - builds the PixelWeaver part-manifest document (build_part_manifest), spec-ordered and byte-deterministic; - renders deterministic per-part PNG strips in both role-indexed and resolved-RGBA forms (render_part_strip); - writes the whole export to a directory (export_parts_to_dir) or a deterministic zip (export_parts_zip_bytes).
Determinism (part-manifest spec section 2.5): identical project state yields a byte-identical part-manifest.json and byte-identical strip PNGs on repeated runs, and pixel-identical strips versus the TypeScript twin in src/lib/parts/part-export.ts. The TS manifest JSON is byte-identical to this module's; strip PIXELS are identical, though compressed bytes may differ across the browser CompressionStream (documented in the TS module).
#PartExportValidationError
Client-fixable validation problems (unregistered pairings, gaps in variant indices, mixed paint on a classed part, ...). HTTP handlers map this to 422. Plain ValueErrors (invariant violations) map to 500.
#_registered_parts
def _registered_parts(project: ProjectState) -> list[dict[str, Any]]Registry entries sorted by z_order ascending (snake_case dicts).
#_variant_canvases
def _variant_canvases(project: ProjectState, part: dict[str, Any]) -> list[tuple[str, CanvasState]](name, canvas) pairs for a part's variants, sorted by variant_index.
#_part_for_canvas
def _part_for_canvas(project: ProjectState, canvas: CanvasState) -> dict[str, Any] | NoneThe registered part owning a canvas (by part_set + part_name).
#validate_part_registry_for_export
def validate_part_registry_for_export(project: ProjectState) -> list[str]Port of the frontend validatePartRegistryForExport (same wording).
#_validate_frame_uniformity
def _validate_frame_uniformity(project: ProjectState) -> tuple[list[str], dict[str, int] | None]Return (problems, frame_spec). Frame spec is None when problems exist.
All variant canvases of all registered parts must share width/height and origin (cross-set, not just within a set).
#_visible_pixel_layers
def _visible_pixel_layers(canvas: CanvasState) -> list[dict[str, Any]]Effectively-visible pixel layers, bottom-to-top.
Server layers are a flat list; group nesting is honored if present (a layer is visible iff it and every ancestor group are visible).
#_layer_opacity_full
def _layer_opacity_full(layer: dict[str, Any]) -> boolTrue if a layer is at full opacity (server scale is 0.0-1.0).
#_validate_compositing_preconditions
def _validate_compositing_preconditions(project: ProjectState) -> list[str]Section 3.3 preconditions per part / variant canvas / layer.
#_canvas_layer_has_paint
def _canvas_layer_has_paint(canvas: CanvasState, layer: dict[str, Any]) -> boolTrue if any frame of a canvas has a non-transparent pixel on this layer.
#validate_part_export
def validate_part_export(project: ProjectState) -> list[str]All export problems: part-sets + registry + frame uniformity + compositing preconditions (empty list = valid).
#_round_half_up
def _round_half_up(value: float) -> intRound-half-up to mirror JavaScript Math.round for non-negative inputs.
Python's built-in round uses banker's rounding (round-half-to-even), which diverges from JS Math.round (round-half-up) for exact half values such as 1000 / 16 == 62.5. The part-export determinism guarantee (spec 2.5) requires byte-identical manifests and pixel-identical strips across the Python and TS implementations, so both sides must use the same rounding rule. All export call sites operate on non-negative values (frame durations, composited channel/alpha values), so floor(x + 0.5) matches JS Math.round exactly.
#_part_pairings
def _part_pairings(project: ProjectState) -> list[dict[str, str]]Deduplicated part-level pairings, ordered (partA < partB), sorted.
#build_part_manifest
def build_part_manifest(project: ProjectState) -> dict[str, Any]Build the spec-ordered part-manifest dict. Validates first.
#manifest_json_bytes
def manifest_json_bytes(project: ProjectState) -> bytesDeterministic part-manifest.json bytes (2-space indent, trailing \n).
Stamps the format_version gate: this is an at-rest/serialization boundary, and strictspec requires every governed document to carry it.
#_build_index_grid
def _build_index_grid(project: ProjectState, part: dict[str, Any]) -> tuple[bytearray, int, int, int, int]Build the role-index grid for a classed part.
Returns (grid, strip_width, strip_height, cell_w, cell_h). Every pixel is a master-palette index or the 0xFF transparent sentinel.
#_render_rgba_part
def _render_rgba_part(project: ProjectState, part: dict[str, Any]) -> bytesRGBA strip for a pure-rgba part: source-over composite of visible rgba pixel layers per cell (normal blend, opacity applied).
#_source_over
def _source_over(base: bytearray, src: bytes, opacity: float) -> NoneIn-place straight-alpha source-over of src onto base (both wh4).
#render_part_strip
def render_part_strip(project: ProjectState, part_name: str, kind: str) -> bytesRender one strip PNG. Validates the whole export first (422 on problems).
kind is "indexed" or "rgba". Unknown part/kind and indexed-on-rgba-part raise ValueError (mapped to 422 by the caller).
#_strip_files
def _strip_files(project: ProjectState) -> list[tuple[str, bytes]](filename, png_bytes) for every strip, alphabetical by filename.
#export_parts_to_dir
def export_parts_to_dir(project: ProjectState, out_dir: Path) -> list[Path]Write part-manifest.json + all strips into out_dir. Returns written paths.
Overwrites the files it owns; never deletes other files in out_dir.
#export_parts_zip_bytes
def export_parts_zip_bytes(project: ProjectState) -> bytesDeterministic zip: part-manifest.json first, then strips alphabetically, fixed timestamps, ZIP_DEFLATED (part-manifest spec section 2.5).