Skip to content

Commit

Permalink
Небольшая оптмизация класса текстур и выкидыш аудио (#3)
Browse files Browse the repository at this point in the history
### Описание PR'ра
Оптимизирован немного класс текстур (его надо переписывать)
Сделан выкидыш класса текстур

### Техническая информация
- [x] PR полностью завершён.
- [x] Мне **НЕ** нужна помощь для завершения PR.
- [ ] Проверено на локальной машине.
- [ ] Документация обновлена. <!-- при условии, что это применимо -->
- [x] Тесты обновлены / добавлены. <!-- при условии, что это применимо
-->

### Изменения
changes: NOT
  • Loading branch information
themanyfaceddemon authored Jul 28, 2024
1 parent b3ad230 commit 1ff33a4
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 73 deletions.
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

0 comments on commit 1ff33a4

Please sign in to comment.