370 lines
13 KiB
Python
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()
|