From 5a83329396f481768cc77ff0af0716dbdda897b3 Mon Sep 17 00:00:00 2001 From: Jennifer Taylor Date: Wed, 20 Sep 2023 21:22:26 +0000 Subject: [PATCH] Allow specifying inverting channels for graphics extracted/imported using TDXT utils. --- bemani/format/tdxt.py | 65 +++++++++++++++++++++++++-------------- bemani/utils/tdxtutils.py | 18 +++++++++-- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/bemani/format/tdxt.py b/bemani/format/tdxt.py index 962e383..6e73aa7 100644 --- a/bemani/format/tdxt.py +++ b/bemani/format/tdxt.py @@ -19,6 +19,8 @@ class TDXT: length_fixup: bool, raw: bytes, img: Optional[Image.Image], + *, + invert_channels: bool = False, ) -> None: self.header_flags1 = header_flags1 self.header_flags2 = header_flags2 @@ -29,6 +31,7 @@ class TDXT: self.fmtflags = fmtflags self.endian = endian self.length_fixup = length_fixup + self.invert_channels = invert_channels self.__raw = raw self.__img = img @@ -39,7 +42,14 @@ class TDXT: @raw.setter def raw(self, newdata: bytes) -> None: self.__raw = newdata - newimg = self._rawToImg(self.width, self.height, self.fmt, self.endian, newdata) + newimg = self._rawToImg( + self.width, + self.height, + self.fmt, + self.endian, + self.invert_channels, + newdata, + ) width, height = newimg.size if width != self.width or height != self.height: raise Exception("Unsupported texture resize operation for TDXT file!") @@ -55,7 +65,7 @@ class TDXT: self.__raw = self._imgToRaw(newimg) @staticmethod - def fromBytes(raw_data: bytes) -> "TDXT": + def fromBytes(raw_data: bytes, *, invert_channels: bool = False) -> "TDXT": # First, check the endianness. (magic,) = struct.unpack_from("4s", raw_data) @@ -126,12 +136,15 @@ class TDXT: endian=endian, length_fixup=length_fixup, raw=raw_data[64:], - img=TDXT._rawToImg(width, height, fmt, endian, raw_data[64:]), + img=TDXT._rawToImg( + width, height, fmt, endian, invert_channels, raw_data[64:] + ), + invert_channels=invert_channels, ) @staticmethod def _rawToImg( - width: int, height: int, fmt: int, endian: str, raw_data: bytes + width: int, height: int, fmt: int, endian: str, invert: bool, raw_data: bytes ) -> Optional[Image.Image]: # Since the AFP file format can be found in both big and little endian, its # possible that some of these loaders might need byteswapping on some platforms. @@ -177,7 +190,7 @@ class TDXT: (width, height), b"".join(newdata), "raw", - "RGB", + "BGR" if invert else "RGB", ) elif fmt == 0x0E: # RGB image, no alpha. Game references D3D9 texture format 22 (R8G8B8). @@ -186,7 +199,7 @@ class TDXT: (width, height), raw_data, "raw", - "RGB", + "BGR" if invert else "RGB", ) elif fmt == 0x10: # Seems to be some sort of RGBA with color swapping. Game references D3D9 texture @@ -196,7 +209,7 @@ class TDXT: (width, height), raw_data, "raw", - "BGRA", + "RGBA" if invert else "BGRA", ) elif fmt == 0x13: # Some 16-bit texture format. Game references D3D9 texture format 25 (A1R5G5B5). @@ -224,7 +237,7 @@ class TDXT: (width, height), b"".join(newdata), "raw", - "RGBA", + "BGRA" if invert else "RGBA", ) elif fmt == 0x15: # RGBA format. Game references D3D9 texture format 21 (A8R8G8B8). @@ -234,7 +247,7 @@ class TDXT: (width, height), raw_data, "raw", - "ARGB", + "ABGR" if invert else "ARGB", ) elif fmt == 0x16: # DXT1 format. Game references D3D9 DXT1 texture format. @@ -298,7 +311,7 @@ class TDXT: (width, height), b"".join(newdata), "raw", - "RGBA", + "BGRA" if invert else "RGBA", ) elif fmt == 0x20: # RGBA format. Game references D3D9 surface format 21 (A8R8G8B8). @@ -307,7 +320,7 @@ class TDXT: (width, height), raw_data, "raw", - "BGRA", + "RGBA" if invert else "BGRA", ) else: img = None @@ -354,15 +367,21 @@ class TDXT: if width != self.width or height != self.height: raise Exception("Unsupported texture resize operation for TDXT file!") + # Ignore alpha, which is basically always in the right place. + if self.invert_channels: + order = (2, 1, 0) + else: + order = (0, 1, 2) + if self.fmt == 0x0B: # 16-bit 565 color RGB format. raw = b"".join( struct.pack( "> 3) & 0x1F) << 11) - | (((pixel[1] >> 2) & 0x3F) << 5) - | ((pixel[2] >> 3) & 0x1F) + (((pixel[order[0]] >> 3) & 0x1F) << 11) + | (((pixel[order[1]] >> 2) & 0x3F) << 5) + | ((pixel[order[2]] >> 3) & 0x1F) ), ) for pixel in imgdata.getdata() @@ -374,9 +393,9 @@ class TDXT: "= 128 else 0x0000) - | (((pixel[0] >> 3) & 0x1F) << 10) - | (((pixel[1] >> 3) & 0x1F) << 5) - | ((pixel[2] >> 3) & 0x1F) + | (((pixel[order[0]] >> 3) & 0x1F) << 10) + | (((pixel[order[1]] >> 3) & 0x1F) << 5) + | ((pixel[order[2]] >> 3) & 0x1F) ), ) for pixel in imgdata.getdata() @@ -387,9 +406,9 @@ class TDXT: struct.pack( "> 4) & 0xF) - | (((pixel[1] >> 4) & 0xF) << 4) - | (((pixel[0] >> 4) & 0xF) << 8) + ((pixel[order[2]] >> 4) & 0xF) + | (((pixel[order[1]] >> 4) & 0xF) << 4) + | (((pixel[order[0]] >> 4) & 0xF) << 8) | (((pixel[3] >> 4) & 0xF) << 12) ), ) @@ -400,9 +419,9 @@ class TDXT: raw = b"".join( struct.pack( " int: with open(fname, "rb") as bfp: - tdxt = TDXT.fromBytes(bfp.read()) + tdxt = TDXT.fromBytes(bfp.read(), invert_channels=invert_channels) if output_fname is None: output_fname = os.path.splitext(os.path.abspath(fname))[0] + ".png" @@ -39,9 +40,10 @@ def extract_texture( def update_texture( fname: str, input_fname: str, + invert_channels: bool = False, ) -> int: with open(fname, "rb") as bfp: - tdxt = TDXT.fromBytes(bfp.read()) + tdxt = TDXT.fromBytes(bfp.read(), invert_channels=invert_channels) if not input_fname.lower().endswith(".png"): raise Exception("Invalid output file format!") @@ -82,6 +84,11 @@ def main() -> int: default=None, help="The PNG file to unpack the texture to.", ) + unpack_parser.add_argument( + "--invert-channels", + action="store_true", + help="Swap the order of R/G/B channels in image.", + ) update_parser = subparsers.add_parser( "update", @@ -97,6 +104,11 @@ def main() -> int: metavar="INFILE", help="The PNG file to update the texture from.", ) + update_parser.add_argument( + "--invert-channels", + action="store_true", + help="Swap the order of R/G/B channels in image.", + ) args = parser.parse_args() @@ -104,11 +116,13 @@ def main() -> int: return extract_texture( args.infile, args.outfile, + invert_channels=args.invert_channels, ) elif args.action == "update": return update_texture( args.outfile, args.infile, + invert_channels=args.invert_channels, ) else: raise Exception(f"Invalid action {args.action}!")