Skip to content

Instantly share code, notes, and snippets.

@luckydonald
Last active January 18, 2022 19:21
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 luckydonald/54c37785bdefb14d0b1f63424fbd3b35 to your computer and use it in GitHub Desktop.
Save luckydonald/54c37785bdefb14d0b1f63424fbd3b35 to your computer and use it in GitHub Desktop.
A script to scale images to a monitor resulution and add the best fitting background color
"""
Makes border aware sizing of desktop wallpapers.
You need to install:
$ pip install Pillow easygui joblib
"""
import glob
import os
import itertools
from PIL import Image
def mkdir_p(path):
"""
like mkdir -p
Creates a folder with all the missing parent folders.
"""
import errno
try:
os.makedirs(path)
except OSError as exc: # Python >2.5
if exc.errno == errno.EEXIST and os.path.isdir(path):
pass
else:
raise
# end try
# end if
# end try
# end def
class Borders(object):
def __init__(self, border_top=0, border_left=0, border_right=0, border_bottom=0):
self.border_top = border_top
self.border_left = border_left
self.border_right = border_right
self.border_bottom = border_bottom
# end def __init__
def __str__(self):
return f't{self.border_top}-l{self.border_left}-r{self.border_right}-b{self.border_bottom}'
# end def
# end class
class Converter(object):
def __init__(self, width, height, hard_border=Borders(), soft_border=Borders(), align_horizontally='center', align_vertically='center'):
self.width = width
self.height = height
self.hard_border = hard_border
self.soft_border = soft_border
self.align_horizontally = align_horizontally
self.align_vertically = align_vertically
# end def
def __str__(self):
return f'{s.converter.width}x{s.converter.height}.h-{self.hard_border}.s-{self.soft_border}.h-{self.align_horizontally}.v-{self.align_vertically}'
# end def
def convert(self, input, output, overwrite_existing=False):
if os.path.exists(output) and not overwrite_existing:
print("Skipped: already existing")
return
# end if
img = Image.open(input)
assert isinstance(img, Image.Image)
copy = img.copy()
assert isinstance(copy, Image.Image)
color = most_frequent_color(copy, 3)
# prepare to scale down if needed.
hard_border_width = self.width-self.hard_border.border_left-self.hard_border.border_right
hard_border_height = self.height-self.hard_border.border_top-self.hard_border.border_bottom
soft_border_width = self.width-max(self.hard_border.border_left, self.soft_border.border_left)-max(self.hard_border.border_right, self.soft_border.border_right)
soft_border_height = self.height-max(self.hard_border.border_top, self.soft_border.border_top)-max(self.hard_border.border_bottom, self.soft_border.border_bottom)
# scales down if needed.
copy.thumbnail((hard_border_width, hard_border_height))
# calculates if we need to act because of the soft border
# check if the width is still bigger as what the soft border requires
if copy.width >= self.width - self.soft_border.border_right-self.soft_border.border_left:
if self.align_horizontally == 'left':
pos_w = max(self.hard_border.border_left, self.soft_border.border_left)
elif self.align_horizontally == 'right':
pos_w = soft_border_width - copy.width
else: # center
pos_w = (soft_border_width/2) - (copy.width/2) + max(self.hard_border.border_left, self.soft_border.border_left)
# end if
else:
if self.align_horizontally == 'left':
pos_w = self.hard_border.border_left
elif self.align_horizontally == 'right':
pos_w = hard_border_width - copy.width
else: # center
pos_w = (hard_border_width/2) - (copy.width/2) + self.hard_border.border_left
# end if
# end if
# check if the height is still bigger as what the soft border requires
if copy.height >= self.height - self.soft_border.border_top - self.soft_border.border_bottom:
if self.align_vertically == 'top':
pos_h = max(self.hard_border.border_top, self.soft_border.border_top)
elif self.align_vertically == 'bottom':
pos_h = soft_border_height - copy.height
else: # center
pos_h = (soft_border_height/2) - (copy.height/2) + max(self.hard_border.border_top, self.soft_border.border_top)
# end if
else:
if self.align_vertically == 'top':
pos_h = self.hard_border.border_top
elif self.align_vertically == 'bottom':
pos_h = hard_border_height - copy.height
else: # center
pos_h = (hard_border_height/2) - (copy.height/2) + self.hard_border.border_top
# end if
# end if
# new image to set the background color with
out = Image.new("RGB", (self.width, self.height), color[0][1])
# past the scaled down one on top of it
out.paste(copy, (int(pos_w), int(pos_h)))
# write it to the disk
out.save(output, "PNG")
# end def
# end class
class Settings(object):
def __init__(self, converter, overwrite_existing=None):
self.converter = converter
self.overwrite_existing = overwrite_existing
# end def
# end class
def rgb_to_array(r, g, b):
return {"r": r, "g": g, "b": b}
# end def
def color_result_to_array(color):
return rgb_to_array(color[1][0],color[1][1],color[1][2])
# end def
def most_frequent_color(image, colors=10):
# image2 = image.convert("P", palette=Image.ADAPTIVE, colors=colors)
# image3 = image2.convert(image.mode)
# del image2
border = 3
image3 = image.convert(image.mode)
assert isinstance(image3, Image.Image)
w, h = image3.size
border_historgram = {}
for i in range(0, border):
for x in range(0+i, w-i):
add_to_histogram(border_historgram, image3, x, 0+i)
add_to_histogram(border_historgram, image3, x, h-1-i)
# end for
for y in range(1+i, h-1-i): # omit the pixels already added.
add_to_histogram(border_historgram, image3, 0+i, y)
add_to_histogram(border_historgram, image3, w-1-i, y)
# end for
# end for
most_frequent_pixels = None
for color, count in border_historgram.items():
if most_frequent_pixels is None:
most_frequent_pixels = []
most_frequent_pixels.append((count, color),)
length = len(most_frequent_pixels)
for i in range(0, length):
if color == most_frequent_pixels[i][1]:
break
if count > most_frequent_pixels[i][0]:
most_frequent_pixels.insert(i, (count, color))
break
elif count < most_frequent_pixels[length - 1][0]:
most_frequent_pixels.append((count, color))
break
del image3
return most_frequent_pixels[:colors]
# end def
def add_to_histogram(border_historgram, image, x, y):
pixel = image.getpixel((x, y))
if pixel in border_historgram:
border_historgram[pixel] += 1
else:
border_historgram[pixel] = 1
class FileInfo(object):
def __init__(self, infile, file_out_dir, converter: Converter):
assert isinstance(c, Converter)
self.converter = converter
self.file_out_dir = os.path.abspath(file_out_dir)
self.infile = os.path.abspath(infile)
self.file, self.ext = os.path.splitext(infile)
self.folder, self.name = os.path.split(self.file)
self.outfile = os.path.join(file_out_dir, "{width}x{height}.{name}.png".format(
width=converter.width, height=converter.height, name=self.name
))
# end def
def output_does_exist(self):
return os.path.exists(self.outfile)
# end class
def process_file(file_info: FileInfo, overwrite_existing=False):
assert isinstance(file_info, FileInfo)
assert isinstance(file_info.converter, Converter)
print("{folder} {name} {ext} > {out}".format(folder=file_info.folder, name=file_info.name, ext=file_info.ext, out=file_info.outfile))
try:
file_info.converter.convert(file_info.infile, file_info.outfile, overwrite_existing=overwrite_existing)
except Exception as e:
print(e)
raise e
# end try
# end def
if __name__ == '__main__':
# c = Converter(1920, 1080, hard_border=Borders(border_top=22)) # mac, HD+
# c = Converter(1280, 800, hard_border=Borders(border_top=22)) # mac
# c = Converter(1920, 1080, soft_border=Borders(border_bottom=40)) #, border_right=180)) # pc
import easygui
from joblib import Parallel, delayed, cpu_count
py_code = """
Settings(
Converter(
1280, 800,
# hard border is for stuff which should always move the center around
hard_border=Borders(
border_top=0,
border_left=0,
border_right=0,
border_bottom=0
),
# soft border is for stuff which should only move the center around if it's actually overlapping the border
# in other words this will make sure that no part of the image appears hidden behind a menu bar, but in any other case it would center the image as if there's no menu bar.
soft_border=Borders(
border_top=22,
border_left=0,
border_right=0,
border_bottom=0
),
align_horizontally='center', # left|center|right
align_vertically='center', # top|center|bottom
),
overwrite_existing = False
)
""".strip()
print("displaying settings dialog")
py_code = easygui.codebox("Converter object creation", title="Converter settings", text=py_code).strip()
s = eval(py_code)
assert isinstance(s, Settings)
if s.overwrite_existing is None:
YES = "Yes, replace"
NO = "No, skip"
print("displaying dialog asking about overwriting")
overwrite = easygui.buttonbox(
msg="Overwrite existing files?", title="Overwrite?", choices=[YES, NO],
default_choice=YES, cancel_choice=NO
)
s.overwrite_existing = (overwrite == YES)
# end if
c = s.converter
assert isinstance(c, Converter)
print("displaying input folder dialog")
# file_dir = easygui.diropenbox("Input Files Folder", "Input Files")
file_dir = '../../good/'
if file_dir is None:
print("No input folder given. Exiting.")
exit(-1)
# end if
print("displaying output folder dialog")
# file_out_dir = easygui.diropenbox("out")
file_out_dir = f'../../good.{s.converter!s}/'
if file_out_dir is None:
print("No output folder given. Exiting.")
exit(-1)
# end if
mkdir_p(file_out_dir)
files = sorted(itertools.chain(
glob.glob(os.path.join(file_dir, "*.jpg")), # JPEG
glob.glob(os.path.join(file_dir, "*.jpeg")), # JPEG
glob.glob(os.path.join(file_dir, "*.jpe")), # JPEG
glob.glob(os.path.join(file_dir, "*.png")), # PNG
))
files = itertools.chain(glob.glob(os.path.join(file_dir, "*.jpg")), glob.glob(os.path.join(file_dir, "*.png")))
try:
cores = cpu_count()
print("Found {n} cpu core{plural_s}.".format(n=cores, plural_s="s" if cores != 1 else ""))
except NotImplementedError:
cores = 4 # did you just assume my cpu count?
print("Could not determine the cpu core count, assuming {n}.".format(n=cores))
# end if
files = (FileInfo(infile, file_out_dir, c) for infile in files)
if not s.overwrite_existing: # so we should not overwrite
# remove the files from the list which already exist.
files = (f_i for f_i in files if not f_i.output_does_exist())
# end if
Parallel(n_jobs=cores)(delayed(process_file)(f_i, file_out_dir) for f_i in files)
# same as
# for infile in files:
# process_file(c, infile, file_out_dir)
# end for
# end __main__
@luckydonald
Copy link
Author

luckydonald commented Oct 5, 2016

Just run it. Is interactive. $ python3 image_sizer.py

You need to install:

$ pip3 install Pillow easygui joblib

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment