Skip to content

Instantly share code, notes, and snippets.

@floer32
Last active December 16, 2015 13:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save floer32/5442935 to your computer and use it in GitHub Desktop.
Save floer32/5442935 to your computer and use it in GitHub Desktop.
A little script I used to take all images in a folder and generate two sizes of it - one thumbnail size, one large-but-not-too-large. For use in an online image gallery. This is still raw and could certainly be refactored a bit, but it gets the job done. This script generated the images for the gallery on FloeringPlastering.com. DEPENDS ON Image…
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
# This script walks all images in current directory and all of its subdirectories
# (and so on). Two copies of each image are made, one of the size specified
# by --large and one of the size specified by --thumb. The copies are saved in
# a subdirectory named according to size, adjacent to the original file.
# For example, given the following structure:
#
# my_images/
# bar/
# foo/
# qux.jpg
# foo/
# bar.jpg
# emmaline.jpg
#
# After you enter 'prepareGallery --large 1024 --thumb 100', you'll have:
#
# my_images/
# 1024/
# emmaline.jpg
# 100/
# emmaline.jpg
# bar/
# foo/
# 1024/
# qux.jpg
# 100/
# qux.jpg
# qux.jpg
# foo/
# 1024/
# bar.jpg
# 100/
# bar.jpg
# bar.jpg
# emmaline.jpg
#
# After the script is done it prints a json index of the files. Just look at the
# output to see what it's like ... or for more explanation keep reading:
#
# - Each 'object' in the JSON will consist of a 'large' attribute and 'thumb' attribute,
# pointing to the relative path (from top directory) to the relevant file.
# - Objects are organized into categories, under the assumption that their parent
# directory is the name of the category. So in the example above you'll have
# something like:
# { 'foo' : ...,
# 'bar' : None,
# }
# where 'foo' contains images under ANY directory 'foo', regardless of
# how deep in the subdirectoreis this directory 'foo' is. In other words,
# this script isn't equipped to respect structures of arbitrary depth. It
# assumes instead that you have a folder of folders, where the subfolders
# are named categories of images.
# - You should be able to use that JSON file to iterate over all this stuff and make a gallery.
# CODE STYLE NOTES:
# - I disagree with the idea that every function should have a docstring;
# if the docstring would merely rephrase the function name, do not write it.
# Thus there are minimal docstrings in my Python code.
# - Anywhere code says 'size' should be considered a tuple of (x, y),
# in order to keep consistent with meaning of 'size' that the Wand API uses.
#
# PRO TIP:
# - To delete the images output by this ... if called with `--large 1024`, do
# find -name '*1024*' -or -name '*100*' | xargs trash-put # or 'rm'
import json
from optparse import OptionParser
import os
import sys
from wand.image import Image
parser = OptionParser()
parser.add_option(
"-l",
"--large",
dest="large_length",
default=1024,
help="size in pixels of longest side of shrunken images",
)
parser.add_option(
"-t",
"--thumb",
dest="thumb_length",
default=100,
help="size in pixels of each side of thumbnails",
)
parser.add_option(
"-i",
"--input",
dest="images_directory",
default=os.getcwd(),
help="directory of images to walk & resize; defaults to current directory",
)
parser.add_option(
"-o",
"--output",
dest="output",
help="write JSON of created images to FILE instead of stdout",
metavar="FILE"
)
parser.add_option(
"-v",
"--verbose",
action="store_true",
dest="verbose",
default=False,
help="print extra status messages to stdout"
)
(options, args) = parser.parse_args()
def print_in_place(string):
if options.verbose:
sys.stdout.write("\r" + string)
sys.stdout.flush()
def find_shrink_factor(original, maximum):
""" Given two lengths `original` and `maximum`, find the factor that
`original` would need to be multiplied by in order to be less than `maximum`.
"""
if original > maximum:
return float(maximum) / float(original)
else:
return 1.0
def find_shrink_factor_to_fit_in_square(original_size, width_of_square):
""" Given a two-tuple like (width, height) and the `width_of_square` that
the original item needs to fit within, return the factor both the width and
height would need to be multiplied by in order to fit the original item
inside the square. A single factor is used for both sides in order to retain
aspect ratio.
"""
factors = [find_shrink_factor(dimension, width_of_square) for dimension in original_size]
return factors[0] if factors[0] < factors[1] else factors[1]
def find_new_size_to_fit_into_square(original_size, width_of_square):
factor = find_shrink_factor_to_fit_in_square(original_size, width_of_square)
return int(factor * original_size[0]), int(factor * original_size[1])
def make_subdirectory_adjacent_to_file(full_path_to_file, name='idk'):
dirname = os.path.dirname(full_path_to_file)
destination_directory = os.path.join(dirname, str(name))
try:
os.mkdir(destination_directory)
except OSError:
pass # directory exists, and that's fine.
return destination_directory
def shrink_image_if_necessary(image, max_width_and_height, print_errors=True):
try:
new_size = find_new_size_to_fit_into_square(image.size, max_width_and_height)
if new_size != image.size:
image.resize(width=new_size[0], height=new_size[1])
except Exception as e:
if print_errors:
print(e)
finally:
return image
def find_factor_for_precrop_shrink(original_size, desired_length_of_shortest_side):
shortest_side_length = original_size[0] if original_size[0] < original_size[1] else original_size[1]
if shortest_side_length > desired_length_of_shortest_side:
return float(desired_length_of_shortest_side) / float(shortest_side_length)
else:
return 1.0
def find_new_size_for_precrop_shrink(original_size, desired_length_of_shortest_side):
factor = find_factor_for_precrop_shrink(original_size, desired_length_of_shortest_side)
width = int(factor * original_size[0])
height = int(factor * original_size[1])
width = width if width >= 100 else 100
height = height if height >= 100 else 100
return width, height
def precrop_shrink(image, thumb_length, print_errors=True):
try:
new_size = find_new_size_for_precrop_shrink(image.size, thumb_length)
if new_size != image.size:
image.resize(width=new_size[0], height=new_size[1])
except Exception as e:
if print_errors:
print(e)
finally:
return image # WARNING: You must close the returned image manually!
def thumbcrop_image(image, length_of_shortest_side, print_errors=True):
""" For thumbnails, we want to the image to be square in the end, but with
the same aspect ratio. One way to achieve that is to shrink the image such
that its shortest side is the same width as the thumbnail-to-be. Then you
crop it from there. """
length_of_shortest_side = int(length_of_shortest_side)
thumb = precrop_shrink(image, length_of_shortest_side, print_errors)
if thumb:
thumb.crop(width=length_of_shortest_side, height=length_of_shortest_side)
return thumb # WARNING: You must close the returned image manually!
def get_parent_dir_of_file(full_path_to_file):
return os.path.basename(os.path.dirname(full_path_to_file))
i = 0
top_level_categories = {}
for current_parent, subdirs, files in os.walk(options.images_directory):
for key in subdirs:
top_level_categories[key] = []
for basename_with_extension in files:
result = {}
full_path_to_file = os.path.join(current_parent, basename_with_extension)
with Image(filename=full_path_to_file) as working_image:
working_image = shrink_image_if_necessary(
working_image,
options.large_length,
print_errors=options.verbose
)
destination_directory = make_subdirectory_adjacent_to_file(
full_path_to_file,
name=options.large_length,
)
new_image_path = os.path.join(
destination_directory,
basename_with_extension
)
working_image.save(filename=new_image_path)
result['large'] = os.path.relpath(new_image_path)
print_in_place(" ".join(["#" + str(i) + "a", new_image_path]))
with Image(filename=full_path_to_file) as working_thumb_image:
working_thumb_image = thumbcrop_image(
working_thumb_image,
options.thumb_length,
print_errors=options.verbose
)
destination_directory_thumb = make_subdirectory_adjacent_to_file(
full_path_to_file,
name=options.thumb_length
)
new_thumb_path = os.path.join(
destination_directory_thumb,
basename_with_extension
)
working_thumb_image.save(filename=new_thumb_path)
result['thumb'] = os.path.relpath(new_thumb_path)
print_in_place(" ".join(["#" + str(i) + "b", new_thumb_path]))
if result:
parent_dir_of_file = get_parent_dir_of_file(full_path_to_file)
top_level_categories[str(parent_dir_of_file)].append(result)
i += 1
for key, value in top_level_categories.items():
top_level_categories[key] = sorted(top_level_categories[key])
json_index = json.dumps(top_level_categories, sort_keys=True, indent=4)
if not options.output:
print(json_index)
else:
with open(options.output, 'w') as f:
f.write(json_index)
if options.verbose:
print("\n")
print("Done.")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment