Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ def decompile_pptx(pptx_path: Path, brand_pack_dir: Path,
f"theme {tokens.brand_name}",
"",
]
pic_count = 0
_pic_counter = [0]
for shape in slide.shapes:
x = _scaled(_px(shape.left), scale)
y = _scaled(_px(shape.top), scale)
Expand All @@ -385,7 +385,7 @@ def decompile_pptx(pptx_path: Path, brand_pack_dir: Path,
line = _emit_one(shape, x, y, w, h,
color_index=color_index, style_index=style_index,
assets_dir=assets_dir, slide_idx=slide_idx,
pic_idx_ref=lambda inc=False, _c=[pic_count]:
pic_idx_ref=lambda inc=False, _c=_pic_counter:
(_c.__setitem__(0, _c[0] + 1) if inc else _c[0]))
if line:
lines.append(line)
Expand Down
29 changes: 14 additions & 15 deletions feinschliff-builder/feinschliff_builder/verify/autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,18 +378,18 @@ def _find_smaller_layout(
if not candidates:
return None

try:
from feinschliff.layout_discovery import find_layout
except ImportError:
return None

current_name = _layout_name(current_layout_rel)
for c in candidates:
layout_id = c["layout"]
if layout_id == current_name:
continue # same as current
# Resolve path
rel = f"layouts/{layout_id}.slide.dsl"
# Verify the layout file exists (layouts live in the core plugin)
_builder_root = Path(__file__).resolve().parents[2]
_core_root = _builder_root.parent / "feinschliff"
if (_core_root / rel).is_file():
return rel
if find_layout(layout_id) is not None:
return f"layouts/{layout_id}.slide.dsl"
return None


Expand Down Expand Up @@ -426,19 +426,18 @@ def _find_larger_layout(
if not candidates:
return None

try:
from feinschliff.layout_discovery import find_layout
except ImportError:
return None

current_name = _layout_name(current_layout_rel)
for c in candidates:
layout_id = c["layout"]
if layout_id == current_name:
continue
rel = f"layouts/{layout_id}.slide.dsl"
# Layouts live in the core feinschliff plugin, which is siblings to
# this builder plugin. Resolve relative to this file's package root.
_builder_root = Path(__file__).resolve().parents[2]
_core_root = _builder_root.parent / "feinschliff"
layout_path = _core_root / rel
if layout_path.is_file():
return rel
if find_layout(layout_id) is not None:
return f"layouts/{layout_id}.slide.dsl"
return None


Expand Down
14 changes: 11 additions & 3 deletions feinschliff-builder/feinschliff_builder/verify/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import hashlib
import json
import tempfile
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Literal
Expand Down Expand Up @@ -64,10 +65,17 @@ def put(self, verdict: CachedVerdict) -> None:
self._data[key] = asdict(verdict)

def save(self) -> None:
self._path.write_text(
json.dumps(self._data, indent=2, ensure_ascii=False),
encoding="utf-8",
payload = json.dumps(self._data, indent=2, ensure_ascii=False)
tmp_fd, tmp_name = tempfile.mkstemp(
dir=self._path.parent, prefix=".verify_cache_", suffix=".tmp"
)
try:
with open(tmp_fd, "w", encoding="utf-8") as fh:
fh.write(payload)
Path(tmp_name).replace(self._path)
except BaseException:
Path(tmp_name).unlink(missing_ok=True)
raise

# ------------------------------------------------------------------
# Internal
Expand Down
4 changes: 2 additions & 2 deletions feinschliff-builder/feinschliff_builder/verify/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,8 @@ def _flatten_content_keys(ctx: dict, prefix: str = "") -> dict[str, str]:
elif isinstance(ctx, list):
for item in ctx:
out.update(_flatten_content_keys(item, f"{prefix}[]"))
elif isinstance(ctx, str):
out[prefix] = ctx
elif ctx is not None:
out[prefix] = str(ctx)
return out


Expand Down
2 changes: 1 addition & 1 deletion feinschliff/feinschliff/brand_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def discover_brands() -> list[BrandPack]:
try:
resolved = load_tokens(d, brands_dir=root)
ip = resolved.raw.get("$image_provider") if isinstance(resolved.raw, dict) else None
if isinstance(ip, dict):
if isinstance(ip, dict) and "kind" in ip:
image_provider_config = ip
except (OSError, ValueError, JSONDecodeError) as exc:
warnings.warn(
Expand Down
2 changes: 1 addition & 1 deletion feinschliff/feinschliff/deck/orchestrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ def _bundled_compounds() -> Path:
local_compounds = dict(compounds)
for cd in layout_compounds:
local_compounds[cd.name] = cd
interp = interpolate_nodes(layout_nodes, entry["content"])
interp = interpolate_nodes(layout_nodes, entry.get("content") or {})
interp = expand_diagram_blocks(
interp,
brand_dir=brand_dir,
Expand Down
6 changes: 4 additions & 2 deletions feinschliff/feinschliff/dsl/expander.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,10 @@ def expand_diagram_blocks(
# body is authored in WxH coords and the renderer rasterizes at WxH.
# PowerPoint downscales on insert. When absent, the slot IS the canvas
# (legacy behavior, preserved bit-for-bit).
virtual_w: int = n.kw_args.get("virtual_w") or w # type: ignore[assignment]
virtual_h: int = n.kw_args.get("virtual_h") or h # type: ignore[assignment]
_vw = n.kw_args.get("virtual_w")
virtual_w: int = _vw if _vw is not None else w # type: ignore[assignment]
_vh = n.kw_args.get("virtual_h")
virtual_h: int = _vh if _vh is not None else h # type: ignore[assignment]

# Resolve body: inline string or external file.
body: str = n.kw_args.get("body") or "" # type: ignore[assignment]
Expand Down
7 changes: 6 additions & 1 deletion feinschliff/feinschliff/dsl/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,12 @@ def load_tokens(brand_root: Path, *, brands_dir: Path | None = None) -> Tokens:
for b in reversed(chain):
tj = b / "tokens.json"
if tj.is_file():
data = json.loads(tj.read_text())
try:
data = json.loads(tj.read_text())
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
raise ValueError(
f"tokens.json in brand '{b.name}' is not valid JSON: {exc}"
) from exc
# `$image_provider` semantics: when the child swaps `kind`, the
# parent's `config` must NOT carry over (it was scoped to a
# different provider). Drop merged's `config` before deep-merge
Expand Down
2 changes: 2 additions & 0 deletions feinschliff/feinschliff/io/image_preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ def _extract_dominant_colors(img: Image.Image, n: int = 3) -> list[tuple[int, in
"""
rgb_img = img.convert("RGB")
total_pixels = rgb_img.width * rgb_img.height
if total_pixels == 0:
return []

# Quantize to 16 colours and collect (count, palette_index) pairs.
# PIL getcolors() returns (count, value) tuples — value is the palette
Expand Down