Allow specifying inverting channels for graphics extracted/imported using TDXT utils.
This commit is contained in:
parent
56e68f211a
commit
5a83329396
@ -19,6 +19,8 @@ class TDXT:
|
|||||||
length_fixup: bool,
|
length_fixup: bool,
|
||||||
raw: bytes,
|
raw: bytes,
|
||||||
img: Optional[Image.Image],
|
img: Optional[Image.Image],
|
||||||
|
*,
|
||||||
|
invert_channels: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.header_flags1 = header_flags1
|
self.header_flags1 = header_flags1
|
||||||
self.header_flags2 = header_flags2
|
self.header_flags2 = header_flags2
|
||||||
@ -29,6 +31,7 @@ class TDXT:
|
|||||||
self.fmtflags = fmtflags
|
self.fmtflags = fmtflags
|
||||||
self.endian = endian
|
self.endian = endian
|
||||||
self.length_fixup = length_fixup
|
self.length_fixup = length_fixup
|
||||||
|
self.invert_channels = invert_channels
|
||||||
self.__raw = raw
|
self.__raw = raw
|
||||||
self.__img = img
|
self.__img = img
|
||||||
|
|
||||||
@ -39,7 +42,14 @@ class TDXT:
|
|||||||
@raw.setter
|
@raw.setter
|
||||||
def raw(self, newdata: bytes) -> None:
|
def raw(self, newdata: bytes) -> None:
|
||||||
self.__raw = newdata
|
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
|
width, height = newimg.size
|
||||||
if width != self.width or height != self.height:
|
if width != self.width or height != self.height:
|
||||||
raise Exception("Unsupported texture resize operation for TDXT file!")
|
raise Exception("Unsupported texture resize operation for TDXT file!")
|
||||||
@ -55,7 +65,7 @@ class TDXT:
|
|||||||
self.__raw = self._imgToRaw(newimg)
|
self.__raw = self._imgToRaw(newimg)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def fromBytes(raw_data: bytes) -> "TDXT":
|
def fromBytes(raw_data: bytes, *, invert_channels: bool = False) -> "TDXT":
|
||||||
# First, check the endianness.
|
# First, check the endianness.
|
||||||
(magic,) = struct.unpack_from("4s", raw_data)
|
(magic,) = struct.unpack_from("4s", raw_data)
|
||||||
|
|
||||||
@ -126,12 +136,15 @@ class TDXT:
|
|||||||
endian=endian,
|
endian=endian,
|
||||||
length_fixup=length_fixup,
|
length_fixup=length_fixup,
|
||||||
raw=raw_data[64:],
|
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
|
@staticmethod
|
||||||
def _rawToImg(
|
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]:
|
) -> Optional[Image.Image]:
|
||||||
# Since the AFP file format can be found in both big and little endian, its
|
# 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.
|
# possible that some of these loaders might need byteswapping on some platforms.
|
||||||
@ -177,7 +190,7 @@ class TDXT:
|
|||||||
(width, height),
|
(width, height),
|
||||||
b"".join(newdata),
|
b"".join(newdata),
|
||||||
"raw",
|
"raw",
|
||||||
"RGB",
|
"BGR" if invert else "RGB",
|
||||||
)
|
)
|
||||||
elif fmt == 0x0E:
|
elif fmt == 0x0E:
|
||||||
# RGB image, no alpha. Game references D3D9 texture format 22 (R8G8B8).
|
# RGB image, no alpha. Game references D3D9 texture format 22 (R8G8B8).
|
||||||
@ -186,7 +199,7 @@ class TDXT:
|
|||||||
(width, height),
|
(width, height),
|
||||||
raw_data,
|
raw_data,
|
||||||
"raw",
|
"raw",
|
||||||
"RGB",
|
"BGR" if invert else "RGB",
|
||||||
)
|
)
|
||||||
elif fmt == 0x10:
|
elif fmt == 0x10:
|
||||||
# Seems to be some sort of RGBA with color swapping. Game references D3D9 texture
|
# Seems to be some sort of RGBA with color swapping. Game references D3D9 texture
|
||||||
@ -196,7 +209,7 @@ class TDXT:
|
|||||||
(width, height),
|
(width, height),
|
||||||
raw_data,
|
raw_data,
|
||||||
"raw",
|
"raw",
|
||||||
"BGRA",
|
"RGBA" if invert else "BGRA",
|
||||||
)
|
)
|
||||||
elif fmt == 0x13:
|
elif fmt == 0x13:
|
||||||
# Some 16-bit texture format. Game references D3D9 texture format 25 (A1R5G5B5).
|
# Some 16-bit texture format. Game references D3D9 texture format 25 (A1R5G5B5).
|
||||||
@ -224,7 +237,7 @@ class TDXT:
|
|||||||
(width, height),
|
(width, height),
|
||||||
b"".join(newdata),
|
b"".join(newdata),
|
||||||
"raw",
|
"raw",
|
||||||
"RGBA",
|
"BGRA" if invert else "RGBA",
|
||||||
)
|
)
|
||||||
elif fmt == 0x15:
|
elif fmt == 0x15:
|
||||||
# RGBA format. Game references D3D9 texture format 21 (A8R8G8B8).
|
# RGBA format. Game references D3D9 texture format 21 (A8R8G8B8).
|
||||||
@ -234,7 +247,7 @@ class TDXT:
|
|||||||
(width, height),
|
(width, height),
|
||||||
raw_data,
|
raw_data,
|
||||||
"raw",
|
"raw",
|
||||||
"ARGB",
|
"ABGR" if invert else "ARGB",
|
||||||
)
|
)
|
||||||
elif fmt == 0x16:
|
elif fmt == 0x16:
|
||||||
# DXT1 format. Game references D3D9 DXT1 texture format.
|
# DXT1 format. Game references D3D9 DXT1 texture format.
|
||||||
@ -298,7 +311,7 @@ class TDXT:
|
|||||||
(width, height),
|
(width, height),
|
||||||
b"".join(newdata),
|
b"".join(newdata),
|
||||||
"raw",
|
"raw",
|
||||||
"RGBA",
|
"BGRA" if invert else "RGBA",
|
||||||
)
|
)
|
||||||
elif fmt == 0x20:
|
elif fmt == 0x20:
|
||||||
# RGBA format. Game references D3D9 surface format 21 (A8R8G8B8).
|
# RGBA format. Game references D3D9 surface format 21 (A8R8G8B8).
|
||||||
@ -307,7 +320,7 @@ class TDXT:
|
|||||||
(width, height),
|
(width, height),
|
||||||
raw_data,
|
raw_data,
|
||||||
"raw",
|
"raw",
|
||||||
"BGRA",
|
"RGBA" if invert else "BGRA",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
img = None
|
img = None
|
||||||
@ -354,15 +367,21 @@ class TDXT:
|
|||||||
if width != self.width or height != self.height:
|
if width != self.width or height != self.height:
|
||||||
raise Exception("Unsupported texture resize operation for TDXT file!")
|
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:
|
if self.fmt == 0x0B:
|
||||||
# 16-bit 565 color RGB format.
|
# 16-bit 565 color RGB format.
|
||||||
raw = b"".join(
|
raw = b"".join(
|
||||||
struct.pack(
|
struct.pack(
|
||||||
"<H",
|
"<H",
|
||||||
(
|
(
|
||||||
(((pixel[0] >> 3) & 0x1F) << 11)
|
(((pixel[order[0]] >> 3) & 0x1F) << 11)
|
||||||
| (((pixel[1] >> 2) & 0x3F) << 5)
|
| (((pixel[order[1]] >> 2) & 0x3F) << 5)
|
||||||
| ((pixel[2] >> 3) & 0x1F)
|
| ((pixel[order[2]] >> 3) & 0x1F)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for pixel in imgdata.getdata()
|
for pixel in imgdata.getdata()
|
||||||
@ -374,9 +393,9 @@ class TDXT:
|
|||||||
"<H",
|
"<H",
|
||||||
(
|
(
|
||||||
(0x8000 if pixel[3] >= 128 else 0x0000)
|
(0x8000 if pixel[3] >= 128 else 0x0000)
|
||||||
| (((pixel[0] >> 3) & 0x1F) << 10)
|
| (((pixel[order[0]] >> 3) & 0x1F) << 10)
|
||||||
| (((pixel[1] >> 3) & 0x1F) << 5)
|
| (((pixel[order[1]] >> 3) & 0x1F) << 5)
|
||||||
| ((pixel[2] >> 3) & 0x1F)
|
| ((pixel[order[2]] >> 3) & 0x1F)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for pixel in imgdata.getdata()
|
for pixel in imgdata.getdata()
|
||||||
@ -387,9 +406,9 @@ class TDXT:
|
|||||||
struct.pack(
|
struct.pack(
|
||||||
"<H",
|
"<H",
|
||||||
(
|
(
|
||||||
((pixel[2] >> 4) & 0xF)
|
((pixel[order[2]] >> 4) & 0xF)
|
||||||
| (((pixel[1] >> 4) & 0xF) << 4)
|
| (((pixel[order[1]] >> 4) & 0xF) << 4)
|
||||||
| (((pixel[0] >> 4) & 0xF) << 8)
|
| (((pixel[order[0]] >> 4) & 0xF) << 8)
|
||||||
| (((pixel[3] >> 4) & 0xF) << 12)
|
| (((pixel[3] >> 4) & 0xF) << 12)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -400,9 +419,9 @@ class TDXT:
|
|||||||
raw = b"".join(
|
raw = b"".join(
|
||||||
struct.pack(
|
struct.pack(
|
||||||
"<BBBB",
|
"<BBBB",
|
||||||
pixel[2],
|
pixel[order[2]],
|
||||||
pixel[1],
|
pixel[order[1]],
|
||||||
pixel[0],
|
pixel[order[0]],
|
||||||
pixel[3],
|
pixel[3],
|
||||||
)
|
)
|
||||||
for pixel in imgdata.getdata()
|
for pixel in imgdata.getdata()
|
||||||
|
@ -13,9 +13,10 @@ from bemani.format import TDXT
|
|||||||
def extract_texture(
|
def extract_texture(
|
||||||
fname: str,
|
fname: str,
|
||||||
output_fname: Optional[str],
|
output_fname: Optional[str],
|
||||||
|
invert_channels: bool = False,
|
||||||
) -> int:
|
) -> int:
|
||||||
with open(fname, "rb") as bfp:
|
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:
|
if output_fname is None:
|
||||||
output_fname = os.path.splitext(os.path.abspath(fname))[0] + ".png"
|
output_fname = os.path.splitext(os.path.abspath(fname))[0] + ".png"
|
||||||
@ -39,9 +40,10 @@ def extract_texture(
|
|||||||
def update_texture(
|
def update_texture(
|
||||||
fname: str,
|
fname: str,
|
||||||
input_fname: str,
|
input_fname: str,
|
||||||
|
invert_channels: bool = False,
|
||||||
) -> int:
|
) -> int:
|
||||||
with open(fname, "rb") as bfp:
|
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"):
|
if not input_fname.lower().endswith(".png"):
|
||||||
raise Exception("Invalid output file format!")
|
raise Exception("Invalid output file format!")
|
||||||
@ -82,6 +84,11 @@ def main() -> int:
|
|||||||
default=None,
|
default=None,
|
||||||
help="The PNG file to unpack the texture to.",
|
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_parser = subparsers.add_parser(
|
||||||
"update",
|
"update",
|
||||||
@ -97,6 +104,11 @@ def main() -> int:
|
|||||||
metavar="INFILE",
|
metavar="INFILE",
|
||||||
help="The PNG file to update the texture from.",
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@ -104,11 +116,13 @@ def main() -> int:
|
|||||||
return extract_texture(
|
return extract_texture(
|
||||||
args.infile,
|
args.infile,
|
||||||
args.outfile,
|
args.outfile,
|
||||||
|
invert_channels=args.invert_channels,
|
||||||
)
|
)
|
||||||
elif args.action == "update":
|
elif args.action == "update":
|
||||||
return update_texture(
|
return update_texture(
|
||||||
args.outfile,
|
args.outfile,
|
||||||
args.infile,
|
args.infile,
|
||||||
|
invert_channels=args.invert_channels,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Invalid action {args.action}!")
|
raise Exception(f"Invalid action {args.action}!")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user