convert_image.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743
  1. #!/usr/bin/python
  2. import optparse
  3. import subprocess
  4. import re
  5. import os
  6. import struct
  7. import copy
  8. import tempfile
  9. import shutil
  10. #
  11. # Config
  12. #
  13. class Config:
  14. in_files = []
  15. out_file = ""
  16. fast = False
  17. type = 0
  18. normal = False
  19. convert_path = ""
  20. no_alpha = False
  21. store_compressed = False
  22. store_uncompressed = True
  23. to_linear_rgb = False
  24. tmp_dir = ""
  25. #
  26. # AnKi texture
  27. #
  28. # Texture type
  29. TT_NONE = 0
  30. TT_2D = 1
  31. TT_CUBE = 2
  32. TT_3D = 3
  33. TT_2D_ARRAY = 4
  34. # Color format
  35. CF_NONE = 0
  36. CF_RGB8 = 1
  37. CF_RGBA8 = 2
  38. # Data compression
  39. DC_NONE = 0
  40. DC_RAW = 1 << 0
  41. DC_ETC2 = 1 << 1
  42. DC_S3TC = 1 << 2
  43. # Texture filtering
  44. TF_DEFAULT = 0
  45. TF_LINEAR = 1
  46. TF_NEAREST = 2
  47. #
  48. # DDS
  49. #
  50. # dwFlags of DDSURFACEDESC2
  51. DDSD_CAPS = 0x00000001
  52. DDSD_HEIGHT = 0x00000002
  53. DDSD_WIDTH = 0x00000004
  54. DDSD_PITCH = 0x00000008
  55. DDSD_PIXELFORMAT = 0x00001000
  56. DDSD_MIPMAPCOUNT = 0x00020000
  57. DDSD_LINEARSIZE = 0x00080000
  58. DDSD_DEPTH = 0x00800000
  59. # ddpfPixelFormat of DDSURFACEDESC2
  60. DDPF_ALPHAPIXELS = 0x00000001
  61. DDPF_FOURCC = 0x00000004
  62. DDPF_RGB = 0x00000040
  63. # dwCaps1 of DDSCAPS2
  64. DDSCAPS_COMPLEX = 0x00000008
  65. DDSCAPS_TEXTURE = 0x00001000
  66. DDSCAPS_MIPMAP = 0x00400000
  67. # dwCaps2 of DDSCAPS2
  68. DDSCAPS2_CUBEMAP = 0x00000200
  69. DDSCAPS2_CUBEMAP_POSITIVEX = 0x00000400
  70. DDSCAPS2_CUBEMAP_NEGATIVEX = 0x00000800
  71. DDSCAPS2_CUBEMAP_POSITIVEY = 0x00001000
  72. DDSCAPS2_CUBEMAP_NEGATIVEY = 0x00002000
  73. DDSCAPS2_CUBEMAP_POSITIVEZ = 0x00004000
  74. DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x00008000
  75. DDSCAPS2_VOLUME = 0x00200000
  76. class DdsHeader:
  77. """ The header of a dds file """
  78. _fields = [
  79. ('dwMagic', '4s'),
  80. ('dwSize', 'I'),
  81. ('dwFlags', 'I'),
  82. ('dwHeight', 'I'),
  83. ('dwWidth', 'I'),
  84. ('dwPitchOrLinearSize', 'I'),
  85. ('dwDepth', 'I'),
  86. ('dwMipMapCount', 'I'),
  87. ('dwReserved1', '44s'),
  88. # Pixel format
  89. ('dwSize', 'I'),
  90. ('dwFlags', 'I'),
  91. ('dwFourCC', '4s'),
  92. ('dwRGBBitCount', 'I'),
  93. ('dwRBitMask', 'I'),
  94. ('dwGBitMask', 'I'),
  95. ('dwBBitMask', 'I'),
  96. ('dwRGBAlphaBitMask', 'I'),
  97. ('dwCaps1', 'I'),
  98. ('dwCaps2', 'I'),
  99. ('dwCapsReserved', '8s'),
  100. ('dwReserved2', 'I')]
  101. def __init__(self, buff):
  102. buff_format = self.get_format()
  103. items = struct.unpack(buff_format, buff)
  104. for field, value in map(None, self._fields, items):
  105. setattr(self, field[0], value)
  106. @classmethod
  107. def get_format(cls):
  108. return '<' + ''.join([f[1] for f in cls._fields])
  109. @classmethod
  110. def get_size(cls):
  111. return struct.calcsize(cls.get_format())
  112. #
  113. # ETC2
  114. #
  115. class PkmHeader:
  116. """ The header of a pkm file """
  117. _fields = [
  118. ("magic", "6s"),
  119. ("type", "H"),
  120. ("width", "H"),
  121. ("height", "H"),
  122. ("origWidth", "H"),
  123. ("origHeight", "H")]
  124. def __init__(self, buff):
  125. buff_format = self.get_format()
  126. items = struct.unpack(buff_format, buff)
  127. for field, value in map(None, self._fields, items):
  128. setattr(self, field[0], value)
  129. @classmethod
  130. def get_format(cls):
  131. return ">" + "".join([f[1] for f in cls._fields])
  132. @classmethod
  133. def get_size(cls):
  134. return struct.calcsize(cls.get_format())
  135. #
  136. # Functions
  137. #
  138. def printi(s):
  139. print("[I] %s" % s)
  140. def printw(s):
  141. print("[W] %s" % s)
  142. def is_power2(num):
  143. """ Returns true if a number is a power of two """
  144. return num != 0 and ((num & (num - 1)) == 0)
  145. def get_base_fname(path):
  146. """ From path/to/a/file.ext return the "file" """
  147. return os.path.splitext(os.path.basename(path))[0]
  148. def parse_commandline():
  149. """ Parse the command line arguments """
  150. parser = optparse.OptionParser(usage = "usage: %prog [options]", \
  151. description = "This program converts a single image or a number " \
  152. "of images (for 3D and 2DArray textures) to AnKi texture format." \
  153. " It requires 4 different applications/executables to " \
  154. "operate: convert, identify, nvcompress and etcpack. These " \
  155. "applications should be in PATH except the convert where you " \
  156. "need to define the executable explicitly")
  157. parser.add_option("-i", "--input", dest = "inp",
  158. type = "string", help = "specify the image(s) to convert. " \
  159. "Seperate with :")
  160. parser.add_option("-o", "--output", dest = "out",
  161. type = "string", help = "specify output AnKi image. ")
  162. parser.add_option("-t", "--type", dest = "type",
  163. type = "string", default = "2D",
  164. help = "type of the image (2D or cube or 3D or 2DArray)")
  165. parser.add_option("-f", "--fast", dest = "fast",
  166. type = "int", action = "store", default = 0,
  167. help = "run the fast version of the converters")
  168. parser.add_option("-n", "--normal", dest = "normal",
  169. type = "int", action = "store", default = 0,
  170. help = "assume the texture is normal")
  171. parser.add_option("-c", "--convert-path", dest = "convert_path",
  172. type = "string", default = "/usr/bin/convert",
  173. help = "the executable where convert tool is " \
  174. "located. Stupid etcpack cannot get it from PATH")
  175. parser.add_option("--no-alpha", dest = "no_alpha",
  176. type = "int", action = "store", default = 0,
  177. help = "remove alpha channel")
  178. parser.add_option("--store-uncompressed", dest = "store_uncompressed",
  179. type = "int", action = "store", default = 0,
  180. help = "store or not uncompressed data")
  181. parser.add_option("--store-compressed", dest = "store_compressed",
  182. type = "int", action = "store", default = 1,
  183. help = "store or not compressed data")
  184. parser.add_option("--to-linear-rgb", dest = "to_linear_rgb",
  185. type = "int", action = "store", default = 0,
  186. help = "assume the input textures are sRGB. If this option is " \
  187. "true then convert them to linear RGB")
  188. parser.add_option("--filter", dest = "filter", type = "string",
  189. default = "default", help = "texture filtering. Can be: " \
  190. "default, linear, nearest")
  191. parser.add_option("--mips_count", dest = "mips_count", type = "int",
  192. default = "0xFFFF", help = "Max number of mipmaps")
  193. # Add the default value on each option when printing help
  194. for option in parser.option_list:
  195. if option.default != ("NO", "DEFAULT"):
  196. option.help += (" " if option.help else "") + "[default: %default]"
  197. (options, args) = parser.parse_args()
  198. if not options.inp or not options.out or not options.convert_path:
  199. parser.error("argument is missing")
  200. if options.type == "2D":
  201. typ = TT_2D
  202. elif options.type == "cube":
  203. typ = TT_CUBE
  204. elif options.type == "3D":
  205. typ = TT_3D
  206. elif options.type == "2DArray":
  207. typ = TT_2D_ARRAY
  208. else:
  209. parser.error("Unrecognized type: " + options.type)
  210. if options.filter == "default":
  211. filter = TF_DEFAULT
  212. elif options.filter == "linear":
  213. filter = TF_LINEAR
  214. elif options.filter == "nearest":
  215. filter = TF_NEAREST
  216. else:
  217. parser.error("Unrecognized type: " + options.filter)
  218. if not options.store_uncompressed \
  219. and not options.store_compressed:
  220. parser.error("One of --store-compressed and --store-uncompressed "\
  221. "should be True")
  222. if int(options.mips_count) <= 0:
  223. parser.error("Wrong number of mipmaps")
  224. config = Config()
  225. config.in_files = options.inp.split(":")
  226. config.out_file = options.out
  227. config.fast = options.fast
  228. config.type = typ
  229. config.normal = options.normal
  230. config.convert_path = options.convert_path
  231. config.no_alpha = options.no_alpha
  232. config.store_uncompressed = options.store_uncompressed
  233. config.store_compressed = options.store_compressed
  234. config.to_linear_rgb = options.to_linear_rgb
  235. config.filter = filter
  236. config.mips_count = int(options.mips_count)
  237. return config
  238. def identify_image(in_file):
  239. """ Return the size of the input image and the internal format """
  240. color_format = CF_NONE
  241. width = 0
  242. height = 0
  243. proc = subprocess.Popen(["identify", "-verbose" , in_file],
  244. stdout=subprocess.PIPE)
  245. stdout_str = proc.stdout.read()
  246. # Make sure the colorspace is what we want
  247. """reg = re.search(r"Colorspace: (.*)", stdout_str)
  248. if not reg or reg.group(1) != "RGB":
  249. raise Exception("Something is wrong with the colorspace")"""
  250. # Get the size of the iamge
  251. reg = re.search(r"Geometry: ([0-9]*)x([0-9]*)\+", stdout_str)
  252. if not reg:
  253. raise Exception("Cannot extract size")
  254. # Identify the color space
  255. """if not re.search(r"red: 8-bit", stdout_str) \
  256. or not re.search(r"green: 8-bit", stdout_str) \
  257. or not re.search(r"blue: 8-bit", stdout_str): \
  258. raise Exception("Incorrect channel depths")"""
  259. if re.search(r"alpha: 8-bit", stdout_str):
  260. color_format = CF_RGBA8
  261. color_format_str = "RGBA"
  262. else:
  263. color_format = CF_RGB8
  264. color_format_str = "RGB"
  265. # print some stuff and return
  266. printi("width: %s, height: %s color format: %s" % \
  267. (reg.group(1), reg.group(2), color_format_str))
  268. return (color_format, int(reg.group(1)), int(reg.group(2)))
  269. def create_mipmaps(in_file, tmp_dir, width_, height_, color_format, \
  270. to_linear_rgb, max_mip_count):
  271. """ Create a number of images for all mipmaps """
  272. printi("Generate mipmaps")
  273. width = width_
  274. height = height_
  275. mips_fnames = []
  276. while width >= 4 and height >= 4:
  277. size_str = "%dx%d" % (width, height)
  278. out_file_str = os.path.join(tmp_dir, get_base_fname(in_file)) \
  279. + "." + size_str
  280. printi(" %s.tga" % out_file_str)
  281. mips_fnames.append(out_file_str)
  282. args = ["convert", in_file]
  283. # to linear
  284. if to_linear_rgb:
  285. if color_format != CF_RGB8:
  286. raise Exception("to linear RGB only supported for RGB textures")
  287. args.append("-set")
  288. args.append("colorspace")
  289. args.append("sRGB")
  290. args.append("-colorspace")
  291. args.append("RGB")
  292. # resize
  293. args.append("-resize")
  294. args.append(size_str)
  295. # alpha
  296. args.append("-alpha")
  297. if color_format == CF_RGB8:
  298. args.append("deactivate")
  299. else:
  300. args.append("activate")
  301. args.append(out_file_str + ".tga")
  302. subprocess.check_call(args)
  303. if(len(mips_fnames) == max_mip_count):
  304. break;
  305. width = width / 2
  306. height = height / 2
  307. return mips_fnames
  308. def create_etc_images(mips_fnames, tmp_dir, fast, color_format, convert_path):
  309. """ Create the etc files """
  310. printi("Creating ETC images")
  311. # Copy the convert tool to the working dir so that etcpack will see it
  312. shutil.copy2(convert_path, \
  313. os.path.join(tmp_dir, os.path.basename(convert_path)))
  314. for fname in mips_fnames:
  315. # Unfortunately we need to flip the image. Use convert again
  316. in_fname = fname + ".tga"
  317. flipped_fname = fname + "_flip.tga"
  318. args = ["convert", in_fname, "-flip", flipped_fname]
  319. subprocess.check_call(args)
  320. in_fname = flipped_fname
  321. printi(" %s" % in_fname)
  322. args = ["etcpack", in_fname, tmp_dir, "-c", "etc2"]
  323. if fast:
  324. args.append("-s")
  325. args.append("fast")
  326. args.append("-f")
  327. if color_format == CF_RGB8:
  328. args.append("RGB")
  329. else:
  330. args.append("RGBA")
  331. # Call the executable AND change the working directory so that etcpack
  332. # will find convert
  333. subprocess.check_call(args, stdout = subprocess.PIPE, cwd = tmp_dir)
  334. def create_dds_images(mips_fnames, tmp_dir, fast, color_format, normal):
  335. """ Create the dds files """
  336. printi("Creating DDS images")
  337. for fname in mips_fnames:
  338. # Unfortunately we need to flip the image. Use convert again
  339. in_fname = fname + ".tga"
  340. flipped_fname = fname + "_flip.tga"
  341. args = ["convert", in_fname, "-flip", flipped_fname]
  342. subprocess.check_call(args)
  343. in_fname = flipped_fname
  344. # Continue
  345. out_fname = os.path.join(tmp_dir, os.path.basename(fname) + ".dds")
  346. printi(" %s" % out_fname)
  347. args = ["nvcompress", "-silent", "-nomips"]
  348. if fast:
  349. args.append("-fast")
  350. if color_format == CF_RGB8:
  351. if not normal:
  352. args.append("-bc1")
  353. else:
  354. args.append("-bc1n")
  355. elif color_format == CF_RGBA8:
  356. args.append("-alpha")
  357. if not normal:
  358. args.append("-bc3")
  359. else:
  360. args.append("-bc3n")
  361. args.append(in_fname)
  362. args.append(out_fname)
  363. subprocess.check_call(args, stdout = subprocess.PIPE)
  364. def write_raw(tex_file, fname, width, height, color_format):
  365. """ Append raw data to the AnKi texture file """
  366. printi(" Appending %s" % fname)
  367. # Read and check the header
  368. uncompressed_tga_header = struct.pack("BBBBBBBBBBBB", \
  369. 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0)
  370. in_file = open(fname, "rb")
  371. tga_header = in_file.read(12)
  372. if len(tga_header) != 12:
  373. raise Exception("Failed reading TGA header")
  374. if uncompressed_tga_header != tga_header:
  375. raise Exception("Incorrect TGA header")
  376. # Read the size and bpp
  377. header6_buff = in_file.read(6)
  378. if len(header6_buff) != 6:
  379. raise Exception("Failed reading TGA header #2")
  380. header6 = struct.unpack("BBBBBB", header6_buff)
  381. img_width = header6[1] * 256 + header6[0]
  382. img_height = header6[3] * 256 + header6[2]
  383. img_bpp = header6[4];
  384. if (color_format != CF_RGB8 or img_bpp != 24) \
  385. and (color_format != CF_RGBA8 or img_bpp != 32):
  386. raise Exception("Unexpected bpp")
  387. if img_width != width or img_height != height:
  388. raise Exception("Unexpected width or height")
  389. # Read the data
  390. data_size = width * height
  391. if color_format == CF_RGB8:
  392. data_size *= 3
  393. else:
  394. data_size *= 4
  395. data = bytearray(in_file.read(data_size))
  396. if len(data) != data_size:
  397. raise Exception("Failed to read all data")
  398. tmp = in_file.read(128)
  399. if len(tmp) != 0:
  400. printw(" File shouldn't contain more data")
  401. # Swap colors
  402. bpp = img_bpp / 8
  403. for i in xrange(0, data_size, bpp):
  404. temp = data[i];
  405. data[i] = data[i + 2];
  406. data[i + 2] = temp;
  407. # Write data to tex_file
  408. tex_file.write(data)
  409. def write_s3tc(out_file, fname, width, height, color_format):
  410. """ Append s3tc data to the AnKi texture file """
  411. # Read header
  412. printi(" Appending %s" % fname)
  413. in_file = open(fname, "rb")
  414. header = in_file.read(DdsHeader.get_size())
  415. if len(header) != DdsHeader.get_size():
  416. raise Exception("Failed to read DDS header")
  417. dds_header = DdsHeader(header)
  418. if dds_header.dwWidth != width or dds_header.dwHeight != height:
  419. raise Exception("Incorrect width")
  420. if color_format == CF_RGB8 \
  421. and dds_header.dwFourCC != "DXT1":
  422. raise Exception("Incorrect format. Expecting DXT1")
  423. if color_format == CF_RGBA8 \
  424. and dds_header.dwFourCC != "DXT5":
  425. raise Exception("Incorrect format. Expecting DXT5")
  426. # Read and write the data
  427. if color_format == CF_RGB8:
  428. block_size = 8
  429. else:
  430. block_size = 16
  431. data_size = (width / 4) * (height / 4) * block_size
  432. data = in_file.read(data_size)
  433. if len(data) != data_size:
  434. raise Exception("Failed to read DDS data")
  435. # Make sure that the file doesn't contain any more data
  436. tmp = in_file.read(1)
  437. if len(tmp) != 0:
  438. printw(" File shouldn't contain more data")
  439. out_file.write(data)
  440. def write_etc(out_file, fname, width, height, color_format):
  441. """ Append etc2 data to the AnKi texture file """
  442. printi(" Appending %s" % fname)
  443. # Read header
  444. in_file = open(fname, "rb")
  445. header = in_file.read(PkmHeader.get_size())
  446. if len(header) != PkmHeader.get_size():
  447. raise Exception("Failed to read PKM header")
  448. pkm_header = PkmHeader(header)
  449. if pkm_header.magic != "PKM 20":
  450. raise Exception("Incorrect PKM header")
  451. if width != pkm_header.width or height != pkm_header.height:
  452. raise Exception("Incorrect PKM width or height")
  453. # Read and write the data
  454. data_size = (pkm_header.width / 4) * (pkm_header.height / 4) * 8
  455. data = in_file.read(data_size)
  456. if len(data) != data_size:
  457. raise Exception("Failed to read PKM data")
  458. # Make sure that the file doesn't contain any more data
  459. tmp = in_file.read(1)
  460. if len(tmp) != 0:
  461. printw(" File shouldn't contain more data")
  462. out_file.write(data)
  463. def convert(config):
  464. """ This is the function that does all the work """
  465. # Invoke app named "identify" to get internal format and width and height
  466. (color_format, width, height) = identify_image(config.in_files[0])
  467. if not is_power2(width) or not is_power2(height):
  468. raise Exception("Image width and height should power of 2")
  469. if color_format == CF_RGBA8 and config.normal:
  470. raise Exception("RGBA image and normal does not make much sense")
  471. for i in range(1, len(config.in_files)):
  472. (color_format_2, width_2, height_2) = identify_image(config.in_files[i])
  473. if width != width_2 or height != height_2 \
  474. or color_format != color_format_2:
  475. raise Exception("Images are not same size and color space")
  476. if config.no_alpha:
  477. color_format = CF_RGB8
  478. # Create images
  479. for in_file in config.in_files:
  480. mips_fnames = create_mipmaps(in_file, config.tmp_dir, width, height, \
  481. color_format, config.to_linear_rgb, config.mips_count)
  482. # Create etc images
  483. create_etc_images(mips_fnames, config.tmp_dir, config.fast, \
  484. color_format, config.convert_path)
  485. # Create dds images
  486. create_dds_images(mips_fnames, config.tmp_dir, config.fast, \
  487. color_format, config.normal)
  488. # Open file
  489. fname = config.out_file
  490. printi("Writing %s" % fname)
  491. tex_file = open(fname, "wb")
  492. # Write header
  493. ak_format = "8sIIIIIIII"
  494. data_compression = 0
  495. if config.store_compressed:
  496. data_compression = data_compression | DC_S3TC | DC_ETC2
  497. if config.store_uncompressed:
  498. data_compression = data_compression | DC_RAW
  499. buff = struct.pack(ak_format,
  500. b"ANKITEX1",
  501. width,
  502. height,
  503. len(config.in_files),
  504. config.type,
  505. color_format,
  506. data_compression,
  507. config.normal,
  508. len(mips_fnames))
  509. tex_file.write(buff)
  510. # Write header padding
  511. header_padding_size = 128 - struct.calcsize(ak_format)
  512. if header_padding_size != 88:
  513. raise Exception("Check the header")
  514. for i in range(0, header_padding_size):
  515. tex_file.write('\0')
  516. # For each compression
  517. for compression in range(0, 3):
  518. tmp_width = width
  519. tmp_height = height
  520. # For each level
  521. for i in range(0, len(mips_fnames)):
  522. # For each image
  523. for in_file in config.in_files:
  524. size_str = "%dx%d" % (tmp_width, tmp_height)
  525. in_base_fname = os.path.join(config.tmp_dir, \
  526. get_base_fname(in_file)) + "." + size_str
  527. # Write RAW
  528. if compression == 0 and config.store_uncompressed:
  529. write_raw(tex_file, in_base_fname + ".tga", \
  530. tmp_width, tmp_height, color_format)
  531. # Write S3TC
  532. elif compression == 1 and config.store_compressed:
  533. write_s3tc(tex_file, in_base_fname + ".dds", \
  534. tmp_width, tmp_height, color_format)
  535. # Write ETC
  536. elif compression == 2 and config.store_compressed:
  537. write_etc(tex_file, in_base_fname + "_flip.pkm", \
  538. tmp_width, tmp_height, color_format)
  539. tmp_width = tmp_width / 2
  540. tmp_height = tmp_height / 2
  541. def main():
  542. """ The main """
  543. # Parse cmd line args
  544. config = parse_commandline();
  545. if config.type == TT_CUBE and len(config.in_files) != 6:
  546. raise Exception("Not enough images for cube generation")
  547. if (config.type == TT_3D or config.type == TT_2D_ARRAY) \
  548. and len(config.in_files) < 2:
  549. #raise Exception("Not enough images for 2DArray/3D texture")
  550. printw("Not enough images for 2DArray/3D texture")
  551. if config.type == TT_2D and len(config.in_files) != 1:
  552. raise Exception("Only one image for 2D textures needed")
  553. if not os.path.isfile(config.convert_path):
  554. raise Exception("Tool convert not found: " + config.convert_path)
  555. # Setup the temp dir
  556. config.tmp_dir = tempfile.mkdtemp("_ankitex")
  557. # Do the work
  558. try:
  559. convert(config)
  560. finally:
  561. shutil.rmtree(config.tmp_dir)
  562. # Done
  563. printi("Done!")
  564. if __name__ == "__main__":
  565. main()