Skip to content

Commit 10a4c03

Browse files
Merge pull request #31 from xcoder-tool/texture-loading
refactor(texture): texture loading accelerated
2 parents 0075af9 + e120049 commit 10a4c03

File tree

7 files changed

+191
-191
lines changed

7 files changed

+191
-191
lines changed

poetry.lock

+108-97
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ sc-compression = "0.6.5"
1111
colorama = "0.4.6"
1212
pylzham = "^0.1.3"
1313
zstandard = "^0.23.0"
14-
pillow = "11.2.0"
14+
pillow = "~11.2.1"
1515
loguru = "0.7.3"
1616

1717

xcoder/bytestream.py

+20-10
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,45 @@
11
import io
2+
import struct
23
from typing import Literal
34

45

5-
class Reader(io.BytesIO):
6+
class Reader:
67
def __init__(
78
self,
89
initial_buffer: bytes = b"",
910
endian: Literal["little", "big"] = "little",
1011
):
11-
super().__init__(initial_buffer)
12+
self._internal_reader = io.BytesIO(initial_buffer)
13+
1214
self.endian: Literal["little", "big"] = endian
15+
self.endian_sign: Literal["<", ">"] = "<" if endian == "little" else ">"
16+
17+
def seek(self, position: int) -> None:
18+
self._internal_reader.seek(position)
1319

14-
def read_integer(self, length: int, signed=False) -> int:
15-
return int.from_bytes(self.read(length), self.endian, signed=signed)
20+
def tell(self) -> int:
21+
return self._internal_reader.tell()
22+
23+
def read(self, size: int) -> bytes:
24+
return self._internal_reader.read(size)
1625

1726
def read_uchar(self) -> int:
18-
return self.read_integer(1)
27+
return struct.unpack("B", self.read(1))[0]
1928

2029
def read_char(self) -> int:
21-
return self.read_integer(1, True)
30+
return struct.unpack("b", self.read(1))[0]
2231

2332
def read_ushort(self) -> int:
24-
return self.read_integer(2)
33+
return struct.unpack(f"{self.endian_sign}H", self.read(2))[0]
2534

2635
def read_short(self) -> int:
27-
return self.read_integer(2, True)
36+
return struct.unpack(f"{self.endian_sign}h", self.read(2))[0]
2837

2938
def read_uint(self) -> int:
30-
return self.read_integer(4)
39+
return struct.unpack(f"{self.endian_sign}I", self.read(4))[0]
3140

3241
def read_int(self) -> int:
33-
return self.read_integer(4, True)
42+
return struct.unpack(f"{self.endian_sign}i", self.read(4))[0]
3443

3544
def read_twip(self) -> float:
3645
return self.read_int() / 20
@@ -72,6 +81,7 @@ def write_string(self, string: str | None = None):
7281
if string is None:
7382
self.write_byte(0xFF)
7483
return
84+
7585
encoded = string.encode()
7686
self.write_byte(len(encoded))
7787
self.write(encoded)

xcoder/console.py

+18-11
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
class Console:
2-
@staticmethod
3-
def progress_bar(message, current, total):
2+
previous_percentage: int = -1
3+
4+
@classmethod
5+
def progress_bar(cls, message: str, current: int, total: int) -> None:
46
percentage = (current + 1) * 100 // total
5-
print("\r", end="")
6-
print(f"[{percentage}%] {message}", end="")
7-
if current + 1 == total:
8-
print()
7+
if percentage == cls.previous_percentage:
8+
return
99

10-
@staticmethod
11-
def percent(current: int, total: int) -> int:
12-
return (current + 1) * 100 // total
10+
print(f"\r[{percentage}%] {message}", end="")
11+
12+
if percentage == 100:
13+
print()
14+
cls.previous_percentage = -1
15+
else:
16+
cls.previous_percentage = percentage
1317

1418
@staticmethod
1519
def ask_integer(message: str):
@@ -20,13 +24,16 @@ def ask_integer(message: str):
2024
pass
2125

2226
@staticmethod
23-
def question(message):
27+
def question(message: str) -> bool:
2428
while True:
2529
answer = input(f"[????] {message} [Y/n] ").lower()
30+
if not answer:
31+
return True
32+
2633
if answer in "ny":
2734
break
2835

