turbine-lib 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,382 @@
1
+ from subfile import Subfile, FILE_TYPE
2
+ from binarydata import BinaryData
3
+ from subfiledata import SubfileData
4
+ import textutils
5
+
6
+
7
+ class TextFragment:
8
+ """Represents a text fragment in a text subfile"""
9
+
10
+ def __init__(self):
11
+ self.fragment_id = 0
12
+ self.text = ""
13
+ self.args = ""
14
+
15
+ def __lt__(self, other):
16
+ return self.fragment_id < other.fragment_id
17
+
18
+
19
+ class TextSubfile(Subfile):
20
+ """Implementation of Subfile for TEXT type files"""
21
+
22
+ @staticmethod
23
+ def build_for_export(file_data):
24
+ result = SubfileData()
25
+ offset = 9 # first 4 bytes - file_id, then 4 bytes - unknown, then 1 byte - unknown
26
+
27
+ text_fragment_num = file_data.to_number(1, offset)
28
+ if (text_fragment_num & 0x80) != 0:
29
+ text_fragment_num = (((text_fragment_num ^ 0x80) << 8) | file_data[offset + 1])
30
+ offset += 1
31
+ offset += 1
32
+
33
+ for i in range(text_fragment_num):
34
+ fragment_id = file_data.to_number(8, offset)
35
+ offset += 8
36
+
37
+ # Making pieces
38
+ pieces, offset = TextSubfile.make_pieces(file_data, offset)
39
+ text = "["
40
+ for j in range(len(pieces) - 1):
41
+ text += pieces[j] + "<--DO_NOT_TOUCH!-->"
42
+ if pieces:
43
+ text += pieces[-1]
44
+ text += "]"
45
+
46
+ # Making argument references
47
+ arg_refs, offset = TextSubfile.make_argument_references(file_data, offset)
48
+ arguments = ""
49
+ for j in range(len(arg_refs) - 1):
50
+ arguments += textutils.to_utf16(arg_refs[j]) + "-"
51
+ if arg_refs:
52
+ arguments += textutils.to_utf16(arg_refs[-1])
53
+
54
+ # Through argument strings are not used, we need to call this function to correctly move offset
55
+ _result, offset = TextSubfile.make_argument_strings(file_data, offset)
56
+
57
+ if len(result.text_data) > 0:
58
+ result.text_data += "|||"
59
+
60
+ result.text_data += textutils.to_utf16(fragment_id) + ":::"
61
+ result.text_data += arguments + ":::"
62
+ result.text_data += text
63
+
64
+ result.options = {"ext": ".txt"}
65
+ return result
66
+
67
+ @staticmethod
68
+ def build_for_import(old_data, data):
69
+ new_file_data = BinaryData()
70
+ offset = 9 # first 8 bytes - file_info. After them:
71
+ # first 4 bytes - file_id, then 4 bytes - unknown, then 1 byte - unknown
72
+
73
+ text_fragment_num = old_data.to_number(1, offset)
74
+ if (text_fragment_num & 0x80) != 0:
75
+ text_fragment_num = (((text_fragment_num ^ 0x80) << 8) | old_data[offset + 1])
76
+ offset += 1
77
+ offset += 1
78
+
79
+ # Adding file info
80
+ new_file_data = new_file_data + old_data.cut_data(0, offset)
81
+
82
+ patch_fragments = TextSubfile.parse_patch_fragments(data)
83
+
84
+ for i in range(text_fragment_num):
85
+ fragment_id = old_data.to_number(8, offset)
86
+ offset += 8
87
+
88
+ new_file_data = new_file_data + BinaryData.from_number(8, fragment_id)
89
+
90
+ id_comp = TextFragment()
91
+ id_comp.fragment_id = fragment_id
92
+
93
+ # Find matching fragment
94
+ fragment_iterator = None
95
+ for frag in patch_fragments:
96
+ if frag.fragment_id == id_comp.fragment_id:
97
+ fragment_iterator = frag
98
+ break
99
+
100
+ if fragment_iterator is None:
101
+ # Retrieving old pieces
102
+ result_data, offset = TextSubfile.get_piece_data(old_data, offset)
103
+ new_file_data = new_file_data + result_data
104
+ # Retrieving old references
105
+ result_data, offset = TextSubfile.get_argument_reference_data(old_data, offset)
106
+ new_file_data = new_file_data + result_data
107
+ # Retrieving old ref_strings
108
+ result_data, offset = TextSubfile.get_argument_strings_data(old_data, offset)
109
+ new_file_data = new_file_data + result_data
110
+ else:
111
+ # Making and adding new pieces
112
+ result_data, offset = TextSubfile.build_pieces(old_data, fragment_iterator, offset)
113
+ new_file_data = new_file_data + result_data
114
+ # Making and adding new references
115
+ result_data, offset = TextSubfile.build_argument_references(old_data, fragment_iterator, offset)
116
+ new_file_data = new_file_data + result_data
117
+ # Making and adding new strings
118
+ result_data, offset = TextSubfile.build_argument_strings(old_data, fragment_iterator, offset)
119
+ new_file_data = new_file_data + result_data
120
+
121
+ # Adding elapsed file data
122
+ new_file_data = new_file_data + old_data.cut_data(offset)
123
+
124
+ return new_file_data
125
+
126
+ @staticmethod
127
+ def parse_patch_fragments(data):
128
+ result = []
129
+ pointer = 0
130
+
131
+ text_data = data.text_data
132
+ while pointer < len(text_data):
133
+ # Parsing fragment_id
134
+ pointer1 = text_data.find(":::", pointer)
135
+ if pointer1 == -1:
136
+ break
137
+
138
+ fragment_id = textutils.from_utf16(text_data[pointer:pointer1])
139
+ pointer = pointer1 + 3
140
+
141
+ fragment = TextFragment()
142
+ fragment.fragment_id = fragment_id
143
+
144
+ # Parsing arguments
145
+ pointer1 = text_data.find(":::", pointer)
146
+ if pointer1 == -1:
147
+ break
148
+
149
+ arguments = text_data[pointer:pointer1]
150
+ pointer = pointer1 + 3
151
+
152
+ if len(arguments) > 0:
153
+ fragment.args = textutils.arguments_from_utf16(arguments)
154
+
155
+ # Parsing text
156
+ pointer1 = text_data.find("|||", pointer)
157
+ if pointer1 == -1:
158
+ pointer1 = len(text_data)
159
+
160
+ fragment.text = text_data[pointer:pointer1]
161
+ pointer = pointer1 + 3
162
+
163
+ result.append(fragment)
164
+
165
+ result.sort(key=lambda x: x.fragment_id)
166
+ return result
167
+
168
+ @staticmethod
169
+ def make_pieces(data, offset):
170
+ # This is a simplified implementation - would need to be expanded based on actual C++ code
171
+ result = []
172
+
173
+ num_pieces = data.to_number(4, offset)
174
+ offset += 4
175
+
176
+ for j in range(num_pieces):
177
+ piece_size = data.to_number(1, offset)
178
+ if (piece_size & 128) != 0:
179
+ piece_size = (((piece_size ^ 128) << 8) | data[offset + 1])
180
+ offset += 1
181
+ offset += 1
182
+
183
+ piece_data = data.cut_data(offset, offset + piece_size * 2)
184
+ piece = ""
185
+
186
+ for k in range(piece_size):
187
+ c = (piece_data[2 * k + 1] << 8) # First byte
188
+ c |= piece_data[2 * k] # Second byte
189
+ piece += chr(c)
190
+
191
+ result.append(piece)
192
+ offset += piece_size * 2
193
+
194
+ return result, offset
195
+
196
+ @staticmethod
197
+ def make_argument_references(data, offset):
198
+ result = []
199
+
200
+ num_references = data.to_number(4, offset)
201
+ offset += 4
202
+
203
+ for j in range(num_references):
204
+ result.append(data.to_number(4, offset))
205
+ offset += 4
206
+
207
+ return result, offset
208
+
209
+ @staticmethod
210
+ def make_argument_strings(data, offset):
211
+ result = []
212
+
213
+ num_arg_strings = data.to_number(1, offset)
214
+ offset += 1
215
+
216
+ for j in range(num_arg_strings):
217
+ num_args = data.to_number(4, offset)
218
+ offset += 4
219
+
220
+ result.append([])
221
+ for k in range(num_args):
222
+ string_size = data.to_number(1, offset)
223
+ if (string_size & 0x80) != 0:
224
+ string_size = (((string_size ^ 0x80) << 8) | data[offset + 1])
225
+ offset += 1
226
+ offset += 1
227
+
228
+ result[j].append(data.cut_data(offset, offset + string_size * 2))
229
+ offset += string_size * 2
230
+
231
+ return result, offset
232
+
233
+ @staticmethod
234
+ def build_pieces(data, new_data, offset):
235
+ # Moving offset pointer in data
236
+ old_offset = offset
237
+ num_pieces = data.to_number(4, offset)
238
+ offset += 4
239
+
240
+ for j in range(num_pieces):
241
+ piece_size = data.to_number(1, offset)
242
+ if (piece_size & 128) != 0:
243
+ piece_size = (((piece_size ^ 128) << 8) | data[offset + 1])
244
+ offset += 1
245
+ offset += 1
246
+ offset += piece_size * 2
247
+
248
+ # Deleting '[' and ']' brackets
249
+ text_data = new_data.text[1:-1] if len(new_data.text) > 2 else ""
250
+
251
+ text_pieces = []
252
+
253
+ DNT = "<--DO_NOT_TOUCH!-->"
254
+ prev = 0
255
+ next_pos = text_data.find(DNT, prev)
256
+
257
+ while next_pos != -1:
258
+ piece = " " if next_pos - prev == 0 else text_data[prev:next_pos]
259
+ text_pieces.append(piece)
260
+ prev = next_pos + len(DNT)
261
+ next_pos = text_data.find(DNT, prev)
262
+
263
+ text_pieces.append(text_data[prev:])
264
+
265
+ # Building BinaryData from pieces
266
+ result_data = BinaryData()
267
+ result_data = result_data + BinaryData.from_number(4, len(text_pieces))
268
+
269
+ for piece in text_pieces:
270
+ piece_size = len(piece)
271
+ if piece_size < 128:
272
+ result_data = result_data + BinaryData.from_number(1, piece_size)
273
+ else:
274
+ result_data = result_data + BinaryData.from_number_raw(2, (piece_size | 32768))
275
+
276
+ for j in range(piece_size):
277
+ result_data = result_data + BinaryData.from_number(2, ord(piece[j]))
278
+
279
+ return result_data, offset
280
+
281
+ @staticmethod
282
+ def build_argument_references(data, new_data, offset):
283
+ # Moving offset pointer in data
284
+ old_offset = offset
285
+ num_references = data.to_number(4, offset)
286
+ offset += 4
287
+ offset += 4 * num_references
288
+
289
+ # If there are no args - making 4 null-bytes and return;
290
+ if not new_data.args:
291
+ result = BinaryData.from_number(4, 0)
292
+ return result, offset
293
+
294
+ # Parsing arguments from list in options["args"]
295
+ args_list = new_data.args
296
+ argument_references = []
297
+
298
+ prev = 0
299
+ next_pos = args_list.find('-', prev)
300
+ while next_pos != -1:
301
+ argument = args_list[prev:next_pos]
302
+ argument_references.append(int(argument))
303
+ prev = next_pos + 1
304
+ next_pos = args_list.find('-', prev)
305
+
306
+ argument = args_list[prev:]
307
+ argument_references.append(int(argument))
308
+
309
+ result = BinaryData()
310
+ result = result + BinaryData.from_number(4, len(argument_references))
311
+ for arg_reference in argument_references:
312
+ result = result + BinaryData.from_number(4, arg_reference)
313
+
314
+ return result, offset
315
+
316
+ @staticmethod
317
+ def build_argument_strings(data, new_data, offset):
318
+ # TODO: IMPLEMENT (never user)
319
+ # Moving offset pointer in data
320
+ old_offset = offset
321
+ num_arg_strings = data.to_number(1, offset)
322
+ offset += 1
323
+
324
+ for j in range(num_arg_strings):
325
+ num_args = data.to_number(4, offset)
326
+ offset += 4
327
+
328
+ for k in range(num_args):
329
+ string_size = data.to_number(1, offset)
330
+ if (string_size & 0x80) != 0:
331
+ string_size = (((string_size ^ 0x80) << 8) | data[offset + 1])
332
+ offset += 1
333
+ offset += 1
334
+ offset += string_size * 2
335
+
336
+ return BinaryData.from_number(1, 0), offset
337
+
338
+ @staticmethod
339
+ def get_piece_data(data, offset):
340
+ old_offset = offset
341
+
342
+ num_pieces = data.to_number(4, offset)
343
+ offset += 4
344
+
345
+ for j in range(num_pieces):
346
+ piece_size = data.to_number(1, offset)
347
+ if (piece_size & 128) != 0:
348
+ piece_size = (((piece_size ^ 128) << 8) | data[offset + 1])
349
+ offset += 1
350
+ offset += 1
351
+ offset += piece_size * 2
352
+
353
+ return data.cut_data(old_offset, offset), offset
354
+
355
+ @staticmethod
356
+ def get_argument_reference_data(data, offset):
357
+ old_offset = offset
358
+ num_references = data.to_number(4, offset)
359
+ offset += 4
360
+ offset += 4 * num_references
361
+ return data.cut_data(old_offset, offset), offset
362
+
363
+ @staticmethod
364
+ def get_argument_strings_data(data, offset):
365
+ old_offset = offset
366
+
367
+ num_arg_strings = data.to_number(1, offset)
368
+ offset += 1
369
+
370
+ for j in range(num_arg_strings):
371
+ num_args = data.to_number(4, offset)
372
+ offset += 4
373
+
374
+ for k in range(num_args):
375
+ string_size = data.to_number(1, offset)
376
+ if (string_size & 0x80) != 0:
377
+ string_size = (((string_size ^ 0x80) << 8) | data[offset + 1])
378
+ offset += 1
379
+ offset += 1
380
+ offset += string_size * 2
381
+
382
+ return data.cut_data(old_offset, offset), offset
@@ -0,0 +1,29 @@
1
+ def to_utf16(x):
2
+ """Convert integer to UTF-16 string"""
3
+ if x == 0:
4
+ return chr(0x30) # '0' character
5
+ res = ""
6
+ while x > 0:
7
+ res += chr(0x30 + x % 10) # '0' + digit
8
+ x //= 10
9
+ return res[::-1] # Reverse string
10
+
11
+ def from_utf16(num_str):
12
+ """Convert UTF-16 string to integer"""
13
+ res = 0
14
+ for c in num_str:
15
+ res = res * 10 + (ord(c) - 0x30) # digit - '0'
16
+ return res
17
+
18
+ def arguments_from_utf16(args_str):
19
+ """Parse arguments from UTF-16 string"""
20
+ if not args_str:
21
+ return ""
22
+
23
+ parts = args_str.split('-')
24
+ result_parts = []
25
+ for part in parts:
26
+ if part: # Skip empty parts
27
+ result_parts.append(str(from_utf16(part)))
28
+
29
+ return '-'.join(result_parts)
turbine_lib/utils.py ADDED
@@ -0,0 +1,117 @@
1
+ def compare_files_byte_by_byte(file1_path, file2_path):
2
+ """
3
+ 比较两个文件的每个字节是否完全相同
4
+
5
+ Args:
6
+ file1_path (str): 第一个文件路径
7
+ file2_path (str): 第二个文件路径
8
+
9
+ Returns:
10
+ bool: 如果两个文件完全相同返回True,否则返回False
11
+ """
12
+ try:
13
+ with open(file1_path, 'rb') as file1, open(file2_path, 'rb') as file2:
14
+ # 按块读取文件进行比较,避免大文件占用过多内存
15
+ while True:
16
+ chunk1 = file1.read(8192) # 每次读取8KB
17
+ chunk2 = file2.read(8192)
18
+
19
+ # 如果两个文件都到达末尾,则文件相同
20
+ if not chunk1 and not chunk2:
21
+ return True
22
+
23
+ # 如果其中一个文件结束而另一个没有,则文件不同
24
+ if not chunk1 or not chunk2:
25
+ return False
26
+
27
+ # 如果当前块不相同,则文件不同
28
+ if chunk1 != chunk2:
29
+ return False
30
+
31
+ except FileNotFoundError as e:
32
+ print(f"文件未找到: {e}")
33
+ return False
34
+ except Exception as e:
35
+ print(f"比较文件时出错: {e}")
36
+ return False
37
+
38
+
39
+ def compare_files_detailed(file1_path, file2_path):
40
+ """
41
+ 比较两个文件的每个字节,并提供详细的差异信息
42
+
43
+ Args:
44
+ file1_path (str): 第一个文件路径
45
+ file2_path (str): 第二个文件路径
46
+
47
+ Returns:
48
+ dict: 包含比较结果和详细信息的字典
49
+ """
50
+ result = {
51
+ 'same': False,
52
+ 'size1': 0,
53
+ 'size2': 0,
54
+ 'first_diff_pos': -1,
55
+ 'error': None
56
+ }
57
+
58
+ try:
59
+ with open(file1_path, 'rb') as file1, open(file2_path, 'rb') as file2:
60
+ # 获取文件大小
61
+ file1.seek(0, 2) # 移动到文件末尾
62
+ file2.seek(0, 2)
63
+ result['size1'] = file1.tell()
64
+ result['size2'] = file2.tell()
65
+
66
+ # 重置文件指针
67
+ file1.seek(0)
68
+ file2.seek(0)
69
+
70
+ # 如果文件大小不同,则肯定不相同
71
+ if result['size1'] != result['size2']:
72
+ return result
73
+
74
+ position = 0
75
+ while True:
76
+ byte1 = file1.read(1)
77
+ byte2 = file2.read(1)
78
+
79
+ # 如果两个文件都到达末尾,则文件相同
80
+ if not byte1 and not byte2:
81
+ result['same'] = True
82
+ break
83
+
84
+ # 如果字节不同,记录位置并退出
85
+ if byte1 != byte2:
86
+ result['first_diff_pos'] = position
87
+ break
88
+
89
+ position += 1
90
+
91
+ except FileNotFoundError as e:
92
+ result['error'] = f"文件未找到: {e}"
93
+ except Exception as e:
94
+ result['error'] = f"比较文件时出错: {e}"
95
+
96
+ return result
97
+
98
+ def compare_files(file_path1, file_path2):
99
+ # 简单比较
100
+ are_same = compare_files_byte_by_byte(file_path1, file_path2)
101
+ print(f"文件是否相同: {are_same}")
102
+
103
+ # 详细比较
104
+ result = compare_files_detailed(file_path1, file_path2)
105
+ if result['error']:
106
+ print(f"错误: {result['error']}")
107
+ elif result['same']:
108
+ print("文件完全相同")
109
+ else:
110
+ if result['size1'] != result['size2']:
111
+ print(f"文件大小不同: {result['size1']} vs {result['size2']}")
112
+ else:
113
+ print(f"文件大小相同但内容不同,第一个差异在位置: {result['first_diff_pos']}")
114
+ if __name__ == '__main__':
115
+ # 简单比较
116
+ compare_files(r'./font/standard/1107296298.fontbin', r'./font/output_17-11-2025-14-01/1107296298.fontbin')
117
+
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: turbine_lib
3
+ Version: 0.1.0
4
+ Summary: 仅支持Windows 32位Python的库
5
+ Classifier: Programming Language :: Python :: 3
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Operating System :: Microsoft :: Windows
8
+ Classifier: Programming Language :: Python :: Implementation :: CPython
9
+ Classifier: Topic :: Software Development :: Libraries
10
+ Requires-Python: <3.13,>=3.7
@@ -0,0 +1,22 @@
1
+ turbine_lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ turbine_lib/binarydata.py,sha256=lu1XSkqGIyLASJPbbf0UJuZ-E2rrLw0EC6Oq7P8DeBs,4505
3
+ turbine_lib/database.py,sha256=4a_Zzcdr_O5-GZK8M2FcmyA06TqYkGiaLZafQv7jRRU,5112
4
+ turbine_lib/datexportapi.py,sha256=680sjmTDXq8WIEIEuIs48wm6BfhF4JaOgXnT_ep_h6Q,12745
5
+ turbine_lib/datfile.py,sha256=-2LM1hwHymfk8RkhqJEO18bq-iD__MIa3yKGaODMYys,13218
6
+ turbine_lib/ddssubfile.py,sha256=k1afrh7aZb03wgHis8M7mRSxAE9o4UsQaEyOI21QliQ,10399
7
+ turbine_lib/font_convert.py,sha256=tuIkq84h6BOSUDMDLRVBK6PV_OulSrbziII9o2hWYzA,12954
8
+ turbine_lib/fontsubfile.py,sha256=gezfd9-iloZ6YHF_mUYijnKYTj_PtpJHvW61qIj5uf8,930
9
+ turbine_lib/subfile.py,sha256=AYJaJR9FYUcLKaUpHgHu4HXU6m81S4BVb9naugws4ko,2767
10
+ turbine_lib/subfiledata.py,sha256=JJJedNBOOtSa_UoztXwqDrnykXDi79BwLuXg0b95VmE,1068
11
+ turbine_lib/textsubfile.py,sha256=ClS5XtbAHNhErcBvgqPxGmxqp9voK0Hajcqw30nDBQk,13310
12
+ turbine_lib/textutils.py,sha256=JcJKd4gjdvR023wTMSCOhyTWs5_kHp3W1sAY0uMPe1Q,759
13
+ turbine_lib/utils.py,sha256=DlQWk15XqDsQYASL7pD6TB5JO8gXyyyu_AJ16ombKn8,3856
14
+ turbine_lib/libs/datexport.dll,sha256=ycZSi4e7juJSVU49Ap23IQ49jXY1uzU2MvX7uRdJha8,238896
15
+ turbine_lib/libs/msvcp71.dll,sha256=35YVb2pUj9b-VnKRjeWuRQnTyBCle__SqR3kWj7Vsjs,499712
16
+ turbine_lib/libs/msvcp90.dll,sha256=BpGM-ZrSbNbPEGiBwNW9shLcC6xFSYBcn1kG49A9FSw,569680
17
+ turbine_lib/libs/msvcr71.dll,sha256=gJSvXuMQcUyuvMru53af-wgEhQO6R4uHnt_vXxok_v4,348160
18
+ turbine_lib/libs/zlib1T.dll,sha256=fiPxt97AbyHE_GVYpIDVldSFPUFjV8xVQRRkhxwenBQ,63000
19
+ turbine_lib-0.1.0.dist-info/METADATA,sha256=NSZ0syR4Jn9hhqNkET6FbyZHFIcRqYm3-pFwBlfuPBM,417
20
+ turbine_lib-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
+ turbine_lib-0.1.0.dist-info/top_level.txt,sha256=LVLiMIuAzr6r6OUWftVdJcXucgUlW6Y6XWw4gUoUWQI,12
22
+ turbine_lib-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ turbine_lib