# Copyright (C) 2014 Adrien Vergé # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import random from threading import Thread import time import PIL.Image import PIL.ImageDraw import PIL.ImageFile from photocollage.collage import Photo QUALITY_SKEL = 0 QUALITY_FAST = 1 QUALITY_BEST = 2 # Try to continue even if the input file is corrupted. # See issue at https://github.com/adrienverge/PhotoCollage/issues/65 PIL.ImageFile.LOAD_TRUNCATED_IMAGES = True class PIL_SUPPORTED_EXTS: """File extensions supported by PIL Compiled from: - http://pillow.readthedocs.org/en/2.3.0/handbook/image-file-formats.html - https://github.com/python-imaging/Pillow/blob/master/PIL/*ImagePlugin.py """ RW = { "BMP": ("bmp",), # "EPS": ("ps", "eps",), # doesn't seem to work "GIF": ("gif",), "IM": ("im",), "JPEG": ("jfif", "jpe", "jpg", "jpeg",), "MSP": ("msp",), "PCX": ("pcx",), "PNG": ("png",), "PPM": ("pbm", "pgm", "ppm",), "TGA": ("tga",), "TIFF": ("tif", "tiff",), "WebP": ("webp",), "XBM": ("xbm",), } RO = { "CUR": ("cur",), "DCX": ("dcx",), "FLI": ("fli", "flc",), "FPX": ("fpx",), "GBR": ("gbr",), "ICO": ("ico",), "IPTC/NAA": ("iim",), "PCD": ("pcd",), "PSD": ("psd",), "SGI": ("bw", "rgb", "rgba", "sgi",), "XPM": ("xpm",), } WO = { # "PALM": ("palm",), # doesn't seem to work # "PDF": ("pdf",), # doesn't seem to work } def random_color(): r = random.randrange(256) g = random.randrange(256) b = random.randrange(256) if r + g + b > 0.7 * 3 * 256: r -= 50 g -= 50 b -= 50 return (r, g, b) class BadPhoto(Exception): def __init__(self, photoname): self.photoname = photoname def build_photolist(filelist): ret = [] for name in filelist: try: img = PIL.Image.open(name) except OSError: raise BadPhoto(name) w, h = img.size orientation = 0 try: exif = img._getexif() if 274 in exif: # orientation tag orientation = exif[274] if orientation == 6 or orientation == 8: w, h = h, w except Exception: pass ret.append(Photo(name, w, h, orientation)) return ret cache = {} class RenderingTask(Thread): """Execution thread to do the actual poster rendering Image computation is a heavy task, that can take several seconds. During this, the program might be unresponding. To avoid this, rendering is done is a separated thread. """ def __init__(self, page, border_width=0.01, border_color=(0, 0, 0), quality=QUALITY_FAST, output_file=None, on_update=None, on_complete=None, on_fail=None): super().__init__() self.page = page self.border_width = border_width self.border_color = border_color self.quality = quality self.output_file = output_file self.on_update = on_update self.on_complete = on_complete self.on_fail = on_fail self.canceled = False def abort(self): self.canceled = True def draw_skeleton(self, canvas): for col in self.page.cols: for c in col.cells: if c.is_extension(): continue color = random_color() x, y, w, h = c.content_coords() xy = (x, y) xY = (x, y + h - 1) Xy = (x + w - 1, y) XY = (x + w - 1, y + h - 1) draw = PIL.ImageDraw.Draw(canvas) draw.line(xy + Xy, fill=color) draw.line(xy + xY, fill=color) draw.line(xY + XY, fill=color) draw.line(Xy + XY, fill=color) draw.line(xy + XY, fill=color) draw.line(xY + Xy, fill=color) return canvas def draw_borders(self, canvas): if self.border_width == 0: return W = self.page.w - 1 H = self.page.h - 1 border = self.border_width - 1 color = self.border_color draw = PIL.ImageDraw.Draw(canvas) draw.rectangle((0, 0) + (border, H), color) draw.rectangle((W - border, 0) + (W, H), color) draw.rectangle((0, 0) + (W, border), color) draw.rectangle((0, H - border) + (W, H), color) for col in self.page.cols: # Draw horizontal borders for c in col.cells[1:]: xy = (col.x, c.y - border / 2) XY = (col.x + col.w, c.y + border / 2) draw.rectangle(xy + XY, color) # Draw vertical borders if col.x > 0: for c in col.cells: if not c.is_extension(): xy = (col.x - border / 2, c.y) XY = (col.x + border / 2, c.y + c.h) draw.rectangle(xy + XY, color) return canvas def resize_photo(self, cell, use_cache=False): # If a thumbnail is already in cache, let's use it. But only if it is # bigger than what we need, because we don't want to lose quality. if (use_cache and cell.photo.filename in cache and cache[cell.photo.filename].size[0] >= int(round(cell.w)) and cache[cell.photo.filename].size[1] >= int(round(cell.h))): img = cache[cell.photo.filename].copy() else: img = PIL.Image.open(cell.photo.filename) # Rotate image is EXIF says so if cell.photo.orientation == 3: img = img.rotate(180, expand=True) elif cell.photo.orientation == 6: img = img.rotate(270, expand=True) elif cell.photo.orientation == 8: img = img.rotate(90, expand=True) if self.quality == QUALITY_FAST: method = PIL.Image.NEAREST else: method = PIL.Image.LANCZOS # renamed from ANTIALIAS in Pillow 10 shape = img.size[0] * cell.h - img.size[1] * cell.w if shape > 0: # image is too thick img = img.resize((int(round(cell.h * img.size[0] / img.size[1])), int(round(cell.h))), method) elif shape < 0: # image is too tall img = img.resize((int(round(cell.w)), int(round(cell.w * img.size[1] / img.size[0]))), method) else: img = img.resize((int(round(cell.w)), int(round(cell.h))), method) # Save this new image to cache (if it is larger than the previous one) if (use_cache and (cell.photo.filename not in cache or cache[cell.photo.filename].size[0] < img.size[0])): cache[cell.photo.filename] = img if shape > 0: # image is too thick width_to_crop = img.size[0] - cell.w img = img.crop(( int(round(width_to_crop * cell.photo.offset_w)), 0, int(round(img.size[0] - width_to_crop * (1 - cell.photo.offset_w))), int(round(cell.h)) )) elif shape < 0: # image is too tall height_to_crop = img.size[1] - cell.h img = img.crop(( 0, int(round(height_to_crop * cell.photo.offset_h)), int(round(cell.w)), int(round(img.size[1] - height_to_crop * (1 - cell.photo.offset_h))) )) return img def paste_photo(self, canvas, cell, img): canvas.paste(img, (int(round(cell.x)), int(round(cell.y)))) return canvas def run(self): try: canvas = PIL.Image.new( "RGB", (int(self.page.w), int(self.page.h)), "white") self.draw_skeleton(canvas) self.draw_borders(canvas) if self.quality != QUALITY_SKEL: n = sum([len([cell for cell in col.cells if not cell.is_extension()]) for col in self.page.cols]) i = 0.0 if self.on_update: self.on_update(canvas, 0.0) last_update = time.time() for col in self.page.cols: for c in col.cells: if self.canceled: # someone clicked "abort" return if c.is_extension(): continue img = self.resize_photo(c, use_cache=True) self.paste_photo(canvas, c, img) # Only needed for interactive rendering if self.on_update: self.draw_borders(canvas) i += 1 now = time.time() if self.on_update and now > last_update + 0.1: self.on_update(canvas, i / n) last_update = now self.draw_borders(canvas) if self.output_file: canvas.save(self.output_file) if self.on_complete: self.on_complete(canvas) except Exception as e: if self.on_fail: self.on_fail(e)