Created
October 19, 2014 15:42
-
-
Save RavuAlHemio/56160b9db87233256558 to your computer and use it in GitHub Desktop.
Renders a nine-patch image. (The border sizes are supplied numerically, not as pixels in the image's border as on Android.)
This file contains hidden or 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
#!/usr/bin/env python3 | |
# | |
# Renders a scaled nine-patch image. | |
# | |
# Released into the public domain. | |
# http://creativecommons.org/publicdomain/zero/1.0/ | |
# | |
from PIL import Image | |
class NinePatch: | |
def __init__(self, left, right, top, bottom): | |
if left < 0 or right < 0 or top < 0 or bottom < 0: | |
raise ValueError("dimensions must be at least zero") | |
self.left_border = left | |
self.right_border = right | |
self.top_border = top | |
self.bottom_border = bottom | |
self.full_image = None | |
self.top_left_image = None | |
self.top_image = None | |
self.top_right_image = None | |
self.left_image = None | |
self.center_image = None | |
self.right_image = None | |
self.bottom_left_image = None | |
self.bottom_image = None | |
self.bottom_right_image = None | |
self.scaled_image = None | |
def load(self, filename): | |
self.full_image = Image.open(filename) | |
def generate_patches(self): | |
if self.full_image is None: | |
raise ValueError("no image has been loaded yet") | |
if self.left_border + self.right_border >= self.full_image.size[0]: | |
raise ValueError("image too small to expand in horizontal direction") | |
if self.top_border + self.bottom_border >= self.full_image.size[1]: | |
raise ValueError("image too small to expand in vertical direction") | |
right_border_start = self.full_image.size[0] - self.right_border | |
bottom_border_start = self.full_image.size[1] - self.bottom_border | |
self.top_left_image = self.full_image.crop(( | |
0, | |
0, | |
self.left_border, | |
self.top_border | |
)) | |
self.top_image = self.full_image.crop(( | |
self.left_border, | |
0, | |
right_border_start, | |
self.top_border | |
)) | |
self.top_right_image = self.full_image.crop(( | |
right_border_start, | |
0, | |
self.full_image.size[0], | |
self.top_border | |
)) | |
self.left_image = self.full_image.crop(( | |
0, | |
self.top_border, | |
self.left_border, | |
bottom_border_start | |
)) | |
self.center_image = self.full_image.crop(( | |
self.left_border, | |
self.top_border, | |
right_border_start, | |
bottom_border_start | |
)) | |
self.right_image = self.full_image.crop(( | |
right_border_start, | |
self.top_border, | |
self.full_image.size[0], | |
bottom_border_start | |
)) | |
self.bottom_left_image = self.full_image.crop(( | |
0, | |
bottom_border_start, | |
self.left_border, | |
self.full_image.size[1] | |
)) | |
self.bottom_image = self.full_image.crop(( | |
self.left_border, | |
bottom_border_start, | |
right_border_start, | |
self.full_image.size[1] | |
)) | |
self.bottom_right_image = self.full_image.crop(( | |
right_border_start, | |
bottom_border_start, | |
self.full_image.size[0], | |
self.full_image.size[1] | |
)) | |
def write_patches(self, filename_format): | |
if self.top_left_image is not None: | |
self.top_left_image.save(filename_format.format("tl")) | |
if self.top_image is not None: | |
self.top_image.save(filename_format.format("t")) | |
if self.top_right_image is not None: | |
self.top_right_image.save(filename_format.format("tr")) | |
if self.left_image is not None: | |
self.left_image.save(filename_format.format("l")) | |
if self.center_image is not None: | |
self.center_image.save(filename_format.format("c")) | |
if self.right_image is not None: | |
self.right_image.save(filename_format.format("r")) | |
if self.bottom_left_image is not None: | |
self.bottom_left_image.save(filename_format.format("bl")) | |
if self.bottom_image is not None: | |
self.bottom_image.save(filename_format.format("b")) | |
if self.bottom_right_image is not None: | |
self.bottom_right_image.save(filename_format.format("br")) | |
def generate_scaled_image(self, width, height, scale_mode=Image.ANTIALIAS): | |
scaled_width = width - self.left_border - self.right_border | |
scaled_height = height - self.top_border - self.bottom_border | |
if scaled_width < 0: | |
raise ValueError("output image not wide enough for this nine-patch") | |
if scaled_height < 0: | |
raise ValueError("output image not high enough for this nine-patch") | |
self.scaled_image = Image.new( | |
self.full_image.mode, | |
(width, height), | |
(0, 0, 0, 0) | |
) | |
right_border_start = width - self.right_border | |
bottom_border_start = height - self.bottom_border | |
supply_mask = (self.full_image.mode == "RGBA") | |
# paste the corners | |
self.scaled_image.paste( | |
self.top_left_image, | |
(0, 0), | |
self.top_left_image if supply_mask else None | |
) | |
self.scaled_image.paste( | |
self.top_right_image, | |
(right_border_start, 0), | |
self.top_right_image if supply_mask else None | |
) | |
self.scaled_image.paste( | |
self.bottom_left_image, | |
(0, bottom_border_start), | |
self.bottom_left_image if supply_mask else None | |
) | |
self.scaled_image.paste( | |
self.bottom_right_image, | |
(right_border_start, bottom_border_start), | |
self.bottom_right_image if supply_mask else None | |
) | |
if scaled_width > 0: | |
# scale top and bottom horizontally | |
scaled_top = self.top_image.resize( | |
(scaled_width, self.top_border), | |
scale_mode | |
) | |
scaled_bottom = self.bottom_image.resize( | |
(scaled_width, self.bottom_border), | |
scale_mode | |
) | |
# paste them | |
self.scaled_image.paste( | |
scaled_top, | |
(self.left_border, 0), | |
scaled_top if supply_mask else None | |
) | |
self.scaled_image.paste( | |
scaled_bottom, | |
(self.left_border, bottom_border_start), | |
scaled_bottom if supply_mask else None | |
) | |
if scaled_height > 0: | |
# scale left and right vertically | |
scaled_left = self.left_image.resize( | |
(self.left_border, scaled_height), | |
scale_mode | |
) | |
scaled_right = self.right_image.resize( | |
(self.right_border, scaled_height), | |
scale_mode | |
) | |
# paste them | |
self.scaled_image.paste( | |
scaled_left, | |
(0, self.top_border), | |
scaled_left if supply_mask else None | |
) | |
self.scaled_image.paste( | |
scaled_right, | |
(right_border_start, self.top_border), | |
scaled_right if supply_mask else None | |
) | |
if scaled_width > 0 and scaled_height > 0: | |
scaled_center = self.center_image.resize( | |
(scaled_width, scaled_height), | |
scale_mode | |
) | |
self.scaled_image.paste( | |
scaled_center, | |
(self.left_border, self.top_border), | |
scaled_center if supply_mask else None | |
) | |
def save_scaled_image(self, filename): | |
if self.scaled_image is None: | |
raise ValueError("no scaled image has been calculated yet") | |
self.scaled_image.save(filename) | |
if __name__ == '__main__': | |
import argparse | |
parser = argparse.ArgumentParser(description="Scales a nine-patch image.") | |
for side in ("left", "right", "top", "bottom"): | |
parser.add_argument( | |
'-' + side[0], '--' + side, | |
metavar='PIXELS', | |
type=int, | |
help="The {0} unscaled border.".format(side), | |
required=True | |
) | |
for dim in ("width", "height"): | |
parser.add_argument( | |
'-' + dim[0].upper(), '--' + dim, | |
metavar='PIXELS', | |
type=int, | |
help="The {0} of the scaled image.".format(side), | |
required=True | |
) | |
parser.add_argument( | |
"input_image", | |
metavar="INFILE", | |
help="The image file to scale.", | |
nargs=1 | |
) | |
parser.add_argument( | |
"output_image", | |
metavar="OUTFILE", | |
help="The filename where the scaled image should be stored.", | |
nargs=1 | |
) | |
args = parser.parse_args() | |
nine_patch = NinePatch(args.left, args.right, args.top, args.bottom) | |
nine_patch.load(args.input_image[0]) | |
nine_patch.generate_patches() | |
nine_patch.generate_scaled_image(args.width, args.height) | |
nine_patch.save_scaled_image(args.output_image[0]) | |
# vim: ts=4 sw=4 et: |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment