| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711 |
- #!/usr/bin/python
- # Copyright (C) 2009-2017, Panagiotis Christopoulos Charitos and contributors.
- # All rights reserved.
- # Code licensed under the BSD License.
- # http://www.anki3d.org/LICENSE
- import argparse
- import subprocess
- import re
- import os
- import struct
- import copy
- import tempfile
- import shutil
- #
- # Config
- #
- class Config:
- in_files = []
- out_file = ""
- fast = False
- type = 0
- normal = False
- convert_path = ""
- no_alpha = False
- store_compressed = False
- store_uncompressed = True
- to_linear_rgb = False
- tmp_dir = ""
- #
- # AnKi texture
- #
- # Texture type
- TT_NONE = 0
- TT_2D = 1
- TT_CUBE = 2
- TT_3D = 3
- TT_2D_ARRAY = 4
- # Color format
- CF_NONE = 0
- CF_RGB8 = 1
- CF_RGBA8 = 2
- # Data compression
- DC_NONE = 0
- DC_RAW = 1 << 0
- DC_ETC2 = 1 << 1
- DC_S3TC = 1 << 2
- # Texture filtering
- TF_DEFAULT = 0
- TF_LINEAR = 1
- TF_NEAREST = 2
- #
- # DDS
- #
- # dwFlags of DDSURFACEDESC2
- DDSD_CAPS = 0x00000001
- DDSD_HEIGHT = 0x00000002
- DDSD_WIDTH = 0x00000004
- DDSD_PITCH = 0x00000008
- DDSD_PIXELFORMAT = 0x00001000
- DDSD_MIPMAPCOUNT = 0x00020000
- DDSD_LINEARSIZE = 0x00080000
- DDSD_DEPTH = 0x00800000
- # ddpfPixelFormat of DDSURFACEDESC2
- DDPF_ALPHAPIXELS = 0x00000001
- DDPF_FOURCC = 0x00000004
- DDPF_RGB = 0x00000040
- # dwCaps1 of DDSCAPS2
- DDSCAPS_COMPLEX = 0x00000008
- DDSCAPS_TEXTURE = 0x00001000
- DDSCAPS_MIPMAP = 0x00400000
- # dwCaps2 of DDSCAPS2
- DDSCAPS2_CUBEMAP = 0x00000200
- DDSCAPS2_CUBEMAP_POSITIVEX = 0x00000400
- DDSCAPS2_CUBEMAP_NEGATIVEX = 0x00000800
- DDSCAPS2_CUBEMAP_POSITIVEY = 0x00001000
- DDSCAPS2_CUBEMAP_NEGATIVEY = 0x00002000
- DDSCAPS2_CUBEMAP_POSITIVEZ = 0x00004000
- DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x00008000
- DDSCAPS2_VOLUME = 0x00200000
- class DdsHeader:
- """ The header of a dds file """
- _fields = [
- ('dwMagic', '4s'),
- ('dwSize', 'I'),
- ('dwFlags', 'I'),
- ('dwHeight', 'I'),
- ('dwWidth', 'I'),
- ('dwPitchOrLinearSize', 'I'),
- ('dwDepth', 'I'),
- ('dwMipMapCount', 'I'),
- ('dwReserved1', '44s'),
- # Pixel format
- ('dwSize', 'I'),
- ('dwFlags', 'I'),
- ('dwFourCC', '4s'),
- ('dwRGBBitCount', 'I'),
- ('dwRBitMask', 'I'),
- ('dwGBitMask', 'I'),
- ('dwBBitMask', 'I'),
- ('dwRGBAlphaBitMask', 'I'),
- ('dwCaps1', 'I'),
- ('dwCaps2', 'I'),
- ('dwCapsReserved', '8s'),
- ('dwReserved2', 'I')]
- def __init__(self, buff):
- buff_format = self.get_format()
- items = struct.unpack(buff_format, buff)
- for field, value in map(None, self._fields, items):
- setattr(self, field[0], value)
- @classmethod
- def get_format(cls):
- return '<' + ''.join([f[1] for f in cls._fields])
- @classmethod
- def get_size(cls):
- return struct.calcsize(cls.get_format())
- #
- # ETC2
- #
- class PkmHeader:
- """ The header of a pkm file """
- _fields = [
- ("magic", "6s"),
- ("type", "H"),
- ("width", "H"),
- ("height", "H"),
- ("origWidth", "H"),
- ("origHeight", "H")]
- def __init__(self, buff):
- buff_format = self.get_format()
- items = struct.unpack(buff_format, buff)
- for field, value in map(None, self._fields, items):
- setattr(self, field[0], value)
- @classmethod
- def get_format(cls):
- return ">" + "".join([f[1] for f in cls._fields])
- @classmethod
- def get_size(cls):
- return struct.calcsize(cls.get_format())
- #
- # Functions
- #
- def printi(s):
- print("[I] %s" % s)
- def printw(s):
- print("[W] %s" % s)
- def is_power2(num):
- """ Returns true if a number is a power of two """
- return num != 0 and ((num & (num - 1)) == 0)
- def get_base_fname(path):
- """ From path/to/a/file.ext return the "file" """
- return os.path.splitext(os.path.basename(path))[0]
- def parse_commandline():
- """ Parse the command line arguments """
- parser = argparse.ArgumentParser(description = "This program converts a single image or a number " \
- "of images (for 3D and 2DArray textures) to AnKi texture format." \
- " It requires 4 different applications/executables to " \
- "operate: convert, identify, nvcompress and etcpack. These " \
- "applications should be in PATH except the convert where you " \
- "need to define the executable explicitly",
- formatter_class = argparse.ArgumentDefaultsHelpFormatter)
- parser.add_argument("-i", "--input", nargs = "+", required = True,
- help = "specify the image(s) to convert. Seperate with space")
- parser.add_argument("-o", "--output", required = True, help = "specify output AnKi image.")
- parser.add_argument("-t", "--type", default = "2D", choices = ["2D", "3D", "2DArray"],
- help = "type of the image (2D or cube or 3D or 2DArray)")
- parser.add_argument("-f", "--fast", type = int, default = 0, help = "run the fast version of the converters")
- parser.add_argument("-n", "--normal", type = int, default = 0, help = "assume the texture is normal")
- parser.add_argument("-c", "--convert-path", default = "/usr/bin/convert",
- help = "the executable where convert tool is located. Stupid etcpack cannot get it from PATH")
- parser.add_argument("--no-alpha", type = int, default = 0, help = "remove alpha channel")
- parser.add_argument("--store-uncompressed", type = int, default = 0, help = "store or not uncompressed data")
- parser.add_argument("--store-compressed", type = int, default = 1, help = "store or not compressed data")
- parser.add_argument("--to-linear-rgb", type = int, default = 0,
- help = "assume the input textures are sRGB. If this option is true then convert them to linear RGB")
- parser.add_argument("--filter", default = "default", choices = ["default", "linear", "nearest"],
- help = "texture filtering. Can be: default, linear, nearest")
- parser.add_argument("--mips-count", type = int, default = 0xFFFF, help = "Max number of mipmaps")
- args = parser.parse_args()
- if args.type == "2D":
- typ = TT_2D
- elif args.type == "cube":
- typ = TT_CUBE
- elif args.type == "3D":
- typ = TT_3D
- elif args.type == "2DArray":
- typ = TT_2D_ARRAY
- else:
- assert 0, "See file"
- if args.filter == "default":
- filter = TF_DEFAULT
- elif args.filter == "linear":
- filter = TF_LINEAR
- elif args.filter == "nearest":
- filter = TF_NEAREST
- else:
- assert 0, "See file"
- if not args.store_uncompressed and not args.store_compressed:
- parser.error("One of --store-compressed and --store-uncompressed should be True")
- if args.mips_count <= 0:
- parser.error("Wrong number of mipmaps")
- config = Config()
- config.in_files = args.input
- config.out_file = args.output
- config.fast = args.fast
- config.type = typ
- config.normal = args.normal
- config.convert_path = args.convert_path
- config.no_alpha = args.no_alpha
- config.store_uncompressed = args.store_uncompressed
- config.store_compressed = args.store_compressed
- config.to_linear_rgb = args.to_linear_rgb
- config.filter = filter
- config.mips_count = args.mips_count
- print(config)
- return config
- def identify_image(in_file):
- """ Return the size of the input image and the internal format """
- color_format = CF_NONE
- width = 0
- height = 0
- proc = subprocess.Popen(["identify", "-verbose" , in_file], stdout=subprocess.PIPE)
- stdout_str = proc.stdout.read()
- # Make sure the colorspace is what we want
- """reg = re.search(r"Colorspace: (.*)", stdout_str)
- if not reg or reg.group(1) != "RGB":
- raise Exception("Something is wrong with the colorspace")"""
- # Get the size of the iamge
- reg = re.search(r"Geometry: ([0-9]*)x([0-9]*)\+", stdout_str)
- if not reg:
- raise Exception("Cannot extract size")
- # Identify the color space
- """if not re.search(r"red: 8-bit", stdout_str) \
- or not re.search(r"green: 8-bit", stdout_str) \
- or not re.search(r"blue: 8-bit", stdout_str): \
- raise Exception("Incorrect channel depths")"""
- if re.search(r"alpha: 8-bit", stdout_str):
- color_format = CF_RGBA8
- color_format_str = "RGBA"
- else:
- color_format = CF_RGB8
- color_format_str = "RGB"
- # print some stuff and return
- printi("width: %s, height: %s color format: %s" % \
- (reg.group(1), reg.group(2), color_format_str))
- return (color_format, int(reg.group(1)), int(reg.group(2)))
- def create_mipmaps(in_file, tmp_dir, width_, height_, color_format, to_linear_rgb, max_mip_count):
- """ Create a number of images for all mipmaps """
- printi("Generate mipmaps")
- width = width_
- height = height_
- mips_fnames = []
- while width >= 4 and height >= 4:
- size_str = "%dx%d" % (width, height)
- out_file_str = os.path.join(tmp_dir, get_base_fname(in_file)) + "." + size_str
- printi(" %s.tga" % out_file_str)
- mips_fnames.append(out_file_str)
- args = ["convert", in_file]
- # to linear
- if to_linear_rgb:
- if color_format != CF_RGB8:
- raise Exception("to linear RGB only supported for RGB textures")
- args.append("-set")
- args.append("colorspace")
- args.append("sRGB")
- args.append("-colorspace")
- args.append("RGB")
- # Add this because it will automatically convert gray-like images to grayscale TGAs
- args.append("-type")
- args.append("TrueColor")
- # resize
- args.append("-resize")
- args.append(size_str)
- # alpha
- args.append("-alpha")
- if color_format == CF_RGB8:
- args.append("deactivate")
- else:
- args.append("activate")
- args.append(out_file_str + ".tga")
- subprocess.check_call(args)
- if(len(mips_fnames) == max_mip_count):
- break;
- width = width / 2
- height = height / 2
- return mips_fnames
- def create_etc_images(mips_fnames, tmp_dir, fast, color_format, convert_path):
- """ Create the etc files """
- printi("Creating ETC images")
- # Copy the convert tool to the working dir so that etcpack will see it
- shutil.copy2(convert_path, \
- os.path.join(tmp_dir, os.path.basename(convert_path)))
- for fname in mips_fnames:
- # Unfortunately we need to flip the image. Use convert again
- in_fname = fname + ".tga"
- flipped_fname = fname + "_flip.tga"
- args = ["convert", in_fname, "-flip", flipped_fname]
- subprocess.check_call(args)
- in_fname = flipped_fname
- printi(" %s" % in_fname)
- args = ["etcpack", in_fname, tmp_dir, "-c", "etc2"]
- if fast:
- args.append("-s")
- args.append("fast")
- args.append("-f")
- if color_format == CF_RGB8:
- args.append("RGB")
- else:
- args.append("RGBA")
- # Call the executable AND change the working directory so that etcpack will find convert
- subprocess.check_call(args, stdout = subprocess.PIPE, cwd = tmp_dir)
- def create_dds_images(mips_fnames, tmp_dir, fast, color_format, normal):
- """ Create the dds files """
- printi("Creating DDS images")
- for fname in mips_fnames:
- # Unfortunately we need to flip the image. Use convert again
- in_fname = fname + ".tga"
- flipped_fname = fname + "_flip.tga"
- args = ["convert", in_fname, "-flip", flipped_fname]
- subprocess.check_call(args)
- in_fname = flipped_fname
- # Continue
- out_fname = os.path.join(tmp_dir, os.path.basename(fname) + ".dds")
- printi(" %s" % out_fname)
- args = ["nvcompress", "-silent", "-nomips"]
- if fast:
- args.append("-fast")
- if color_format == CF_RGB8:
- if not normal:
- args.append("-bc1")
- else:
- args.append("-bc1n")
- elif color_format == CF_RGBA8:
- args.append("-alpha")
- if not normal:
- args.append("-bc3")
- else:
- args.append("-bc3n")
- args.append(in_fname)
- args.append(out_fname)
- subprocess.check_call(args, stdout = subprocess.PIPE)
- def write_raw(tex_file, fname, width, height, color_format):
- """ Append raw data to the AnKi texture file """
- printi(" Appending %s" % fname)
- # Read and check the header
- uncompressed_tga_header = struct.pack("BBBBBBBBBBBB", 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0)
- in_file = open(fname, "rb")
- tga_header = in_file.read(12)
- if len(tga_header) != 12:
- raise Exception("Failed reading TGA header")
- if uncompressed_tga_header != tga_header:
- raise Exception("Incorrect TGA header")
- # Read the size and bpp
- header6_buff = in_file.read(6)
- if len(header6_buff) != 6:
- raise Exception("Failed reading TGA header #2")
- header6 = struct.unpack("BBBBBB", header6_buff)
- img_width = header6[1] * 256 + header6[0]
- img_height = header6[3] * 256 + header6[2]
- img_bpp = header6[4];
- if (color_format != CF_RGB8 or img_bpp != 24) and (color_format != CF_RGBA8 or img_bpp != 32):
- raise Exception("Unexpected bpp")
- if img_width != width or img_height != height:
- raise Exception("Unexpected width or height")
- # Read the data
- data_size = width * height
- if color_format == CF_RGB8:
- data_size *= 3
- else:
- data_size *= 4
- data = bytearray(in_file.read(data_size))
- if len(data) != data_size:
- raise Exception("Failed to read all data")
- tmp = in_file.read(128)
- if len(tmp) != 0:
- printw(" File shouldn't contain more data")
- # Swap colors
- bpp = img_bpp / 8
- for i in xrange(0, data_size, bpp):
- temp = data[i];
- data[i] = data[i + 2];
- data[i + 2] = temp;
- # Write data to tex_file
- tex_file.write(data)
- def write_s3tc(out_file, fname, width, height, color_format):
- """ Append s3tc data to the AnKi texture file """
- # Read header
- printi(" Appending %s" % fname)
- in_file = open(fname, "rb")
- header = in_file.read(DdsHeader.get_size())
- if len(header) != DdsHeader.get_size():
- raise Exception("Failed to read DDS header")
- dds_header = DdsHeader(header)
- if dds_header.dwWidth != width or dds_header.dwHeight != height:
- raise Exception("Incorrect width")
- if color_format == CF_RGB8 and dds_header.dwFourCC != "DXT1":
- raise Exception("Incorrect format. Expecting DXT1")
- if color_format == CF_RGBA8 and dds_header.dwFourCC != "DXT5":
- raise Exception("Incorrect format. Expecting DXT5")
- # Read and write the data
- if color_format == CF_RGB8:
- block_size = 8
- else:
- block_size = 16
- data_size = (width / 4) * (height / 4) * block_size
- data = in_file.read(data_size)
- if len(data) != data_size:
- raise Exception("Failed to read DDS data")
- # Make sure that the file doesn't contain any more data
- tmp = in_file.read(1)
- if len(tmp) != 0:
- printw(" File shouldn't contain more data")
- out_file.write(data)
- def write_etc(out_file, fname, width, height, color_format):
- """ Append etc2 data to the AnKi texture file """
- printi(" Appending %s" % fname)
- # Read header
- in_file = open(fname, "rb")
- header = in_file.read(PkmHeader.get_size())
- if len(header) != PkmHeader.get_size():
- raise Exception("Failed to read PKM header")
- pkm_header = PkmHeader(header)
- if pkm_header.magic != "PKM 20":
- raise Exception("Incorrect PKM header")
- if width != pkm_header.width or height != pkm_header.height:
- raise Exception("Incorrect PKM width or height")
- # Read and write the data
- data_size = (pkm_header.width / 4) * (pkm_header.height / 4) * 8
- data = in_file.read(data_size)
- if len(data) != data_size:
- raise Exception("Failed to read PKM data")
- # Make sure that the file doesn't contain any more data
- tmp = in_file.read(1)
- if len(tmp) != 0:
- printw(" File shouldn't contain more data")
- out_file.write(data)
- def convert(config):
- """ This is the function that does all the work """
- # Invoke app named "identify" to get internal format and width and height
- (color_format, width, height) = identify_image(config.in_files[0])
- if not is_power2(width) or not is_power2(height):
- raise Exception("Image width and height should power of 2")
- if color_format == CF_RGBA8 and config.normal:
- raise Exception("RGBA image and normal does not make much sense")
- for i in range(1, len(config.in_files)):
- (color_format_2, width_2, height_2) = identify_image(config.in_files[i])
- if width != width_2 or height != height_2 \
- or color_format != color_format_2:
- raise Exception("Images are not same size and color space")
- if config.no_alpha:
- color_format = CF_RGB8
- # Create images
- for in_file in config.in_files:
- mips_fnames = create_mipmaps(in_file, config.tmp_dir, width, height, color_format, config.to_linear_rgb,
- config.mips_count)
- # Create etc images
- create_etc_images(mips_fnames, config.tmp_dir, config.fast, color_format, config.convert_path)
- # Create dds images
- create_dds_images(mips_fnames, config.tmp_dir, config.fast, color_format, config.normal)
- # Open file
- fname = config.out_file
- printi("Writing %s" % fname)
- tex_file = open(fname, "wb")
- # Write header
- ak_format = "8sIIIIIIII"
- data_compression = 0
- if config.store_compressed:
- data_compression = data_compression | DC_S3TC | DC_ETC2
- if config.store_uncompressed:
- data_compression = data_compression | DC_RAW
- buff = struct.pack(ak_format,
- b"ANKITEX1",
- width,
- height,
- len(config.in_files),
- config.type,
- color_format,
- data_compression,
- config.normal,
- len(mips_fnames))
- tex_file.write(buff)
- # Write header padding
- header_padding_size = 128 - struct.calcsize(ak_format)
- if header_padding_size != 88:
- raise Exception("Check the header")
- for i in range(0, header_padding_size):
- tex_file.write('\0')
- # For each compression
- for compression in range(0, 3):
- tmp_width = width
- tmp_height = height
- # For each level
- for i in range(0, len(mips_fnames)):
- # For each image
- for in_file in config.in_files:
- size_str = "%dx%d" % (tmp_width, tmp_height)
- in_base_fname = os.path.join(config.tmp_dir, get_base_fname(in_file)) + "." + size_str
- # Write RAW
- if compression == 0 and config.store_uncompressed:
- write_raw(tex_file, in_base_fname + ".tga", tmp_width, tmp_height, color_format)
- # Write S3TC
- elif compression == 1 and config.store_compressed:
- write_s3tc(tex_file, in_base_fname + ".dds", tmp_width, tmp_height, color_format)
- # Write ETC
- elif compression == 2 and config.store_compressed:
- write_etc(tex_file, in_base_fname + "_flip.pkm", tmp_width, tmp_height, color_format)
- tmp_width = tmp_width / 2
- tmp_height = tmp_height / 2
- def main():
- """ The main """
- # Parse cmd line args
- config = parse_commandline();
- if config.type == TT_CUBE and len(config.in_files) != 6:
- raise Exception("Not enough images for cube generation")
- if (config.type == TT_3D or config.type == TT_2D_ARRAY) and len(config.in_files) < 2:
- #raise Exception("Not enough images for 2DArray/3D texture")
- printw("Not enough images for 2DArray/3D texture")
- printi("Number of images %u" % len(config.in_files))
- if config.type == TT_2D and len(config.in_files) != 1:
- raise Exception("Only one image for 2D textures needed")
- if not os.path.isfile(config.convert_path):
- raise Exception("Tool convert not found: " + config.convert_path)
- # Setup the temp dir
- config.tmp_dir = tempfile.mkdtemp("_ankitex")
- # Do the work
- try:
- convert(config)
- finally:
- shutil.rmtree(config.tmp_dir)
- # Done
- printi("Done!")
- if __name__ == "__main__":
- main()
|