29-
return "ny".index(answer)
36+
return bool("ny".index(answer))
3037

3138

3239
if __name__ == "__main__":

xcoder/images.py

+32-47
Original file line numberDiff line numberDiff line change
@@ -20,49 +20,44 @@
2020
CHUNK_SIZE = 32
2121

2222

23-
def load_image_from_buffer(img: Image.Image) -> None:
24-
width, height = img.size
25-
loaded_image = img.load()
26-
if loaded_image is None:
27-
raise Exception("loaded_image is None")
28-
23+
def load_image_from_buffer(pixel_type: int, width: int, height: int) -> Image.Image:
2924
with open("pixel_buffer", "rb") as pixel_buffer:
30-
channel_count = int.from_bytes(pixel_buffer.read(1), "little")
25+
pixel_buffer.read(1)
26+
27+
return Image.frombuffer(
28+
get_format_by_pixel_type(pixel_type), (width, height), pixel_buffer.read()
29+
)
3130

32-
for y in range(height):
33-
for x in range(width):
34-
loaded_image[x, y] = tuple(pixel_buffer.read(channel_count))
3531

32+
def join_image(pixel_type: int, width: int, height: int) -> Image.Image:
33+
mode = get_format_by_pixel_type(pixel_type)
34+
image = Image.new(mode, (width, height))
3635

37-
def join_image(img: Image.Image) -> None:
3836
with open("pixel_buffer", "rb") as pixel_buffer:
3937
channel_count = int.from_bytes(pixel_buffer.read(1), "little")
4038

41-
width, height = img.size
42-
loaded_image = img.load()
43-
if loaded_image is None:
44-
raise Exception("loaded_image is None")
39+
chunk_count_x = math.ceil(width / CHUNK_SIZE)
40+
chunk_count_y = math.ceil(height / CHUNK_SIZE)
41+
chunk_count = chunk_count_x * chunk_count_y
4542

46-
x_chunks_count = width // CHUNK_SIZE
47-
y_chunks_count = height // CHUNK_SIZE
43+
for chunk_index in range(chunk_count):
44+
chunk_x = chunk_index % chunk_count_x
45+
chunk_y = chunk_index // chunk_count_x
4846

49-
for y_chunk in range(y_chunks_count + 1):
50-
for x_chunk in range(x_chunks_count + 1):
51-
for y in range(CHUNK_SIZE):
52-
pixel_y = y_chunk * CHUNK_SIZE + y
53-
if pixel_y >= height:
54-
break
47+
chunk_width = min(width - chunk_x * CHUNK_SIZE, CHUNK_SIZE)
48+
chunk_height = min(height - chunk_y * CHUNK_SIZE, CHUNK_SIZE)
49+
50+
sub_image = Image.frombuffer(
51+
mode,
52+
(chunk_width, chunk_height),
53+
pixel_buffer.read(channel_count * chunk_width * chunk_height),
54+
)
5555

56-
for x in range(CHUNK_SIZE):
57-
pixel_x = x_chunk * CHUNK_SIZE + x
58-
if pixel_x >= width:
59-
break
56+
image.paste(sub_image, (chunk_x * CHUNK_SIZE, chunk_y * CHUNK_SIZE))
6057

61-
loaded_image[pixel_x, pixel_y] = tuple(
62-
pixel_buffer.read(channel_count)
63-
)
58+
Console.progress_bar(locale.join_pic, chunk_index, chunk_count)
6459

65-
Console.progress_bar(locale.join_pic, y_chunk, y_chunks_count + 1)
60+
return image
6661

6762

