Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Небольшая оптмизация класса текстур и выкидыш аудио #3

Merged
merged 7 commits into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ venv.bak/
.vs/*
.vscode/*

# Data
[Dd]ata/*
# Server data
Content/Servers/*

# Texture
Sprites/
Content/Compiled/*
*_compiled*.gif
*_compiled*.png

# PyQT6
*_ui.py
Empty file.
6 changes: 6 additions & 0 deletions Code/systems/audio_system/audio_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from pydub import AudioSegment
from pydub.playback import play


class AudioManager:
__slots__ = []
3 changes: 3 additions & 0 deletions Code/systems/texture_system/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
from systems.texture_system.color import Color
from systems.texture_system.texture_system import TextureSystem

__all__ = ['Color', 'TextureSystem']
68 changes: 68 additions & 0 deletions Code/systems/texture_system/color.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from typing import Tuple


class Color:
__slots__ = ['_r', '_g', '_b', '_a']

def __init__(self, r: int, g: int, b: int, a: int) -> None:
self.r = r
self.g = g
self.b = b
self.a = a

@property
def r(self) -> int:
return self._r

@r.setter
def r(self, value: int) -> None:
if not (0 <= value <= 255):
raise ValueError("Invalid value for r. It must be between 0 and 255")

self._r = value

@property
def g(self) -> int:
return self._g

@g.setter
def g(self, value: int) -> None:
if not (0 <= value <= 255):
raise ValueError("Invalid value for g. It must be between 0 and 255")

self._g = value

@property
def b(self) -> int:
return self._b

@b.setter
def b(self, value: int) -> None:
if not (0 <= value <= 255):
raise ValueError("Invalid value for b. It must be between 0 and 255")

self._b = value

@property
def a(self) -> int:
return self._a

@a.setter
def a(self, value: int) -> None:
if not (0 <= value <= 255):
raise ValueError("Invalid value for a. It must be between 0 and 255")

self._a = value

def __str__(self) -> str:
return f"{self.r}_{self.g}_{self.b}_{self.a}"

def __repr__(self) -> str:
return f"Color(r={self.r}, g={self.g}, b={self.b}, a={self.a})"

@staticmethod
def from_tuple(value: Tuple[int, int, int, int]) -> "Color":
return Color(value[0], value[1], value[2], value[3])

def to_tuple(self) -> Tuple[int, int, int, int]:
return (self.r, self.g, self.r, self.a)
66 changes: 21 additions & 45 deletions Code/systems/texture_system/texture_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@

import yaml
from PIL import Image, ImageSequence
from root_path import ROOT_PATH
from systems.texture_system.color import Color


class TextureSystem:
"""Статический класс TextureSystem отвечает за управление текстурами, включая их загрузку, изменение цвета, и объединение слоев в одно изображение или GIF.
"""
__slots__ = []
DEFAULT_FPS: int = 24
DEFAULT_COLOR: Tuple[int, int, int, int] = (255, 255, 255, 255)
DEFAULT_COLOR: Color = Color(255, 255, 255, 255)

@staticmethod
def _get_hash_list(layers: List[Dict[str, Any]]) -> str:
Expand Down Expand Up @@ -54,32 +56,6 @@ def _slice_image(image: Image.Image, frame_width: int, frame_height: int, num_fr

return frames

@staticmethod
def _get_color_str(color: Tuple[int, int, int, int]) -> str:
"""Возвращает строковое представление цвета.

Args:
color (Tuple[int, int, int, int]): Цвет в формате RGBA.

Returns:
str: Строковое представление цвета.
"""
TextureSystem._validate_color(color)
return '_'.join(map(str, color))

@staticmethod
def _validate_color(color: Tuple[int, int, int, int]) -> None:
"""Проверяет, что цвет в формате RGBA имеет корректные значения.

Args:
color (Tuple[int, int, int, int]): Цвет в формате RGBA.

Raises:
ValueError: Если значения цвета находятся вне диапазона 0-255.
"""
if not all(0 <= c <= 255 for c in color):
raise ValueError("Invalid RGBA color format for texture. All values must be between 0 и 255")

@staticmethod
def get_textures(path: str) -> List[Dict[str, Any]]:
"""Загружает текстуры из указанного пути.
Expand All @@ -93,7 +69,7 @@ def get_textures(path: str) -> List[Dict[str, Any]]:
with open(f"{path}/info.yml", 'r') as file:
info = yaml.safe_load(file)

return info.get('Sprites', [])
return info.get('Texture', [])

@staticmethod
def get_state_info(path: str, state: str) -> Tuple[int, int, int, bool]:
Expand All @@ -112,7 +88,7 @@ def get_state_info(path: str, state: str) -> Tuple[int, int, int, bool]:
with open(f"{path}/info.yml", 'r') as file:
info = yaml.safe_load(file)

info = info.get('Sprites', [])
info = info.get('Texture', [])

sprite_info = next((sprite for sprite in info if sprite['name'] == state), None)
if not sprite_info:
Expand All @@ -126,21 +102,21 @@ def get_state_info(path: str, state: str) -> Tuple[int, int, int, bool]:
return frame_width, frame_height, num_frames, is_mask

@staticmethod
def _get_compiled(path: str, state: str, color: Optional[Tuple[int, int, int, int]] = None, is_gif: bool = False) -> Union[Image.Image, List[Image.Image], None]:
def _get_compiled(path: str, state: str, color: Optional[Color] = None, is_gif: bool = False) -> Union[Image.Image, List[Image.Image], None]:
"""Проверяет наличие компилированного изображения или GIF.

Args:
path (str): Путь к файлу.
state (str): Имя состояния.
color (Optional[Tuple[int, int, int, int]], optional): Цвет в формате RGBA. По умолчанию None.
color (Optional[Color], optional): Цвет в формате RGBA. По умолчанию None.
is_gif (bool, optional): Указывает, является ли изображение GIF. По умолчанию False.

Returns:
Union[Image.Image, List[Image.Image], None]: Изображение или список кадров, если существует, иначе None.
"""
image_path: str = f"{path}/{state}"
if color:
image_path += f"_compiled_{TextureSystem._get_color_str(color)}"
image_path += f"_compiled_{color}"

image_path += ".gif" if is_gif else ".png"

Expand All @@ -155,13 +131,13 @@ def _get_compiled(path: str, state: str, color: Optional[Tuple[int, int, int, in
return None

@staticmethod
def get_image_recolor(path: str, state: str, color: Tuple[int, int, int, int] = DEFAULT_COLOR) -> Image.Image:
def get_image_recolor(path: str, state: str, color: Color = DEFAULT_COLOR) -> Image.Image:
"""Возвращает перекрашенное изображение указанного состояния.

Args:
path (str): Путь к файлу.
state (str): Имя состояния.
color (Tuple[int, int, int, int], optional): Цвет в формате RGBA. По умолчанию DEFAULT_COLOR.
color (Color, optional): Цвет в формате RGBA. По умолчанию DEFAULT_COLOR.

Returns:
Image.Image: Перекрашенное изображение.
Expand All @@ -174,16 +150,16 @@ def get_image_recolor(path: str, state: str, color: Tuple[int, int, int, int] =
image = image.convert("RGBA")
new_colored_image = [
(
int(pixel[0] * color[0] / 255),
int(pixel[0] * color[1] / 255),
int(pixel[0] * color[2] / 255),
int(pixel[0] * color.r / 255),
int(pixel[0] * color.g / 255),
int(pixel[0] * color.b / 255),
pixel[3]
) if pixel[3] != 0 else pixel
for pixel in image.getdata()
]

image.putdata(new_colored_image)
image.save(f"{path}/{state}_compiled_{TextureSystem._get_color_str(color)}.png")
image.save(f"{path}/{state}_compiled_{color}.png")
return image

@staticmethod
Expand All @@ -207,13 +183,13 @@ def get_image(path: str, state: str) -> Image.Image:
raise FileNotFoundError(f"Image file for state '{state}' not found in path '{path}'.")

@staticmethod
def get_gif_recolor(path: str, state: str, color: Tuple[int, int, int, int] = DEFAULT_COLOR, fps: int = DEFAULT_FPS) -> List[Image.Image]:
def get_gif_recolor(path: str, state: str, color: Color = DEFAULT_COLOR, fps: int = DEFAULT_FPS) -> List[Image.Image]:
"""Возвращает перекрашенный GIF указанного состояния.

Args:
path (str): Путь к файлу.
state (str): Имя состояния.
color (Tuple[int, int, int, int], optional): Цвет в формате RGBA. По умолчанию DEFAULT_COLOR.
color (Color, optional): Цвет в формате RGBA. По умолчанию DEFAULT_COLOR.
fps (int, optional): Частота кадров. По умолчанию DEFAULT_FPS.

Returns:
Expand All @@ -229,7 +205,7 @@ def get_gif_recolor(path: str, state: str, color: Tuple[int, int, int, int] = DE

frames = TextureSystem._slice_image(image, frame_width, frame_height, num_frames)

output_path = f"{path}/{state}_compiled_{TextureSystem._get_color_str(color)}.gif"
output_path = f"{path}/{state}_compiled_{color}.gif"
frames[0].save(output_path, save_all=True, append_images=frames[1:], duration=1000//fps, loop=0)

return frames
Expand Down Expand Up @@ -289,7 +265,7 @@ def merge_layers(layers: List[Dict[str, Any]], fps: int = DEFAULT_FPS) -> Union[
Returns:
Union[Image.Image, List[Image.Image]]: Объединенное изображение или список кадров GIF.
"""
base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'Sprites', 'compiled'))
base_path = os.path.join(ROOT_PATH, 'Content', 'Compiled')
if not os.path.exists(base_path):
os.makedirs(base_path)

Expand Down Expand Up @@ -335,7 +311,7 @@ def merge_layers(layers: List[Dict[str, Any]], fps: int = DEFAULT_FPS) -> Union[
final_images[i] = final_image_expanded
else:
if is_mask:
final_image = TextureSystem.get_image_recolor(first_layer['path'], first_layer['state'], first_layer['color'])
final_image = TextureSystem.get_image_recolor(first_layer['path'], first_layer['state'], Color.from_tuple(first_layer['color']))
else:
final_image = TextureSystem.get_image(first_layer['path'], first_layer['state'])

Expand All @@ -349,7 +325,7 @@ def merge_layers(layers: List[Dict[str, Any]], fps: int = DEFAULT_FPS) -> Union[

if is_gif:
if is_mask:
recolored_frames = TextureSystem.get_gif_recolor(layer['path'], layer['state'], layer['color'], fps)
recolored_frames = TextureSystem.get_gif_recolor(layer['path'], layer['state'], Color.from_tuple(layer['color']), fps)
for i in range(max_frames):
recolored_frame_expanded = Image.new("RGBA", (max_width, max_height))
frame_to_use = recolored_frames[min(i, len(recolored_frames) - 1)] # Используем последний кадр, если i превышает количество кадров
Expand All @@ -370,7 +346,7 @@ def merge_layers(layers: List[Dict[str, Any]], fps: int = DEFAULT_FPS) -> Union[
final_images.append(normal_frame_expanded)
else:
if is_mask:
recolored_image = TextureSystem.get_image_recolor(layer['path'], layer['state'], layer['color'])
recolored_image = TextureSystem.get_image_recolor(layer['path'], layer['state'], Color.from_tuple(layer['color']))
recolored_image_expanded = Image.new("RGBA", (max_width, max_height))
recolored_image_expanded.paste(recolored_image, (0, 0))
for i in range(len(final_images)):
Expand Down
32 changes: 7 additions & 25 deletions Tests/Texture/TextureSystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,16 @@
import yaml
from PIL import Image

from Code.systems.texture_system import TextureSystem
from Code.systems.texture_system import *


class TestTextureSystem(unittest.TestCase):
def setUp(self):
self.test_dir = 'test_sprites'
self.test_dir = 'test_texture'
os.makedirs(self.test_dir, exist_ok=True)
self.compiled_dir = os.path.abspath(os.path.join(self.test_dir, 'compiled'))
os.makedirs(self.compiled_dir, exist_ok=True)

info_data = {
'Sprites': [
'Texture': [
{
'name': 'state1',
'size': {'x': 100, 'y': 100},
Expand Down Expand Up @@ -68,22 +66,6 @@ def test_get_hash_list(self):
expected_hash = hashlib.sha256(pickle.dumps(layers)).hexdigest()
self.assertEqual(TextureSystem._get_hash_list(layers), expected_hash)

def test_get_color_str(self):
color = (255, 128, 64, 32)
expected_str = '255_128_64_32'
self.assertEqual(TextureSystem._get_color_str(color), expected_str)

def test_validate_color(self):
valid_color = (255, 128, 64, 32)
try:
TextureSystem._validate_color(valid_color)
except ValueError:
self.fail("_validate_color raised ValueError unexpectedly!")

invalid_color = (256, 128, 64, 32)
with self.assertRaises(ValueError):
TextureSystem._validate_color(invalid_color)

def test_slice_image(self):
image = Image.new('RGBA', (450, 150), 'white')
frames = TextureSystem._slice_image(image, 150, 150, 3)
Expand Down Expand Up @@ -129,7 +111,7 @@ def test_get_state_info(self):
def test_get_compiled_png(self):
path = self.test_dir
state = 'state1'
color = (255, 255, 255, 255)
color = Color(255, 255, 255, 255)

image = TextureSystem.get_image_recolor(path, state, color)
compiled_image = TextureSystem._get_compiled(path, state, color, is_gif=False)
Expand All @@ -139,7 +121,7 @@ def test_get_compiled_png(self):
def test_get_compiled_gif(self):
path = self.test_dir
state = 'state1'
color = (255, 255, 255, 255)
color = Color(255, 255, 255, 255)

gif_frames = TextureSystem.get_gif_recolor(path, state, color)
compiled_gif_frames = TextureSystem._get_compiled(path, state, color, is_gif=True)
Expand All @@ -151,7 +133,7 @@ def test_get_compiled_gif(self):
def test_get_image_recolor(self):
path = self.test_dir
state = 'state1'
color = (255, 0, 0, 255)
color = Color(255, 0, 0, 255)
image = TextureSystem.get_image_recolor(path, state, color)
expected_path = os.path.join(path, f'{state}_compiled_255_0_0_255.png')
self.assertTrue(os.path.exists(expected_path))
Expand All @@ -169,7 +151,7 @@ def test_get_image(self):
def test_get_gif_recolor(self):
path = self.test_dir
state = 'state2'
color = (255, 0, 0, 255)
color = Color(255, 0, 0, 255)
gif_frames = TextureSystem.get_gif_recolor(path, state, color)
expected_path = os.path.join(path, f'{state}_compiled_255_0_0_255.gif')
self.assertTrue(os.path.exists(expected_path))
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
msgpack
Pillow
pydub
PyQt6
PyYAML
requests
Loading