Last active
January 18, 2022 19:21
-
-
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
This file contains 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
""" | |
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__ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Just run it. Is interactive.
$ python3 image_sizer.py
You need to install: