From b82a5499e5dd580131c677a951b46ba81771250e Mon Sep 17 00:00:00 2001 From: Trimatix <1JasperLaw@gmail.com> Date: Sat, 13 May 2023 12:39:51 +0200 Subject: [PATCH 01/13] initial draft --- src/AEPi/codec.py | 5 ++- src/AEPi/image/AEI.py | 72 +++++++++++++++++++++++++++++++++---- src/tests/image/test_AEI.py | 26 ++++++++++++-- 3 files changed, 91 insertions(+), 12 deletions(-) diff --git a/src/AEPi/codec.py b/src/AEPi/codec.py index b78e2e7..7f5e726 100644 --- a/src/AEPi/codec.py +++ b/src/AEPi/codec.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -from io import BytesIO from typing import Dict, Optional, Type, TypeVar, Iterable from PIL.Image import Image @@ -25,11 +24,11 @@ def compress(cls, im: Image, format: CompressionFormat, quality: Optional[Compre @classmethod @abstractmethod - def decompress(cls, fp: BytesIO, format: CompressionFormat) -> Image: + def decompress(cls, fp: bytes, format: CompressionFormat, quality: Optional[CompressionQuality]) -> Image: """Decompress a `format`-compressed BGRA image into a BGRA Image. :param fp: The compressed image to decompress - :type im: BytesIO + :type im: bytes :param format: The compression format :type format: CompressionFormat :return: `fp`, decompressed into a BGRA image diff --git a/src/AEPi/image/AEI.py b/src/AEPi/image/AEI.py index 9051c9f..7813db6 100644 --- a/src/AEPi/image/AEI.py +++ b/src/AEPi/image/AEI.py @@ -1,7 +1,7 @@ import io from os import PathLike from types import TracebackType -from typing import List, Optional, Set, Tuple, Type, TypeVar, Union, overload +from typing import List, Optional, Set, Tuple, Type, TypeVar, Union, cast, overload from PIL import Image from ..lib import binaryio, imageOps @@ -279,11 +279,71 @@ def read(cls, fp: Union[str, PathLike, io.BytesIO]) -> "AEI": :return: A new AEI file object, containing the decoded contents of `fp` :rtype: AEI """ - # if tempFp := (not isinstance(fp, io.BytesIO)): - # fp = open(fp, "rb") - raise NotImplementedError() - # if tempFp: - # fp.close() + file: Union[io.BufferedReader, io.BytesIO] + + if tempFp := (not isinstance(fp, io.BytesIO)): + file = open(fp, "rb") + + if isinstance(fp, io.StringIO): + raise ValueError("fp must be of binary type, not StringIO") + + elif isinstance(fp, (str, PathLike)): + file = open(fp, mode="rb") + + else: + file = fp + + def readInt(length: int): + binary = file.read(length) + return int.from_bytes(binary) + + bFileType = file.read(len(FILE_TYPE_HEADER)) + if bFileType != FILE_TYPE_HEADER: + raise ValueError(f"Given file is of unknown type '{str(bFileType, encoding='utf-8')}' expected '{str(FILE_TYPE_HEADER, encoding='utf-8')}'") + + formatId = readInt(1) + format = CompressionFormat(formatId) + imageCodec = codec.decompressorFor(format) + + width = readInt(2) + height = readInt(2) + numTextures = readInt(2) + + textures: List[Texture] = [] + for i in range(numTextures): + texX = readInt(2) + texY = readInt(2) + texWidth = readInt(2) + texHeight = readInt(2) + textures.append(Texture(texX, texY, texWidth, texHeight)) + + if format.isCompressed: + imageLength = readInt(4) + else: + imageLength = 4 * width * height + + compressed = file.read(imageLength) + + symbolGroups = readInt(2) + + if symbolGroups > 0: + if tempFp: + file.close() + raise ValueError("AEIs with symbols are not yet supported") + + bQuality = file.read(1) + quality = None if bQuality == b'' else cast(CompressionQuality, int.from_bytes(bQuality)) + + decompressed = imageCodec.decompress(compressed, format, quality) + + if tempFp: + file.close() + + aei = AEI(decompressed, format=format, quality=quality) + for tex in textures: + aei.addTexture(tex) + + return aei def write(self, fp: Optional[io.BytesIO] = None, format: Optional[CompressionFormat] = None, quality: Optional[CompressionQuality] = None) -> io.BytesIO: diff --git a/src/tests/image/test_AEI.py b/src/tests/image/test_AEI.py index 62e8059..0d19ae1 100644 --- a/src/tests/image/test_AEI.py +++ b/src/tests/image/test_AEI.py @@ -2,6 +2,7 @@ from PIL.Image import Image from AEPi import AEI, Texture, CompressionFormat +from AEPi.codecs import EtcPakCodec from AEPi.codec import ImageCodecAdaptor, supportsFormats import pytest from PIL import Image @@ -36,7 +37,7 @@ def compress(cls, im, format, quality): return COMPRESSED @classmethod - def decompress(cls, fp, format): + def decompress(cls, fp, format, quality): return DECOMPRESSED #region dimensions @@ -117,7 +118,7 @@ def test_getHeight_getsHeight(): assert aei.height == 5 #endregion dimensions -#region write +#region aei files def test_write_isCorrect(): with AEI(DECOMPRESSED) as aei, BytesIO() as outBytes, open(PIXEL_AEI_PATH, "rb") as expected: @@ -127,7 +128,26 @@ def test_write_isCorrect(): actualText = outBytes.read() assert expectedText == actualText -#endregion write + +def test_read_readsImage(): + with AEI.read(SMILEY_AEI_PATH) as aei, Image.open(SMILEY_PNG_PATH) as png: + assert aei.width == png.width + assert aei.height == png.height + + for x in range(png.width): + for y in range(png.height): + assert aei._image.getpixel((x, y)) == png.getpixel((x, y)) + + +def test_read_readsTextures(): + with AEI.read(SMILEY_AEI_PATH) as aei, Image.open(SMILEY_PNG_PATH) as png: + assert aei.textures[0].x == 0 + assert aei.textures[0].y == 0 + assert aei.textures[0].width == png.width + assert aei.textures[0].height == png.height + + +#endregion aei files #region textures def test_addTexture_addsTexture(): From 472147af152dd190ca613834b35fb6b5038f0598 Mon Sep 17 00:00:00 2001 From: Trimatix <1JasperLaw@gmail.com> Date: Sat, 13 May 2023 16:48:09 +0200 Subject: [PATCH 02/13] remove extra assets --- src/tests/assets/pixel dxt5.aei | Bin 46 -> 0 bytes src/tests/assets/pixel etc1.aei | Bin 46 -> 0 bytes src/tests/assets/smiley dxt5.aei | Bin 286 -> 0 bytes src/tests/assets/smiley etc1.aei | Bin 286 -> 0 bytes 4 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/tests/assets/pixel dxt5.aei delete mode 100644 src/tests/assets/pixel etc1.aei delete mode 100644 src/tests/assets/smiley dxt5.aei delete mode 100644 src/tests/assets/smiley etc1.aei diff --git a/src/tests/assets/pixel dxt5.aei b/src/tests/assets/pixel dxt5.aei deleted file mode 100644 index b33aa19825e19b5988f09a8b28c2d6c1b5e87065..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46 pcmZ>C&CE?qPi0VHWB@@B0MP;<1}i&=WSIX?RBrXERjU{nm;sg32TK3| diff --git a/src/tests/assets/pixel etc1.aei b/src/tests/assets/pixel etc1.aei deleted file mode 100644 index c45745a33c1a1d4c81b5ee12e684afb449d29b80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46 kcmZ>C&CE?qPi1gmWB@@B0MPHq)$ diff --git a/src/tests/assets/smiley dxt5.aei b/src/tests/assets/smiley dxt5.aei deleted file mode 100644 index 18c55efc72b7101ce13e916ac3553072f9eefa1f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 286 zcmZ>C&CE?qPi0UMU=UznWB>sW3(REw4+i)Dx9<%750oHAr2;P!J`GcY)WGB7X$0KC4B AumAu6 diff --git a/src/tests/assets/smiley etc1.aei b/src/tests/assets/smiley etc1.aei deleted file mode 100644 index 7d0075bab083cd6da5ed3b782779d141c9b48483..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 286 zcmZ>C&CE?qPi1frU=UznWB>sW3&cDVml`i9c=s+k{ikQjeqrOkcYpl&!30w8;oC2y2|L;VH Qc{#ehFh0HPXJB9k05BPWy#N3J From 63ae5e7bb0767da14d1ae7bea31817c9d1446178 Mon Sep 17 00:00:00 2001 From: Trimatix <1JasperLaw@gmail.com> Date: Mon, 15 May 2023 12:24:57 +0200 Subject: [PATCH 03/13] test read with mock codec --- src/AEPi/image/AEI.py | 4 ++-- src/tests/image/test_AEI.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/AEPi/image/AEI.py b/src/AEPi/image/AEI.py index 7813db6..256598e 100644 --- a/src/AEPi/image/AEI.py +++ b/src/AEPi/image/AEI.py @@ -295,7 +295,7 @@ def read(cls, fp: Union[str, PathLike, io.BytesIO]) -> "AEI": def readInt(length: int): binary = file.read(length) - return int.from_bytes(binary) + return int.from_bytes(binary, byteorder="little") bFileType = file.read(len(FILE_TYPE_HEADER)) if bFileType != FILE_TYPE_HEADER: @@ -310,7 +310,7 @@ def readInt(length: int): numTextures = readInt(2) textures: List[Texture] = [] - for i in range(numTextures): + for _ in range(numTextures): texX = readInt(2) texY = readInt(2) texWidth = readInt(2) diff --git a/src/tests/image/test_AEI.py b/src/tests/image/test_AEI.py index 0d19ae1..670f7f0 100644 --- a/src/tests/image/test_AEI.py +++ b/src/tests/image/test_AEI.py @@ -130,21 +130,21 @@ def test_write_isCorrect(): def test_read_readsImage(): - with AEI.read(SMILEY_AEI_PATH) as aei, Image.open(SMILEY_PNG_PATH) as png: - assert aei.width == png.width - assert aei.height == png.height + with AEI.read(PIXEL_AEI_PATH) as aei: + assert aei.width == DECOMPRESSED.width + assert aei.height == DECOMPRESSED.height - for x in range(png.width): - for y in range(png.height): - assert aei._image.getpixel((x, y)) == png.getpixel((x, y)) + for x in range(DECOMPRESSED.width): + for y in range(DECOMPRESSED.height): + assert aei._image.getpixel((x, y)) == DECOMPRESSED.getpixel((x, y)) def test_read_readsTextures(): - with AEI.read(SMILEY_AEI_PATH) as aei, Image.open(SMILEY_PNG_PATH) as png: + with AEI.read(PIXEL_AEI_PATH) as aei: assert aei.textures[0].x == 0 assert aei.textures[0].y == 0 - assert aei.textures[0].width == png.width - assert aei.textures[0].height == png.height + assert aei.textures[0].width == DECOMPRESSED.width + assert aei.textures[0].height == DECOMPRESSED.height #endregion aei files From a34f97973381481453910a61269962d9b0b24537 Mon Sep 17 00:00:00 2001 From: Trimatix <1JasperLaw@gmail.com> Date: Mon, 15 May 2023 12:30:34 +0200 Subject: [PATCH 04/13] cleanup --- src/AEPi/image/AEI.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/AEPi/image/AEI.py b/src/AEPi/image/AEI.py index 256598e..148d888 100644 --- a/src/AEPi/image/AEI.py +++ b/src/AEPi/image/AEI.py @@ -332,7 +332,7 @@ def readInt(length: int): raise ValueError("AEIs with symbols are not yet supported") bQuality = file.read(1) - quality = None if bQuality == b'' else cast(CompressionQuality, int.from_bytes(bQuality)) + quality = None if bQuality == b'' else cast(CompressionQuality, int.from_bytes(bQuality, byteorder="little")) decompressed = imageCodec.decompress(compressed, format, quality) @@ -382,6 +382,7 @@ def write(self, fp: Optional[io.BytesIO] = None, format: Optional[CompressionFor return fp +#region write-util def _writeHeaderMeta(self, fp: io.BytesIO, format: CompressionFormat): fp.write(FILE_TYPE_HEADER) @@ -431,6 +432,7 @@ def _writeFooterMeta(self, fp: io.BytesIO, quality: Optional[CompressionQuality] if quality is not None: fp.write(binaryio.uint8(quality, ENDIANNESS)) +#endregion write-util def close(self): """Close the underlying image. From a5810dbd3e92653eb7129198cb8ab9942350f551 Mon Sep 17 00:00:00 2001 From: Trimatix <1JasperLaw@gmail.com> Date: Fri, 23 Feb 2024 22:38:02 +0100 Subject: [PATCH 05/13] wrap file read in a try-finally to avoid memory leak --- src/AEPi/image/AEI.py | 76 +++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/src/AEPi/image/AEI.py b/src/AEPi/image/AEI.py index 148d888..cfc54d2 100644 --- a/src/AEPi/image/AEI.py +++ b/src/AEPi/image/AEI.py @@ -1,7 +1,7 @@ import io from os import PathLike from types import TracebackType -from typing import List, Optional, Set, Tuple, Type, TypeVar, Union, cast, overload +from typing import Any, List, Optional, Set, Tuple, Type, TypeVar, Union, cast, overload from PIL import Image from ..lib import binaryio, imageOps @@ -270,7 +270,7 @@ def getTexture(self, val1: Union[Texture, int], y: Optional[int] = None, width: @classmethod - def read(cls, fp: Union[str, PathLike, io.BytesIO]) -> "AEI": + def read(cls, fp: Union[str, PathLike[Any], io.BytesIO]) -> "AEI": """Read an AEI file from bytes, or a file. `fp` can be a path to a file, or an in-memory buffer containing the contents of an encoded AEI file, including metadata. @@ -279,17 +279,15 @@ def read(cls, fp: Union[str, PathLike, io.BytesIO]) -> "AEI": :return: A new AEI file object, containing the decoded contents of `fp` :rtype: AEI """ + if isinstance(fp, io.StringIO): + raise ValueError("fp must be of binary type, not StringIO") + file: Union[io.BufferedReader, io.BytesIO] if tempFp := (not isinstance(fp, io.BytesIO)): file = open(fp, "rb") - - if isinstance(fp, io.StringIO): - raise ValueError("fp must be of binary type, not StringIO") - elif isinstance(fp, (str, PathLike)): file = open(fp, mode="rb") - else: file = fp @@ -297,47 +295,47 @@ def readInt(length: int): binary = file.read(length) return int.from_bytes(binary, byteorder="little") - bFileType = file.read(len(FILE_TYPE_HEADER)) - if bFileType != FILE_TYPE_HEADER: - raise ValueError(f"Given file is of unknown type '{str(bFileType, encoding='utf-8')}' expected '{str(FILE_TYPE_HEADER, encoding='utf-8')}'") + try: + bFileType = file.read(len(FILE_TYPE_HEADER)) + if bFileType != FILE_TYPE_HEADER: + raise ValueError(f"Given file is of unknown type '{str(bFileType, encoding='utf-8')}' expected '{str(FILE_TYPE_HEADER, encoding='utf-8')}'") - formatId = readInt(1) - format = CompressionFormat(formatId) - imageCodec = codec.decompressorFor(format) + formatId = readInt(1) + format = CompressionFormat(formatId) + imageCodec = codec.decompressorFor(format) - width = readInt(2) - height = readInt(2) - numTextures = readInt(2) + width = readInt(2) + height = readInt(2) + numTextures = readInt(2) - textures: List[Texture] = [] - for _ in range(numTextures): - texX = readInt(2) - texY = readInt(2) - texWidth = readInt(2) - texHeight = readInt(2) - textures.append(Texture(texX, texY, texWidth, texHeight)) + textures: List[Texture] = [] + for _ in range(numTextures): + texX = readInt(2) + texY = readInt(2) + texWidth = readInt(2) + texHeight = readInt(2) + textures.append(Texture(texX, texY, texWidth, texHeight)) - if format.isCompressed: - imageLength = readInt(4) - else: - imageLength = 4 * width * height + if format.isCompressed: + imageLength = readInt(4) + else: + imageLength = 4 * width * height - compressed = file.read(imageLength) + compressed = file.read(imageLength) - symbolGroups = readInt(2) + symbolGroups = readInt(2) - if symbolGroups > 0: - if tempFp: - file.close() - raise ValueError("AEIs with symbols are not yet supported") - - bQuality = file.read(1) - quality = None if bQuality == b'' else cast(CompressionQuality, int.from_bytes(bQuality, byteorder="little")) + if symbolGroups > 0: + raise ValueError("AEIs with symbols are not yet supported") + + bQuality = file.read(1) + quality = None if bQuality == b'' else cast(CompressionQuality, int.from_bytes(bQuality, byteorder="little")) - decompressed = imageCodec.decompress(compressed, format, quality) + decompressed = imageCodec.decompress(compressed, format, quality) - if tempFp: - file.close() + finally: + if tempFp: + file.close() aei = AEI(decompressed, format=format, quality=quality) for tex in textures: From e27984bb3ebdbe02ee60d43cfdd8e3c0cf926d5f Mon Sep 17 00:00:00 2001 From: Trimatix <1JasperLaw@gmail.com> Date: Sun, 25 Feb 2024 18:53:30 +0100 Subject: [PATCH 06/13] add width and height as decompressor args --- src/AEPi/codec.py | 6 +++++- src/AEPi/codecs/EtcPakCodec.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/AEPi/codec.py b/src/AEPi/codec.py index 7f5e726..c2d798c 100644 --- a/src/AEPi/codec.py +++ b/src/AEPi/codec.py @@ -24,13 +24,17 @@ def compress(cls, im: Image, format: CompressionFormat, quality: Optional[Compre @classmethod @abstractmethod - def decompress(cls, fp: bytes, format: CompressionFormat, quality: Optional[CompressionQuality]) -> Image: + def decompress(cls, fp: bytes, format: CompressionFormat, width: int, height: int, quality: Optional[CompressionQuality]) -> Image: """Decompress a `format`-compressed BGRA image into a BGRA Image. :param fp: The compressed image to decompress :type im: bytes :param format: The compression format :type format: CompressionFormat + :param width: The width of the image + :type width: int + :param height: The height of the image + :type height: int :return: `fp`, decompressed into a BGRA image :rtype: Image """ diff --git a/src/AEPi/codecs/EtcPakCodec.py b/src/AEPi/codecs/EtcPakCodec.py index 5d523cf..331d845 100644 --- a/src/AEPi/codecs/EtcPakCodec.py +++ b/src/AEPi/codecs/EtcPakCodec.py @@ -1,4 +1,5 @@ from io import BytesIO +from typing import Optional from PIL.Image import Image from ..codec import ImageCodecAdaptor, supportsFormats from ..constants import CompressionFormat, CompressionQuality @@ -30,6 +31,6 @@ def compress(cls, im: Image, format: CompressionFormat, quality: CompressionQual @classmethod - def decompress(cls, fp: BytesIO, format: CompressionFormat, quality: CompressionQuality) -> Image: + def decompress(cls, fp: bytes, format: CompressionFormat, width: int, height: int, quality: Optional[CompressionQuality]) -> Image: raise NotImplementedError(f"Codec {EtcPakCodec.__name__} is not capable of decompression") \ No newline at end of file From 7b9a17cddd081201574634f1c0a601685acb75eb Mon Sep 17 00:00:00 2001 From: Trimatix <1JasperLaw@gmail.com> Date: Sun, 25 Feb 2024 20:05:44 +0100 Subject: [PATCH 07/13] add test for multi-texture AEIs --- src/tests/assets/Copy TEMP_dxt1smileybgra.aei | Bin 0 -> 158 bytes src/tests/assets/TEMP_dxt3smileybgra.aei | Bin 0 -> 286 bytes src/tests/assets/TEMP_dxt5smileybgra.aei | Bin 0 -> 1050 bytes ...TC_twotextures_nomipmap_nosymbols_high.aei | Bin 0 -> 294 bytes src/tests/assets/dxt5 smiley.aei | Bin 0 -> 286 bytes src/tests/image/test_AEI.py | 38 +++++++++++++----- 6 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 src/tests/assets/Copy TEMP_dxt1smileybgra.aei create mode 100644 src/tests/assets/TEMP_dxt3smileybgra.aei create mode 100644 src/tests/assets/TEMP_dxt5smileybgra.aei create mode 100644 src/tests/assets/_MERGE PROTECTION/smiley_ATC_twotextures_nomipmap_nosymbols_high.aei create mode 100644 src/tests/assets/dxt5 smiley.aei diff --git a/src/tests/assets/Copy TEMP_dxt1smileybgra.aei b/src/tests/assets/Copy TEMP_dxt1smileybgra.aei new file mode 100644 index 0000000000000000000000000000000000000000..716a6c2bedf97a64b3f6bff6855ff5bf850e5821 GIT binary patch literal 158 zcmZ>C&CE?qPi0ULU=UznWB>sWs{u&g|KGkd^gj;T9;EXB|4<-+&>>LT6-vXzMMLlZ jX9p?%3sn!*7Z?K5f5r0ue?~```p{5LYmm93Ky}Ojr<*&6 literal 0 HcmV?d00001 diff --git a/src/tests/assets/TEMP_dxt3smileybgra.aei b/src/tests/assets/TEMP_dxt3smileybgra.aei new file mode 100644 index 0000000000000000000000000000000000000000..7cca58bb0b096e5f5894868716dd2b4e061dae47 GIT binary patch literal 286 zcmZ>C&CE?qPi0UPU=UznWB>sW3&i{n1^55A?+pDg+@WrzqeA7mUdpVOLw!6B4^ff)eK3$Hx@ literal 0 HcmV?d00001 diff --git a/src/tests/assets/TEMP_dxt5smileybgra.aei b/src/tests/assets/TEMP_dxt5smileybgra.aei new file mode 100644 index 0000000000000000000000000000000000000000..c1ddc8053d0c27c5ba327b99565959b9116d631c GIT binary patch literal 1050 zcmZ>C&CE?qPi0UMU=UznWB>sW3(REw4+i)Dx9<%750oHAr2;P!J`GcY)WGB7Yh0$>o3 zK-Xb)1uk){iifBmv3|s-4kTw`u>pvufN2mP9TN&3Wc`HHVv~pH-+)bus$$kTz9gFm O(gVUK!S-MbO#1-;+fG~n literal 0 HcmV?d00001 diff --git a/src/tests/assets/_MERGE PROTECTION/smiley_ATC_twotextures_nomipmap_nosymbols_high.aei b/src/tests/assets/_MERGE PROTECTION/smiley_ATC_twotextures_nomipmap_nosymbols_high.aei new file mode 100644 index 0000000000000000000000000000000000000000..f0b4cd7558c418de5536f95a548adee3be2cc3a2 GIT binary patch literal 294 zcmZ>C&CE?qPh}7kU=UznVgLaSB*?(X!0;amzAOCy&kzdZ{r^uC|0+T~!X$_og#QPw z0Kz|y#)otNL*(t@0?#jinJZudQ1jvLh44SaEr80yl>dJo^8Y{cVK^6RK7!w_3S!#> HLxdRsSUBF8 literal 0 HcmV?d00001 diff --git a/src/tests/assets/dxt5 smiley.aei b/src/tests/assets/dxt5 smiley.aei new file mode 100644 index 0000000000000000000000000000000000000000..18c55efc72b7101ce13e916ac3553072f9eefa1f GIT binary patch literal 286 zcmZ>C&CE?qPi0UMU=UznWB>sW3(REw4+i)Dx9<%750oHAr2;P!J`GcY)WGB7X$0KC4B AumAu6 literal 0 HcmV?d00001 diff --git a/src/tests/image/test_AEI.py b/src/tests/image/test_AEI.py index 20d7822..bd0099c 100644 --- a/src/tests/image/test_AEI.py +++ b/src/tests/image/test_AEI.py @@ -42,6 +42,8 @@ def compress(cls, im, format, quality): @classmethod def decompress(cls, fp, format, quality): + if USE_SMILEY: + return smileyImage() return DECOMPRESSED #region dimensions @@ -123,14 +125,7 @@ def test_getHeight_getsHeight(): #endregion dimensions #region aei files - -def test_write_isCorrect(): - with AEI(DECOMPRESSED) as aei, BytesIO() as outBytes, open(PIXEL_AEI_PATH, "rb") as expected: - aei.write(outBytes, format=CompressionFormat.ATC, quality=3) - expectedText = expected.read() - outBytes.seek(0) - actualText = outBytes.read() - assert expectedText == actualText +#region read def test_read_readsImage(): @@ -151,8 +146,30 @@ def test_read_readsTextures(): assert aei.textures[0].height == DECOMPRESSED.height -#endregion aei files - +def test_read_twoTextures_isCorrect(): + global USE_SMILEY + USE_SMILEY = True + with AEI.read(SMILEY_AEI_2TEXTURES_PATH) as aei: + assert len(aei.textures) == 2 + assert (aei.textures[0].width, aei.textures[0].height) == (8, 8) + assert (aei.textures[0].x, aei.textures[0].y) == (0, 0) + assert (aei.textures[1].width, aei.textures[1].height) == (8, 8) + assert (aei.textures[1].x, aei.textures[1].y) == (8, 8) + USE_SMILEY = False + + +#endregion read +#endregion write + +def test_write_isCorrect(): + with AEI(DECOMPRESSED) as aei, BytesIO() as outBytes, open(PIXEL_AEI_PATH, "rb") as expected: + aei.write(outBytes, format=CompressionFormat.ATC, quality=3) + expectedText = expected.read() + outBytes.seek(0) + actualText = outBytes.read() + assert expectedText == actualText + + def test_write_twoTextures_isCorrect(): global USE_SMILEY USE_SMILEY = True @@ -168,6 +185,7 @@ def test_write_twoTextures_isCorrect(): USE_SMILEY = False #endregion write +#endregion aei files #region textures def test_addTexture_addsTexture(): From 744ccf2f4dd33b04efa65c6e5eeb63c3262c12db Mon Sep 17 00:00:00 2001 From: Trimatix <1JasperLaw@gmail.com> Date: Sun, 25 Feb 2024 20:06:27 +0100 Subject: [PATCH 08/13] add Texture.shape and size properties --- src/AEPi/image/texture.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/AEPi/image/texture.py b/src/AEPi/image/texture.py index 993adb3..489c0b1 100644 --- a/src/AEPi/image/texture.py +++ b/src/AEPi/image/texture.py @@ -1,6 +1,29 @@ +from typing import Tuple + + class Texture: def __init__(self, x: int, y: int, width: int, height: int) -> None: self.x = x self.y = y self.width = width self.height = height + + + @property + def size(self) -> Tuple[int, int]: + """The shape of the texture region, given as a (width, height) tuple. + + :return: The shape of the texture region, as a (width, height) tuple + :rtype: Tuple[int, int] + """ + return (self.width, self.height) + + + @property + def position(self) -> Tuple[int, int]: + """The coordinates of the texture region relative to the AEI origin, given as an (x, y) tuple. + + :return: The coordinates of the texture region, as an (x, y) tuple + :rtype: Tuple[int, int] + """ + return (self.x, self.y) From 19bbc8ad8400f11dc07ad4b77ff3448eccf3eb58 Mon Sep 17 00:00:00 2001 From: Trimatix <1JasperLaw@gmail.com> Date: Sun, 25 Feb 2024 20:11:28 +0100 Subject: [PATCH 09/13] add property setters --- src/AEPi/image/texture.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/AEPi/image/texture.py b/src/AEPi/image/texture.py index 489c0b1..967a4ee 100644 --- a/src/AEPi/image/texture.py +++ b/src/AEPi/image/texture.py @@ -17,6 +17,16 @@ def size(self) -> Tuple[int, int]: :rtype: Tuple[int, int] """ return (self.width, self.height) + + + @size.setter + def size(self, value: Tuple[int, int]) -> None: + """Set the shape of the texture region. + + :param value: The shape of the texture region, as a (width, height) tuple + :type value: Tuple[int, int] + """ + (self.width, self.height) = value @property @@ -27,3 +37,13 @@ def position(self) -> Tuple[int, int]: :rtype: Tuple[int, int] """ return (self.x, self.y) + + + @position.setter + def position(self, value: Tuple[int, int]) -> None: + """Set the coordinates of the texture region relative to the AEI origin. + + :param value: The coordinates of the texture region, as an (x, y) tuple + :type value: Tuple[int, int] + """ + (self.x, self.y) = value From 0b89792ded9965f6db42e9d9763fa7f54ab31efc Mon Sep 17 00:00:00 2001 From: Trimatix <1JasperLaw@gmail.com> Date: Sun, 25 Feb 2024 20:13:06 +0100 Subject: [PATCH 10/13] rename to match AEI --- src/AEPi/image/texture.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AEPi/image/texture.py b/src/AEPi/image/texture.py index 967a4ee..465f693 100644 --- a/src/AEPi/image/texture.py +++ b/src/AEPi/image/texture.py @@ -10,7 +10,7 @@ def __init__(self, x: int, y: int, width: int, height: int) -> None: @property - def size(self) -> Tuple[int, int]: + def shape(self) -> Tuple[int, int]: """The shape of the texture region, given as a (width, height) tuple. :return: The shape of the texture region, as a (width, height) tuple @@ -19,8 +19,8 @@ def size(self) -> Tuple[int, int]: return (self.width, self.height) - @size.setter - def size(self, value: Tuple[int, int]) -> None: + @shape.setter + def shape(self, value: Tuple[int, int]) -> None: """Set the shape of the texture region. :param value: The shape of the texture region, as a (width, height) tuple From addfb9f92b815ab05e47dedf2a95cf94bc2898b3 Mon Sep 17 00:00:00 2001 From: Trimatix <1JasperLaw@gmail.com> Date: Sun, 25 Feb 2024 20:13:26 +0100 Subject: [PATCH 11/13] add test for reading multi-texture AEIs --- src/AEPi/image/AEI.py | 5 +++-- src/tests/image/test_AEI.py | 8 ++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/AEPi/image/AEI.py b/src/AEPi/image/AEI.py index 1084ec4..b8f75f4 100644 --- a/src/AEPi/image/AEI.py +++ b/src/AEPi/image/AEI.py @@ -18,11 +18,12 @@ class AEI: """An Abyss Engine Image file. Contains a set of textures, each with an image and coordinates. Each texture must fall within the bounds of the AEI shape. - The AEI shape is mutable. - `format` and `quality` can be set in the constructor, or on call of `AEI.write`. + The AEI shape is mutable, through the `shape` property. + The coordinate origin (0, 0) is the top-left of the AEI. An AEI can be constructed either with its dimensions, or with an image. If an image is used, the AEI is created with a copy of the image. + `format` and `quality` can be set in the constructor, or on call of `AEI.write`. Use the `addTexture` and `removeTexture` helper methods for texture management. diff --git a/src/tests/image/test_AEI.py b/src/tests/image/test_AEI.py index bd0099c..79973a4 100644 --- a/src/tests/image/test_AEI.py +++ b/src/tests/image/test_AEI.py @@ -151,10 +151,10 @@ def test_read_twoTextures_isCorrect(): USE_SMILEY = True with AEI.read(SMILEY_AEI_2TEXTURES_PATH) as aei: assert len(aei.textures) == 2 - assert (aei.textures[0].width, aei.textures[0].height) == (8, 8) - assert (aei.textures[0].x, aei.textures[0].y) == (0, 0) - assert (aei.textures[1].width, aei.textures[1].height) == (8, 8) - assert (aei.textures[1].x, aei.textures[1].y) == (8, 8) + assert aei.textures[0].shape == (8, 8) + assert aei.textures[0].position == (0, 0) + assert aei.textures[1].shape == (8, 8) + assert aei.textures[1].position == (8, 8) USE_SMILEY = False From 983fcbd690a60a3ec3b80a1ba55a1afd9291478a Mon Sep 17 00:00:00 2001 From: Trimatix <1JasperLaw@gmail.com> Date: Sun, 25 Feb 2024 20:24:39 +0100 Subject: [PATCH 12/13] remove accidental asset adds --- src/tests/assets/Copy TEMP_dxt1smileybgra.aei | Bin 158 -> 0 bytes src/tests/assets/TEMP_dxt3smileybgra.aei | Bin 286 -> 0 bytes src/tests/assets/TEMP_dxt5smileybgra.aei | Bin 1050 -> 0 bytes ...y_ATC_twotextures_nomipmap_nosymbols_high.aei | Bin 294 -> 0 bytes src/tests/assets/dxt5 smiley.aei | Bin 286 -> 0 bytes 5 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/tests/assets/Copy TEMP_dxt1smileybgra.aei delete mode 100644 src/tests/assets/TEMP_dxt3smileybgra.aei delete mode 100644 src/tests/assets/TEMP_dxt5smileybgra.aei delete mode 100644 src/tests/assets/_MERGE PROTECTION/smiley_ATC_twotextures_nomipmap_nosymbols_high.aei delete mode 100644 src/tests/assets/dxt5 smiley.aei diff --git a/src/tests/assets/Copy TEMP_dxt1smileybgra.aei b/src/tests/assets/Copy TEMP_dxt1smileybgra.aei deleted file mode 100644 index 716a6c2bedf97a64b3f6bff6855ff5bf850e5821..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 158 zcmZ>C&CE?qPi0ULU=UznWB>sWs{u&g|KGkd^gj;T9;EXB|4<-+&>>LT6-vXzMMLlZ jX9p?%3sn!*7Z?K5f5r0ue?~```p{5LYmm93Ky}Ojr<*&6 diff --git a/src/tests/assets/TEMP_dxt3smileybgra.aei b/src/tests/assets/TEMP_dxt3smileybgra.aei deleted file mode 100644 index 7cca58bb0b096e5f5894868716dd2b4e061dae47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 286 zcmZ>C&CE?qPi0UPU=UznWB>sW3&i{n1^55A?+pDg+@WrzqeA7mUdpVOLw!6B4^ff)eK3$Hx@ diff --git a/src/tests/assets/TEMP_dxt5smileybgra.aei b/src/tests/assets/TEMP_dxt5smileybgra.aei deleted file mode 100644 index c1ddc8053d0c27c5ba327b99565959b9116d631c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1050 zcmZ>C&CE?qPi0UMU=UznWB>sW3(REw4+i)Dx9<%750oHAr2;P!J`GcY)WGB7Yh0$>o3 zK-Xb)1uk){iifBmv3|s-4kTw`u>pvufN2mP9TN&3Wc`HHVv~pH-+)bus$$kTz9gFm O(gVUK!S-MbO#1-;+fG~n diff --git a/src/tests/assets/_MERGE PROTECTION/smiley_ATC_twotextures_nomipmap_nosymbols_high.aei b/src/tests/assets/_MERGE PROTECTION/smiley_ATC_twotextures_nomipmap_nosymbols_high.aei deleted file mode 100644 index f0b4cd7558c418de5536f95a548adee3be2cc3a2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 294 zcmZ>C&CE?qPh}7kU=UznVgLaSB*?(X!0;amzAOCy&kzdZ{r^uC|0+T~!X$_og#QPw z0Kz|y#)otNL*(t@0?#jinJZudQ1jvLh44SaEr80yl>dJo^8Y{cVK^6RK7!w_3S!#> HLxdRsSUBF8 diff --git a/src/tests/assets/dxt5 smiley.aei b/src/tests/assets/dxt5 smiley.aei deleted file mode 100644 index 18c55efc72b7101ce13e916ac3553072f9eefa1f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 286 zcmZ>C&CE?qPi0UMU=UznWB>sW3(REw4+i)Dx9<%750oHAr2;P!J`GcY)WGB7X$0KC4B AumAu6 From ad8d6efd2da29c29b5e30dc135c958c475022598 Mon Sep 17 00:00:00 2001 From: Trimatix <1JasperLaw@gmail.com> Date: Sun, 25 Feb 2024 20:32:13 +0100 Subject: [PATCH 13/13] fix pyright errors --- src/AEPi/codecs/EtcPakCodec.py | 2 +- src/AEPi/image/AEI.py | 2 +- src/tests/image/test_AEI.py | 4 ++-- src/tests/test_codec.py | 13 +++++++------ 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/AEPi/codecs/EtcPakCodec.py b/src/AEPi/codecs/EtcPakCodec.py index 331d845..ad6dac3 100644 --- a/src/AEPi/codecs/EtcPakCodec.py +++ b/src/AEPi/codecs/EtcPakCodec.py @@ -16,7 +16,7 @@ ]) class EtcPakCodec(ImageCodecAdaptor): @classmethod - def compress(cls, im: Image, format: CompressionFormat, quality: CompressionQuality) -> bytes: + def compress(cls, im: Image, format: CompressionFormat, quality: Optional[CompressionQuality]) -> bytes: if format is CompressionFormat.DXT5: if im.mode != "RGBA": im = im.convert("RGBA") diff --git a/src/AEPi/image/AEI.py b/src/AEPi/image/AEI.py index b8f75f4..c980307 100644 --- a/src/AEPi/image/AEI.py +++ b/src/AEPi/image/AEI.py @@ -330,7 +330,7 @@ def read(cls, fp: Union[str, PathLike[Any], io.BytesIO]) -> "AEI": bQuality = readUInt8(file, ENDIANNESS, None) quality = cast(Optional[CompressionQuality], bQuality) - decompressed = imageCodec.decompress(compressed, format, quality) + decompressed = imageCodec.decompress(compressed, format, width, height, quality) finally: if tempFp: diff --git a/src/tests/image/test_AEI.py b/src/tests/image/test_AEI.py index 79973a4..436fb3b 100644 --- a/src/tests/image/test_AEI.py +++ b/src/tests/image/test_AEI.py @@ -41,7 +41,7 @@ def compress(cls, im, format, quality): return COMPRESSED @classmethod - def decompress(cls, fp, format, quality): + def decompress(cls, fp, format, width, height, quality): if USE_SMILEY: return smileyImage() return DECOMPRESSED @@ -159,7 +159,7 @@ def test_read_twoTextures_isCorrect(): #endregion read -#endregion write +#region write def test_write_isCorrect(): with AEI(DECOMPRESSED) as aei, BytesIO() as outBytes, open(PIXEL_AEI_PATH, "rb") as expected: diff --git a/src/tests/test_codec.py b/src/tests/test_codec.py index b8ea011..cfa6edf 100644 --- a/src/tests/test_codec.py +++ b/src/tests/test_codec.py @@ -2,33 +2,34 @@ from AEPi.codec import ImageCodecAdaptor, supportsFormats, compressorFor, decompressorFor from AEPi.constants import CompressionFormat import pytest +from PIL.Image import Image @supportsFormats(compresses=[CompressionFormat.DXT5]) class Dxt5Compressor(ImageCodecAdaptor): @classmethod - def compress(cls, im, format, quality): pass + def compress(cls, im, format, quality): return b'' @classmethod - def decompress(cls, fp, format): pass + def decompress(cls, fp, format, width, height, quality): return Image() @supportsFormats(decompresses=[CompressionFormat.ETC1]) class Etc1Decompressor(ImageCodecAdaptor): @classmethod - def compress(cls, im, format, quality): pass + def compress(cls, im, format, quality): return b'' @classmethod - def decompress(cls, fp, format): pass + def decompress(cls, fp, format, width, height, quality): return Image() @supportsFormats(both=[CompressionFormat.PVRTCI2A]) class PvrCodec(ImageCodecAdaptor): @classmethod - def compress(cls, im, format, quality): pass + def compress(cls, im, format, quality): return b'' @classmethod - def decompress(cls, fp, format): pass + def decompress(cls, fp, format, width, height, quality): return Image() @pytest.mark.parametrize(("format", "codec"),