diff --git a/Scripts/dds.py b/Scripts/dds.py index 28fc498..09e6687 100644 --- a/Scripts/dds.py +++ b/Scripts/dds.py @@ -1,4 +1,5 @@ import os +import subprocess import struct import re import math @@ -74,7 +75,8 @@ class DDSPixelFlags(enum.IntFlag): texture_formats = { - 'DXT1' : s3tc.DXT1Texture + 'DXT1' : s3tc.DXT1Texture, + 'DXT5' : s3tc.DXT5Texture } class DDSFile: diff --git a/Scripts/ddsck.py b/Scripts/ddsck.py index 9edd432..313b588 100755 --- a/Scripts/ddsck.py +++ b/Scripts/ddsck.py @@ -1,43 +1,66 @@ #!/usr/bin/env python3 +"""Checks any number of dds files for common issues""" import sys import os import os.path +import enum import argparse import dds -parser = argparse.ArgumentParser(description="Checks any number of dds files for common issues, including formats not supported by KSP, and DXT5 textures that don't use the alpha channel.") +class Check(enum.Enum): + """Kinds of check to perform""" + TRANPARENCY = 1 + FORMAT = 2 + +parser = argparse.ArgumentParser(description= __doc__) parser.add_argument('files', type=str, nargs='*', help = "input dds files") -modes = parser.add_mutually_exclusive_group() -modes.add_argument('--transparency', '-t', dest='mode', action='store_const', const="transparency", default="none", help = "Generate a list of files that fail the transparency check") -modes.add_argument('--format', '-f', dest='mode', action='store_const', const="format", default="none", help = "Generate a list of files that fail the format check") +parser.add_argument('--transparency', '-t', + action='append_const', dest='checks', const=Check.TRANPARENCY, + help = "Check textures for unnecessary transparency map") +parser.add_argument('--format', '-f', + action='append_const', dest='checks', const=Check.FORMAT, + help = "Check texture formats") +parser.add_argument('--all', '-a', + action='store_const', dest='checks', const=[Check.TRANPARENCY, Check.FORMAT], + help = "Perform all checks") + +parser.add_argument('--formats', nargs='+', default = ['DXT1', 'DXT5'], + help="Valid texture formats to check against (default: %(default)s)") -parser.add_argument('--convertcmd', type=str, metavar='CMD', default=dds.convertcmd, help="name of imagemagick's convert tool (default: %(default)s)") -parser.add_argument('--infocmd', type=str, metavar='CMD', default=dds.infocmd, help="name of the nvidia dds info tool (default: %(default)s)") +parser.add_argument('--convertcmd', type=str, metavar='CMD', default=dds.convertcmd, + help="name of imagemagick's convert tool (default: %(default)s)") args = parser.parse_args() dds.convertcmd = args.convertcmd -dds.infocmd = args.infocmd - -for argv in args.files: - file = os.path.abspath(argv) - - info = dds.nvinfo(file) - format = info["format"] - alpha = dds.alpha(file) - - if format == "DXT1": - pass - elif format == "DXT5": - if alpha > 254: - if args.mode == 'none': - print(f'[{argv}]: Image is DXT5 but has no alpha channel') - elif args.mode == 'transparency': - print(file) + +def printerror(path, shortpath, message): + """either prints a message or just the path depending on if stdout is a tty""" + if sys.stdout.isatty(): + print(f'[{shortpath}]: {message}') else: - if args.mode == 'none': - print(f'[{argv}]: incompatible format') - elif args.mode == 'format': - print(file) \ No newline at end of file + sys.stdout.write(path+'\n') + +def checkfile(shortpath, checks, formats): + """check a single dds file for common issues""" + path = os.path.abspath(shortpath) + + with open(path, 'rb') as file: + ddsfile = dds.DDSFile() + ddsfile.read_header(file) + four_cc = ddsfile.four_cc + if four_cc == '': + four_cc = "uncompressed" + + alpha = dds.alpha(path) + + if Check.TRANPARENCY in checks and four_cc == 'DXT5' and alpha > 254: + printerror(path, shortpath, 'Image is DXT5 but has no alpha channel') + + if Check.FORMAT in checks and four_cc not in formats: + printerror(path, shortpath, f'Incompatible format: {four_cc}') + +for shortpath in args.files: + checkfile(shortpath, args.checks, args.formats) diff --git a/Scripts/dxt5to1.py b/Scripts/dxt5to1.py new file mode 100755 index 0000000..5597d3a --- /dev/null +++ b/Scripts/dxt5to1.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""converts DXT5 dds files to DXT1 without generation loss.""" + +import sys +import os +import os.path +import argparse +import dds + +parser = argparse.ArgumentParser( + description=__doc__) +parser.add_argument('files', type=str, nargs='*', + help = "input dds files. Paths are read from stdin if none are given") + +args = parser.parse_args() + +if len(args.files) < 1 and not sys.stdin.isatty(): + args.files = sys.stdin.read().splitlines() + +for argv in args.files: + path = os.path.abspath(argv) + + # read DDS file + with open(path, 'rb') as file: + ddsfile = dds.DDSFile(file) + + # check its actually dxt5 + if ddsfile.four_cc != 'DXT5': + print(f'[{argv}] file is not DXT5, aborting.') + continue + print(f'[{argv}] converting to DXT1') + + # make modifications + ddsfile.four_cc = 'DXT1' + ddsfile.textures = [tex.to_dxt1() for tex in ddsfile.textures] + ddsfile.linear_size //= 2 + + # write DDS file + with open(path, 'wb') as file: + ddsfile.write(file) diff --git a/Scripts/s3tc.py b/Scripts/s3tc.py index 9d0c1b5..d6cf600 100644 --- a/Scripts/s3tc.py +++ b/Scripts/s3tc.py @@ -1,62 +1,133 @@ import struct import math +import operator +from functools import reduce from color import Color -def byte_slice(byte): - return [byte & 0x03, - (byte & 0x0C) >> 2, - (byte & 0x30) >> 4, - (byte & 0xC0) >> 6] +def bit_slice(value, size, count): + mask = (2 ** size) - 1 + return [(value >> offset) & mask for offset in range(0, size * count, size)] -def byte_unslice(indices): - return indices[0] | (indices[1] << 2) | (indices[2] << 4) | (indices[3] << 6) +def bit_merge(values, size): + offsets = range(0, len(values) * size, size) + return reduce(operator.__or__, map(operator.lshift, values, offsets)) -class DXT1Texture: - def __init__(self, width = 4, height = 4, file = None): - self.width = width - self.height = height - self.block_width = math.ceil(width / 4) - self.block_height = math.ceil(height / 4) - self.block_count = self.block_width * self.block_height - self.blocks = [] +def triple_slice(triplet): + values = bit_slice(bit_merge(triplet, 8), 3, 8) + return [values[0:4], values[4:8]] + +def triple_merge(rows): + values = rows[0] + rows[1] + return bit_slice(bit_merge(values, 3), 8, 3) + +class DXT1Block: + size = 8 + + def __init__(self, file = None): + self.color0 = Color() + self.color1 = Color() + self.indices = [[0] * 4] * 4 if file: self.read(file) def __repr__(self): - return f'{self.block_count} blocks: {self.blocks}' + return repr(self.__dict__) + + def __str__(self): + return f'color0: {str(self.color0)} color1: {str(self.color1)}, indices:{self.indices}' def read(self, file): - for x in range(self.block_count): - self.blocks.append(DXT1Block(file)) + block = struct.unpack_from('<2H4B', file.read(self.size)) + + self.color0 = Color.from_565(block[0]) + self.color1 = Color.from_565(block[1]) + self.indices = [bit_slice(row, 2, 4) for row in block[2:6]] def write(self, file): - for block in self.blocks: - block.write(file) + file.write(struct.pack('<2H4B', + self.color0.to_565(), self.color1.to_565(), + *(bit_merge(row, 2) for row in self.indices))) -class DXT1Block: - size = 8 + +class DXT5Block: + size = 16 def __init__(self, file = None): self.color0 = Color() self.color1 = Color() self.indices = [[0] * 4] * 4 + self.alpha0 = 1 + self.alpha1 = 1 + self.alpha_indices = [[0] * 4] * 4 + if file: self.read(file) def __repr__(self): - return f'color0: ({repr(self.color0)}) color1: ({repr(self.color1)}), indices:{self.indices}' - - def __str__(self): - return f'color0: {str(self.color0)} color1: {str(self.color1)}, indices:{self.indices}' + return repr(self.__dict__) def read(self, file): - block = struct.unpack_from('