#!/usr/bin/env python """ Create image grid """ import os import argparse import math import logging from pathlib import Path import filetype from PIL import Image, ImageDraw, ImageFont from util import log params = None def wrap(text: str, font: ImageFont.ImageFont, length: int): lines = [''] for word in text.split(): line = f'{lines[-1]} {word}'.strip() if font.getlength(line) <= length: lines[-1] = line else: lines.append(word) return '\n'.join(lines) def grid(images, labels = None, width = 0, height = 0, border = 0, square = False, horizontal = False, vertical = False): # pylint: disable=redefined-outer-name if horizontal: rows = 1 elif vertical: rows = len(images) elif square: rows = round(math.sqrt(len(images))) else: rows = math.floor(math.sqrt(len(images))) cols = math.ceil(len(images) / rows) size = [0, 0] if width == 0: w = max([i.size[0] for i in images]) size[0] = cols * w + cols * border else: size[0] = width w = round(width / cols) if height == 0: h = max([i.size[1] for i in images]) size[1] = rows * h + rows * border else: size[1] = height h = round(height / rows) size = tuple(size) image = Image.new('RGB', size = size, color = 'black') # pylint: disable=redefined-outer-name font = ImageFont.truetype('DejaVuSansMono', round(w / 40)) for i, img in enumerate(images): # pylint: disable=redefined-outer-name x = (i % cols * w) + (i % cols * border) y = (i // cols * h) + (i // cols * border) img.thumbnail((w, h), Image.Resampling.HAMMING) image.paste(img, box=(x + int(border / 2), y + int(border / 2))) if labels is not None and len(images) == len(labels): ctx = ImageDraw.Draw(image) label = wrap(labels[i], font, w) ctx.text((x + 1 + round(w / 200), y + 1 + round(w / 200)), label, font = font, fill = (0, 0, 0)) ctx.text((x, y), label, font = font, fill = (255, 255, 255)) log.info({ 'grid': { 'images': len(images), 'rows': rows, 'cols': cols, 'cell': [w, h] } }) return image if __name__ == '__main__': log.info({ 'create grid' }) parser = argparse.ArgumentParser(description='image grid utility') parser.add_argument("--square", default = False, action='store_true', help = "create square grid") parser.add_argument("--horizontal", default = False, action='store_true', help = "create horizontal grid") parser.add_argument("--vertical", default = False, action='store_true', help = "create vertical grid") parser.add_argument("--width", type = int, default = 0, required = False, help = "fixed grid width") parser.add_argument("--height", type = int, default = 0, required = False, help = "fixed grid height") parser.add_argument("--border", type = int, default = 0, required = False, help = "image border") parser.add_argument('--nolabels', default = False, action='store_true', help = "do not print image labels") parser.add_argument('--debug', default = False, action='store_true', help = "print extra debug information") parser.add_argument('output', type = str) parser.add_argument('input', type = str, nargs = '*') params = parser.parse_args() output = params.output if params.output.lower().endswith('.jpg') else params.output + '.jpg' if params.debug: log.setLevel(logging.DEBUG) log.debug({ 'debug': True }) log.debug({ 'args': params.__dict__ }) images = [] labels = [] for f in params.input: path = Path(f) if path.is_dir(): files = [os.path.join(f, file) for file in os.listdir(f) if os.path.isfile(os.path.join(f, file))] elif path.is_file(): files = [f] else: log.warning({ 'grid not a valid file/folder', f}) continue files.sort() for file in files: if not filetype.is_image(file): continue if file.lower().endswith('.heic'): from pi_heif import register_heif_opener register_heif_opener() log.debug(file) img = Image.open(file) # img.verify() images.append(img) fp = Path(file) if not params.nolabels: labels.append(fp.stem) # log.info({ 'folder': path.parent, 'labels': labels }) if len(images) > 0: image = grid( images = images, labels = labels, width = params.width, height = params.height, border = params.border, square = params.square, horizontal = params.horizontal, vertical = params.vertical) image.save(output, 'JPEG', optimize = True, quality = 60) log.info({ 'grid': { 'file': output, 'size': list(image.size) } }) else: log.info({ 'grid': 'nothing to do' })