Skip to content

Commit 71bbb94

Browse files
committed
Sanitize product and view names before using them as paths
A product collection named "JNR 12 BICON " (trailing space, straight off the STEP filename) made every render fail with "cannot save". Windows strips trailing spaces and dots when it creates a directory entry, so os.makedirs quietly produced "CC_11_JNR 12 BICON" while the render path still pointed at "CC_11_JNR 12 BICON \...", and that directory genuinely does not exist. The mismatch is invisible from Python: os.path.isdir strips the same trailing space on the way in and reports the folder as present. Route the directory and file names through safe_name(), which trims trailing spaces and dots, replaces characters Windows rejects, and sidesteps the reserved device names. The label prefix already keeps each product in its own folder, so names that clean up to the same text still cannot collide.
1 parent 87e6704 commit 71bbb94

1 file changed

Lines changed: 34 additions & 2 deletions

File tree

‎multiview_product_renderer/__init__.py‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,38 @@ def product_label(start_prefix, start_number, offset, per_letter):
675675
return f"{alpha_pair(base + bumps)}_{num:02d}"
676676

677677

678+
# Characters Windows refuses outright, plus the separators that would quietly
679+
# redirect a write into some other directory.
680+
BAD_PATH_CHARS = '<>:"/\\|?*'
681+
RESERVED_NAMES = (
682+
{"CON", "PRN", "AUX", "NUL"}
683+
| {f"COM{i}" for i in range(1, 10)}
684+
| {f"LPT{i}" for i in range(1, 10)}
685+
)
686+
687+
688+
def safe_name(name, fallback="unnamed"):
689+
"""A collection or view name reduced to one usable path component.
690+
691+
These names are free text and usually arrive from a STEP filename, so they
692+
carry whatever the CAD export happened to contain. A trailing space is the
693+
one that bites: asked for 'BICON ', Windows quietly creates 'BICON', then
694+
refuses to write 'BICON \\shot.png' because that directory really does not
695+
exist, and the render dies on the first product with 'cannot save'. The
696+
mismatch hides well, because os.path.isdir strips the same trailing space
697+
on the way in and cheerfully agrees the folder is there.
698+
"""
699+
cleaned = "".join(
700+
"_" if c in BAD_PATH_CHARS or ord(c) < 32 else c for c in str(name)
701+
)
702+
# The name we ask for has to match the name the filesystem will hand back,
703+
# or every later write into it misses.
704+
cleaned = cleaned.strip().rstrip(" .")
705+
if cleaned.split(".")[0].upper() in RESERVED_NAMES:
706+
cleaned = f"_{cleaned}"
707+
return cleaned or fallback
708+
709+
678710
STEPPER_OPERATORS = (
679711
"import_scene.occ_import_step",
680712
"import_scene.occ",
@@ -1607,13 +1639,13 @@ def execute(self, context):
16071639
lc.exclude = (other.name != pc.name)
16081640

16091641
label = product_label(s.start_prefix, s.start_number, i, s.products_per_letter)
1610-
product_dir = os.path.join(out_root, f"{label}_{pc.name}")
1642+
product_dir = os.path.join(out_root, safe_name(f"{label}_{pc.name}"))
16111643
os.makedirs(product_dir, exist_ok=True)
16121644

16131645
for cam in cameras:
16141646
scene.camera = cam
16151647
view_name = cam.name.replace("MV_Cam_", "")
1616-
base = f"{label}_{pc.name}_{view_name}"
1648+
base = safe_name(f"{label}_{pc.name}_{view_name}")
16171649

16181650
if mode == 'FULL':
16191651
_do_full_render(scene, os.path.join(product_dir, base))

0 commit comments

Comments
 (0)