335 lines
9.8 KiB
Python
335 lines
9.8 KiB
Python
|
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('"', '\\"')
|