Skip to content

Instantly share code, notes, and snippets.

@mountainstorm
Created December 24, 2013 11:44
Show Gist options
  • Save mountainstorm/8112146 to your computer and use it in GitHub Desktop.
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')
#!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