''' Transpose an animation from type="vertical_frames" to type="sheet_2d" Usage: recompose-anim Options: path The image file. aspect_w The width of each frame in the multi-frame file. aspect_h The height of each frame in the multi-frame file. frames_w The number of frames across the new image should be. frames_h The number of frames down the new image should be. length Total length of the animation in seconds (which is the necessary argument when type="vertical_frames"). This value is only required since this program shows Lua in standard output: The value will be automatically converted to a frame_length (single-frame time in seconds) argument for the node which is the argument necessary when type="sheet_2d". Examples: # [Mod] Mom and Pop Furniture [mapop]: recompose-anim mp_channel_rainbow.png 64 48 8 8 2 recompose-anim mp_channel_cube.png 40 40 5 5 3 recompose-anim mp_channel_blast.png 64 64 8 8 5 # ^ 5 5 is optional, but ideal since 1200/48 is 25 as square textures # are efficient (but power of 2 textures such as 512x512 are better # and more efficient on older video cards such as on old iPhones) ''' from __future__ import print_function import sys import os from collections import OrderedDict from PIL import Image def echo0(*args, **kwargs): dst = sys.stderr if 'file' in kwargs: dst = kwargs['file'] del kwargs['file'] print(*args, file=dst, **kwargs) def usage(): echo0(__doc__) echo0() option_metas = [ { 'name': "path", }, { 'name': "aspect_w", 'type': "int", }, { 'name': "aspect_h", 'type': "int", }, { 'name': "frames_w", 'type': "int", }, { 'name': "frames_h", 'type': "int", }, { 'name': "length", 'type': "int", }, ] class Framer: def __init__(self): self.frames = [] def load_vertical_strip(self, image, aspect_w, aspect_h): results = OrderedDict() results['-- in_count'] = 0 # results['-- out_count'] = 0 # set by save methods instead. w, h = image.size inverse_ar = float(aspect_h) / float(aspect_w) # ^ inverse aspect ratio frame_h = int(round(inverse_ar*w)) if h % frame_h != 0: raise ValueError( "Height {} of image is not evenly divisible by the" " calculated frame_height {}--calculated using" " frame_height = (aspect_h/aspect_w)*image_w = ({}/{})*{}" "".format(h, frame_h, aspect_h, aspect_w, w) ) top = 0 left = 0 right = w # exclusive bottom = top + frame_h # exclusive while top < h: frame = image.crop((left, top, right, bottom)) results['-- in_count'] += 1 self.frames.append(frame) # results['-- out_count'] += 1 top += frame_h bottom += frame_h return results def save_sheet_2d(self, path, frames_w, frames_h): results = OrderedDict() if not self.frames: raise RuntimeError("You must load first.") frame_w, frame_h = self.frames[0].size w = frame_w * frames_w h = frame_h * frames_h image = Image.new('RGBA', (w, h), 0) top = 0 index = 0 dest_count = frames_w * frames_h time_scale = float(dest_count) / float(len(self.frames)) if time_scale > 1.0: time_scale = 1.0 # just leave some blank inv_time_scale = 1.0 / time_scale last_dest_frame = 0 for y_frame in range(frames_h): left = 0 for x_frame in range(frames_w): if index >= len(self.frames): # The animation is complete. break source_index = int(round(inv_time_scale * float(index))) echo0("{} saved as frame {}".format(source_index, index)) image.paste(self.frames[source_index], (left, top)) # ^ paste copies alpha (alpha_composite does overlay) last_dest_frame = index left += frame_w index += 1 top += frame_h results['-- out_count'] = last_dest_frame + 1 image.save(path) return results def recompose_anim(options): """Recompose a vertical strip animation Convert from Minetest animation type="vertical_frames" to type="sheet_2d" (transpose to horizontal then split into rows). Args: options (dict): Configure how to read and convert the animation. For all options, see docstring of transpose-anim Python file. Returns: dict: The values that should be used in the texture table (or any of the tiles tables) in the minetest.register_node call. Additional values are in comment notation: '-- in_count' (the detected input frame count) '-- out_count' (the generated output frame count) """ path = options.get('path') if path is None: raise ValueError("You must specify a path.") results = OrderedDict() if not os.path.isfile(path): raise FileNotFoundError(path) for option_meta in option_metas: name = option_meta['name'] value = options.get(name) if value is None: raise ValueError( "{} is required.".format(name) ) option_type_name = option_meta.get('type') if option_type_name: if type(value).__name__ != option_type_name: raise ValueError( "{} should be a(n) {} but got {} {}." "".format(name, option_type_name, type(value).__name__, repr(value)) ) framer = Framer() img = Image.open(options['path']) rgba = img.convert("RGBA") load_results = framer.load_vertical_strip( rgba, options['aspect_w'], options['aspect_h'], ) nameNoExt, dotExt = os.path.splitext(options['path']) results['name'] = nameNoExt+"_sheet_2d"+dotExt load_results.update(framer.save_sheet_2d( results['name'], options['frames_w'], options['frames_h'], )) # ^ update gets the new '-- out_count' results['type'] = "sheet_2d" results['frames_w'] = options['frames_w'] results['frames_h'] = options['frames_h'] results.update(load_results) # Add the frame count comment(s) results['frame_length'] = options['length'] / load_results['-- in_count'] # ^ It is the in_count not out_count, since 'length' is based on original time_scale = ( float(load_results['-- out_count']) / float(load_results['-- in_count']) ) inv_time_scale = 1.0 / time_scale results['frame_length'] = results['frame_length'] * inv_time_scale # ^ frame_length should be scaled by the output vs input # (inverse since time becomes longer if frame count became less) return results def main(): options = {} for i, option_meta in enumerate(option_metas): argi = i + 1 if len(sys.argv) <= argi: usage() sys.stderr.write("Error: You must specify all options. Missing {}" "".format(option_meta['name'])) missing_count = len(option_metas) - (len(sys.argv) - 1) if missing_count > 1: echo0(" (and the rest after it above).") else: echo0(".") echo0('- If the texture is used in the mod, you can find' ' potential value(s) for in the Lua file' ' (Use same values as used in register_node).' ''.format()) return 1 arg = sys.argv[argi] if option_meta.get('type') == "int": try: arg = int(arg) except ValueError: usage() echo0("Error: {} should be a number but got {}." "".format(option_meta['name'], arg)) return 1 options[option_meta['name']] = arg if not os.path.isfile(options['path']): # Avoid an exception & use CLI-style (pipe & filter) logic usage() echo0('Error: "{}" does not exist.'.format(options['path'])) return 1 results = recompose_anim(options) echo0("") echo0("-- Patching instructions (change the following arguments in") echo0(" the animation table which is in the Tile definition table):") echo0("-- Remove:") echo0('- type = "vertical_strip",') echo0('- aspect_w = {},'.format(options['aspect_w'])) echo0('- aspect_h = {},'.format(options['aspect_h'])) echo0('- length = {},'.format(options['length'])) frame_length = results['frame_length'] echo0("-- Add:") new_path = results['name'] del results['name'] for key, value in results.items(): if isinstance(value, str): print(' {} = "{}",'.format(key, value)) else: print(' {} = {},'.format(key, value)) print(' frame_length = {},'.format(frame_length)) print('-- Also (in Tile definition, but outside of animation table)') print('-- change texture name (or overwrite original "{}"' ''.format(os.path.basename(options['path']))) print('- & keep Lua same) with:') print('-- name="{}"'.format(new_path)) return 0