Coverage for paintmaskanimator/timesheet_file.py: 91%
69 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 04:50 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-11-02 04:50 +0000
1"""Time sheet files: XDTS (exchange) and TDTS (Toei Digital Timesheet).
3Both are a one-line signature followed by JSON, and share the time-table
4structure (``duration`` / ``timeTableHeaders`` / ``fields`` → ``tracks`` →
5``frames``). TDTS wraps the tables in ``timeSheets[0]`` together with a sheet
6header (作品情報・作業伝票) and adds display settings to each table.
8A track lists only the frames where the instruction changes; a frame with no
9entry continues the previous one. The sheet ends with a ``SYMBOL_NULL_CELL``
10at ``frame == duration``.
11"""
12from __future__ import annotations
14import json
16from .errors import OperationError
17from .i18n import tr
19XDTS_SIGNATURE = "exchangeDigitalTimeSheet Save Data"
20TDTS_SIGNATURE = "toeiDigitalTimeSheet Save Data"
21XDTS_VERSION = 5
22TDTS_VERSION = 11
23#: ``.xtds`` is a common misspelling that some tools write.
24SUFFIXES = (".xdts", ".xtds", ".tdts")
25FORMATS = ("xdts", "tdts")
27NULL_CELL = "SYMBOL_NULL_CELL"
28HYPHEN = "SYMBOL_HYPHEN"
31def load_time_table(raw_text):
32 """The first time table of an XDTS or TDTS file, as a dict."""
33 text = str(raw_text or "").lstrip("")
34 signature, _, body = text.partition("\n")
35 signature = signature.strip()
36 if signature not in (XDTS_SIGNATURE, TDTS_SIGNATURE):
37 raise OperationError(tr("XDTS/TDTSの先頭識別文字列が一致しません。"))
38 try:
39 payload = json.loads(body)
40 except json.JSONDecodeError as exc:
41 raise OperationError(tr("タイムシートのJSONを解析できません。\n{exc}").format(exc=exc)) from exc
42 if not isinstance(payload, dict): 42 ↛ 43line 42 didn't jump to line 43 because the condition on line 42 was never true
43 raise OperationError(tr("タイムシート情報がありません。"))
44 if signature == XDTS_SIGNATURE:
45 if int(payload.get("version", -1)) != XDTS_VERSION: 45 ↛ 46line 45 didn't jump to line 46 because the condition on line 45 was never true
46 raise OperationError(tr("対応しているXDTSバージョンは5です。"))
47 tables = payload.get("timeTables") or []
48 else:
49 sheets = payload.get("timeSheets") or []
50 tables = (sheets[0].get("timeTables") or []) if sheets and isinstance(sheets[0], dict) else []
51 if not tables or not isinstance(tables[0], dict): 51 ↛ 52line 51 didn't jump to line 52 because the condition on line 51 was never true
52 raise OperationError(tr("タイムシート情報がありません。"))
53 return tables[0]
56def format_label(raw_text):
57 """``"TDTS version 11"`` or ``"XDTS version 5"``, for display."""
58 if str(raw_text or "").lstrip("\ufeff").startswith(TDTS_SIGNATURE):
59 return f"TDTS version {TDTS_VERSION}"
60 return f"XDTS version {XDTS_VERSION}"
63def _frames(values, *, sparse):
64 """Frame entries for one track; *values* has one instruction per frame."""
65 frames = []
66 previous = None
67 for frame, value in enumerate(values):
68 if sparse and (value == HYPHEN or (value == NULL_CELL and previous == NULL_CELL)):
69 continue
70 entry = {"id": 0, "values": [value]}
71 if sparse:
72 entry = {"fontColorId": 0, **entry}
73 frames.append({"frame": frame, "data": [entry]})
74 if value != HYPHEN:
75 previous = value
76 if sparse and previous != NULL_CELL:
77 frames.append({
78 "frame": len(values),
79 "data": [{"fontColorId": 0, "id": 0, "values": [NULL_CELL]}],
80 })
81 return frames
84def xdts_text(name, layer_names, tracks, duration):
85 """XDTS v5 text. *tracks* holds one instruction per frame for each layer."""
86 payload = {
87 "timeTables": [{
88 "duration": duration,
89 "name": name,
90 "timeTableHeaders": [{"fieldId": 0, "names": list(layer_names)}],
91 "fields": [{
92 "fieldId": 0,
93 "tracks": [
94 {"trackNo": index, "frames": _frames(values, sparse=False)}
95 for index, values in enumerate(tracks)
96 ],
97 }],
98 }],
99 "version": XDTS_VERSION,
100 }
101 return XDTS_SIGNATURE + "\n" + json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
104# The defaults Toei's application writes for an empty sheet; the other
105# fields' header names are what it shows for those columns.
106_FONT_COLORS = [[0, 0, 0], [224, 0, 0], [32, 128, 32], [32, 32, 192], [192, 32, 192], [255, 128, 32]]
107_DEFAULT_HEADERS = [
108 {"fieldId": 1, "names": [""]},
109 {"fieldId": 2, "names": [""]},
110 {"fieldId": 3, "names": ["S1", "S2"]},
111 {"fieldId": 4, "names": ["a", "b", "c", "d", "e", "f", "g"]},
112 {"fieldId": 5, "names": ["1", "2"]},
113]
114_DUMMY_KOMAS = 24
117def _work_slip():
118 process = {"assistant": "", "chief": "", "director": "", "free": "", "freeRole": ""}
119 track = {"artist": "", "numberOfSheets": 0}
120 return {
121 "memo": "",
122 "processes": [dict(process) for _ in range(3)],
123 "works": [
124 {"enable": False, "tracks": [dict(track) for _ in range(7)]}
125 for _ in range(2)
126 ],
127 }
130def tdts_text(name, layer_names, tracks, duration, *, cut="", episode="", scene=""):
131 """TDTS v11 text, laid out as Toei's application saves it."""
132 payload = {
133 "timeSheets": [{
134 "free": [],
135 "header": {
136 "cut": str(cut),
137 "episode": str(episode),
138 "scene": str(scene),
139 "showHeadDummy": False,
140 "timeTableFontColors": [list(color) for color in _FONT_COLORS],
141 "workSlip": _work_slip(),
142 },
143 "timeTables": [{
144 "color": 0,
145 "direction": "",
146 "duration": duration,
147 "fields": [{
148 "fieldId": 0,
149 "tracks": [
150 {"frames": _frames(values, sparse=True), "trackNo": index}
151 for index, values in enumerate(tracks)
152 ],
153 }],
154 "footDummykomas": _DUMMY_KOMAS,
155 "headDummykomas": _DUMMY_KOMAS,
156 "name": name,
157 "operatorName": "",
158 "timeTableHeaders": [
159 {"fieldId": 0, "names": list(layer_names)},
160 *[dict(header, names=list(header["names"])) for header in _DEFAULT_HEADERS],
161 ],
162 }],
163 }],
164 "version": TDTS_VERSION,
165 }
166 return TDTS_SIGNATURE + "\n" + json.dumps(payload, ensure_ascii=False, indent=4) + "\n"
169def sheet_text(fmt, name, layer_names, tracks, duration, **header):
170 """The time sheet in *fmt* (``"xdts"`` or ``"tdts"``)."""
171 if fmt == "tdts":
172 return tdts_text(name, layer_names, tracks, duration, **header)
173 return xdts_text(name, layer_names, tracks, duration)