#!/usr/bin/env python3 """Validate a VisiCut settings directory against VisiCut's own conventions. Mirrors de.thomas_oster.visicut.misc.Helper.toPathName and LaserPropertyManager.getLaserPropertiesFile. """ import os, sys, re import xml.etree.ElementTree as ET ALLOWED = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-") def to_path_name(name): return "".join(c if c in ALLOWED else "_%d_" % ord(c) for c in name) def jfloat(x): """Java Float.toString for the values we care about.""" f = float(x) s = repr(f) return s ROOT = sys.argv[1] if len(sys.argv) > 1 else "." problems = [] def P(cat, msg): problems.append((cat, msg)) # ---- profiles ------------------------------------------------------------- profiles = {} for fn in sorted(os.listdir(os.path.join(ROOT, "profiles"))): p = os.path.join(ROOT, "profiles", fn) try: root = ET.parse(p).getroot() except ET.ParseError as e: P("xml", "profiles/%s: malformed XML: %s" % (fn, e)); continue name = root.findtext("name") profiles[name] = root.tag if to_path_name(name) + ".xml" != fn: P("name", "profiles/%s should be named %s.xml (name=%r)" % (fn, to_path_name(name), name)) # Expected XML element per profile kind. LaserPropertyManager only registers XStream # aliases for PowerSpeedFocus[Frequency]Property, FloatPowerSpeedFocusFrequencyProperty and # LaosCutterProperty -- any other short element name fails to deserialize. The Epilog driver # wants an EpilogEngraveProperty for raster parts; VisiCut converts the PowerSpeedFocusProperty # it reads here into one, copying power/speed/focus. EXPECT = { "vectorProfile": "PowerSpeedFocusFrequencyProperty", "rasterProfile": "PowerSpeedFocusProperty", "raster3dProfile":"PowerSpeedFocusProperty", } # ---- materials ------------------------------------------------------------ materials = {} # name -> dict for fn in sorted(os.listdir(os.path.join(ROOT, "materials"))): if not fn.endswith(".xml"): continue p = os.path.join(ROOT, "materials", fn) try: root = ET.parse(p).getroot() except ET.ParseError as e: P("xml", "materials/%s: MALFORMED XML - VisiCut cannot load this file: %s" % (fn, e)); continue name = root.findtext("name") th = [float(x.text) for x in root.findall("materialThicknesses/float")] expect_fn = to_path_name(name) + ".xml" if expect_fn != fn: P("name", "materials/%s: VisiCut expects %s (name=%r)" % (fn, expect_fn, name)) if name in materials: P("dup", "duplicate material name %r (%s and %s)" % (name, materials[name]["file"], fn)) materials[name] = {"file": fn, "th": th, "dir": to_path_name(name), "thumb": root.findtext("thumbnailPath")} if th != sorted(th): P("order", "materials/%s: thicknesses not sorted: %s" % (fn, th)) if len(th) != len(set(th)): P("dup", "materials/%s: duplicate thicknesses %s" % (fn, th)) bydir = {} for n, m in materials.items(): bydir.setdefault(m["dir"], []).append(n) # ---- laserprofiles -------------------------------------------------------- lp_root = os.path.join(ROOT, "laserprofiles") covered = set() for dev in sorted(os.listdir(lp_root)): devdir = os.path.join(lp_root, dev) if not os.path.isdir(devdir): continue for mdir in sorted(os.listdir(devdir)): mpath = os.path.join(devdir, mdir) if not os.path.isdir(mpath): continue mnames = bydir.get(mdir) if not mnames: P("orphan", "laserprofiles/%s/%s/: no material has this encoded name " "-> invisible in VisiCut (would decode to %r)" % (dev, mdir, mdir)) continue m = materials[mnames[0]] for tdir in sorted(os.listdir(mpath)): tpath = os.path.join(mpath, tdir) if not os.path.isdir(tpath): continue if not tdir.endswith("mm"): P("thick", "laserprofiles/%s/%s/%s: not a mm folder" % (dev, mdir, tdir)); continue tval = tdir[:-2] if not any(jfloat(t) == tval for t in m["th"]): P("thick", "laserprofiles/%s/%s/%s: thickness not listed in materials/%s (%s)" % (dev, mdir, tdir, m["file"], ", ".join(jfloat(t) for t in m["th"]))) else: covered.add((dev, mnames[0], tval)) for pf in sorted(os.listdir(tpath)): if not pf.endswith(".xml"): continue pname = None for n in profiles: if to_path_name(n) + ".xml" == pf: pname = n if pname is None: P("orphan", "laserprofiles/%s/%s/%s/%s: no matching profiles/*.xml entry" % (dev, mdir, tdir, pf)) continue f = os.path.join(tpath, pf) try: r = ET.parse(f).getroot() except ET.ParseError as e: P("xml", "%s: malformed XML: %s" % (f, e)); continue want = EXPECT[profiles[pname]] for child in r: if child.tag != want: P("class", "laserprofiles/%s/%s/%s/%s: <%s> but profile %r is a %s -> needs <%s>" % (dev, mdir, tdir, pf, child.tag, pname, profiles[pname], want)) for k, lo, hi in (("power",0,100), ("speed",1,100), ("frequency",10,5000)): v = child.findtext(k) if v is None: continue if not (lo <= float(v) <= hi): P("range", "laserprofiles/%s/%s/%s/%s: %s=%s out of range %d..%d" % (dev, mdir, tdir, pf, k, v, lo, hi)) # ---- coverage ------------------------------------------------------------- for dev in sorted(os.listdir(lp_root)): if not os.path.isdir(os.path.join(lp_root, dev)): continue for n, m in sorted(materials.items()): for t in m["th"]: if (dev, n, jfloat(t)) not in covered: P("missing", "%s / %r / %smm: material offers this thickness but there are no laser settings" % (dev, n, jfloat(t))) # ---- thumbnails ----------------------------------------------------------- for n, m in sorted(materials.items()): auto = os.path.join(ROOT, "materials", m["dir"] + ".png") if m["thumb"]: ref = os.path.join(ROOT, "materials", m["thumb"]) if not os.path.exists(ref): P("thumb", "materials/%s: thumbnailPath %r does not exist" % (m["file"], m["thumb"])) elif not os.path.exists(auto): P("thumb", "materials/%s: no thumbnail (expected materials/%s.png)" % (m["file"], m["dir"])) order = ["xml","name","dup","class","range","orphan","thick","missing","order","thumb"] for cat in order: rows = [m for c, m in problems if c == cat] if rows: print("\n## %s (%d)" % (cat.upper(), len(rows))) for r in rows: print(" - " + r) print("\nTOTAL: %d problems, %d materials, %d profiles" % (len(problems), len(materials), len(profiles))) sys.exit(1 if problems else 0)