convert_image.py 18 KB

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