6863
def _add_pixel(
@@ -130,7 +125,7 @@ def get_format_by_pixel_type(pixel_type: int) -> str:
130125
raise Exception(locale.unknown_pixel_type % pixel_type)
131126

132127

133-
def load_texture(reader: Reader, pixel_type: int, img: Image.Image) -> None:
128+
def load_texture(reader: Reader, pixel_type: int, width: int, height: int) -> None:
134129
channel_count = get_channel_count_by_pixel_type(pixel_type)
135130
read_pixel = get_read_function(pixel_type)
136131
if read_pixel is None:
@@ -139,18 +134,12 @@ def load_texture(reader: Reader, pixel_type: int, img: Image.Image) -> None:
139134
with open("pixel_buffer", "wb") as pixel_buffer:
140135
pixel_buffer.write(channel_count.to_bytes(1, "little"))
141136

142-
width, height = img.size
143-
point = -1
144137
for y in range(height):
145-
for x in range(width):
146-
pixel = read_pixel(reader)
147-
for channel in pixel:
148-
pixel_buffer.write(channel.to_bytes(1, "little"))
138+
pixel_buffer.write(
139+
b"".join([bytearray(read_pixel(reader)) for _ in range(width)])
140+
)
149141

150-
curr = Console.percent(y, height)
151-
if curr > point:
152-
Console.progress_bar(locale.crt_pic, y, height)
153-
point = curr
142+
Console.progress_bar(locale.crt_pic, y, height)
154143

155144

156145
def save_texture(writer: Writer, image: Image.Image, pixel_type: int) -> None:
@@ -161,16 +150,12 @@ def save_texture(writer: Writer, image: Image.Image, pixel_type: int) -> None:
161150
width, height = image.size
162151

163152
pixels = image.getdata()
164-
point = -1
165153
for y in range(height):
166154
for x in range(width):
167155
# noinspection PyTypeChecker
168156
writer.write(encode_pixel(pixels[y * width + x]))
169157

170-
curr = Console.percent(y, height)
171-
if curr > point:
172-
Console.progress_bar(locale.writing_pic, y, height)
173-
point = curr
158+
Console.progress_bar(locale.writing_pic, y, height)
174159

175160

176161
def transform_image(

xcoder/objects/texture.py

+9-17
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,7 @@
66
import zstandard
77
from PIL import Image
88

9-
from xcoder.images import (
10-
get_format_by_pixel_type,
11-
join_image,
12-
load_image_from_buffer,
13-
load_texture,
14-
)
9+
from xcoder.images import join_image, load_image_from_buffer, load_texture
1510
from xcoder.pvr_tex_tool import get_image_from_ktx_data
1611

1712
if TYPE_CHECKING:
@@ -59,17 +54,14 @@ def load(self, swf: SupercellSWF, tag: int, has_texture: bool):
5954
)
6055
return
6156

62-
image = Image.new(
63-
get_format_by_pixel_type(self.pixel_type), (self.width, self.height)
64-
)
57+
try:
58+
load_texture(swf.reader, self.pixel_type, self.width, self.height)
6559

66-
load_texture(swf.reader, self.pixel_type, image)
67-
68-
if tag in (27, 28, 29):
69-
join_image(image)
70-
else:
71-
load_image_from_buffer(image)
72-
73-
os.remove("pixel_buffer")
60+
if tag in (27, 28, 29):
61+
image = join_image(self.pixel_type, self.width, self.height)
62+
else:
63+
image = load_image_from_buffer(self.pixel_type, self.width, self.height)
64+
finally:
65+
os.remove("pixel_buffer")
7466

7567
self.image = image

xcoder/pixel_utils.py

+3-8
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,7 @@ def get_channel_count_by_pixel_type(pixel_type: int) -> int:
2727

2828

2929
def _read_rgba8(reader: Reader) -> PixelChannels:
30-
return (
31-
reader.read_uchar(),
32-
reader.read_uchar(),
33-
reader.read_uchar(),
34-
reader.read_uchar(),
35-
)
30+
return tuple(reader.read(4))
3631

3732

3833
def _read_rgba4(reader: Reader) -> PixelChannels:
@@ -61,11 +56,11 @@ def _read_rgb565(reader: Reader) -> PixelChannels:
6156

6257

6358
def _read_luminance8_alpha8(reader: Reader) -> PixelChannels:
64-
return (reader.read_uchar(), reader.read_uchar())[::-1]
59+
return tuple(reader.read(2))[::-1]
6560

6661

6762
def _read_luminance8(reader: Reader) -> PixelChannels:
68-
return (reader.read_uchar(),)
63+
return tuple(reader.read(1))
6964

7065

7166
def _write_rgba8(pixel: PixelChannels) -> bytes:

0 commit comments

Comments
 (0)