Skip to content

Instantly share code, notes, and snippets.

@quag
Created June 24, 2021 19:03
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save quag/e219f69670cd395d4a59a392557df284 to your computer and use it in GitHub Desktop.
Save quag/e219f69670cd395d4a59a392557df284 to your computer and use it in GitHub Desktop.
A partial Treesheets cts file parser in python
from __future__ import annotations
import datetime
import enum
from dataclasses import dataclass, field
from typing import Iterator, Optional
class CT(enum.Enum):
DATA = 0
CODE = 1
VARD = 2
VIEWH = 3
VARU = 4
VIEWV = 5
class DS(enum.Enum):
GRID = 0
BLOBSHIER = 1
BLOBLINE = 2
class TS(enum.Enum):
TEXT = 0
GRID = 1
BOTH = 2
NEITHER = 3
class Style(enum.Flag):
Bold = 1
Italic = 2
Fixed = 4
Underline = 8
Strikethru = 16
@dataclass
class Color:
value: int
def __str__(self):
return f"#{self.value:0>6X}"
def __repr__(self):
return f"Color(0x{self.value:0>6X})"
@dataclass
class Document:
cell: Cell
tags: set[str]
@dataclass
class Cell:
celltype: CT = field(default=CT.DATA)
cellcolor: Color = field(default=Color(0xFFFFFF))
textcolor: Color = field(default=Color(0x000000))
drawstyle: DS = field(default=DS.GRID)
text: Optional[Text] = field(default=None)
grid: Optional[Grid] = field(default=None)
@dataclass
class Grid:
xs: int = field(default=0)
ys: int = field(default=0)
bordercolor: Color = field(default=Color(0xA0A0A0))
user_grid_outer_spacing: int = field(default=3)
verticaltextandgrid: bool = field(default=True)
folded: bool = field(default=False)
colwidths: list[int] = field(default_factory=list)
cells: list[Cell] = field(default_factory=list)
def rowIter(self) -> Iterator[Iterator[Cell]]:
for i in range(0, len(self.cells), self.xs):
yield (self.cells[j] for j in range(i, i + self.xs))
@dataclass
class Text:
t: str = field(default="")
relsize: int = field(default=0)
stylebits: Style = field(default=Style(0))
extent: int = field(default=0)
imageId: Optional[int] = field(default=None)
lastedit: datetime.datetime = field(default=datetime.datetime.now())
filtered: bool = field(default=False)
def bold(self) -> bool:
return Style.Bold in self.stylebits
def italic(self) -> bool:
return Style.Italic in self.stylebits
def fixed(self) -> bool:
return Style.Fixed in self.stylebits
def underline(self) -> bool:
return Style.Underline in self.stylebits
def strikethru(self) -> bool:
return Style.Strikethru in self.stylebits
def fontPercentage(self) -> int:
return self.relsize * -10 + 100
from __future__ import annotations
import datetime
import zlib
from dataclasses import dataclass
from . import model
__all__ = "load", "CtsDecodeError"
class CtsDecodeError(ValueError):
...
def load(fp):
buff = fp.read()
version, compressedData = readHeader(Cursor(memoryview(buff)))
view = memoryview(zlib.decompress(compressedData))
cur = Cursor(view)
document = readDocument(version, cur)
rest = cur.read()
if len(rest) != 0:
raise CtsDecodeError(rest)
return document
def readHeader(cur):
if cur.buf(4) != b"TSFF":
raise CtsDecodeError("File does not start with “TSFF”")
version = cur.u8()
if version > 19:
raise CtsDecodeError(f"Unsupported version ({version})")
if cur.buf(1) != b"D":
raise CtsDecodeError("Images not supported yet")
return version, cur.read()
def readDocument(version, cur) -> model.Document:
cell = readCell(version, cur)
tags = readTags(version, cur)
return model.Document(cell, tags)
def readCell(version, cur) -> model.Cell:
cell = model.Cell()
cell.celltype = model.CT(cur.u8())
if version >= 8:
cell.cellcolor = cur.color()
cell.textcolor = cur.color()
if version >= 15:
cell.drawstyle = model.DS(cur.u8())
ts_type = model.TS(cur.u8())
if ts_type not in (
model.TS.TEXT,
model.TS.GRID,
model.TS.BOTH,
model.TS.NEITHER,
):
raise CtsDecodeError(f"Unknown TS_TYPE ({ts_type})")
if ts_type in (model.TS.TEXT, model.TS.BOTH):
cell.text = readText(version, cur)
if ts_type in (model.TS.GRID, model.TS.BOTH):
cell.grid = readGrid(version, cur)
return cell
def readText(version, cur) -> model.Text:
text = model.Text()
text.t = cur.string()
if version <= 11:
_ = cur.u32() # numlines
text.relsize = cur.i32()
text.imageId = cur.u32()
if text.imageId == 0xFFFFFFFF:
text.imageId = None
if version >= 7:
text.stylebits = model.Style(cur.u32())
if version >= 14:
text.lastedit = cur.timestamp()
else:
text.lastedit = datetime.datetime.now()
return text
def readGrid(version, cur) -> model.Grid:
grid = model.Grid()
grid.xs = cur.u32()
grid.ys = cur.u32()
if version >= 10:
grid.bordercolor = cur.color()
grid.user_grid_outer_spacing = cur.u32()
if version >= 11:
grid.verticaltextandgrid = cur.bool()
if version >= 16:
grid.folded = cur.bool()
if grid.folded and version <= 17:
# // Before v18, folding would use the image slot. So if this cell/ contains an image, clear it.
# TODO: clear the parent cell's text.image field.
...
if version >= 13:
grid.colwidths = [cur.u32() for i in range(grid.xs)]
for y in range(grid.ys):
for x in range(grid.xs):
cell = readCell(version, cur)
grid.cells.append(cell)
return grid
def readTags(version, cur) -> set[str]:
tags = set()
if version >= 11:
while True:
tag = cur.string()
if len(tag) == 0:
break
tags.add(tag)
return tags
@dataclass
class Cursor:
view: memoryview
def read(self, n: int = None) -> memoryview:
if n is None:
result = self.view
self.view = memoryview(b"")
else:
result = self.view[:n]
self.view = self.view[n:]
return result
def buf(self, n: int) -> memoryview:
result = self.read(n)
if len(result) != n:
raise CtsDecodeError("EOF")
else:
return result
def u8(self) -> int:
return self.buf(1)[0]
def u32(self) -> int:
return self.buf(4).cast("I")[0]
def i32(self) -> int:
return self.buf(4).cast("i")[0]
def u64(self) -> int:
return self.buf(8).cast("Q")[0]
def color(self) -> model.Color:
return model.Color(self.u32())
def string(self) -> str:
length = self.u32()
return self.buf(length).tobytes().decode("utf-8")
def bool(self) -> bool:
return self.u8() != 0
def timestamp(self) -> datetime.datetime:
milliseconds = self.u64()
seconds, milliseconds = divmod(milliseconds, 1000)
return datetime.datetime.fromtimestamp(seconds) + datetime.timedelta(
milliseconds=milliseconds
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment