warzone2100/tools/tile_texpage2mipmap_tileset/texpage2mipmap.py

370 lines
13 KiB
Python

#!/usr/bin/env python
""" Splits and scales tertile texpages into mipmaps.
Finds and filters files based on the 'extension' configuration setting, and
of those, groups where available to allow mipmap output to be created from
as few as one input texpage and as many texpages as there are output
resolutions.
Filenames are grouped together if, after all cased letters are lowered and
file extensions are stripped, they have identical names. if, after the
dotted is stripped they still have at least one period in the filename, and
only numeric digits proceed that period, then all text up to, but not
including the last remaining period is used in the comparison. Thus, the
following filenames are grouped together to represent different resolutions
of the same conceptual texpage:
tertilec1hw.tga
tertilec1hw.53.pcx
tertilec1hw.128.pcx
However, 'tertilec1hw.a23.pcx' will create a new 'tertilec1hw.a23' group.
Files are handled on a first-come basis, if two files that are considered
to be part of the same group also have the same resolution, the first one
found will be used, and all subsequent ones with the same resolution in
that group will be discarded. Since this behavior is unpredictable and
operating-system-dependent, it should not be relied upon, and the user
should take care to have have no more than one texpage of each resolution
per group.
"""
__version__ = "1.2"
__author__ = "Kevin Gillette"
# --------------------------------------------------------------------------
# texpage_convert.py v1.2 by Kevin Gillette (kage)
# --------------------------------------------------------------------------
# ***** BEGIN GPL LICENSE BLOCK *****
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# ***** END GPL LICENCE BLOCK *****
# --------------------------------------------------------------------------
import os, sys, shutil
from subprocess import Popen, PIPE
def ds_ppm_parser(file):
tokens = list()
gen = iter(file)
for line in gen:
comment = line.find('#')
if comment != -1:
line = line[:comment]
tokens.extend(line.split())
if len(tokens) >= 4: break
else:
raise RuntimeError('Invalid PPM file')
header = [tokens[0]] + map(int, tokens[1:4])
yield header
del tokens[:4]
magicnum, w, h, maxval = header
magicnum = magicnum.lower()
if magicnum == "p3":
while True:
while len(tokens) >= 3:
yield map(int, tokens[:3])
del tokens[:3]
try:
tokens.extend(gen.next().split())
except StopIteration:
break
elif magicnum == "p6":
if len(tokens) < 1:
tokens = list(gen)
if maxval < 256: chunksize = 1
else: chunksize = 2
raster, tokens = ''.join(tokens), list()
for i in xrange(0, len(raster), chunksize):
num = ord(raster[i])
if chunksize > 1: num = (num << 8) + ord(raster[i + 1])
tokens.append(num)
if len(tokens) >= 3:
yield tokens[:3]
del tokens[:3]
else:
raise RuntimeError('Parser can only handle P3 or P6 files')
def dump_to_radar(gen, outfile):
gen.next()
for rgb in gen:
rgb = [hex(int(c)).split('x')[-1].zfill(2) for c in rgb]
outfile.write(''.join(rgb) + '\n')
def handle_conf(iterable, config):
""" Simple configuration loader.
When provided with an iterable datatype, be it a file object (from
which it will extract one directive per line), or arguments from the
command line (each distinct "word" will represent a full directive),
each token will either be ignored (blank lines and comments), unset a
directive (nothing after the equal-sign), or set a directive.
Unsetting a directive will cause scripted defaults to be used, and not
setting a given directive will have the same effect.
"""
for text in iterable:
text = text.lstrip().rstrip('\n')
if text.startswith('#'): continue
parts = text.split('=', 1)
if len(parts) == 2:
parts[0] = parts[0].rstrip()
if parts[1]:
print 'setting "%s" to "%s"' % tuple(parts)
config[parts[0]] = parts[1]
else:
print 'defaulting "%s"' % parts[0]
del config[parts[0]]
def makedir(exportpath, dirname):
""" Given a basename, create a directory in exportpath
If the directory already exists, remove all png's contained therein.
If it exists but is not a directory, rename the file to something
similar but unused. If no exception is raised, then a directory by
the specified name will have been created and its absolute path will
have been returned as a string.
"""
dirpath = os.path.join(exportpath, dirname)
if os.path.exists(dirpath):
if os.path.isdir(dirpath):
print dirname, "already exists: removing contained png files"
for fn in os.listdir(dirpath):
if fn.lower().endswith('.png'): os.remove(os.path.join(dirpath, fn))
return dirpath
else:
count = 0
while os.path.exists("%s.%i" % (dirpath, count)):
count += 1
new_path = "%s.%i" % (dirpath, count)
shutil.move(dirpath, new_path)
print "error:", dirname, "is not a directory. moving file to", new_path
print "creating directory:", dirname
os.mkdir(dirpath)
return dirpath
def nearest_resolution(initial_index, arr):
""" Find the best resolution from which to scale.
initial_index - the position in the global variable 'resolutions' of
the desired output resolution.
arr - same length as 'resolutions' and contains only True and False
values. True represents a full-quality texpage for use in scaling,
while False represents resolutions for which scaling will be
needed to generate.
Resolutions greater than the desired one are always favored above
smaller resolutions, with priority being given to resolutions closer
to the desired output resolution.
"""
for i in range(initial_index + 1, len(arr)):
if arr[i]: return (i, True)
seq = range(0, initial_index)
seq.reverse()
for i in seq:
if arr[i]: return (i, False)
assert False, "should always have at least one input resolution"
def process():
is_windows = use_shell = os.name is 'nt' or sys.platform is 'win32'
conf = dict()
scriptloc = os.path.abspath(os.path.dirname(sys.argv[0]))
f = os.path.splitext(os.path.basename(sys.argv[0]))[0] + ".conf"
f = os.path.join(scriptloc, f)
if os.path.exists(f):
print "reading config data from", f
handle_conf(open(f, "rU"), conf)
print
print "scanning script arguments"
handle_conf(sys.argv[1:], conf)
print
log = conf.get('log')
if log:
print "using log:", log
sys.stdout = sys.stderr = open(log, 'wt')
del log
impath = conf.get('imagemagick-path', '')
identify_path = os.path.join(impath, 'identify')
convert_path = os.path.join(impath, 'convert')
exportpath = os.path.abspath(conf.get('export-path', '.'))
importpath = os.path.abspath(conf.get('import-path', '.'))
dir_contents = os.listdir(importpath)
extensions = conf.get('extensions', '.png .pcx .tga').lower().split()
try:
resolutions = set(map(int, conf.get('resolutions', '16 32 64 128').split()))
except ValueError:
sys.exit("error: the 'resolutions' directive in", f, "must contain only base-ten integers")
try:
columns = int(conf.get('columns', 9))
except ValueError:
sys.exit("error: the 'columns' directive in", f, "must contain only base-ten integers")
resolutions = list(resolutions)
resolutions.sort()
filter_increase = conf.get('filter-increase')
if filter_increase == 'default': filter_increase = None
filter_decrease = conf.get('filter-decrease')
if filter_decrease == 'default': filter_decrease = None
names = dict()
for f in dir_contents:
name, ext = os.path.splitext(f.lower())
if ext not in extensions: continue
fpath = os.path.join(importpath, f)
print "fpath:", fpath
pieces = name.rsplit('.', 1)
is_radar = False
if len(pieces) == 2:
if pieces[1].isdigit():
name = pieces[0]
if pieces[1] == 'radar':
is_radar = True
name = pieces[0]
args = [identify_path, '-format', '%w %h', fpath]
p = Popen(args, stdout=PIPE, stderr=PIPE, shell=use_shell)
o = p.communicate()
if p.returncode:
print "args for identify:"
print args
print "ignoring %s: %s" % (f, o[1])
continue
w, h = map(int, o[0].split())
if is_radar:
if w != columns:
print "ignoring %s: radar image is not", columns, "pixels wide." % f
continue
res = names.setdefault(name, [False] * (len(resolutions) + 1) + [h])
if res[-2]:
print "ignoring %s: radar image already found for %s" % (f, name)
continue
if res[-1] != h:
print "ignoring %s: group has %i tile rows. this has %i rows" % \
(f, res[-1], h)
continue
args = [convert_path, fpath, '-depth', '8', 'ppm:-']
p = Popen(args, stdout=PIPE, stderr=PIPE, shell=use_shell)
out = open(os.path.join(exportpath, name + '.radar'), 'wb')
dump_to_radar(ds_ppm_parser(p.stdout), out)
out.close()
if p.wait():
print "args for convert:"
print args
sys.exit("error while running convert on %s: %s" % \
(f, p.stderr.read()))
res[-2] = True
print "using %s to generate the radar file for %s" % (f, name)
continue
tilesize, extra = divmod(w, columns)
print f + ": tiles determined to be", tilesize, "pixels per dimension"
if tilesize not in resolutions:
print "ignoring %s: does not use one of the listed tile resolutions" % f
continue
fixed_w = tilesize * columns
rows, extra = divmod(h, tilesize)
fixed_h = tilesize * rows
if fixed_h != h or fixed_w != w:
print "ignoring %s: expected dimensions of %ix%i, but found %ix%i" % \
(f, fixed_w, fixed_h, w, h)
continue
index = resolutions.index(tilesize)
res = names.setdefault(name, [False] * (len(resolutions) + 1) + [rows])
if rows != res[-1]:
print "ignoring %s: group has %i tile rows. this has %i rows" % \
(f, res[-1], h)
continue
if res[index]:
print "ignoring %s: resolution already filled" % f
continue
dirpath = makedir(exportpath, "%s-%i" % (name, tilesize))
print "splitting tiles from", f, "into", dirpath, "at", tilesize, "resolution"
args = [convert_path, '-depth', '8']
args.extend(['-crop', '%ix%i' % (tilesize, tilesize)])
args.extend([fpath, os.path.join(dirpath, 'tile-%02d.png')])
p = Popen(args, stderr=PIPE, shell=use_shell)
o = p.communicate()
if p.returncode:
print "args for convert:"
print args
exit("error while running convert on %s: %s" % (f, o[1]))
res[index] = True
def filesortkey(name):
try:
return int(name[name.find('-') + 1:name.find('.')])
except ValueError:
return -1
for name, levels in names.iteritems():
for i, res in enumerate(resolutions):
if not True in levels[:-2]: continue
if levels[i]:
if levels[-2]: continue
out = open(os.path.join(exportpath, name + ".radar"), 'wb')
args = [convert_path, None, '-sample', '1x1!', '-depth', '8', 'ppm:-']
dirpath = os.path.join(exportpath, "%s-%i" % (name, res))
files = os.listdir(dirpath)
files.sort(key=filesortkey)
print "generating radar file from files in", dirpath
for f in files:
if not f.endswith('.png'): continue
args[1] = os.path.join(dirpath, f)
p = Popen(args, stdout=PIPE, stderr=PIPE, shell=use_shell)
dump_to_radar(ds_ppm_parser(p.stdout), out)
if p.wait():
print "args for convert:"
print args
sys.exit("error while running convert on %s: %s" % \
(f, p.stderr.read()))
out.close()
levels[-2] = True
continue
dirpath = makedir(exportpath, "%s-%i" % (name, res))
index, scale_down = nearest_resolution(i, levels[:-2])
input_res = resolutions[index]
input_dirpath = os.path.join(exportpath, "%s-%i" % (name, input_res))
print "resizing tiles from", input_dirpath, "to", dirpath
for f in os.listdir(input_dirpath):
if not f.endswith('.png'): continue
args = [convert_path, os.path.join(input_dirpath, f)]
if scale_down:
if filter_decrease:
args.extend(['-filter', filter_decrease])
elif filter_increase:
args.extend(['-filter', filter_increase])
args.extend(['-resize', "%ix%i!" % (res, res)])
args.append(os.path.join(dirpath, f))
p = Popen(args, stdout=PIPE, stderr=PIPE, shell=use_shell)
if p.wait():
print "args for convert:"
print args
sys.exit("error while running convert on %s: %s" % \
(f, p.stderr.read()))
if __name__ == '__main__':
process()