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

1"""Time sheet files: XDTS (exchange) and TDTS (Toei Digital Timesheet). 

2 

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. 

7 

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 

13 

14import json 

15 

16from .errors import OperationError 

17from .i18n import tr 

18 

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") 

26 

27NULL_CELL = "SYMBOL_NULL_CELL" 

28HYPHEN = "SYMBOL_HYPHEN" 

29 

30 

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] 

54 

55 

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}" 

61 

62 

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 

82 

83 

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" 

102 

103 

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 

115 

116 

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 } 

128 

129 

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" 

167 

168 

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)