Created
June 24, 2021 19:03
-
-
Save quag/e219f69670cd395d4a59a392557df284 to your computer and use it in GitHub Desktop.
A partial Treesheets cts file parser in python
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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