diff --git a/main.py b/main.py
index 3882029..0a12752 100644
--- a/main.py
+++ b/main.py
@@ -1,21 +1,20 @@
-# Refactored by Vorono4ka
-
import time
+from system.lib.config import config
+from system.lib.main_menu import (
+ check_auto_update,
+ check_files_updated,
+ menu,
+ refill_menu,
+)
+from system.localization import locale
+
try:
from loguru import logger
except ImportError:
raise RuntimeError("Please, install loguru using pip")
from system import clear
-from system.lib import (
- config,
- locale,
- refill_menu,
- menu,
- check_auto_update,
- check_files_updated,
-)
from system.lib.features.initialization import initialize
@@ -50,4 +49,3 @@ def main():
main()
except KeyboardInterrupt:
logger.info("Exit.")
- pass
diff --git a/system/bytestream.py b/system/bytestream.py
index 1b49c7a..ae9213b 100644
--- a/system/bytestream.py
+++ b/system/bytestream.py
@@ -1,5 +1,5 @@
import io
-from typing import Literal, Optional
+from typing import Literal
class Reader(io.BytesIO):
@@ -37,7 +37,7 @@ def read_twip(self) -> float:
def read_string(self) -> str:
length = self.read_uchar()
- if length != 255:
+ if length != 0xFF:
return self.read(length).decode()
return ""
@@ -68,9 +68,9 @@ def write_uint32(self, integer: int):
def write_int32(self, integer: int):
self.write_int(integer, 4, True)
- def write_string(self, string: Optional["str"] = None):
+ def write_string(self, string: str | None = None):
if string is None:
- self.write_byte(255)
+ self.write_byte(0xFF)
return
encoded = string.encode()
self.write_byte(len(encoded))
diff --git a/system/languages/en-EU.json b/system/languages/en-EU.json
index 0fc6509..4ef8972 100644
--- a/system/languages/en-EU.json
+++ b/system/languages/en-EU.json
@@ -70,7 +70,6 @@
"resizing": "Resizing...",
"split_pic": "Splitting picture...",
"writing_pic": "Writing pixels...",
- "header_done": "Header wrote!",
"compressing_with": "Compressing texture with %s...",
"compression_error": "Compression failed",
"compression_done": "Compression done!",
diff --git a/system/languages/ru-RU.json b/system/languages/ru-RU.json
index afa55b6..e844355 100644
--- a/system/languages/ru-RU.json
+++ b/system/languages/ru-RU.json
@@ -70,7 +70,6 @@
"resizing": "Изменяем размер...",
"split_pic": "Разделяем картинку...",
"writing_pic": "Конвертируем пиксели...",
- "header_done": "Заголовок записан!",
"compressing_with": "Сохраняем с применением %s сжатия...",
"compression_error": "Сжатие не удалось",
"compression_done": "Сжатие прошло успешно!",
diff --git a/system/languages/ua-UA.json b/system/languages/ua-UA.json
index b10f1f3..47ba54b 100644
--- a/system/languages/ua-UA.json
+++ b/system/languages/ua-UA.json
@@ -70,7 +70,6 @@
"resizing": "змінюємо розмір...",
"split_pic": "Розділюємо зоображення...",
"writing_pic": "Записуємо пікселі...",
- "header_done": "Написали Header!",
"compressing_with": "Запаковуємо з %s...",
"compression_error": "Запаковування не вдалося",
"compression_done": "Запаковування виконане!",
diff --git a/system/lib/__init__.py b/system/lib/__init__.py
index 65fe1cf..97bbcc3 100644
--- a/system/lib/__init__.py
+++ b/system/lib/__init__.py
@@ -1,17 +1,7 @@
import sys
-import time
from loguru import logger
-from system import clear
-from system.lib.config import config
-from system.lib.console import Console
-from system.lib.features.directories import clear_directories
-from system.lib.features.initialization import initialize
-from system.lib.features.update.check import check_for_outdated, check_update, get_tags
-from system.lib.menu import Menu, menu
-from system.localization import locale
-
logger.remove()
logger.add(
"./logs/info/{time:YYYY-MM-DD}.log",
@@ -28,183 +18,3 @@
level="ERROR",
)
logger.add(sys.stdout, format="[{level}] {message}", level="INFO")
-
-
-locale.load(config.language)
-
-
-def check_auto_update():
- if config.auto_update and time.time() - config.last_update > 60 * 60 * 24 * 7:
- check_update()
- config.last_update = int(time.time())
- config.dump()
-
-
-def check_files_updated():
- if config.has_update:
- logger.opt(colors=True).info(f'{locale.update_done % ""}')
- if Console.question(locale.done_qu):
- latest_tag = get_tags("vorono4ka", "xcoder")[0]
- latest_tag_name = latest_tag["name"][1:]
-
- config.has_update = False
- config.version = latest_tag_name
- config.last_update = int(time.time())
- config.dump()
- else:
- exit()
-
-
-# noinspection PyUnresolvedReferences
-@logger.catch()
-def refill_menu():
- menu.categories.clear()
-
- sc_category = Menu.Category(0, locale.sc_label)
- ktx_category = Menu.Category(1, locale.ktx_label)
- csv_category = Menu.Category(2, locale.csv_label)
- other = Menu.Category(10, locale.other_features_label)
-
- menu.add_category(sc_category)
- menu.add_category(ktx_category)
- menu.add_category(csv_category)
- menu.add_category(other)
-
- try:
- import sc_compression
-
- del sc_compression
- except ImportError:
- logger.warning(locale.install_to_unlock % "sc-compression")
- else:
- from system.lib.features.csv.compress import compress_csv
- from system.lib.features.csv.decompress import decompress_csv
-
- try:
- import PIL
-
- del PIL
- except ImportError:
- logger.warning(locale.install_to_unlock % "PILLOW")
- else:
- from system.lib.features.sc.decode import (
- decode_and_render_objects,
- decode_textures_only,
- )
- from system.lib.features.sc.encode import (
- collect_objects_and_encode,
- encode_textures_only,
- )
-
- sc_category.add(
- Menu.Item(
- name=locale.decode_sc,
- description=locale.decode_sc_description,
- handler=decode_textures_only,
- )
- )
- sc_category.add(
- Menu.Item(
- name=locale.encode_sc,
- description=locale.encode_sc_description,
- handler=encode_textures_only,
- )
- )
- sc_category.add(
- Menu.Item(
- name=locale.decode_by_parts,
- description=locale.decode_by_parts_description,
- handler=decode_and_render_objects,
- )
- )
- sc_category.add(
- Menu.Item(
- name=locale.encode_by_parts,
- description=locale.encode_by_parts_description,
- handler=collect_objects_and_encode,
- )
- )
- sc_category.add(
- Menu.Item(
- name=locale.overwrite_by_parts,
- description=locale.overwrite_by_parts_description,
- handler=lambda: collect_objects_and_encode(True),
- )
- )
-
- from system.lib.features.ktx import (
- convert_ktx_textures_to_png,
- convert_png_textures_to_ktx,
- )
- from system.lib.pvr_tex_tool import can_use_pvr_tex_tool
-
- if can_use_pvr_tex_tool():
- ktx_category.add(
- Menu.Item(
- name=locale.ktx_from_png_label,
- description=locale.ktx_from_png_description,
- handler=convert_png_textures_to_ktx,
- )
- )
- ktx_category.add(
- Menu.Item(
- name=locale.png_from_ktx_label,
- description=locale.png_from_ktx_description,
- handler=convert_ktx_textures_to_png,
- )
- )
-
- csv_category.add(
- Menu.Item(
- name=locale.decompress_csv,
- description=locale.decompress_csv_description,
- handler=decompress_csv,
- )
- )
- csv_category.add(
- Menu.Item(
- name=locale.compress_csv,
- description=locale.compress_csv_description,
- handler=compress_csv,
- )
- )
-
- other.add(
- Menu.Item(
- name=locale.check_update,
- description=locale.version % config.version,
- handler=check_update,
- )
- )
- other.add(Menu.Item(name=locale.check_for_outdated, handler=check_for_outdated))
- other.add(
- Menu.Item(
- name=locale.reinit,
- description=locale.reinit_description,
- handler=lambda: (initialize(), refill_menu()),
- )
- )
- other.add(
- Menu.Item(
- name=locale.change_language,
- description=locale.change_lang_description % config.language,
- handler=lambda: (config.change_language(locale.change()), refill_menu()),
- )
- )
- other.add(
- Menu.Item(
- name=locale.clear_directories,
- description=locale.clean_dirs_description,
- handler=lambda: clear_directories()
- if Console.question(locale.clear_qu)
- else -1,
- )
- )
- other.add(
- Menu.Item(
- name=locale.toggle_update_auto_checking,
- description=locale.enabled if config.auto_update else locale.disabled,
- handler=lambda: (config.toggle_auto_update(), refill_menu()),
- )
- )
- other.add(Menu.Item(name=locale.exit, handler=lambda: (clear(), exit())))
diff --git a/system/lib/config.py b/system/lib/config.py
index 33fbf6b..eefee68 100644
--- a/system/lib/config.py
+++ b/system/lib/config.py
@@ -16,6 +16,7 @@ def __init__(self):
"has_update",
"last_update",
"auto_update",
+ "should_render_movie_clips",
)
self.initialized: bool = False
@@ -24,6 +25,7 @@ def __init__(self):
self.has_update: bool = False
self.last_update: int = -1
self.auto_update: bool = False
+ self.should_render_movie_clips: bool = False
self.load()
diff --git a/system/lib/features/cut_sprites.py b/system/lib/features/cut_sprites.py
index 2644ed7..82829e4 100644
--- a/system/lib/features/cut_sprites.py
+++ b/system/lib/features/cut_sprites.py
@@ -1,7 +1,12 @@
import os
from pathlib import Path
+from system.lib.config import config
from system.lib.console import Console
+from system.lib.matrices import Matrix2x3
+from system.lib.objects.renderable.renderable_factory import (
+ create_renderable_from_plain,
+)
from system.lib.swf import SupercellSWF
from system.localization import locale
@@ -11,32 +16,24 @@ def render_objects(swf: SupercellSWF, output_folder: Path):
os.makedirs(output_folder / "shapes", exist_ok=True)
os.makedirs(output_folder / "movie_clips", exist_ok=True)
- # TODO: Too slow, fix it
- # movie_clips_skipped = 0
- # movie_clip_count = len(swf.movie_clips)
- # for movie_clip_index in range(movie_clip_count):
- # movie_clip = swf.movie_clips[movie_clip_index]
- #
- # rendered_movie_clip = movie_clip.render(swf)
- # if sum(rendered_movie_clip.size) >= 2:
- # clip_name = movie_clip.export_name or movie_clip.id
- # rendered_movie_clip.save(f"{output_folder}/movie_clips/{clip_name}.png")
- # else:
- # # For debug:
- # # logger.warning(f'MovieClip {movie_clip.id} cannot be rendered.')
- # movie_clips_skipped += 1
- #
- # Console.progress_bar(
- # "Rendering movie clips (%d/%d). Skipped count: %d"
- # % (movie_clip_index + 1, movie_clip_count, movie_clips_skipped),
- # movie_clip_index,
- # movie_clip_count,
- # )
-
- print()
-
shapes_count = len(swf.shapes)
+
swf.xcod_writer.write_uint16(shapes_count)
+ for shape_index in range(shapes_count):
+ shape = swf.shapes[shape_index]
+
+ regions_count = len(shape.regions)
+ swf.xcod_writer.write_uint16(shape.id)
+ swf.xcod_writer.write_uint16(regions_count)
+ for region_index in range(regions_count):
+ region = shape.regions[region_index]
+
+ swf.xcod_writer.write_ubyte(region.texture_index)
+ swf.xcod_writer.write_ubyte(region.get_point_count())
+
+ for i in range(region.get_point_count()):
+ swf.xcod_writer.write_uint16(int(region.get_u(i)))
+ swf.xcod_writer.write_uint16(int(region.get_v(i)))
for shape_index in range(shapes_count):
shape = swf.shapes[shape_index]
@@ -47,30 +44,36 @@ def render_objects(swf: SupercellSWF, output_folder: Path):
shapes_count,
)
- rendered_shape = shape.render()
+ rendered_shape = create_renderable_from_plain(swf, shape).render(Matrix2x3())
rendered_shape.save(f"{output_folder}/shapes/{shape.id}.png")
regions_count = len(shape.regions)
for region_index in range(regions_count):
region = shape.regions[region_index]
- rendered_region = region.render(use_original_size=True)
+ rendered_region = region.get_image()
rendered_region.save(f"{output_folder}/shape_{shape.id}_{region_index}.png")
- for shape_index in range(shapes_count):
- shape = swf.shapes[shape_index]
+ if config.should_render_movie_clips:
+ movie_clips_skipped = 0
+ movie_clip_count = len(swf.movie_clips)
+ for movie_clip_index in range(movie_clip_count):
+ movie_clip = swf.movie_clips[movie_clip_index]
- regions_count = len(shape.regions)
- swf.xcod_writer.write_uint16(shape.id)
- swf.xcod_writer.write_uint16(regions_count)
- for region_index in range(regions_count):
- region = shape.regions[region_index]
-
- swf.xcod_writer.write_ubyte(region.texture_index)
- swf.xcod_writer.write_ubyte(region.get_points_count())
+ rendered_movie_clip = create_renderable_from_plain(swf, movie_clip).render(
+ Matrix2x3()
+ )
+ if sum(rendered_movie_clip.size) >= 2:
+ clip_name = movie_clip.export_name or movie_clip.id
+ rendered_movie_clip.save(f"{output_folder}/movie_clips/{clip_name}.png")
+ else:
+ # For debug:
+ # logger.warning(f'MovieClip {movie_clip.id} cannot be rendered.')
+ movie_clips_skipped += 1
- for i in range(region.get_points_count()):
- swf.xcod_writer.write_uint16(int(region.get_u(i)))
- swf.xcod_writer.write_uint16(int(region.get_v(i)))
- swf.xcod_writer.write_ubyte(1 if region.is_mirrored else 0)
- swf.xcod_writer.write_byte(region.rotation // 90)
+ Console.progress_bar(
+ "Rendering movie clips (%d/%d). Skipped count: %d"
+ % (movie_clip_index + 1, movie_clip_count, movie_clips_skipped),
+ movie_clip_index,
+ movie_clip_count,
+ )
diff --git a/system/lib/features/files.py b/system/lib/features/files.py
index 4e0e7cf..661dd81 100644
--- a/system/lib/features/files.py
+++ b/system/lib/features/files.py
@@ -7,43 +7,24 @@
from system.localization import locale
-def write_sc(output_filename: str | os.PathLike, buffer: bytes, use_lzham: bool):
+def write_sc(
+ output_filename: str | os.PathLike,
+ buffer: bytes,
+ signature: Signatures,
+ version: int | None = None,
+):
with open(output_filename, "wb") as file_out:
- logger.info(locale.header_done)
+ file_out.write(compress(buffer, signature, version))
- if use_lzham:
- logger.info(locale.compressing_with % "LZHAM")
- # Why is this here? It's included in the compression module
- # file_out.write(struct.pack("<4sBI", b"SCLZ", 18, len(buffer)))
- compressed = compress(buffer, Signatures.SCLZ)
-
- file_out.write(compressed)
- else:
- logger.info(locale.compressing_with % "LZMA")
- compressed = compress(buffer, Signatures.SC, 3)
- file_out.write(compressed)
- logger.info(locale.compression_done)
- print()
-
-
-def open_sc(input_filename: str) -> tuple[bytes, bool]:
- use_lzham = False
+def open_sc(input_filename: str) -> tuple[bytes, Signatures]:
with open(input_filename, "rb") as f:
file_data = f.read()
try:
if b"START" in file_data:
file_data = file_data[: file_data.index(b"START")]
- decompressed_data, signature = decompress(file_data)
-
- if signature.name != Signatures.NONE:
- logger.info(locale.detected_comp % signature.name.upper())
-
- if signature == Signatures.SCLZ:
- use_lzham = True
+ return decompress(file_data)
except TypeError:
logger.info(locale.decompression_error)
exit(1)
-
- return decompressed_data, use_lzham
diff --git a/system/lib/features/place_sprites.py b/system/lib/features/place_sprites.py
index 2cf8171..753f1ac 100644
--- a/system/lib/features/place_sprites.py
+++ b/system/lib/features/place_sprites.py
@@ -1,12 +1,11 @@
import os
from pathlib import Path
-from typing import List
-from PIL import Image, ImageDraw
+from PIL import Image
-from system.lib import Console
-from system.lib.helper import get_sides, get_size
-from system.lib.images import get_format_by_pixel_type
+from system.lib.console import Console
+from system.lib.images import create_filled_polygon_image, get_format_by_pixel_type
+from system.lib.math.polygon import get_rect
from system.lib.xcod import FileInfo
from system.localization import locale
@@ -15,7 +14,7 @@
def place_sprites(
file_info: FileInfo, folder: Path, overwrite: bool = False
-) -> List[Image.Image]:
+) -> list[Image.Image]:
files_to_overwrite = os.listdir(folder / ("overwrite" if overwrite else ""))
texture_files = os.listdir(folder / "textures")
@@ -40,59 +39,53 @@ def place_sprites(
)
for region_index, region_info in enumerate(shape_info.regions):
- texture_size = (
- sheets[region_info.texture_id].width,
- sheets[region_info.texture_id].height,
- )
+ texture_width = sheets[region_info.texture_id].width
+ texture_height = sheets[region_info.texture_id].height
filename = f"shape_{shape_info.id}_{region_index}.png"
if filename not in files_to_overwrite:
continue
- img_mask = Image.new("L", texture_size, 0)
- ImageDraw.Draw(img_mask).polygon(region_info.points, fill=MASK_COLOR)
- bbox = img_mask.getbbox()
-
- if not bbox:
- min_x = min(i[0] for i in region_info.points)
- min_y = min(i[1] for i in region_info.points)
- max_x = max(i[0] for i in region_info.points)
- max_y = max(i[1] for i in region_info.points)
+ rect = get_rect(region_info.points)
- if max_y - min_y != 0:
- for _y in range(max_y - min_y):
- img_mask.putpixel((max_x - 1, min_y + _y - 1), MASK_COLOR)
+ img_mask = create_filled_polygon_image(
+ "L", texture_width, texture_height, region_info.points, MASK_COLOR
+ )
- elif max_x - min_x != 0:
- for _x in range(max_x - min_x):
- img_mask.putpixel((min_x + _x - 1, max_y - 1), MASK_COLOR)
+ if rect.width == 0 or rect.height == 0:
+ if rect.height != 0:
+ for _y in range(int(rect.height)):
+ img_mask.putpixel(
+ (int(rect.right - 1), int(rect.top + _y - 1)), MASK_COLOR
+ )
+ rect.right += 1
+ elif rect.width != 0:
+ for _x in range(int(rect.width)):
+ img_mask.putpixel(
+ (int(rect.left + _x - 1), int(rect.bottom - 1)), MASK_COLOR
+ )
+ rect.bottom += 1
else:
- img_mask.putpixel((max_x - 1, max_y - 1), MASK_COLOR)
-
- left, top, right, bottom = get_sides(region_info.points)
- if left == right:
- right += 1
- if top == bottom:
- bottom += 1
-
- width, height = get_size(left, top, right, bottom)
- left = int(left)
- top = int(top)
-
- bbox = int(left), int(top), int(right), int(bottom)
-
- tmp_region = Image.open(
+ img_mask.putpixel(
+ (int(rect.right - 1), int(rect.bottom - 1)), MASK_COLOR
+ )
+ rect.right += 1
+ rect.bottom += 1
+
+ x = int(rect.left)
+ y = int(rect.top)
+ width = int(rect.width)
+ height = int(rect.height)
+ bbox = int(rect.left), int(rect.top), int(rect.right), int(rect.bottom)
+
+ region_image = Image.open(
f'{folder}{"/overwrite" if overwrite else ""}/{filename}'
).convert("RGBA")
- if region_info.is_mirrored:
- tmp_region = tmp_region.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
- tmp_region = tmp_region.rotate(region_info.rotation, expand=True)
- tmp_region = tmp_region.resize((width, height), Image.Resampling.LANCZOS)
sheets[region_info.texture_id].paste(
- Image.new("RGBA", (width, height)), (left, top), img_mask.crop(bbox)
+ Image.new("RGBA", (width, height)), (x, y), img_mask.crop(bbox)
)
- sheets[region_info.texture_id].paste(tmp_region, (left, top), tmp_region)
+ sheets[region_info.texture_id].paste(region_image, (x, y), region_image)
print()
return sheets
diff --git a/system/lib/features/sc/__init__.py b/system/lib/features/sc/__init__.py
index 94b0f6f..efb8969 100644
--- a/system/lib/features/sc/__init__.py
+++ b/system/lib/features/sc/__init__.py
@@ -1,6 +1,5 @@
import struct
from pathlib import Path
-from typing import List
from loguru import logger
from PIL import Image
@@ -16,7 +15,7 @@
def compile_sc(
output_folder: Path,
file_info: FileInfo,
- sheets: List[Image.Image],
+ sheets: list[Image.Image],
):
sc = Writer()
@@ -56,4 +55,12 @@ def compile_sc(
sc.write(bytes(5))
- write_sc(output_folder / f"{file_info.name}.sc", sc.getvalue(), file_info.use_lzham)
+ logger.info(locale.compressing_with % file_info.signature.name.upper())
+ write_sc(
+ output_folder / f"{file_info.name}.sc",
+ sc.getvalue(),
+ file_info.signature,
+ file_info.signature_version,
+ )
+ logger.info(locale.compression_done)
+ print()
diff --git a/system/lib/features/sc/decode.py b/system/lib/features/sc/decode.py
index b9f0273..7e5bcd5 100644
--- a/system/lib/features/sc/decode.py
+++ b/system/lib/features/sc/decode.py
@@ -3,7 +3,9 @@
from pathlib import Path
from loguru import logger
+from sc_compression import Signatures
+from system.bytestream import Writer
from system.lib.features.cut_sprites import render_objects
from system.lib.swf import SupercellSWF
from system.localization import locale
@@ -25,7 +27,7 @@ def decode_textures_only():
swf = SupercellSWF()
base_name = os.path.basename(file).rsplit(".", 1)[0]
try:
- texture_loaded, use_lzham = swf.load(f"{input_folder / file}")
+ texture_loaded, signature = swf.load(f"{input_folder / file}")
if not texture_loaded:
logger.error(locale.not_found % f"{base_name}_tex.sc")
continue
@@ -37,7 +39,7 @@ def decode_textures_only():
)
_save_meta_file(
- swf, objects_output_folder, base_name.rstrip("_"), use_lzham
+ swf, objects_output_folder, base_name.rstrip("_"), signature
)
_save_textures(swf, objects_output_folder, base_name)
except Exception as exception:
@@ -66,7 +68,7 @@ def decode_and_render_objects():
base_name = os.path.basename(file).rsplit(".", 1)[0]
swf = SupercellSWF()
- texture_loaded, use_lzham = swf.load(input_folder / file)
+ texture_loaded, signature = swf.load(input_folder / file)
if not texture_loaded:
logger.error(locale.not_found % f"{base_name}_tex.sc")
continue
@@ -79,7 +81,7 @@ def decode_and_render_objects():
_save_textures(swf, objects_output_folder / "textures", base_name)
render_objects(swf, objects_output_folder)
- _save_meta_file(swf, objects_output_folder, base_name, use_lzham)
+ _save_meta_file(swf, objects_output_folder, base_name, signature)
except Exception as exception:
logger.exception(
locale.error
@@ -113,10 +115,16 @@ def _save_textures(swf: SupercellSWF, textures_output: Path, base_name: str) ->
def _save_meta_file(
- swf: SupercellSWF, objects_output_folder: Path, base_name: str, use_lzham: bool
+ swf: SupercellSWF,
+ objects_output_folder: Path,
+ base_name: str,
+ signature: Signatures,
) -> None:
- with open(objects_output_folder / f"{base_name}.xcod", "wb") as xcod_file:
- xcod_file.write(b"XCOD")
- xcod_file.write(bool.to_bytes(use_lzham, 1, "big"))
- xcod_file.write(int.to_bytes(len(swf.textures), 1, "big"))
- xcod_file.write(swf.xcod_writer.getvalue())
+ writer = Writer()
+ writer.write(b"XCOD")
+ writer.write_string(signature.name)
+ writer.write_ubyte(len(swf.textures))
+ writer.write(swf.xcod_writer.getvalue())
+
+ with open(objects_output_folder / f"{base_name}.xcod", "wb") as file:
+ file.write(writer.getvalue())
diff --git a/system/lib/helper.py b/system/lib/helper.py
deleted file mode 100644
index 4c87e03..0000000
--- a/system/lib/helper.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from typing import List, Tuple, TypeAlias
-
-from system.lib.objects.point import Point
-
-PointType: TypeAlias = Tuple[float, float] | Tuple[int, int] | Point
-
-
-def get_size(left: float, top: float, right: float, bottom: float) -> Tuple[int, int]:
- """Returns width and height of given rect.
-
- :param left: left side of polygon
- :param top: top side of polygon
- :param right: right side of polygon
- :param bottom: bottom side of polygon
- :return: width, height
- """
- return int(right - left), int(bottom - top)
-
-
-def get_sides(
- points: List[Tuple[float, float]] | List[Tuple[int, int]] | List[Point]
-) -> Tuple[float, float, float, float]:
- """Calculates and returns rect sides.
-
- :param points: polygon points
- :return: left, top, right, bottom
- """
-
- if len(points) > 0:
- point: PointType = points[0]
- if isinstance(point, Point):
- left = min(point.x for point in points) # type: ignore
- top = min(point.y for point in points) # type: ignore
- right = max(point.x for point in points) # type: ignore
- bottom = max(point.y for point in points) # type: ignore
- elif isinstance(point, tuple):
- left = min(x for x, _ in points) # type: ignore
- top = min(y for _, y in points) # type: ignore
- right = max(x for x, _ in points) # type: ignore
- bottom = max(y for _, y in points) # type: ignore
- else:
- raise TypeError("Unknown point type.")
-
- return left, top, right, bottom
- raise ValueError("Empty points list.")
diff --git a/system/lib/images.py b/system/lib/images.py
index 242f0e6..697fcab 100644
--- a/system/lib/images.py
+++ b/system/lib/images.py
@@ -1,10 +1,12 @@
import math
import PIL.PyAccess
-from PIL import Image
+from PIL import Image, ImageDraw
from system.bytestream import Reader, Writer
from system.lib.console import Console
+from system.lib.math.point import Point
+from system.lib.matrices import Matrix2x3
from system.lib.pixel_utils import (
get_channel_count_by_pixel_type,
get_read_function,
@@ -17,20 +19,19 @@
def load_image_from_buffer(img: Image.Image) -> None:
width, height = img.size
- # noinspection PyTypeChecker
img_loaded: PIL.PyAccess.PyAccess = img.load() # type: ignore
with open("pixel_buffer", "rb") as pixel_buffer:
- channels_count = int.from_bytes(pixel_buffer.read(1), "little")
+ channel_count = int.from_bytes(pixel_buffer.read(1), "little")
for y in range(height):
for x in range(width):
- img_loaded[x, y] = tuple(pixel_buffer.read(channels_count))
+ img_loaded[x, y] = tuple(pixel_buffer.read(channel_count))
def join_image(img: Image.Image) -> None:
with open("pixel_buffer", "rb") as pixel_buffer:
- channels_count = int.from_bytes(pixel_buffer.read(1), "little")
+ channel_count = int.from_bytes(pixel_buffer.read(1), "little")
width, height = img.size
# noinspection PyTypeChecker
@@ -52,14 +53,14 @@ def join_image(img: Image.Image) -> None:
break
loaded_img[pixel_x, pixel_y] = tuple(
- pixel_buffer.read(channels_count)
+ pixel_buffer.read(channel_count)
)
Console.progress_bar(locale.join_pic, y_chunk, y_chunks_count + 1)
-def split_image(img: Image.Image):
- def add_pixel(pixel: tuple):
+def split_image(img: Image.Image) -> None:
+ def add_pixel(pixel: tuple) -> None:
loaded_image[pixel_index % width, int(pixel_index / width)] = pixel
width, height = img.size
@@ -122,7 +123,6 @@ def load_texture(reader: Reader, pixel_type: int, img: Image.Image) -> None:
with open("pixel_buffer", "wb") as pixel_buffer:
pixel_buffer.write(channel_count.to_bytes(1, "little"))
- print()
width, height = img.size
point = -1
@@ -138,14 +138,14 @@ def load_texture(reader: Reader, pixel_type: int, img: Image.Image) -> None:
point = curr
-def save_texture(writer: Writer, img: Image.Image, pixel_type: int):
+def save_texture(writer: Writer, image: Image.Image, pixel_type: int) -> None:
write_pixel = get_write_function(pixel_type)
if write_pixel is None:
raise Exception(locale.unknown_pixel_type % pixel_type)
- width, height = img.size
+ width, height = image.size
- pixels = img.getdata()
+ pixels = image.getdata()
point = -1
for y in range(height):
for x in range(width):
@@ -157,7 +157,9 @@ def save_texture(writer: Writer, img: Image.Image, pixel_type: int):
point = curr
-def transform_image(image, scale_x, scale_y, angle, x, y):
+def transform_image(
+ image: Image.Image, scale_x: float, scale_y: float, angle: float, x: float, y: float
+) -> Image.Image:
im_orig = image
image = Image.new("RGBA", im_orig.size, (255, 255, 255, 255))
image.paste(im_orig)
@@ -198,13 +200,13 @@ def transform_image(image, scale_x, scale_y, angle, x, y):
return image.transform(
(translated_w, translated_h),
- Image.AFFINE,
+ Image.Transform.AFFINE,
(a, b, c, d, e, f),
- resample=Image.BILINEAR,
+ resample=Image.Resampling.BILINEAR,
)
-def translate_image(image, x, y):
+def translate_image(image, x: float, y: float) -> Image.Image:
w, h = image.size
translated_w = int(math.ceil(w + math.fabs(x)))
@@ -216,23 +218,29 @@ def translate_image(image, x, y):
return image.transform(
(translated_w, translated_h),
- Image.AFFINE,
+ Image.Transform.AFFINE,
(1, 0, -x, 0, 1, -y),
- resample=Image.BILINEAR,
+ resample=Image.Resampling.BILINEAR,
)
-def transform_image_by_matrix(image, matrix: list or tuple):
- scale_x, rotation_x, x = matrix[:3]
- rotation_y, scale_y, y = matrix[3:]
- return transform_image(
- image, scale_x, scale_y, math.atan2(rotation_x, rotation_y), x, y
+def transform_image_by_matrix(image: Image.Image, matrix: Matrix2x3):
+ new_width = abs(int(matrix.apply_x(image.width, image.height)))
+ new_height = abs(int(matrix.apply_y(image.width, image.height)))
+
+ return image.transform(
+ (new_width, new_height),
+ Image.Transform.AFFINE,
+ (matrix.a, matrix.b, matrix.x, matrix.c, matrix.d, matrix.y),
+ resample=Image.Resampling.BILINEAR,
)
-if __name__ == "__main__":
- transform_image_by_matrix(
- Image.open("../../test_0.png"),
- [1.0458984375, 0.0, -127.65, 0.0, 1.0458984375, -700.0],
- ).show()
- input()
+def create_filled_polygon_image(
+ mode: str, width: int, height: int, polygon: list[Point], color: int
+) -> Image.Image:
+ mask_image = Image.new(mode, (width, height), 0)
+ drawable_image = ImageDraw.Draw(mask_image)
+ drawable_image.polygon([point.as_tuple() for point in polygon], fill=color)
+
+ return mask_image
diff --git a/system/lib/main_menu.py b/system/lib/main_menu.py
new file mode 100644
index 0000000..62987ad
--- /dev/null
+++ b/system/lib/main_menu.py
@@ -0,0 +1,191 @@
+import time
+
+from loguru import logger
+
+from system import clear
+from system.lib.config import config
+from system.lib.console import Console
+from system.lib.features.directories import clear_directories
+from system.lib.features.initialization import initialize
+from system.lib.features.update.check import check_for_outdated, check_update, get_tags
+from system.lib.menu import Menu
+from system.localization import locale
+
+menu = Menu()
+
+
+def check_auto_update():
+ if config.auto_update and time.time() - config.last_update > 60 * 60 * 24 * 7:
+ check_update()
+ config.last_update = int(time.time())
+ config.dump()
+
+
+def check_files_updated():
+ if config.has_update:
+ logger.opt(colors=True).info(f'{locale.update_done % ""}')
+ if Console.question(locale.done_qu):
+ latest_tag = get_tags("vorono4ka", "xcoder")[0]
+ latest_tag_name = latest_tag["name"][1:]
+
+ config.has_update = False
+ config.version = latest_tag_name
+ config.last_update = int(time.time())
+ config.dump()
+ else:
+ exit()
+
+
+# noinspection PyUnresolvedReferences
+@logger.catch()
+def refill_menu():
+ menu.categories.clear()
+
+ sc_category = Menu.Category(0, locale.sc_label)
+ ktx_category = Menu.Category(1, locale.ktx_label)
+ csv_category = Menu.Category(2, locale.csv_label)
+ other = Menu.Category(10, locale.other_features_label)
+
+ menu.add_category(sc_category)
+ menu.add_category(ktx_category)
+ menu.add_category(csv_category)
+ menu.add_category(other)
+
+ try:
+ import sc_compression
+
+ del sc_compression
+ except ImportError:
+ logger.warning(locale.install_to_unlock % "sc-compression")
+ else:
+ from system.lib.features.csv.compress import compress_csv
+ from system.lib.features.csv.decompress import decompress_csv
+
+ try:
+ import PIL
+
+ del PIL
+ except ImportError:
+ logger.warning(locale.install_to_unlock % "PILLOW")
+ else:
+ from system.lib.features.sc.decode import (
+ decode_and_render_objects,
+ decode_textures_only,
+ )
+ from system.lib.features.sc.encode import (
+ collect_objects_and_encode,
+ encode_textures_only,
+ )
+
+ sc_category.add(
+ Menu.Item(
+ name=locale.decode_sc,
+ description=locale.decode_sc_description,
+ handler=decode_textures_only,
+ )
+ )
+ sc_category.add(
+ Menu.Item(
+ name=locale.encode_sc,
+ description=locale.encode_sc_description,
+ handler=encode_textures_only,
+ )
+ )
+ sc_category.add(
+ Menu.Item(
+ name=locale.decode_by_parts,
+ description=locale.decode_by_parts_description,
+ handler=decode_and_render_objects,
+ )
+ )
+ sc_category.add(
+ Menu.Item(
+ name=locale.encode_by_parts,
+ description=locale.encode_by_parts_description,
+ handler=collect_objects_and_encode,
+ )
+ )
+ sc_category.add(
+ Menu.Item(
+ name=locale.overwrite_by_parts,
+ description=locale.overwrite_by_parts_description,
+ handler=lambda: collect_objects_and_encode(True),
+ )
+ )
+
+ from system.lib.features.ktx import (
+ convert_ktx_textures_to_png,
+ convert_png_textures_to_ktx,
+ )
+ from system.lib.pvr_tex_tool import can_use_pvr_tex_tool
+
+ if can_use_pvr_tex_tool():
+ ktx_category.add(
+ Menu.Item(
+ name=locale.ktx_from_png_label,
+ description=locale.ktx_from_png_description,
+ handler=convert_png_textures_to_ktx,
+ )
+ )
+ ktx_category.add(
+ Menu.Item(
+ name=locale.png_from_ktx_label,
+ description=locale.png_from_ktx_description,
+ handler=convert_ktx_textures_to_png,
+ )
+ )
+
+ csv_category.add(
+ Menu.Item(
+ name=locale.decompress_csv,
+ description=locale.decompress_csv_description,
+ handler=decompress_csv,
+ )
+ )
+ csv_category.add(
+ Menu.Item(
+ name=locale.compress_csv,
+ description=locale.compress_csv_description,
+ handler=compress_csv,
+ )
+ )
+
+ other.add(
+ Menu.Item(
+ name=locale.check_update,
+ description=locale.version % config.version,
+ handler=check_update,
+ )
+ )
+ other.add(Menu.Item(name=locale.check_for_outdated, handler=check_for_outdated))
+ other.add(
+ Menu.Item(
+ name=locale.reinit,
+ description=locale.reinit_description,
+ handler=lambda: (initialize(), refill_menu()),
+ )
+ )
+ other.add(
+ Menu.Item(
+ name=locale.change_language,
+ description=locale.change_lang_description % config.language,
+ handler=lambda: (config.change_language(locale.change()), refill_menu()),
+ )
+ )
+ other.add(
+ Menu.Item(
+ name=locale.clear_directories,
+ description=locale.clean_dirs_description,
+ handler=lambda: (
+ clear_directories() if Console.question(locale.clear_qu) else -1
+ ),
+ )
+ )
+ other.add(
+ Menu.Item(
+ name=locale.toggle_update_auto_checking,
+ description=locale.enabled if config.auto_update else locale.disabled,
+ handler=lambda: (config.toggle_auto_update(), refill_menu()),
+ )
+ )
+ other.add(Menu.Item(name=locale.exit, handler=lambda: (clear(), exit())))
diff --git a/system/lib/math/__init__.py b/system/lib/math/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/system/lib/objects/point.py b/system/lib/math/point.py
similarity index 66%
rename from system/lib/objects/point.py
rename to system/lib/math/point.py
index 70bfc4a..f1b24b0 100644
--- a/system/lib/objects/point.py
+++ b/system/lib/math/point.py
@@ -1,6 +1,3 @@
-from typing import Tuple
-
-
class Point:
def __init__(self, x: float = 0, y: float = 0):
self.x: float = x
@@ -11,6 +8,16 @@ def __eq__(self, other):
return self.x == other.x and self.y == other.y
return False
+ def __mul__(self, other):
+ if isinstance(other, int):
+ self.x *= other
+ self.y *= other
+ if isinstance(other, float):
+ self.x *= other
+ self.y *= other
+
+ return self
+
def __add__(self, other):
if isinstance(other, Point):
self.x += other.x
@@ -26,13 +33,7 @@ def __neg__(self):
return self
def __repr__(self):
- return str(self.position)
+ return str(self.as_tuple())
- @property
- def position(self) -> Tuple[float, float]:
+ def as_tuple(self) -> tuple[float, float]:
return self.x, self.y
-
- @position.setter
- def position(self, value: Tuple[float, float]):
- self.x = value[0]
- self.y = value[1]
diff --git a/system/lib/math/polygon.py b/system/lib/math/polygon.py
new file mode 100644
index 0000000..1880c10
--- /dev/null
+++ b/system/lib/math/polygon.py
@@ -0,0 +1,104 @@
+from enum import IntEnum
+from math import atan2, degrees
+from typing import TypeAlias
+
+from system.lib.math.point import Point
+from system.lib.math.rect import Rect
+from system.lib.matrices import Matrix2x3
+
+Polygon: TypeAlias = list[Point]
+
+
+class PointOrder(IntEnum):
+ CLOCKWISE = 0
+ COUNTER_CLOCKWISE = 1
+
+
+def get_polygon_sum_of_edges(polygon: Polygon) -> int:
+ """
+ Mostly like signed area, but two times bigger and more accurate with signs.
+
+ https://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order/1165943#1165943
+
+ :param polygon:
+ :return:
+ """
+ points_sum = 0
+ for i in range(len(polygon)):
+ p1 = polygon[i]
+ p2 = polygon[(i + 1) % len(polygon)]
+
+ points_sum += (p2.x - p1.x) * (p1.y + p2.y)
+ return points_sum
+
+
+def get_polygon_point_order(polygon: Polygon) -> PointOrder | None:
+ sum_of_edges = get_polygon_sum_of_edges(polygon)
+ if sum_of_edges > 0:
+ return PointOrder.CLOCKWISE
+ elif sum_of_edges < 0:
+ return PointOrder.COUNTER_CLOCKWISE
+
+ return None
+
+
+def compare_polygons(
+ polygon1: Polygon,
+ polygon2: Polygon,
+) -> tuple[float, bool]:
+ """Calculates rotation and if polygon is mirrored.
+
+ :param polygon1: shape polygon
+ :param polygon2: sheet polygon
+ :return: rotation degrees, is polygon mirrored
+ """
+
+ polygon1_order = get_polygon_point_order(polygon1)
+ polygon2_order = get_polygon_point_order(polygon2)
+
+ mirroring = polygon1_order != polygon2_order
+
+ dx = (polygon1[1].x - polygon1[0].x) * (-1 if mirroring else 1)
+ dy = polygon1[1].y - polygon1[0].y
+ du = polygon2[1].x - polygon2[0].x
+ dv = polygon2[1].y - polygon2[0].y
+
+ # Solution from https://stackoverflow.com/a/21484228/14915825
+ angle_radians = atan2(dy, dx) - atan2(dv, du)
+ angle = degrees(angle_radians)
+
+ return angle, mirroring
+
+
+def get_rect(polygon: list[Point]) -> Rect:
+ """Calculates polygon bounds and returns rect.
+
+ :param polygon: polygon points
+ :return: Rect object
+ """
+
+ rect = Rect(left=100000, top=100000, right=-100000, bottom=-100000)
+
+ for point in polygon:
+ rect.add_point(point.x, point.y)
+
+ return rect
+
+
+def apply_matrix(polygon: Polygon, matrix: Matrix2x3 | None = None) -> Polygon:
+ """Applies affine matrix to the given polygon. If matrix is none, returns points.
+
+ :param polygon: polygon points
+ :param matrix: Affine matrix
+ """
+
+ if matrix is None:
+ return polygon
+
+ return [
+ Point(
+ matrix.apply_x(point.x, point.y),
+ matrix.apply_y(point.x, point.y),
+ )
+ for point in polygon
+ ]
diff --git a/system/lib/math/rect.py b/system/lib/math/rect.py
new file mode 100644
index 0000000..011fc94
--- /dev/null
+++ b/system/lib/math/rect.py
@@ -0,0 +1,45 @@
+from typing import Self
+
+
+class Rect:
+ def __init__(
+ self, *, left: float = 0, top: float = 0, right: float = 0, bottom: float = 0
+ ):
+ self.left: float = left
+ self.top: float = top
+ self.right: float = right
+ self.bottom: float = bottom
+
+ @property
+ def width(self) -> float:
+ return self.right - self.left
+
+ @property
+ def height(self) -> float:
+ return self.bottom - self.top
+
+ def as_tuple(self) -> tuple[float, float, float, float]:
+ return self.left, self.top, self.right, self.bottom
+
+ def add_point(self, x: float, y: float):
+ if x < self.left:
+ self.left = x
+ if x > self.right:
+ self.right = x
+ if y < self.top:
+ self.top = y
+ if y > self.bottom:
+ self.bottom = y
+
+ def merge_bounds(self, other: Self):
+ if other.left < self.left:
+ self.left = other.left
+ if other.right > self.right:
+ self.right = other.right
+ if other.top < self.top:
+ self.top = other.top
+ if other.bottom > self.bottom:
+ self.bottom = other.bottom
+
+ def __str__(self):
+ return f"Rect{self.left, self.top, self.right, self.bottom}"
diff --git a/system/lib/matrices/__init__.py b/system/lib/matrices/__init__.py
index e69de29..7a68aa0 100644
--- a/system/lib/matrices/__init__.py
+++ b/system/lib/matrices/__init__.py
@@ -0,0 +1,5 @@
+__all__ = ["MatrixBank", "Matrix2x3", "ColorTransform"]
+
+from .color_transform import ColorTransform
+from .matrix2x3 import Matrix2x3
+from .matrix_bank import MatrixBank
diff --git a/system/lib/matrices/matrix2x3.py b/system/lib/matrices/matrix2x3.py
index dddb5b4..9bbba4e 100644
--- a/system/lib/matrices/matrix2x3.py
+++ b/system/lib/matrices/matrix2x3.py
@@ -1,3 +1,6 @@
+from math import atan2, cos, degrees, hypot, sin
+from typing import Self
+
from system.bytestream import Reader
DEFAULT_MULTIPLIER = 1024
@@ -5,14 +8,28 @@
class Matrix2x3:
- def __init__(self):
- self.shear_x: float = 0
- self.shear_y: float = 0
- self.scale_x: float = 1
- self.scale_y: float = 1
+ """
+ self matrix looks like:
+ [a c x]
+ [b d y]
+ """
+
+ def __init__(self, matrix: Self | None = None):
+ self.a: float = 1
+ self.b: float = 0
+ self.c: float = 0
+ self.d: float = 1
self.x: float = 0
self.y: float = 0
+ if matrix is not None:
+ self.a = matrix.a
+ self.b = matrix.b
+ self.c = matrix.c
+ self.d = matrix.d
+ self.x = matrix.x
+ self.y = matrix.y
+
def load(self, reader: Reader, tag: int):
divider: int
if tag == 8:
@@ -22,15 +39,52 @@ def load(self, reader: Reader, tag: int):
else:
raise ValueError(f"Unsupported matrix tag: {tag}")
- self.scale_x = reader.read_int() / divider
- self.shear_x = reader.read_int() / divider
- self.shear_y = reader.read_int() / divider
- self.scale_y = reader.read_int() / divider
+ self.a = reader.read_int() / divider
+ self.b = reader.read_int() / divider
+ self.c = reader.read_int() / divider
+ self.d = reader.read_int() / divider
self.x = reader.read_twip()
self.y = reader.read_twip()
def apply_x(self, x: float, y: float):
- return x * self.scale_x + y * self.shear_y + self.x
+ return x * self.a + y * self.c + self.x
def apply_y(self, x: float, y: float):
- return y * self.scale_y + x * self.shear_x + self.y
+ return y * self.d + x * self.b + self.y
+
+ def multiply(self, matrix: Self) -> Self:
+ a = (self.a * matrix.a) + (self.b * matrix.c)
+ b = (self.a * matrix.b) + (self.b * matrix.d)
+ c = (self.d * matrix.d) + (self.c * matrix.b)
+ d = (self.d * matrix.c) + (self.c * matrix.a)
+ x = matrix.apply_x(self.x, self.y)
+ y = matrix.apply_y(self.x, self.y)
+
+ self.a = a
+ self.b = b
+ self.d = c
+ self.c = d
+ self.x = x
+ self.y = y
+
+ return self
+
+ def get_angle_radians(self) -> float:
+ theta = atan2(self.b, self.a)
+ return theta
+
+ def get_angle(self) -> float:
+ return degrees(self.get_angle_radians())
+
+ def get_scale(self) -> tuple[float, float]:
+ scale_x = hypot(self.a, self.b)
+
+ theta = self.get_angle_radians()
+ sin_theta = sin(theta)
+
+ scale_y = self.d / cos(theta) if abs(sin_theta) <= 0.01 else self.c / sin_theta
+
+ return scale_x, scale_y
+
+ def __str__(self):
+ return f"Matrix2x3{self.a, self.b, self.c, self.d, self.x, self.y}"
diff --git a/system/lib/matrices/matrix_bank.py b/system/lib/matrices/matrix_bank.py
index 59fe031..a1e045b 100644
--- a/system/lib/matrices/matrix_bank.py
+++ b/system/lib/matrices/matrix_bank.py
@@ -1,25 +1,23 @@
-from typing import List
-
from system.lib.matrices.color_transform import ColorTransform
from system.lib.matrices.matrix2x3 import Matrix2x3
class MatrixBank:
def __init__(self):
- self.matrices: List[Matrix2x3] = []
- self.color_transforms: List[ColorTransform] = []
+ self._matrices: list[Matrix2x3] = []
+ self._color_transforms: list[ColorTransform] = []
def init(self, matrix_count: int, color_transform_count: int):
- self.matrices = []
+ self._matrices = []
for i in range(matrix_count):
- self.matrices.append(Matrix2x3())
+ self._matrices.append(Matrix2x3())
- self.color_transforms = []
+ self._color_transforms = []
for i in range(color_transform_count):
- self.color_transforms.append(ColorTransform())
+ self._color_transforms.append(ColorTransform())
def get_matrix(self, index: int) -> Matrix2x3:
- return self.matrices[index]
+ return self._matrices[index]
def get_color_transform(self, index: int) -> ColorTransform:
- return self.color_transforms[index]
+ return self._color_transforms[index]
diff --git a/system/lib/menu.py b/system/lib/menu.py
index 385b9c3..4eac6de 100644
--- a/system/lib/menu.py
+++ b/system/lib/menu.py
@@ -113,6 +113,3 @@ def choice(self):
@staticmethod
def _print_divider_line(console_width: int) -> None:
print((console_width - 1) * "-")
-
-
-menu = Menu()
diff --git a/system/lib/objects/movie_clip.py b/system/lib/objects/movie_clip.py
deleted file mode 100644
index 196ea9a..0000000
--- a/system/lib/objects/movie_clip.py
+++ /dev/null
@@ -1,190 +0,0 @@
-from __future__ import annotations
-
-from math import ceil
-from typing import TYPE_CHECKING, List, Tuple
-
-from PIL import Image
-
-from system.bytestream import Reader
-from system.lib.helper import get_size
-from system.lib.matrices.matrix_bank import MatrixBank
-from system.lib.objects.shape import Shape
-
-if TYPE_CHECKING:
- from system.lib.swf import SupercellSWF
-
-
-CACHE = {}
-
-
-class MovieClipFrame:
- def __init__(self):
- self._elements_count: int = 0
- self._label: str | None = None
-
- self._elements: List[Tuple[int, int, int]] = []
-
- def load(self, reader: Reader) -> None:
- self._elements_count = reader.read_short()
- self._label = reader.read_string()
-
- def get_elements_count(self) -> int:
- return self._elements_count
-
- def set_elements(self, elements: List[Tuple[int, int, int]]) -> None:
- self._elements = elements
-
- def get_elements(self) -> List[Tuple[int, int, int]]:
- return self._elements
-
- def get_element(self, index: int) -> Tuple[int, int, int]:
- return self._elements[index]
-
-
-class MovieClip:
- def __init__(self):
- super().__init__()
-
- self.id = -1
- self.export_name: str | None = None
- self.fps: int = 30
- self.frames_count: int = 0
- self.frames: List[MovieClipFrame] = []
- self.frame_elements: List[Tuple[int, int, int]] = []
- self.blends: List[int] = []
- self.binds: List[int] = []
- self.matrix_bank_index: int = 0
-
- def load(self, swf: SupercellSWF, tag: int):
- self.id = swf.reader.read_ushort()
-
- self.fps = swf.reader.read_char()
- self.frames_count = swf.reader.read_ushort()
-
- if tag in (3, 14):
- pass
- else:
- if tag == 49:
- swf.reader.read_char() # unknown
-
- transforms_count = swf.reader.read_uint()
-
- for i in range(transforms_count):
- child_index = swf.reader.read_ushort()
- matrix_index = swf.reader.read_ushort()
- color_transform_index = swf.reader.read_ushort()
-
- self.frame_elements.append(
- (child_index, matrix_index, color_transform_index)
- )
-
- binds_count = swf.reader.read_ushort()
-
- for i in range(binds_count):
- bind_id = swf.reader.read_ushort() # bind_id
- self.binds.append(bind_id)
-
- if tag in (12, 35, 49):
- for i in range(binds_count):
- blend = swf.reader.read_char() # blend
- self.blends.append(blend)
-
- for i in range(binds_count):
- swf.reader.read_string() # bind_name
-
- elements_used = 0
-
- while True:
- frame_tag = swf.reader.read_uchar()
- frame_length = swf.reader.read_int()
-
- if frame_tag == 0:
- break
-
- if frame_tag == 11:
- frame = MovieClipFrame()
- frame.load(swf.reader)
- frame.set_elements(
- self.frame_elements[
- elements_used : elements_used + frame.get_elements_count()
- ]
- )
- self.frames.append(frame)
-
- elements_used += frame.get_elements_count()
- elif frame_tag == 41:
- self.matrix_bank_index = swf.reader.read_uchar()
- else:
- swf.reader.read(frame_length)
-
- def render(self, swf: SupercellSWF, matrix=None) -> Image.Image:
- if self in CACHE:
- return CACHE[self].copy()
-
- matrix_bank = swf.get_matrix_bank(self.matrix_bank_index)
-
- # TODO: make it faster
- left, top, right, bottom = self.get_sides(swf)
-
- width, height = get_size(left, top, right, bottom)
- size = ceil(width), ceil(height)
- image = Image.new("RGBA", size)
-
- frame = self.frames[0]
- for child_index, matrix_index, _ in frame.get_elements():
- if matrix_index != 65535:
- matrix = matrix_bank.get_matrix(matrix_index)
- else:
- matrix = None
-
- display_object = swf.get_display_object(self.binds[child_index])
- if isinstance(display_object, Shape):
- rendered_shape = display_object.render(matrix)
-
- # TODO: fix position
- position = display_object.get_position()
- x = int(abs(left) + position[0])
- y = int(abs(top) + position[1])
-
- image.paste(rendered_shape, (x, y), rendered_shape)
-
- CACHE[self] = image
-
- return image
-
- def get_sides(self, swf: SupercellSWF) -> Tuple[float, float, float, float]:
- matrix_bank: MatrixBank = swf.get_matrix_bank(self.matrix_bank_index)
-
- left = 0
- top = 0
- right = 0
- bottom = 0
-
- for frame in self.frames:
- for child_index, matrix_index, _ in frame.get_elements():
- if matrix_index != 65535:
- matrix = matrix_bank.get_matrix(matrix_index)
- else:
- matrix = None
-
- display_object = swf.get_display_object(self.binds[child_index])
- if isinstance(display_object, Shape):
- display_object.apply_matrix(matrix)
-
- (
- shape_left,
- shape_top,
- shape_right,
- shape_bottom,
- ) = display_object.get_sides()
-
- left = min(left, shape_left)
- top = min(top, shape_top)
- right = max(right, shape_right)
- bottom = max(bottom, shape_bottom)
-
- return left, top, right, bottom
-
- def get_position(self) -> Tuple[float, float]:
- left, top, _, _ = self.get_sides()
- return left, top
diff --git a/system/lib/objects/movie_clip/__init__.py b/system/lib/objects/movie_clip/__init__.py
new file mode 100644
index 0000000..bcb10e4
--- /dev/null
+++ b/system/lib/objects/movie_clip/__init__.py
@@ -0,0 +1,4 @@
+__all__ = ["MovieClip", "MovieClipFrame"]
+
+from system.lib.objects.movie_clip.movie_clip import MovieClip
+from system.lib.objects.movie_clip.movie_clip_frame import MovieClipFrame
diff --git a/system/lib/objects/movie_clip/movie_clip.py b/system/lib/objects/movie_clip/movie_clip.py
new file mode 100644
index 0000000..f1c495d
--- /dev/null
+++ b/system/lib/objects/movie_clip/movie_clip.py
@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from ..plain_object import PlainObject
+from .movie_clip_frame import MovieClipFrame
+
+if TYPE_CHECKING:
+ from system.lib.swf import SupercellSWF
+
+
+CACHE = {}
+
+
+class MovieClip(PlainObject):
+ def __init__(self):
+ super().__init__()
+
+ self.id = -1
+ self.export_name: str | None = None
+ self.fps: int = 30
+ self.frame_count: int = 0
+ self.frames: list[MovieClipFrame] = []
+ self.frame_elements: list[tuple[int, int, int]] = []
+ self.blends: list[int] = []
+ self.binds: list[int] = []
+ self.matrix_bank_index: int = 0
+
+ def load(self, swf: SupercellSWF, tag: int):
+ self.id = swf.reader.read_ushort()
+
+ self.fps = swf.reader.read_char()
+ self.frame_count = swf.reader.read_ushort()
+
+ if tag in (3, 14):
+ pass
+ else:
+ if tag == 49:
+ swf.reader.read_char() # unknown
+
+ transforms_count = swf.reader.read_uint()
+
+ for i in range(transforms_count):
+ child_index = swf.reader.read_ushort()
+ matrix_index = swf.reader.read_ushort()
+ color_transform_index = swf.reader.read_ushort()
+
+ self.frame_elements.append(
+ (child_index, matrix_index, color_transform_index)
+ )
+
+ binds_count = swf.reader.read_ushort()
+
+ for i in range(binds_count):
+ bind_id = swf.reader.read_ushort() # bind_id
+ self.binds.append(bind_id)
+
+ if tag in (12, 35, 49):
+ for i in range(binds_count):
+ blend = swf.reader.read_char() # blend
+ self.blends.append(blend)
+
+ for i in range(binds_count):
+ swf.reader.read_string() # bind_name
+
+ elements_used = 0
+
+ while True:
+ frame_tag = swf.reader.read_uchar()
+ frame_length = swf.reader.read_int()
+
+ if frame_tag == 0:
+ break
+
+ if frame_tag == 11:
+ frame = MovieClipFrame()
+ frame.load(swf.reader)
+ frame.set_elements(
+ self.frame_elements[
+ elements_used : elements_used + frame.get_elements_count()
+ ]
+ )
+ self.frames.append(frame)
+
+ elements_used += frame.get_elements_count()
+ elif frame_tag == 41:
+ self.matrix_bank_index = swf.reader.read_uchar()
+ else:
+ swf.reader.read(frame_length)
diff --git a/system/lib/objects/movie_clip/movie_clip_frame.py b/system/lib/objects/movie_clip/movie_clip_frame.py
new file mode 100644
index 0000000..0e92108
--- /dev/null
+++ b/system/lib/objects/movie_clip/movie_clip_frame.py
@@ -0,0 +1,25 @@
+from system.bytestream import Reader
+
+
+class MovieClipFrame:
+ def __init__(self):
+ self._elements_count: int = 0
+ self._label: str | None = None
+
+ self._elements: list[tuple[int, int, int]] = []
+
+ def load(self, reader: Reader) -> None:
+ self._elements_count = reader.read_short()
+ self._label = reader.read_string()
+
+ def get_elements_count(self) -> int:
+ return self._elements_count
+
+ def set_elements(self, elements: list[tuple[int, int, int]]) -> None:
+ self._elements = elements
+
+ def get_elements(self) -> list[tuple[int, int, int]]:
+ return self._elements
+
+ def get_element(self, index: int) -> tuple[int, int, int]:
+ return self._elements[index]
diff --git a/system/lib/objects/plain_object.py b/system/lib/objects/plain_object.py
new file mode 100644
index 0000000..86d3e7b
--- /dev/null
+++ b/system/lib/objects/plain_object.py
@@ -0,0 +1,13 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from system.lib.swf import SupercellSWF
+
+
+class PlainObject(ABC):
+ @abstractmethod
+ def load(self, swf: SupercellSWF, tag: int):
+ ...
diff --git a/system/lib/objects/renderable/__init__.py b/system/lib/objects/renderable/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/system/lib/objects/renderable/display_object.py b/system/lib/objects/renderable/display_object.py
new file mode 100644
index 0000000..c089e48
--- /dev/null
+++ b/system/lib/objects/renderable/display_object.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+
+from PIL import Image
+
+from system.lib.math.rect import Rect
+from system.lib.matrices import ColorTransform, Matrix2x3
+
+
+class DisplayObject(ABC):
+ def __init__(self):
+ self._matrix = Matrix2x3()
+ self._color_transform = ColorTransform()
+
+ @abstractmethod
+ def calculate_bounds(self, matrix: Matrix2x3) -> Rect:
+ ...
+
+ @abstractmethod
+ def render(self, matrix: Matrix2x3) -> Image.Image:
+ ...
+
+ def set_matrix(self, matrix: Matrix2x3):
+ self._matrix = matrix
+
+ def set_color_transform(self, color_transform: ColorTransform):
+ self._color_transform = color_transform
diff --git a/system/lib/objects/renderable/renderable_factory.py b/system/lib/objects/renderable/renderable_factory.py
new file mode 100644
index 0000000..c351939
--- /dev/null
+++ b/system/lib/objects/renderable/renderable_factory.py
@@ -0,0 +1,29 @@
+from system.lib.objects import Shape
+from system.lib.objects.movie_clip.movie_clip import MovieClip
+from system.lib.objects.plain_object import PlainObject
+from system.lib.objects.renderable.display_object import DisplayObject
+from system.lib.objects.renderable.renderable_movie_clip import RenderableMovieClip
+from system.lib.objects.renderable.renderable_shape import RenderableShape
+from system.lib.swf import SupercellSWF
+
+
+def create_renderable_from_plain(
+ swf: SupercellSWF, plain_object: PlainObject
+) -> DisplayObject:
+ if isinstance(plain_object, Shape):
+ return RenderableShape(plain_object)
+ if isinstance(plain_object, MovieClip):
+ children = []
+
+ for bind_id in plain_object.binds:
+ bind_object = swf.get_display_object(bind_id)
+
+ display_object = None
+ if bind_object is not None:
+ display_object = create_renderable_from_plain(swf, bind_object)
+
+ children.append(display_object)
+
+ return RenderableMovieClip.create_from_plain(swf, plain_object, children)
+
+ raise Exception(f"Unsupported object type: {plain_object}")
diff --git a/system/lib/objects/renderable/renderable_movie_clip.py b/system/lib/objects/renderable/renderable_movie_clip.py
new file mode 100644
index 0000000..c8cdaeb
--- /dev/null
+++ b/system/lib/objects/renderable/renderable_movie_clip.py
@@ -0,0 +1,108 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from PIL import Image
+
+from system.lib.math.rect import Rect
+from system.lib.matrices import ColorTransform, Matrix2x3, MatrixBank
+from system.lib.objects.movie_clip.movie_clip import MovieClip
+from system.lib.objects.movie_clip.movie_clip_frame import MovieClipFrame
+from system.lib.objects.renderable.display_object import DisplayObject
+
+if TYPE_CHECKING:
+ from system.lib.swf import SupercellSWF
+
+
+class RenderableMovieClip(DisplayObject):
+ def __init__(self):
+ super().__init__()
+
+ self._id = -1
+ self._export_name: str | None = None
+ self._fps: int = 30
+ self._frame_count: int = 0
+ self._frames: list[MovieClipFrame] = []
+ self._frame_elements: list[tuple[int, int, int]] = []
+ self._blends: list[int] = []
+ self._binds: list[int] = []
+ self._matrix_bank: MatrixBank | None = None
+
+ self._children: list[DisplayObject] = []
+ self._frame_children: list[DisplayObject] = []
+
+ @staticmethod
+ def create_from_plain(
+ swf: SupercellSWF, movie_clip: MovieClip, children: list[DisplayObject]
+ ) -> RenderableMovieClip:
+ clip = RenderableMovieClip()
+
+ clip._id = movie_clip.id
+ clip._matrix_bank = swf.get_matrix_bank(movie_clip.matrix_bank_index)
+
+ clip._export_name = movie_clip.export_name
+ clip._fps = movie_clip.fps
+ clip._frame_count = movie_clip.frame_count
+ clip._frames = movie_clip.frames
+ clip._frame_elements = movie_clip.frame_elements
+ clip._blends = movie_clip.blends
+ clip._binds = movie_clip.binds
+ clip._children = children
+
+ clip.set_frame(0)
+
+ return clip
+
+ def render(self, matrix: Matrix2x3) -> Image.Image:
+ matrix_multiplied = Matrix2x3(self._matrix)
+ matrix_multiplied.multiply(matrix)
+
+ bounds = self.calculate_bounds(matrix)
+
+ image = Image.new("RGBA", (int(bounds.width), int(bounds.height)))
+
+ for child in self._frame_children:
+ rendered_child = child.render(matrix_multiplied)
+ child_bounds = child.calculate_bounds(matrix_multiplied)
+
+ x = int(child_bounds.left - bounds.left)
+ y = int(child_bounds.top - bounds.top)
+
+ image.paste(rendered_child, (x, y), rendered_child)
+
+ return image
+
+ def calculate_bounds(self, matrix: Matrix2x3) -> Rect:
+ matrix_multiplied = Matrix2x3(self._matrix)
+ matrix_multiplied.multiply(matrix)
+
+ rect = Rect()
+
+ for child in self._frame_children:
+ rect.merge_bounds(child.calculate_bounds(matrix_multiplied))
+
+ return rect
+
+ def set_frame(self, frame_index: int):
+ self._frame_children = []
+
+ frame = self._frames[frame_index]
+ for child_index, matrix_index, color_transform_index in frame.get_elements():
+ matrix = Matrix2x3()
+ if matrix_index != 0xFFFF:
+ matrix = self._matrix_bank.get_matrix(matrix_index)
+
+ color_transform = ColorTransform()
+ if color_transform_index != 0xFFFF:
+ color_transform = self._matrix_bank.get_color_transform(
+ color_transform_index
+ )
+
+ child = self._children[child_index]
+ if child is None:
+ continue
+
+ child.set_matrix(matrix)
+ child.set_color_transform(color_transform)
+
+ self._frame_children.append(child)
diff --git a/system/lib/objects/renderable/renderable_shape.py b/system/lib/objects/renderable/renderable_shape.py
new file mode 100644
index 0000000..598beec
--- /dev/null
+++ b/system/lib/objects/renderable/renderable_shape.py
@@ -0,0 +1,46 @@
+from __future__ import annotations
+
+from PIL import Image
+
+from system.lib.math.rect import Rect
+from system.lib.matrices import Matrix2x3
+from system.lib.objects import Shape
+from system.lib.objects.renderable.display_object import DisplayObject
+
+
+class RenderableShape(DisplayObject):
+ def __init__(self, shape: Shape):
+ super().__init__()
+
+ self._id = shape.id
+ self._regions = shape.regions
+
+ def render(self, matrix: Matrix2x3) -> Image.Image:
+ matrix_multiplied = Matrix2x3(self._matrix)
+ matrix_multiplied.multiply(matrix)
+
+ bounds = self.calculate_bounds(matrix)
+
+ image = Image.new("RGBA", (int(bounds.width), int(bounds.height)))
+
+ for region in self._regions:
+ rendered_region = region.render(matrix_multiplied)
+ region_bounds = region.calculate_bounds(matrix_multiplied)
+
+ x = int(region_bounds.left - bounds.left)
+ y = int(region_bounds.top - bounds.top)
+
+ image.paste(rendered_region, (x, y), rendered_region)
+
+ return image
+
+ def calculate_bounds(self, matrix: Matrix2x3) -> Rect:
+ matrix_multiplied = Matrix2x3(self._matrix)
+ matrix_multiplied.multiply(matrix)
+
+ rect = Rect()
+
+ for region in self._regions:
+ rect.merge_bounds(region.calculate_bounds(matrix_multiplied))
+
+ return rect
diff --git a/system/lib/objects/shape.py b/system/lib/objects/shape.py
deleted file mode 100644
index 3949442..0000000
--- a/system/lib/objects/shape.py
+++ /dev/null
@@ -1,296 +0,0 @@
-from __future__ import annotations
-
-from math import atan2, ceil, degrees
-from typing import TYPE_CHECKING, List, Optional, Tuple
-
-from PIL import Image, ImageDraw
-
-from system.lib.helper import get_sides, get_size
-from system.lib.matrices.matrix2x3 import Matrix2x3
-from system.lib.objects.point import Point
-from system.lib.objects.texture import SWFTexture
-
-if TYPE_CHECKING:
- from system.lib.swf import SupercellSWF
-
-
-class Shape:
- def __init__(self):
- self.id = 0
- self.regions: List[Region] = []
-
- def load(self, swf: SupercellSWF, tag: int):
- self.id = swf.reader.read_ushort()
-
- swf.reader.read_ushort() # regions_count
- if tag == 18:
- swf.reader.read_ushort() # point_count
-
- while True:
- region_tag = swf.reader.read_char()
- region_length = swf.reader.read_uint()
-
- if region_tag == 0:
- return
- elif region_tag in (4, 17, 22):
- region = Region()
- region.load(swf, region_tag)
- self.regions.append(region)
- else:
- swf.reader.read(region_length)
-
- def render(self, matrix=None):
- for region in self.regions:
- region.apply_matrix(matrix)
-
- shape_left, shape_top, shape_right, shape_bottom = self.get_sides()
-
- width, height = get_size(shape_left, shape_top, shape_right, shape_bottom)
- size = ceil(width), ceil(height)
-
- image = Image.new("RGBA", size)
-
- for region in self.regions:
- rendered_region = region.render()
-
- region_left, region_top = region.get_position()
-
- x = int(abs(shape_left) + region_left)
- y = int(abs(shape_top) + region_top)
-
- image.paste(rendered_region, (x, y), rendered_region)
-
- return image
-
- def apply_matrix(self, matrix: Optional[Matrix2x3] = None) -> None:
- """Calls apply_matrix method for all regions.
-
- :param matrix: Affine matrix
- """
-
- for region in self.regions:
- region.apply_matrix(matrix)
-
- def get_position(self) -> Tuple[float, float]:
- left, top, _, _ = self.get_sides()
- return left, top
-
- def get_sides(self) -> Tuple[float, float, float, float]:
- left = 0
- top = 0
- right = 0
- bottom = 0
- for region in self.regions:
- region_left, region_top, region_right, region_bottom = region.get_sides()
- left = min(left, region_left)
- right = max(right, region_right)
- top = min(top, region_top)
- bottom = max(bottom, region_bottom)
-
- return left, top, right, bottom
-
-
-class Region:
- def __init__(self):
- self.texture_index = 0
- self.rotation = 0
- self.is_mirrored = 0
-
- self._points_count = 0
- self._xy_points: List[Point] = []
- self._uv_points: List[Point] = []
- self._transformed_points: List[Point] = []
-
- self.texture: SWFTexture
-
- def load(self, swf: SupercellSWF, tag: int):
- self.texture_index = swf.reader.read_uchar()
-
- self.texture = swf.textures[self.texture_index]
-
- self._points_count = 4
- if tag != 4:
- self._points_count = swf.reader.read_uchar()
-
- self._xy_points = [_class() for _class in [Point] * self._points_count]
- self._uv_points = [_class() for _class in [Point] * self._points_count]
-
- multiplier = 0.5 if swf.use_lowres_texture else 1
-
- for i in range(self._points_count):
- self._xy_points[i].x = swf.reader.read_int() / 20
- self._xy_points[i].y = swf.reader.read_int() / 20
- for i in range(self._points_count):
- u, v = (
- swf.reader.read_ushort()
- * swf.textures[self.texture_index].width
- / 0xFFFF
- * multiplier,
- swf.reader.read_ushort()
- * swf.textures[self.texture_index].height
- / 0xFFFF
- * multiplier,
- )
- u_rounded, v_rounded = map(ceil, (u, v))
- if int(u) == u_rounded:
- u_rounded += 1
- if int(v) == v_rounded:
- v_rounded += 1
-
- self._uv_points[i].position = (u_rounded, v_rounded)
-
- def render(self, use_original_size: bool = False) -> Image.Image:
- self.apply_matrix(None)
-
- left, top, right, bottom = self.get_sides()
- width, height = get_size(left, top, right, bottom)
- width, height = max(width, 1), max(height, 1)
-
- self.rotation, self.is_mirrored = self.calculate_rotation(True)
-
- rendered_region = self.get_image()
- if sum(rendered_region.size) == 2:
- fill_color = rendered_region.getpixel((0, 0))
-
- # noinspection PyTypeChecker
- rendered_polygon = Image.new(rendered_region.mode, (width, height))
- drawable_image = ImageDraw.Draw(rendered_polygon)
- drawable_image.polygon(
- [(point.x - left, point.y - top) for point in self._transformed_points],
- fill=fill_color,
- )
- return rendered_polygon
-
- rendered_region = rendered_region.rotate(-self.rotation, expand=True)
- if self.is_mirrored:
- rendered_region = rendered_region.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
- if use_original_size:
- return rendered_region
- return rendered_region.resize((width, height), Image.Resampling.LANCZOS)
-
- def get_image(self) -> Image.Image:
- left, top, right, bottom = get_sides(self._uv_points)
- width, height = get_size(left, top, right, bottom)
- width, height = max(width, 1), max(height, 1)
- if width + height == 1: # The same speed as without this return
- return Image.new(
- "RGBA",
- (width, height),
- color=self.texture.image.get_pixel(left, top), # type: ignore
- )
-
- if width == 1:
- right += 1
-
- if height == 1:
- bottom += 1
-
- bbox = int(left), int(top), int(right), int(bottom)
-
- color = 255
- img_mask = Image.new("L", (self.texture.width, self.texture.height), 0)
- ImageDraw.Draw(img_mask).polygon(
- [point.position for point in self._uv_points], fill=color
- )
-
- rendered_region = Image.new("RGBA", (width, height))
- rendered_region.paste(
- self.texture.image.crop(bbox), (0, 0), img_mask.crop(bbox)
- )
-
- return rendered_region
-
- def get_points_count(self):
- return self._points_count
-
- def get_uv(self, index: int):
- return self._uv_points[index]
-
- def get_u(self, index: int):
- return self._uv_points[index].x
-
- def get_v(self, index: int):
- return self._uv_points[index].y
-
- def get_xy(self, index: int):
- return self._xy_points[index]
-
- def get_x(self, index: int):
- return self._xy_points[index].x
-
- def get_y(self, index: int):
- return self._xy_points[index].y
-
- def get_position(self) -> Tuple[float, float]:
- left, top, _, _ = get_sides(self._transformed_points)
- return left, top
-
- def get_sides(self) -> Tuple[float, float, float, float]:
- return get_sides(self._transformed_points)
-
- def apply_matrix(self, matrix: Optional[Matrix2x3] = None) -> None:
- """Applies affine matrix to shape (xy) points.
- If matrix is none, copies the points.
-
- :param matrix: Affine matrix
- """
-
- self._transformed_points = self._xy_points
- if matrix is not None:
- self._transformed_points = []
- for point in self._xy_points:
- self._transformed_points.append(
- Point(
- matrix.apply_x(point.x, point.y),
- matrix.apply_y(point.x, point.y),
- )
- )
-
- def calculate_rotation(
- self,
- round_to_nearest: bool = False,
- custom_points: Optional[List[Point]] = None,
- ) -> tuple[int, bool]:
- """Calculates rotation and if region is mirrored.
-
- :param round_to_nearest: should round to a multiple of 90
- :param custom_points: transformed points, replacement of self._xy_points
- :return: rotation angle, is mirroring
- """
-
- def is_clockwise(points: List[Point]):
- points_sum = 0
- for i in range(len(points)):
- x1, y1 = points[(i + 1) % len(points)].position
- x2, y2 = points[i].position
- points_sum += (x1 - x2) * (y1 + y2)
- return points_sum > 0
-
- xy_points = self._xy_points
- if custom_points is not None:
- xy_points = custom_points
-
- is_uv_clockwise = is_clockwise(self._uv_points)
- is_xy_clockwise = is_clockwise(xy_points)
-
- mirroring = not (is_uv_clockwise == is_xy_clockwise)
-
- dx = xy_points[1].x - xy_points[0].x
- dy = xy_points[1].y - xy_points[0].y
- du = self._uv_points[1].x - self._uv_points[0].x
- dv = self._uv_points[1].y - self._uv_points[0].y
-
- angle_xy = degrees(atan2(dy, dx)) % 360
- angle_uv = degrees(atan2(dv, du)) % 360
-
- angle = angle_xy - angle_uv
-
- if mirroring:
- angle -= 180
-
- angle = (angle + 360) % 360
-
- if round_to_nearest:
- angle = round(angle / 90) * 90
-
- return int(angle), mirroring
diff --git a/system/lib/objects/shape/__init__.py b/system/lib/objects/shape/__init__.py
new file mode 100644
index 0000000..841f84e
--- /dev/null
+++ b/system/lib/objects/shape/__init__.py
@@ -0,0 +1,4 @@
+__all__ = ["Shape", "Region"]
+
+from system.lib.objects.shape.region import Region
+from system.lib.objects.shape.shape import Shape
diff --git a/system/lib/objects/shape/region.py b/system/lib/objects/shape/region.py
new file mode 100644
index 0000000..6a3e8ad
--- /dev/null
+++ b/system/lib/objects/shape/region.py
@@ -0,0 +1,134 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from PIL import Image
+
+from system.lib.images import create_filled_polygon_image
+from system.lib.math.point import Point
+from system.lib.math.polygon import apply_matrix, compare_polygons, get_rect
+from system.lib.math.rect import Rect
+from system.lib.matrices import Matrix2x3
+
+if TYPE_CHECKING:
+ from system.lib.objects import SWFTexture
+ from system.lib.swf import SupercellSWF
+
+
+class Region:
+ def __init__(self):
+ self.texture_index: int = 0
+
+ self._texture: SWFTexture | None = None
+ self._point_count = 0
+ self._xy_points: list[Point] = []
+ self._uv_points: list[Point] = []
+
+ self._cache_image: Image.Image | None = None
+
+ def load(self, swf: SupercellSWF, tag: int):
+ self.texture_index = swf.reader.read_uchar()
+
+ self._texture = swf.textures[self.texture_index]
+
+ self._point_count = 4
+ if tag != 4:
+ self._point_count = swf.reader.read_uchar()
+
+ self._xy_points: list[Point] = [Point() for _ in range(self._point_count)]
+ self._uv_points: list[Point] = [Point() for _ in range(self._point_count)]
+
+ for i in range(self._point_count):
+ x = swf.reader.read_int() / 20
+ y = swf.reader.read_int() / 20
+
+ self._xy_points[i] = Point(x, y)
+
+ for i in range(self._point_count):
+ if tag == 4:
+ u = swf.reader.read_ushort() * 0xFFFF / self._texture.width
+ v = swf.reader.read_ushort() * 0xFFFF / self._texture.height
+ else:
+ u = swf.reader.read_ushort() * self._texture.width / 0xFFFF
+ v = swf.reader.read_ushort() * self._texture.height / 0xFFFF
+
+ self._uv_points[i] = Point(u, v) * (0.5 if swf.use_lowres_texture else 1)
+
+ def render(self, matrix: Matrix2x3) -> Image.Image:
+ transformed_points = apply_matrix(self._xy_points, matrix)
+
+ rect = get_rect(transformed_points)
+ width, height = max(int(rect.width), 1), max(int(rect.height), 1)
+
+ rendered_region = self.get_image()
+ if rendered_region.width + rendered_region.height <= 2:
+ fill_color: int = rendered_region.getpixel((0, 0)) # type: ignore
+
+ return create_filled_polygon_image(
+ rendered_region.mode, width, height, transformed_points, fill_color
+ )
+
+ self.rotation, self.is_mirrored = compare_polygons(
+ transformed_points, self._uv_points
+ )
+
+ rendered_region = rendered_region.rotate(-self.rotation, expand=True)
+ if self.is_mirrored:
+ rendered_region = rendered_region.transpose(Image.Transpose.FLIP_LEFT_RIGHT)
+
+ return rendered_region.resize((width, height), Image.Resampling.BILINEAR)
+
+ def get_image(self) -> Image.Image:
+ # Note: it's 100% safe and very helpful for rendering movie clips
+ if self._cache_image is not None:
+ return self._cache_image
+
+ rect = get_rect(self._uv_points)
+
+ width = max(int(rect.width), 1)
+ height = max(int(rect.height), 1)
+ if width + height <= 2: # The same speed as without this return
+ return Image.new(
+ "RGBA",
+ (1, 1),
+ color=self._texture.image.getpixel((int(rect.left), int(rect.top))),
+ )
+
+ mask_image = create_filled_polygon_image(
+ "L", self._texture.width, self._texture.height, self._uv_points, 0xFF
+ )
+
+ rendered_region = Image.new("RGBA", (width, height))
+ rendered_region.paste(
+ self._texture.image.crop(rect.as_tuple()),
+ (0, 0),
+ mask_image.crop(rect.as_tuple()),
+ )
+
+ self._cache_image = rendered_region
+
+ return rendered_region
+
+ def get_point_count(self):
+ return self._point_count
+
+ def get_uv(self, index: int):
+ return self._uv_points[index]
+
+ def get_u(self, index: int):
+ return self._uv_points[index].x
+
+ def get_v(self, index: int):
+ return self._uv_points[index].y
+
+ def get_xy(self, index: int):
+ return self._xy_points[index]
+
+ def get_x(self, index: int):
+ return self._xy_points[index].x
+
+ def get_y(self, index: int):
+ return self._xy_points[index].y
+
+ def calculate_bounds(self, matrix: Matrix2x3 | None = None) -> Rect:
+ return get_rect(apply_matrix(self._xy_points, matrix))
diff --git a/system/lib/objects/shape/shape.py b/system/lib/objects/shape/shape.py
new file mode 100644
index 0000000..2d5dc4d
--- /dev/null
+++ b/system/lib/objects/shape/shape.py
@@ -0,0 +1,37 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from system.lib.objects.plain_object import PlainObject
+from system.lib.objects.shape import Region
+
+if TYPE_CHECKING:
+ from system.lib.swf import SupercellSWF
+
+
+class Shape(PlainObject):
+ def __init__(self):
+ super().__init__()
+
+ self.id = 0
+ self.regions: list[Region] = []
+
+ def load(self, swf: SupercellSWF, tag: int):
+ self.id = swf.reader.read_ushort()
+
+ swf.reader.read_ushort() # regions_count
+ if tag == 18:
+ swf.reader.read_ushort() # point_count
+
+ while True:
+ region_tag = swf.reader.read_char()
+ region_length = swf.reader.read_uint()
+
+ if region_tag == 0:
+ return
+ elif region_tag in (4, 17, 22):
+ region = Region()
+ region.load(swf, region_tag)
+ self.regions.append(region)
+ else:
+ swf.reader.read(region_length)
diff --git a/system/lib/objects/texture.py b/system/lib/objects/texture.py
index a3b2a56..c2cc499 100644
--- a/system/lib/objects/texture.py
+++ b/system/lib/objects/texture.py
@@ -40,31 +40,34 @@ def load(self, swf: SupercellSWF, tag: int, has_texture: bool):
if not has_texture:
return
+ khronos_texture_data = None
if tag == 45:
# noinspection PyUnboundLocalVariable
khronos_texture_data = swf.reader.read(khronos_texture_length)
- self.image = get_image_from_ktx_data(khronos_texture_data)
- return
elif tag == 47:
with open(
swf.filepath.parent / self.khronos_texture_filename, "rb"
) as file:
decompressor = zstandard.ZstdDecompressor()
- decompressed = decompressor.decompress(file.read())
- self.image = get_image_from_ktx_data(decompressed)
+ khronos_texture_data = decompressor.decompress(file.read())
+
+ if khronos_texture_data is not None:
+ self.image = get_image_from_ktx_data(khronos_texture_data).resize(
+ (self.width, self.height), Image.Resampling.LANCZOS
+ )
return
- img = Image.new(
+ image = Image.new(
get_format_by_pixel_type(self.pixel_type), (self.width, self.height)
)
- load_texture(swf.reader, self.pixel_type, img)
+ load_texture(swf.reader, self.pixel_type, image)
if tag in (27, 28, 29):
- join_image(img)
+ join_image(image)
else:
- load_image_from_buffer(img)
+ load_image_from_buffer(image)
os.remove("pixel_buffer")
- self.image = img
+ self.image = image
diff --git a/system/lib/swf.py b/system/lib/swf.py
index 9645b61..c203c28 100644
--- a/system/lib/swf.py
+++ b/system/lib/swf.py
@@ -1,13 +1,14 @@
import os
from pathlib import Path
-from typing import List, Tuple
from loguru import logger
+from sc_compression import Signatures
from system.bytestream import Reader, Writer
from system.lib.features.files import open_sc
from system.lib.matrices.matrix_bank import MatrixBank
from system.lib.objects import MovieClip, Shape, SWFTexture
+from system.lib.objects.plain_object import PlainObject
from system.localization import locale
DEFAULT_HIGHRES_SUFFIX = "_highres"
@@ -27,9 +28,9 @@ def __init__(self):
self.use_lowres_texture: bool = False
- self.shapes: List[Shape] = []
- self.movie_clips: List[MovieClip] = []
- self.textures: List[SWFTexture] = []
+ self.shapes: list[Shape] = []
+ self.movie_clips: list[MovieClip] = []
+ self.textures: list[SWFTexture] = []
self.xcod_writer = Writer("big")
@@ -47,38 +48,43 @@ def __init__(self):
self._text_field_count: int = 0
self._export_count: int = 0
- self._export_ids: List[int] = []
- self._export_names: List[str] = []
+ self._export_ids: list[int] = []
+ self._export_names: list[str] = []
- self._matrix_banks: List[MatrixBank] = []
+ self._matrix_banks: list[MatrixBank] = []
self._matrix_bank: MatrixBank | None = None
- def load(self, filepath: str | os.PathLike) -> Tuple[bool, bool]:
+ def load(self, filepath: str | os.PathLike) -> tuple[bool, Signatures]:
self._filepath = Path(filepath)
- texture_loaded, use_lzham = self._load_internal(
+ texture_loaded, signature = self._load_internal(
self._filepath, self._filepath.name.endswith("_tex.sc")
)
if not texture_loaded:
if self._use_uncommon_texture:
- texture_loaded, use_lzham = self._load_internal(
+ texture_loaded, signature = self._load_internal(
self._uncommon_texture_path, True
)
else:
texture_path = str(self._filepath)[:-3] + SupercellSWF.TEXTURE_EXTENSION
- texture_loaded, use_lzham = self._load_internal(texture_path, True)
+ texture_loaded, signature = self._load_internal(texture_path, True)
- return texture_loaded, use_lzham
+ return texture_loaded, signature
def _load_internal(
self, filepath: str | os.PathLike, is_texture_file: bool
- ) -> Tuple[bool, bool]:
+ ) -> tuple[bool, Signatures]:
self.filename = os.path.basename(filepath)
logger.info(locale.collecting_inf % self.filename)
- decompressed_data, use_lzham = open_sc(filepath)
+ decompressed_data, signature = open_sc(filepath)
+
+ if signature.name != Signatures.NONE:
+ logger.info(locale.detected_comp % signature.name.upper())
+ print()
+
self.reader = Reader(decompressed_data)
del decompressed_data
@@ -127,7 +133,7 @@ def _load_internal(
if isinstance(movie_clip, MovieClip):
movie_clip.export_name = export_name
- return loaded, use_lzham
+ return loaded, signature
def _load_tags(self, is_texture_file: bool) -> bool:
has_texture = True
@@ -163,6 +169,7 @@ def _load_tags(self, is_texture_file: bool) -> bool:
texture.height,
)
)
+ print()
self.xcod_writer.write_ubyte(tag)
self.xcod_writer.write_ubyte(texture.pixel_type)
@@ -213,7 +220,7 @@ def _load_tags(self, is_texture_file: bool) -> bool:
def get_display_object(
self, target_id: int, name: str | None = None, *, raise_error: bool = False
- ) -> Shape | MovieClip | None:
+ ) -> PlainObject | None:
for shape in self.shapes:
if shape.id == target_id:
return shape
diff --git a/system/lib/xcod.py b/system/lib/xcod.py
index 79b07c8..1b27806 100644
--- a/system/lib/xcod.py
+++ b/system/lib/xcod.py
@@ -3,11 +3,12 @@
import os
from dataclasses import dataclass
from pathlib import Path
-from typing import Tuple
from loguru import logger
+from sc_compression import Signatures
from system.bytestream import Reader
+from system.lib.math.point import Point
from system.localization import locale
@@ -15,7 +16,7 @@
class SheetInfo:
file_type: int
pixel_type: int
- size: Tuple[int, int]
+ size: tuple[int, int]
@property
def width(self) -> int:
@@ -29,9 +30,7 @@ def height(self) -> int:
@dataclass
class RegionInfo:
texture_id: int
- points: list[tuple[int, int]]
- is_mirrored: bool
- rotation: int
+ points: list[Point]
@dataclass
@@ -43,7 +42,8 @@ class ShapeInfo:
@dataclass
class FileInfo:
name: str
- use_lzham: bool
+ signature: Signatures
+ signature_version: int | None
sheets: list[SheetInfo]
shapes: list[ShapeInfo]
@@ -57,7 +57,9 @@ def parse_info(metadata_file_path: Path, has_detailed_info: bool) -> FileInfo:
ensure_magic_known(reader)
- file_info = FileInfo(os.path.splitext(metadata_file_path.name)[0], False, [], [])
+ file_info = FileInfo(
+ os.path.splitext(metadata_file_path.name)[0], Signatures.NONE, None, [], []
+ )
parse_base_info(file_info, reader)
if has_detailed_info:
@@ -67,8 +69,9 @@ def parse_info(metadata_file_path: Path, has_detailed_info: bool) -> FileInfo:
def parse_base_info(file_info: FileInfo, reader: Reader) -> None:
- use_lzham = reader.read_uchar() == 1
- file_info.use_lzham = use_lzham
+ file_info.signature = Signatures.SC
+ file_info.signature_version = 1 if reader.read_string() == "LZMA" else 3
+
sheets_count = reader.read_uchar()
for i in range(sheets_count):
file_type = reader.read_uchar()
@@ -91,13 +94,11 @@ def parse_detailed_info(file_info: FileInfo, reader: Reader) -> None:
texture_id, points_count = reader.read_uchar(), reader.read_uchar()
points = [
- (reader.read_ushort(), reader.read_ushort())
+ Point(reader.read_ushort(), reader.read_ushort())
for _ in range(points_count)
]
- is_mirrored, rotation = reader.read_uchar() == 1, reader.read_char() * 90
-
- regions.append(RegionInfo(texture_id, points, is_mirrored, rotation))
+ regions.append(RegionInfo(texture_id, points))
file_info.shapes.append(ShapeInfo(shape_id, regions))
diff --git a/system/localization.py b/system/localization.py
index 3249633..1837014 100644
--- a/system/localization.py
+++ b/system/localization.py
@@ -1,6 +1,8 @@
import json
import os
+from system.lib.config import config
+
DEFAULT_STRING = "NO LOCALE"
@@ -76,7 +78,6 @@ def __init__(self):
self.resizing: str = DEFAULT_STRING
self.split_pic: str = DEFAULT_STRING
self.writing_pic: str = DEFAULT_STRING
- self.header_done: str = DEFAULT_STRING
self.compressing_with: str = DEFAULT_STRING
self.compression_error: str = DEFAULT_STRING
self.compression_done: str = DEFAULT_STRING
@@ -144,3 +145,4 @@ def change(self):
locale = Locale()
+locale.load(config.language)
diff --git a/tests/polygon_test.py b/tests/polygon_test.py
new file mode 100644
index 0000000..c15120a
--- /dev/null
+++ b/tests/polygon_test.py
@@ -0,0 +1,59 @@
+from system.lib.math.point import Point
+from system.lib.math.polygon import PointOrder, Polygon, get_polygon_point_order
+
+
+def create_polygon_from_tuple(*polygon: tuple[float, float]) -> Polygon:
+ return [Point(x, y) for x, y in polygon]
+
+
+def assert_equals(expected, existing) -> None:
+ assert expected == existing, f"Got {existing}, while {expected=}"
+
+
+def test1():
+ polygon = create_polygon_from_tuple(
+ (4.0, 4.0), (5.0, -2.0), (-1.0, -4.0), (-6.0, 0.0), (-2.0, 5.0), (0.0, 1.0)
+ )
+
+ assert_equals(PointOrder.CLOCKWISE, get_polygon_point_order(polygon))
+ polygon = create_polygon_from_tuple((0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0))
+
+ assert_equals(PointOrder.CLOCKWISE, get_polygon_point_order(polygon))
+
+ polygon = create_polygon_from_tuple(
+ (160.0, -73.0),
+ (89.0, -73.0),
+ (89.0, -10.0),
+ (143.0, 10.0),
+ (156.0, 10.0),
+ (160.0, -46.0),
+ )
+
+ assert_equals(PointOrder.CLOCKWISE, get_polygon_point_order(polygon))
+
+ polygon = create_polygon_from_tuple(
+ (5.0, 0.0), (6.0, 4.0), (4.0, 5.0), (1.0, 5.0), (1.0, 0.0)
+ )
+
+ assert_equals(PointOrder.COUNTER_CLOCKWISE, get_polygon_point_order(polygon))
+
+ polygon = create_polygon_from_tuple(
+ (0.0, 0.0), (11.0, 0.0), (0.0, 10.0), (10.0, 10.0)
+ )
+
+ assert_equals(PointOrder.COUNTER_CLOCKWISE, get_polygon_point_order(polygon))
+
+ polygon = create_polygon_from_tuple(
+ (20.0, -73.0),
+ (91.0, -73.0),
+ (91.0, -10.0),
+ (37.0, 10.0),
+ (24.0, 10.0),
+ (20.0, -46.0),
+ )
+
+ assert_equals(PointOrder.COUNTER_CLOCKWISE, get_polygon_point_order(polygon))
+
+
+if __name__ == "__main__":
+ test1()