Skip to content

Instantly share code, notes, and snippets.

@RavuAlHemio
Created October 19, 2014 15:42
Show Gist options
  • Save RavuAlHemio/56160b9db87233256558 to your computer and use it in GitHub Desktop.
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.)
#!/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