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,353 @@
1
+ import datetime
2
+ import os
3
+ import struct
4
+ from utils import compare_files_byte_by_byte, compare_files_detailed, compare_files
5
+
6
+
7
+ # bmfont
8
+
9
+
10
+ class SourceCharInfo:
11
+ def __init__(self, id, x, y, width, height, xoffset, yoffset, xadvance, page, channel):
12
+ self.Id = id
13
+ self.X = x
14
+ self.Y = y
15
+ self.Width = width
16
+ self.Height = height
17
+ self.XOffset = xoffset
18
+ self.YOffset = yoffset
19
+ self.XAdvance = xadvance
20
+ self.Page = page
21
+ self.Channel = channel
22
+ # 计算rightBearingX = XAdvance - XOffset - Width
23
+ self.RightBearingX = xadvance - xoffset - width
24
+
25
+
26
+ class Padding:
27
+ def __init__(self, up, right, down, left):
28
+ self.Left = left
29
+ self.Right = right
30
+ self.Up = up
31
+ self.Down = down
32
+
33
+
34
+ class Spacing:
35
+ def __init__(self, horizontal, vertical):
36
+ self.Horizontal = horizontal
37
+ self.Vertical = vertical
38
+
39
+
40
+ class SourceFontInfo:
41
+ def __init__(self, filename):
42
+ self.Filename = filename
43
+ self.IsValid = False
44
+
45
+ # 初始化所有属性
46
+ self.FontName = ""
47
+ self.FontSize = 0
48
+ self.Bold = False
49
+ self.Italic = False
50
+ self.FixedHeight = False
51
+ self.Charset = 0
52
+ self.Unicode = False
53
+ self.StretchH = 0
54
+ self.Smooth = False
55
+ self.Antialiasing = 0
56
+ self.Padding = None
57
+ self.Spacing = None
58
+ self.Outline = 0
59
+ self.LineHeight = 0
60
+ self.Base = 0
61
+ self.ScaleW = 0
62
+ self.ScaleH = 0
63
+ self.Pages = 0
64
+ self.AlphaChannel = 0
65
+ self.RedChannel = 0
66
+ self.GreenChannel = 0
67
+ self.BlueChannel = 0
68
+ self.DdsFilename = ""
69
+ self.CharsCount = 0
70
+ self.Chars = []
71
+ self.DdsWidth = 0
72
+ self.DdsHeight = 0
73
+
74
+ if filename:
75
+ self.IsValid = self._read(filename)
76
+ # pass
77
+
78
+ @property
79
+ def Filename(self):
80
+ return self._filename
81
+
82
+ @Filename.setter
83
+ def Filename(self, value):
84
+ self._filename = value
85
+ # if value:
86
+ # self.IsValid = self._read(value)
87
+
88
+ def _read(self, filename):
89
+ try:
90
+ with open(filename, 'rb') as f:
91
+ data = f.read()
92
+
93
+ # file identifier: BMF3
94
+ if data[0] != 66 or data[1] != 77 or data[2] != 70 or data[3] != 3:
95
+ return False
96
+
97
+ # 使用内存视图来模拟BinaryReader
98
+ offset = 0
99
+
100
+ # file identifier
101
+ offset += 4
102
+
103
+ # block type 1: info
104
+ # read type identifier and size
105
+ offset += 1 # 跳过类型标识符
106
+ block1_size = struct.unpack_from('<I', data, offset)[0]
107
+ offset += 4
108
+
109
+ # read content
110
+ self.FontSize = struct.unpack_from('<h', data, offset)[0]
111
+ offset += 2
112
+
113
+ bit_field_byte = struct.unpack_from('B', data, offset)[0]
114
+ offset += 1
115
+
116
+ # 解析bit field
117
+ self.Smooth = bool(bit_field_byte & 1)
118
+ self.Unicode = bool(bit_field_byte & 2)
119
+ self.Italic = bool(bit_field_byte & 4)
120
+ self.Bold = bool(bit_field_byte & 8)
121
+ self.FixedHeight = bool(bit_field_byte & 16)
122
+
123
+ self.Charset = struct.unpack_from('B', data, offset)[0]
124
+ offset += 1
125
+ self.StretchH = struct.unpack_from('<H', data, offset)[0]
126
+ offset += 2
127
+ self.Antialiasing = struct.unpack_from('B', data, offset)[0]
128
+ offset += 1
129
+
130
+ # Padding
131
+ padding_up = struct.unpack_from('B', data, offset)[0]
132
+ offset += 1
133
+ padding_right = struct.unpack_from('B', data, offset)[0]
134
+ offset += 1
135
+ padding_down = struct.unpack_from('B', data, offset)[0]
136
+ offset += 1
137
+ padding_left = struct.unpack_from('B', data, offset)[0]
138
+ offset += 1
139
+ self.Padding = Padding(padding_up, padding_right, padding_down, padding_left)
140
+
141
+ # Spacing
142
+ spacing_horizontal = struct.unpack_from('B', data, offset)[0]
143
+ offset += 1
144
+ spacing_vertical = struct.unpack_from('B', data, offset)[0]
145
+ offset += 1
146
+ self.Spacing = Spacing(spacing_horizontal, spacing_vertical)
147
+
148
+ self.Outline = struct.unpack_from('B', data, offset)[0]
149
+ offset += 1
150
+
151
+ # FontName
152
+ font_name_bytes = data[offset:offset + (block1_size - 14)]
153
+ # self.FontName = font_name_bytes.decode('utf-8').rstrip('\x00') # 移除可能的空字符
154
+ offset += (block1_size - 14)
155
+
156
+ # block type 2: common
157
+ # read type identifier and size
158
+ offset += 1 # 跳过类型标识符
159
+ block2_size = struct.unpack_from('<I', data, offset)[0]
160
+ offset += 4
161
+
162
+ # read content
163
+ self.LineHeight = struct.unpack_from('<H', data, offset)[0]
164
+ offset += 2
165
+ self.Base = struct.unpack_from('<H', data, offset)[0]
166
+ offset += 2
167
+ self.ScaleW = struct.unpack_from('<H', data, offset)[0]
168
+ offset += 2
169
+ self.ScaleH = struct.unpack_from('<H', data, offset)[0]
170
+ offset += 2
171
+ self.Pages = struct.unpack_from('<H', data, offset)[0]
172
+ offset += 2
173
+
174
+ # skip bit field
175
+ offset += 1
176
+
177
+ self.AlphaChannel = struct.unpack_from('B', data, offset)[0]
178
+ offset += 1
179
+ self.RedChannel = struct.unpack_from('B', data, offset)[0]
180
+ offset += 1
181
+ self.GreenChannel = struct.unpack_from('B', data, offset)[0]
182
+ offset += 1
183
+ self.BlueChannel = struct.unpack_from('B', data, offset)[0]
184
+ offset += 1
185
+
186
+ # block type 3: pages
187
+ # read type identifier and size
188
+ offset += 1 # 跳过类型标识符
189
+ block3_size = struct.unpack_from('<I', data, offset)[0]
190
+ offset += 4
191
+
192
+ # read content
193
+ # assume we have no more than 1 page here
194
+ dds_filename_bytes = data[offset:offset + block3_size - 1] # 去掉结尾的null字符
195
+ self.DdsFilename = dds_filename_bytes.decode('utf-8')
196
+ offset += block3_size
197
+
198
+ # block type 4: chars
199
+ # read type identifier and size
200
+ offset += 1 # 跳过类型标识符
201
+ block4_size = struct.unpack_from('<I', data, offset)[0]
202
+ offset += 4
203
+
204
+ # read content
205
+ self.CharsCount = block4_size // 20
206
+ self.Chars = []
207
+ for i in range(self.CharsCount):
208
+ id = struct.unpack_from('<I', data, offset)[0]
209
+ offset += 4
210
+ x = struct.unpack_from('<H', data, offset)[0]
211
+ offset += 2
212
+ y = struct.unpack_from('<H', data, offset)[0]
213
+ offset += 2
214
+ width = struct.unpack_from('<H', data, offset)[0]
215
+ offset += 2
216
+ height = struct.unpack_from('<H', data, offset)[0]
217
+ offset += 2
218
+ xoffset = struct.unpack_from('<h', data, offset)[0]
219
+ offset += 2
220
+ yoffset = struct.unpack_from('<h', data, offset)[0]
221
+ offset += 2
222
+ xadvance = struct.unpack_from('<h', data, offset)[0]
223
+ offset += 2
224
+ page = struct.unpack_from('B', data, offset)[0]
225
+ offset += 1
226
+ channel = struct.unpack_from('B', data, offset)[0]
227
+ offset += 1
228
+
229
+ char_info = SourceCharInfo(id, x, y, width, height, xoffset, yoffset, xadvance, page, channel)
230
+ self.Chars.append(char_info)
231
+
232
+ # skip block 5 (如果存在)
233
+
234
+ return True
235
+ except Exception as e:
236
+ raise Exception(f"Error reading font file: {e}")
237
+
238
+
239
+ # 目标字体
240
+ class DestCharInfo:
241
+ def __init__(self, info):
242
+ self.Id = int(info.Id) & 0xFFFF # unsigned word
243
+ self.X = int(info.X) & 0xFFFF # unsigned word
244
+ self.Y = int(info.Y) & 0xFFFF # unsigned word
245
+ self.Width = int(info.Width) & 0xFF # unsigned byte
246
+ self.Height = int(info.Height) & 0xFF # unsigned byte
247
+ self.XOffset = int(info.XOffset) # signed byte
248
+ # 确保XOffset在有符号字节范围内 [-128, 127]
249
+ if self.XOffset > 127:
250
+ self.XOffset -= 256
251
+ elif self.XOffset < -128:
252
+ self.XOffset += 256
253
+
254
+ self.YOffset = int(info.YOffset) # signed byte处理,但存储为unsigned byte
255
+ # 确保YOffset在有符号字节范围内 [-128, 127],但转换为unsigned byte [0, 255]
256
+ if self.YOffset > 127:
257
+ self.YOffset -= 256
258
+ elif self.YOffset < -128:
259
+ self.YOffset += 256
260
+ # 转换为unsigned byte范围
261
+ self.YOffset = self.YOffset & 0xFF
262
+
263
+ # 计算rightBearingX
264
+ self.RightBearingX = int(info.XAdvance - info.XOffset - info.Width) # signed byte
265
+ if self.RightBearingX > 127:
266
+ self.RightBearingX -= 256
267
+ elif self.RightBearingX < -128:
268
+ self.RightBearingX += 256
269
+
270
+ self.XAdvance = int(info.XAdvance) # signed byte
271
+ if self.XAdvance > 127:
272
+ self.XAdvance -= 256
273
+ elif self.XAdvance < -128:
274
+ self.XAdvance += 256
275
+
276
+
277
+ class DestFontInfo:
278
+ def __init__(self, info):
279
+ self.FontSize = abs(info.FontSize)
280
+ self.Base = info.Base
281
+ self.LineHeight = int(info.LineHeight) & 0xFFFF # short
282
+ # if self.LineHeight > 32767:
283
+ # self.LineHeight -= 65536
284
+ self.Charset = int(info.Charset) & 0xFFFF # short
285
+ # if self.Charset > 32767:
286
+ # self.Charset -= 65536
287
+ self.CharsCount = info.CharsCount
288
+ self.Padding = int(info.Padding.Left) & 0xFF # byte
289
+ self.ScaleW = info.FontSize # 新增:用于作为width
290
+ self.ScaleH = info.FontSize # 新增:用于作为height
291
+
292
+ self.Chars = []
293
+ for i in range(self.CharsCount):
294
+ self.Chars.append(DestCharInfo(info.Chars[i]))
295
+
296
+ def import_font(self, fontbin_filename):
297
+ # 创建输出目录
298
+ now = datetime.datetime.now()
299
+ dir_name = f"output_{now.strftime('%Y-%m-%d')}"
300
+ dir_path = os.path.join(os.getcwd(), dir_name)
301
+ os.makedirs(dir_path, exist_ok=True)
302
+
303
+ # 构建输出文件路径
304
+
305
+ path = os.path.join(dir_path, fontbin_filename)
306
+ # 写入处理后的数据
307
+ with open(path, 'wb') as fs:
308
+ # 写入头部数据 - 根据新格式调整
309
+ fs.write(struct.pack('<I', 0)) # masterFileID
310
+ fs.write(struct.pack('<I', self.ScaleW)) # width
311
+ fs.write(struct.pack('<I', self.ScaleH)) # height
312
+ fs.write(struct.pack('<I', self.CharsCount)) # CharsCount
313
+
314
+ # 写入字符信息 - 根据新格式调整
315
+ for i in range(self.CharsCount):
316
+ char = self.Chars[i]
317
+ fs.write(struct.pack('<B', char.Width)) # Chars[i].Width
318
+ fs.write(struct.pack('<B', char.Height)) # Chars[i].Height
319
+ fs.write(struct.pack('<b', char.XOffset)) # Chars[i].bearingX
320
+ fs.write(struct.pack('<b', char.RightBearingX)) # Chars[i].rightBearingX
321
+ fs.write(struct.pack('<B', char.YOffset)) # Chars[i].bearingY
322
+ fs.write(struct.pack('<H', char.Id)) # Chars[i].id
323
+ fs.write(struct.pack('<H', char.X)) # Chars[i].X
324
+ fs.write(struct.pack('<H', char.Y)) # Chars[i].Y
325
+
326
+ # 写入结尾数据
327
+ fs.write(struct.pack('<I', 0)) # p
328
+ fs.write(struct.pack('<I', 0)) # q
329
+ fs.write(struct.pack('<I', 0)) # r
330
+ fs.write(struct.pack('<I', 0)) # ddsId1
331
+ fs.write(struct.pack('<I', 0)) # ddsId2
332
+ return path
333
+
334
+
335
+ def fnt_to_fontbin(bmfont_file):
336
+ """
337
+ bmfont转换为fontbin
338
+ """
339
+ source_font = SourceFontInfo(bmfont_file)
340
+ if not source_font.IsValid:
341
+ print("Invalid source_font file!")
342
+ raise ValueError("Invalid font file!")
343
+ destFontInfo = DestFontInfo(source_font)
344
+ output_fontbin = destFontInfo.import_font("template.fontbin")
345
+ print(f'fontbin生成目录:{output_fontbin}')
346
+ return output_fontbin
347
+
348
+
349
+ if __name__ == '__main__':
350
+ font = SourceFontInfo(r'bmfont1.14a\output.fnt')
351
+ destFontInfo = DestFontInfo(font)
352
+ output_fontbin = destFontInfo.import_font('test.fontbin')
353
+ print(f"生成字体成功:{output_fontbin}")
@@ -0,0 +1,34 @@
1
+ from subfile import Subfile
2
+ from binarydata import BinaryData
3
+ from subfiledata import SubfileData
4
+
5
+
6
+ class FONT:
7
+ """Marker class for FONT file type"""
8
+ pass
9
+
10
+
11
+ class SubfileFONT(Subfile):
12
+
13
+ @staticmethod
14
+ def build_for_export(file_data):
15
+ """
16
+ Build data for export from FONT file
17
+ :param file_data: BinaryData with file data
18
+ :return: SubfileData with processed data
19
+ """
20
+ result = SubfileData()
21
+ result.binary_data = file_data
22
+ result.options["ext"] = ".fontbin"
23
+ return result
24
+
25
+ @staticmethod
26
+ def build_for_import(old_data, data):
27
+ """
28
+ Build data for import to FONT file
29
+ :param old_data: BinaryData with old data
30
+ :param data: SubfileData with new data
31
+ :return: BinaryData with processed data
32
+ """
33
+ # return old_data.cut_data(0, 4) + data.binary_data.cut_data(4)
34
+ return data.binary_data
Binary file
Binary file
Binary file
Binary file
Binary file
turbine_lib/subfile.py ADDED
@@ -0,0 +1,88 @@
1
+ from enum import Enum
2
+
3
+ class FILE_TYPE(Enum):
4
+ TEXT = '.text'
5
+ JPG = '.jpg'
6
+ DDS='.dds'
7
+ WAV = '.wav'
8
+ OGG = '.ogg'
9
+ FONT = '.fontbin'
10
+ UNKNOWN = '.unknown'
11
+
12
+ def file_type_from_string(ext):
13
+ """Convert string extension to FILE_TYPE enum"""
14
+ if ext == ".txt":
15
+ return FILE_TYPE.TEXT
16
+ elif ext == ".jpg":
17
+ return FILE_TYPE.JPG
18
+ elif ext == ".dds":
19
+ return FILE_TYPE.DDS
20
+ elif ext == ".wav":
21
+ return FILE_TYPE.WAV
22
+ elif ext == ".ogg":
23
+ return FILE_TYPE.OGG
24
+ elif ext == ".fontbin":
25
+ return FILE_TYPE.FONT
26
+ else:
27
+ return FILE_TYPE.UNKNOWN
28
+
29
+ def file_type_from_file_contents(file_id, file_data):
30
+ """Determine file type from file contents"""
31
+ # Text check based on file_id
32
+ if (file_id >> 24) == 0x25:
33
+ return FILE_TYPE.TEXT
34
+
35
+ # Font check based on file_id
36
+ if (file_id >> 24) == 0x42:
37
+ return FILE_TYPE.FONT
38
+
39
+ # jpeg / dds check
40
+ if (file_id >> 24) == 0x41:
41
+ soi = file_data.to_number(2, 24)
42
+ # long long marker = file_data.ToNumber<2>(26)
43
+
44
+ # auto markerSize = header.ToNumber<short>(28)
45
+ # auto four = header.ToNumber<int>(30)
46
+
47
+ if soi == 0xD8FF:
48
+ return FILE_TYPE.JPG
49
+ return FILE_TYPE.DDS
50
+
51
+ # Ogg and Wav check
52
+ if len(file_data) > 11:
53
+ if file_data[8] == 0x4F and file_data[9] == 0x67 and file_data[10] == 0x67 and file_data[11] == 0x53:
54
+ return FILE_TYPE.OGG
55
+
56
+ if file_data[8] == 0x52 and file_data[9] == 0x49 and file_data[10] == 0x46 and file_data[11] == 0x46:
57
+ return FILE_TYPE.WAV
58
+
59
+ return FILE_TYPE.UNKNOWN
60
+
61
+ def string_from_file_type(file_type):
62
+ """Convert FILE_TYPE enum to string extension"""
63
+ if file_type == FILE_TYPE.TEXT.value or file_type == FILE_TYPE.TEXT:
64
+ return ".txt"
65
+ elif file_type == FILE_TYPE.JPG.value or file_type == FILE_TYPE.JPG:
66
+ return ".jpg"
67
+ elif file_type == FILE_TYPE.DDS.value or file_type == FILE_TYPE.DDS:
68
+ return ".dds"
69
+ elif file_type == FILE_TYPE.WAV.value or file_type == FILE_TYPE.WAV:
70
+ return ".wav"
71
+ elif file_type == FILE_TYPE.OGG.value or file_type == FILE_TYPE.OGG:
72
+ return ".ogg"
73
+ elif file_type == FILE_TYPE.FONT.value or file_type == FILE_TYPE.FONT:
74
+ return ".fontbin"
75
+ else:
76
+ return ".subfile"
77
+
78
+ # Template class for Subfile - will be implemented in specific subfile type files
79
+ class Subfile:
80
+ """Base class for subfile processing"""
81
+
82
+ @staticmethod
83
+ def build_for_import(old_data, outer_data):
84
+ raise NotImplementedError("Subclasses must implement this method")
85
+
86
+ @staticmethod
87
+ def build_for_export(inner_data):
88
+ raise NotImplementedError("Subclasses must implement this method")
@@ -0,0 +1,30 @@
1
+ import yaml
2
+
3
+ class SubfileData:
4
+
5
+ def __init__(self, binary_data=None, text_data="", options=None):
6
+ if binary_data is None:
7
+ # Default constructor
8
+ from binarydata import BinaryData
9
+ self.binary_data = BinaryData()
10
+ self.text_data = ""
11
+ self.options = yaml.safe_load("{}") if options is None else options
12
+ else:
13
+ # Constructor with parameters
14
+ self.binary_data = binary_data
15
+ self.text_data = text_data
16
+ self.options = options if options is not None else yaml.safe_load("{}")
17
+
18
+ def empty(self):
19
+ return (len(self.binary_data) == 0 and
20
+ len(self.text_data) == 0 and
21
+ (self.options is None or self.options == yaml.safe_load("{}")))
22
+
23
+ def __eq__(self, other):
24
+ if not isinstance(other, SubfileData):
25
+ return False
26
+ return (self.binary_data == other.binary_data and
27
+ self.text_data == other.text_data)
28
+
29
+ def __ne__(self, other):
30
+ return not self.__eq__(other)