import py from PIL import Image, ImageFilter here = py.magic.autopath().dirpath() class ImageException(Exception): pass def combine_images(image1, image2, bbox): """ pastes image2 into image1, scaling and changing perspective image1 and image2 are PIL.Image objects, bounding_box is an 8-tuple with the X and Y coordinates of the 4 corner points where image2 should be pasted on image1 """ size = bbox.size square = bbox.surrounding_square() newbbox = bbox.offsetted_bbox((0, 0)).inverted() cut = image2.transform(bbox.size, Image.QUAD, newbbox.bbox, Image.BICUBIC) # paste the results over the first image using the mask ret = image1.copy() # XXX target position is wrong here! should be using bbox' surrounding # square somehow... ret.paste(cut, (square[0], square[1]), cut) return ret class BoundingBox(object): def __init__(self, tlX, tlY, blX, blY, brX, brY, trX, trY): self.tlX = tlX self.tlY = tlY self.blX = blX self.blY = blY self.brX = brX self.brY = brY self.trX = trX self.trY = trY self.bbox = (tlX, tlY, blX, blY, brX, brY, trX, trY) def surrounding_square(self): minX = min(self.tlX, self.blX) maxX = max(self.trX, self.brX) minY = min(self.tlY, self.trY) maxY = max(self.blY, self.brY) return (minX, minY, maxX, maxY) def serialize(self): return repr(self.bbox) def unserialize(cls, str): tup = eval(str) assert isinstance(tup, tuple) and len(tup) == 8 return cls(*tup) unserialize = classmethod(unserialize) def offsetted_bbox(self, offset): """ return a BoundingBox with the same dimensions as this, but with 0, 0 point at 'offset' """ offsetX, offsetY = offset square = self.surrounding_square() minX = square[0] - offsetX minY = square[1] - offsetY return BoundingBox(self.tlX - minX, self.tlY - minY, self.blX - minX, self.blY - minY, self.brX - minX, self.brY - minY, self.trX - minX, self.trY - minY) def _size(self): square = self.surrounding_square() return (square[2] - square[0], square[3] - square[1]) size = property(_size) def inverted(self): """ special function to calculate bbox values for Image.transform """ # XXX not sure about this one: PIL's QUAD transform is a bit of a # mystery to me, tbh... square = self.surrounding_square() inverted = (square[0] - self.tlX, square[1] - self.tlY, square[0] - self.blX, square[3] + (square[3] - self.blY), square[2] + (square[2] - self.brX), square[3] + (square[3] - self.brY), square[2] + (square[2] - self.trX), square[1] - self.trY) return BoundingBox(*inverted) class ImageWrapper(object): """ base class for image objects image objects wrap a filesystem image in its original format (scaled versions will be managed by the instances) """ def __init__(self, image, scale): assert isinstance(image, Image.Image), \ 'argument 1 is not a PIL.Image object' self.image = image self.scale = scale def rescale(self, newscale): """ rescale the image to match newscale (in pixels per meter) and fit the result into retsize returns a new self.__class__ instance """ orgscale = self.scale ratio = float(newscale) / float(orgscale) img = self.image newsize = (int(img.size[0] * ratio), int(img.size[1] * ratio)) return self.__class__(self.image.resize(newsize, Image.ANTIALIAS), newscale) def add_transparent_border(self, retsize): """ adds a transparent border evenly around ourselves returns a new self.__class__ instance """ if self.image.size[0] > retsize[0] or self.image.size[1] > retsize[1]: raise ImageException('image is larger than retsize!') pasteX = (retsize[0] - self.image.size[0]) / 2 pasteY = (retsize[1] - self.image.size[1]) / 2 whiteimg = Image.new('L', retsize) whiteimg.paste(255, (0, 0, retsize[0], retsize[1])) # create the mask image for pasting maskimg = whiteimg.paste(0, (pasteX, pasteY, pasteX + retsize[0], pasteY + retsize[1])) new = Image.new('RGBA', retsize) # make image entirely transparent new.paste((0, 0, 0, 0), (0, 0, retsize[0], retsize[1])) # XXX for now we just paste it in the center, perhaps this should be # adjusted at some point to make it look nicer... new.paste(self.image, (pasteX, pasteY, pasteX + self.image.size[0], pasteY + self.image.size[1]), maskimg) return ImageWrapper(new, self.scale) class SVNImage(object): """ image stored in SVN Subversion properties are used to store metadata on the images """ def __init__(self, path): self.path = py.path.svnwc(path) assert self.path.check(versioned=True), 'not a Subversion image' def _scale(self): return int(self.path.propget('img:scale')) def _set_scale(self, scale): self.path.propset('img:scale', scale) scale = property(_scale, _set_scale) _image = None def image(self): if self._image is None: self._image = ImageWrapper(Image.open(self.path.strpath), self.scale) return self._image image = property(image) class Painting(SVNImage): def paste_into_room(self, room): scaled = self.image.rescale(room.scale) size = room.bbox.size bordered = scaled.add_transparent_border(size) return ImageWrapper(combine_images(room.image.image, bordered.image, room.bbox), room.scale) def create(cls, path, scale): i = cls(path) i.scale = scale return i create = classmethod(create) def thumbnail(self, size): # XXX add caching ret = self.image.image.copy() ret.thumbnail((size, size), Image.ANTIALIAS) return ret def box(self, box, boxsize): boxwidth = box[2] - box[0] boxheight = box[3] - box[1] pimage = self.image.image.crop(box) factor = float(boxsize) / max(boxwidth, boxheight) pimage = pimage.resize((boxwidth * factor, boxheight * factor), Image.ANTIALIAS) return pimage class Room(SVNImage): def _bbox(self): return BoundingBox.unserialize(self.path.propget('img:bbox')) def _set_bbox(self, bbox): self.path.propset('img:bbox', bbox.serialize()) bbox = property(_bbox, _set_bbox) def create(cls, path, scale, bbox): i = cls(path) i.scale = scale i.bbox = bbox return i create = classmethod(create) def _toolarge(self): img = self.image.image toolarge = Image.open(here.join('theme/toolarge.png').strpath) img.paste(toolarge, (0, 0), toolarge) return img toolarge = property(_toolarge) class Cache(object): def __init__(self, cachepath): self.cachepath = cachepath.ensure(dir=True) def get(self, key, created_after=-1): path = self.cachepath.join(key) if path.check() and (created_after == -1 or path.mtime() > created_after): return path.read() return None def save(self, key, imagedata): path = self.cachepath.join(key) path.write(imagedata) if __name__ == '__main__': here = py.magic.autopath().dirpath() roombbox = BoundingBox(399, 71, 396, 196, 543, 198, 545, 71) room = Room(here.join('rooms/oval.jpg')) painting = Painting(here.join('paintings/skip.jpg')) room_with_painting = painting.paste_into_room(room) room_with_painting.image.save( here.join('room_with_painting.jpg').strpath)