Created
December 24, 2013 11:44
-
-
Save mountainstorm/8112146 to your computer and use it in GitHub Desktop.
Simple script which takes a selection of files and creates a Starling 2D texture texture & atlas; cropping any redundant border (transparent bits). Requires Pillow (new PIL - 'easy_install Pillow')
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!env python3 | |
# coding: utf-8 | |
# Copyright (c) 2013 Mountainstorm | |
# | |
# Permission is hereby granted, free of charge, to any person obtaining a copy | |
# of this software and associated documentation files (the "Software"), to deal | |
# in the Software without restriction, including without limitation the rights | |
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
# copies of the Software, and to permit persons to whom the Software is | |
# furnished to do so, subject to the following conditions: | |
# | |
# The above copyright notice and this permission notice shall be included in all | |
# copies or substantial portions of the Software. | |
# | |
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
# SOFTWARE. | |
import sys | |
import argparse | |
import os | |
import itertools | |
from PIL import Image | |
# max size allowed by Stage3D | |
MAX_TEXTURE_WIDTH = 2048 | |
MAX_TEXTURE_HEIGHT = 2048 | |
# These trim functions are optimised and abuse the histogram calculations as | |
# thats the fastest way I've found to do it. | |
# | |
# What we do is create a historgram for a full row/column of border color | |
# (where border color is the top, left color). We then crop the image and do | |
# histogrm matching | |
def calcTrimV(im): | |
u'''Calculates vertical space to trim from image; returns (top, bottom)''' | |
width, height = im.size | |
bdr = im.crop([0, 0, 1, 1]).histogram() | |
# create a histogram for a horizontal row of border | |
bdr = [i * width for i in bdr] | |
# check rows from top to bottom | |
top = height | |
for y in range(0, height): | |
row = im.crop([0, y, width, y+1]).histogram() | |
if row != bdr: | |
top = y | |
break | |
# now check from bottom to top | |
bottom = 0 | |
for y in range(height, top, -1): | |
row = im.crop([0, y-1, width, y]).histogram() | |
if row != bdr: | |
bottom = height - y | |
break | |
return (top, bottom) | |
def calcTrimH(im): | |
u'''Calculates horizontal space to trim from image; returns (left, right)''' | |
width, height = im.size | |
bdr = im.crop([0, 0, 1, 1]).histogram() | |
# create a histogram for a vertical column of border | |
bdr = [i * height for i in bdr] | |
# check columns from left to right | |
left = width | |
for x in range(0, width): | |
row = im.crop([x, 0, x+1, height]).histogram() | |
if row != bdr: | |
left = x | |
break | |
# now check from right to left | |
right = 0 | |
for x in range(width, left, -1): | |
row = im.crop([x-1, 0, x, height]).histogram() | |
if row != bdr: | |
right = width - x | |
break | |
return (left, right) | |
def trimImage(im): | |
u'''Trim border from image; returns ((left, top, right, bottom), croppedimg)''' | |
width, height = im.size | |
# triming before calculating left/right saves 10ms | |
top, bottom = calcTrimV(im) | |
left, right = calcTrimH(im.crop([0, top, width, height-bottom])) | |
box = [left, top, width-right, height-bottom] | |
#print(top, bottom, left, right) | |
return (box, im.crop(box)) | |
def createAtlas(inputFiles, name, outputPath): | |
u'''Creates the texture, atlas and lays out the images; returns success''' | |
# the maximum image we can create is a 2048x2048 image (so Stage3D claims) | |
# we're going to make one massive image and paste into it as we go. Layout | |
# will be simple, left to right, then top to bottom; looping at 2048 wide | |
# simple - but quick | |
err = True | |
outputTex = Image.new(u'RGBA', (MAX_TEXTURE_WIDTH, MAX_TEXTURE_HEIGHT)) | |
outputAtlas = open(outputPath + u'.xml', u'wt') | |
outputAtlas.write(u'<?xml version="1.0" encoding="UTF-8"?>\n') | |
outputAtlas.write( | |
u'<TextureAtlas imagePath=\'%s.png\'>\n' % os.path.basename(outputPath) | |
) | |
x = 0 # current x to paste at | |
y = 0 # current y to paste at | |
rowHeight = 0 # max height of this row | |
fidx = 0 | |
for fname in inputFiles: | |
path = os.path.join(dname, fname) | |
print(u'Processing: ' + fname) | |
# trim image | |
im = Image.open(path) | |
box, img = trimImage(im) | |
width, height = img.size | |
# check if the trimmed version will fit on this row | |
if x + width > MAX_TEXTURE_WIDTH: | |
# move to next row | |
x = 0 | |
y += rowHeight | |
rowHeight = 0 | |
# check we have space to paste | |
if y + height > MAX_TEXTURE_HEIGHT: | |
print(u'resulting image is too large to be loaded') | |
err = False | |
break | |
# paste image and write out atlas info | |
outputTex.paste(img, (x, y)) | |
outputAtlas.write( | |
u''' <SubTexture name='%s_%04u' x='%u' y='%u' width='%u' height='%u' frameX='%u' frameY='%u' frameWidth='%u' frameHeight='%u'/>\n''' % ( | |
name, fidx, # name | |
x, y, # x, y | |
width, height, # width, height | |
# I don't really understand why these are negative | |
-box[0], -box[1], # frameX, frameY | |
im.size[0], im.size[1] # frameWidth, frameSize | |
) | |
) | |
# update x and row height | |
x += width | |
rowHeight = max(rowHeight, height) | |
fidx += 1 | |
outputAtlas.write(u'</TextureAtlas>\n') | |
outputAtlas.close() | |
width = MAX_TEXTURE_WIDTH | |
if y == 0: | |
width = x # still on first row | |
outputTex = outputTex.crop([0, 0, width, y + rowHeight]) | |
outputTex.save(outputPath + u'.png') | |
return err | |
x = 0 | |
frameIdx = 0 | |
for frame in frames: | |
origsize, box, img = frame | |
outputTex.paste(img, (x, 0)) | |
x += img.size[0] | |
frameIdx += 1 | |
outputAtlas.write(u'</TextureAtlas>\n') | |
outputAtlas.close() | |
outputTex.save(outputPath + u'.png') | |
if __name__ == u'__main__': | |
# setup the argument parser | |
parser = argparse.ArgumentParser(description=u'''Create a sprite atlas from | |
a collection of images; trimming any border they might have''') | |
parser.add_argument(u'input', help=u'''path stub for the images; all png's | |
in the same directory and which start with the same stub will be added')''') | |
parser.add_argument(u'name', help=u'''the name to give the texture (param to | |
getTextures)''') | |
parser.add_argument(u'output', help=u'''output path (excluding extension); a | |
.png and .xml file will be created''') | |
args = parser.parse_args() | |
# find all the input files and sort so they are in the right order | |
bname = os.path.basename(args.input) | |
dname = os.path.dirname(args.input) | |
inputFiles = os.listdir(dname) | |
for i in range(len(inputFiles), 0, -1): | |
ext = os.path.splitext(inputFiles[i-1])[1] | |
if inputFiles[i-1].find(bname) != 0 or ext != u'.png': | |
del inputFiles[i-1] | |
inputFiles = sorted(inputFiles) | |
# layout all the images, trimmed and create the atlas files | |
if not createAtlas(inputFiles, args.name, args.output): | |
sys.exit(1) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment