commit db6129d356f5234a659f27b76d353a1823e56aee Author: jan Date: Tue Feb 20 22:23:20 2024 -0800 initial commit diff --git a/k2t.py b/k2t.py new file mode 100644 index 0000000..cb8abad --- /dev/null +++ b/k2t.py @@ -0,0 +1,334 @@ +from typing import Self, Type, Any +import tomllib +from dataclasses import dataclass, fields +from xml.etree import ElementTree + + +@dataclass(slots=True) +class Entry: + name: str + source: str | None + frame_color: str | None + fill_color: str | None + + members: list | None = None + expanded: bool = False + + dither_pattern: str = 'C1' + line_style: str = 'C0' + frame_brightness: int = 0 + fill_brightness: int = 0 + width: int = 1 + animation: int = 0 + + valid: bool = True + visible: bool = True + transparent: bool = False + marked: bool = False + xfill: bool = False + + @classmethod + def from_xml(cls: Type[Self], el: ElementTree.Element) -> Self: + assert el.tag in ('properties', 'group-members') + + members = [] + args: dict[str, Any] = { + 'frame_color': None, + 'fill_color': None, + 'source': None, + } + for child in el: + if not child.text: + continue + + tag = child.tag + dtag = tag.replace('-', '_') + if tag in ('frame-color', 'fill-color'): + args[dtag] = child.text[1:] + elif tag in ('dither-pattern', 'line-style', 'source', 'name'): + args[dtag] = child.text + elif tag in ('valid', 'visible', 'transparent', 'marked', 'xfill', 'expanded'): + val = { + 'false': False, + 'true': True, + }[child.text.lower()] + args[tag] = val + elif tag in ('frame-brightness', 'fill-brightness', 'width', 'animation'): + args[dtag] = int(child.text) + elif tag == 'group-members': + members.append(Entry.from_xml(child)) + else: + raise Exception(f'Unexpected tag: {child.tag}') + + if not members: + members = None + + return cls(members=members, **args) + + def to_xml(self, is_member: bool = False) -> ElementTree.Element: + main_tag = 'group-members' if is_member else 'properties' + el = ElementTree.Element(main_tag) + for field in fields(self): + val = getattr(self, field.name) + if val is None or field.name == 'members': + continue + + if (val == field.default + and (field not in ('dither-pattern', 'line-style') + or self.source is None + )): + continue + + tag = field.name.replace('_', '-') + if tag in ('frame-color', 'fill-color'): + val = '#' + val + if not isinstance(val, str): + val = str(val).lower() + el2 = ElementTree.Element(tag) + el2.text = val + el.append(el2) + + if self.members: + for member in self.members: + el.append(member.to_xml(is_member=True)) + + return el + + def to_toml(self, depth: int = 0) -> str: + parts = ('layers',) + ('members',) * depth + + s = '[[' + '.'.join(parts) + ']]\n' + for field in fields(self): + val = getattr(self, field.name) + if val == field.default or val is None: + continue + + if isinstance(val, str): + vstr = '"' + toml_escape(val) + '"' + elif isinstance(val, bool): + vstr = str(val).lower() + elif isinstance(val, int): + vstr = str(val) + + key = field.name.replace('_', '-') + s += f'{key} = {vstr}\n' + + s += '\n' + + if self.members: + for member in self.members: + s += member.to_toml(depth + 1) + + return s + + @classmethod + def from_toml(cls: Type[Self], tree: dict) -> Self: + members = [] + args: dict[str, Any] = { + 'frame_color': None, + 'fill_color': None, + 'source': None, + } + for key, val in tree.items(): + if key != 'members': + args[key.replace('-', '_')] = val + + if 'members' in tree: + for child in tree['members']: + members.append(Entry.from_toml(child)) + + if not members: + members = None + + return cls(members=members, **args) + + +@dataclass(slots=True) +class Pattern: + pattern: str # 2D, Nx8 or Nx16 or Nx32 + name: str | None = None + order: int | None = None + + @classmethod + def from_xml(cls: Type[Self], el: ElementTree.Element) -> Self: + assert el.tag == 'custom-dither-pattern' + + lines = [] + args: dict[str, Any] = {} + for child in el: + if not child.text: + continue + + tag = child.tag + if tag == 'name': + args[tag] = child.text + elif tag == 'order': + args[tag] = int(child.text) + elif tag == 'pattern': + for line in child: + assert line.tag == 'line' + assert line.text is not None + lines.append(line.text) + else: + raise Exception(f'Unexpected tag: {tag}') + + if not lines: + raise Exception('No pattern found in custom-dither-pattern!') + pattern = '\n'.join(lines) + + return cls(pattern=pattern, **args) + + def to_xml(self) -> ElementTree.Element: + el = ElementTree.Element('custom-dither-pattern') + if self.name is not None: + el2 = ElementTree.Element('name') + el2.text = self.name + el.append(el2) + if self.order is not None: + el2 = ElementTree.Element('order') + el2.text = str(self.order) + el.append(el2) + + pat = ElementTree.Element('pattern') + for line in self.pattern.strip().split('\n'): + el2 = ElementTree.Element('line') + el2.text = line + pat.append(el2) + el.append(pat) + + return el + + def to_toml(self) -> str: + s = '[[patterns]]\n' + if self.name is not None: + s += f'name = "{toml_escape(self.name)}"\n' + if self.order is not None: + s += f'order = {self.order}\n' + + s += 'pattern = """\n' + s += self.pattern + '"""\n\n' + + return s + + @classmethod + def from_toml(cls: Type[Self], tree: dict) -> Self: + lines = tree['pattern'].split('\n') + n_px = len(lines[0]) + assert all(len(line) == n_px for line in lines[1:]) + return cls(**tree) + + +@dataclass(slots=True) +class LineStyle: + pattern: str # 1D, up to 32 values + name: str | None = None + order: int | None = None + + @classmethod + def from_xml(cls: Type[Self], el: ElementTree.Element) -> Self: + assert el.tag == 'custom-line-style' + + args: dict[str, Any] = {} + for child in el: + if not child.text: + continue + + tag = child.tag + if tag in ('name', 'pattern'): + args[tag] = child.text + elif tag == 'order': + args[tag] = int(child.text) + else: + raise Exception(f'Unexpected tag: {tag}') + return cls(**args) + + def to_xml(self) -> ElementTree.Element: + el = ElementTree.Element('custom-line-style') + if self.name is not None: + el2 = ElementTree.Element('name') + el2.text = self.name + el.append(el2) + if self.order is not None: + el2 = ElementTree.Element('order') + el2.text = str(self.order) + el.append(el2) + el2 = ElementTree.Element('pattern') + el2.text = self.pattern + el.append(el2) + return el + + def to_toml(self) -> str: + s = '[[linestyles]]\n' + if self.name is not None: + s += f'name = "{toml_escape(self.name)}"\n' + if self.order is not None: + s += f'order = {self.order}\n' + s += f'pattern = "{self.pattern}"\n\n' + return s + + @classmethod + def from_toml(cls: Type[Self], tree: dict) -> Self: + assert len(tree['pattern']) <= 32 + return cls(**tree) + + +def k2t(kfile: str, tfile: str) -> None: + tree = ElementTree.parse(kfile) + root = tree.getroot() + + assert root.tag == 'layer-properties' + name = None + entries = [] + patterns = [] + linestyles = [] + for child in root: + if child.tag == 'custom-dither-pattern': + patterns.append(Pattern.from_xml(child)) + elif child.tag == 'custom-line-style': + linestyles.append(LineStyle.from_xml(child)) + elif child.tag == 'properties': + entries.append(Entry.from_xml(child)) + elif child.tag == 'name': + name = child.text + else: + raise Exception(f'Unexpected tag: {child.tag}') + + if name is not None: + print(f'Discarding tab name: {name}') + + with open(tfile, 'wt') as ff: + for entry in entries: + ff.write(entry.to_toml()) + for pattern in patterns: + ff.write(pattern.to_toml()) + for linestyle in linestyles: + ff.write(linestyle.to_toml()) + + +def t2k(tfile: str, kfile: str) -> None: + root = ElementTree.Element('layer-properties') + + with open(tfile, 'rb') as ff: + ttree = tomllib.load(ff) + + entries = [Entry.from_toml(entry) + for entry in ttree['layers']] + patterns = [Pattern.from_toml(pat) + for pat in ttree['patterns']] + linestyles = [LineStyle.from_toml(ls) + for ls in ttree['linestyles']] + + for entry in entries: + root.append(entry.to_xml()) + for pattern in patterns: + root.append(pattern.to_xml()) + for linestyle in linestyles: + root.append(linestyle.to_xml()) + + ktree = ElementTree.ElementTree(root) + ElementTree.indent(ktree) + ktree.write(kfile) + + +def toml_escape(string: str) -> str: + return string.replace('\\', '\\\\').replace('"', '\\"')