From 718936110b9511631fa1f4396be992752bf8b719 Mon Sep 17 00:00:00 2001 From: Alex Xu Date: Thu, 22 Aug 2013 22:45:26 -0400 Subject: include renpy --- unrpyc/renpy/display/im.py | 1562 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1562 insertions(+) create mode 100644 unrpyc/renpy/display/im.py (limited to 'unrpyc/renpy/display/im.py') diff --git a/unrpyc/renpy/display/im.py b/unrpyc/renpy/display/im.py new file mode 100644 index 0000000..d90fb51 --- /dev/null +++ b/unrpyc/renpy/display/im.py @@ -0,0 +1,1562 @@ +# Copyright 2004-2013 Tom Rothamel +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# This file contains the new image code, which includes provisions for +# size-based caching and constructing images from operations (like +# cropping and scaling). + +import renpy.display + +import math +import zipfile +import io +import threading +import time + + +# This is an entry in the image cache. +class CacheEntry(object): + + def __init__(self, what, surf): + + # The object that is being cached (which needs to be + # hashable and comparable). + self.what = what + + # The pygame surface corresponding to the cached object. + self.surf = surf + + # The size of this image. + w, h = surf.get_size() + self.size = w * h + + # The time when this cache entry was last used. + self.time = 0 + +# This is the singleton image cache. +class Cache(object): + + def __init__(self): + + # The current arbitrary time. (Increments by one for each + # interaction.) + self.time = 0 + + # A map from Image object to CacheEntry. + self.cache = { } + + # A list of Image objects that we want to preload. + self.preloads = [ ] + + # False if this is not the first preload in this tick. + self.first_preload_in_tick = True + + # The total size of the current generation of images. + self.size_of_current_generation = 0 + + # The total size of everything in the cache. + self.total_cache_size = 0 + + # A lock that must be held when updating the cache. + self.lock = threading.Condition() + + # A lock that mist be held to notify the preload thread. + self.preload_lock = threading.Condition() + + # Is the preload_thread alive? + self.keep_preloading = True + + # A map from image object to surface, only for objects that have + # been pinned into memory. + self.pin_cache = { } + + # Images that we tried, and failed, to preload. + self.preload_blacklist = set() + + # The size of the cache, in pixels. + self.cache_limit = 0 + + # The preload thread. + self.preload_thread = threading.Thread(target=self.preload_thread_main, name="preloader") + self.preload_thread.setDaemon(True) + self.preload_thread.start() + + # Have we been added this tick? + self.added = set() + + # A list of (time, filename, preload) tuples. This is updated when + # config.developer is True and an image is loaded. Preload is a + # flag that is true if the image was loaded from the preload + # thread. The log is limited to 100 entries, and the newest entry + # is first. + # + # This is only updated when config.developer is True. + self.load_log = [ ] + + + def init(self): + """ + Updates the cache object to make use of settings that might be provided + by the game-maker. + """ + + self.cache_limit = renpy.config.image_cache_size * renpy.config.screen_width * renpy.config.screen_height + + def quit(self): #@ReservedAssignment + if not self.preload_thread.isAlive(): + return + + with self.preload_lock: + self.keep_preloading = False + self.preload_lock.notify() + + self.preload_thread.join() + + + # Clears out the cache. + def clear(self): + + self.lock.acquire() + + self.preloads = [ ] + self.pin_cache = { } + self.cache = { } + self.first_preload_in_tick = True + self.size_of_current_generation = 0 + self.total_cache_size = 0 + + self.added.clear() + + self.lock.release() + + # Increments time, and clears the list of images to be + # preloaded. + def tick(self): + + with self.lock: + self.time += 1 + self.preloads = [ ] + self.first_preload_in_tick = True + self.size_of_current_generation = 0 + self.added.clear() + + if renpy.config.debug_image_cache: + renpy.display.ic_log.write("----") + filename, line = renpy.exports.get_filename_line() + renpy.display.ic_log.write("%s %d", filename, line) + + # The preload thread can deal with this update, so we don't need + # to lock things. + def end_tick(self): + self.preloads = [ ] + + + # This returns the pygame surface corresponding to the provided + # image. It also takes care of updating the age of images in the + # cache to be current, and maintaining the size of the current + # generation of images. + def get(self, image, predict=False): + + if not isinstance(image, ImageBase): + raise Exception("Expected an image of some sort, but got" + str(image) + ".") + + if not image.cache: + surf = image.load() + renpy.display.render.mutated_surface(surf) + return surf + + # First try to grab the image out of the cache without locking it. + ce = self.cache.get(image, None) + + # Otherwise, we load the image ourselves. + if ce is None: + + try: + if image in self.pin_cache: + surf = self.pin_cache[image] + else: + surf = image.load() + + except: + raise + + with self.lock: + + ce = CacheEntry(image, surf) + + if image not in self.cache: + self.total_cache_size += ce.size + + self.cache[image] = ce + + # Indicate that this surface had changed. + renpy.display.render.mutated_surface(ce.surf) + + if renpy.config.debug_image_cache: + if predict: + renpy.display.ic_log.write("Added %r (%.02f%%)", ce.what, 100.0 * self.total_cache_size / self.cache_limit) + else: + renpy.display.ic_log.write("Total Miss %r", ce.what) + + renpy.display.draw.load_texture(ce.surf) + + + # Move it into the current generation. This isn't protected by + # a lock, so in certain circumstances we could have an + # inaccurate size - but that will be cured at the end of the + # current generation. + + if ce.time != self.time: + ce.time = self.time + self.size_of_current_generation += ce.size + + # Done... return the surface. + return ce.surf + + + # This kills off a given cache entry. + def kill(self, ce): + + # Should never happen... but... + if ce.time == self.time: + self.size_of_current_generation -= ce.size + + self.total_cache_size -= ce.size + del self.cache[ce.what] + + if renpy.config.debug_image_cache: + renpy.display.ic_log.write("Removed %r", ce.what) + + def cleanout(self): + """ + Cleans out the cache, if it's gotten too large. Returns True + if the cache is smaller than the size limit, or False if it's + bigger and we don't want to continue preloading. + """ + + # If we're within the limit, return. + if self.total_cache_size <= self.cache_limit: + return True + + # If we're outside the cache limit, we need to go and start + # killing off some of the entries until we're back inside it. + + for ce in sorted(iter(self.cache.values()), key=lambda a : a.time): + + if ce.time == self.time: + # If we're bigger than the limit, and there's nothing + # to remove, we should stop the preloading right away. + return False + + # Otherwise, kill off the given cache entry. + self.kill(ce) + + # If we're in the limit, we're done. + if self.total_cache_size <= self.cache_limit: + break + + return True + + + # Called to report that a given image would like to be preloaded. + def preload_image(self, im): + + if not isinstance(im, ImageBase): + return + + with self.lock: + + if im in self.added: + return + + self.added.add(im) + + if im in self.cache: + self.get(im) + in_cache = True + else: + self.preloads.append(im) + in_cache = False + + if not in_cache: + + with self.preload_lock: + self.preload_lock.notify() + + if in_cache and renpy.config.debug_image_cache: + renpy.display.ic_log.write("Kept %r", im) + + + def start_prediction(self): + """ + Called at the start of prediction, to ensure the thread runs + at least once to clean out the cache. + """ + + with self.preload_lock: + self.preload_lock.notify() + + def preload_thread_main(self): + + while self.keep_preloading: + + self.preload_lock.acquire() + self.preload_lock.wait() + self.preload_lock.release() + + while self.preloads and self.keep_preloading: + + start = time.time() + + # If the size of the current generation is bigger than the + # total cache size, stop preloading. + with self.lock: + + # If the cache is overfull, clean it out. + if not self.cleanout(): + + if renpy.config.debug_image_cache: + for i in self.preloads: + renpy.display.ic_log.write("Overfull %r", i) + + self.preloads = [ ] + + break + + try: + image = self.preloads.pop(0) + + if image not in self.preload_blacklist: + try: + self.get(image, True) + except: + self.preload_blacklist.add(image) + except: + pass + + with self.lock: + self.cleanout() + + # If we have time, preload pinned images. + if self.keep_preloading and not renpy.game.less_memory: + + workset = set(renpy.store._cache_pin_set) + + # Remove things that are not in the workset from the pin cache, + # and remove things that are in the workset from pin cache. + for i in list(self.pin_cache.keys()): + + if i in workset: + workset.remove(i) + else: + surf = self.pin_cache[i] + + del self.pin_cache[i] + + + # For each image in the worklist... + for image in workset: + + if image in self.preload_blacklist: + continue + + # If we have normal preloads, break out. + if self.preloads: + break + + try: + surf = image.load() + self.pin_cache[image] = surf + renpy.display.draw.load_texture(surf) + except: + self.preload_blacklist.add(image) + + def add_load_log(self, filename): + + if not renpy.config.developer: + return + + preload = (threading.current_thread() is self.preload_thread) + + self.load_log.insert(0, (time.time(), filename, preload)) + + while len(self.load_log) > 100: + self.load_log.pop() + + + +# The cache object. +cache = Cache() + +def free_memory(): + """ + Frees some memory. + """ + + renpy.display.draw.free_memory() + cache.clear() + + +class ImageBase(renpy.display.core.Displayable): + """ + This is the base class for all of the various kinds of images that + we can possibly have. + """ + + __version__ = 1 + + def after_upgrade(self, version): + if version < 1: + self.cache = True + + def __init__(self, *args, **properties): + + self.rle = properties.pop('rle', None) + self.cache = properties.pop('cache', True) + + properties.setdefault('style', 'image') + + super(ImageBase, self).__init__(**properties) + self.identity = (type(self).__name__, ) + args + + + def __hash__(self): + return hash(self.identity) + + def __eq__(self, other): + + if not isinstance(other, ImageBase): + return False + + return self.identity == other.identity + + def __repr__(self): + return "<" + " ".join([repr(i) for i in self.identity]) + ">" + + def load(self): + """ + This function is called by the image cache code to cause this + image to be loaded. It's expected that children of this class + would override this. + """ + + assert False + + def render(self, w, h, st, at): + + im = cache.get(self) + texture = renpy.display.draw.load_texture(im) + + w, h = im.get_size() + rv = renpy.display.render.Render(w, h) + rv.blit(texture, (0, 0)) + return rv + + def predict_one(self): + renpy.display.predict.image(self) + + def predict_files(self): + """ + Returns a list of files that will be accessed when this image + operation is performed. + """ + + return [ ] + +class Image(ImageBase): + """ + This image manipulator loads an image from a file. + """ + + def __init__(self, filename, **properties): + """ + @param filename: The filename that the image will be loaded from. + """ + + super(Image, self).__init__(filename, **properties) + self.filename = filename + + def get_mtime(self): + return renpy.loader.get_mtime(self.filename) + + def load(self, unscaled=False): + + cache.add_load_log(self.filename) + + try: + + if unscaled: + surf = renpy.display.pgrender.load_image_unscaled(renpy.loader.load(self.filename), self.filename) + else: + surf = renpy.display.pgrender.load_image(renpy.loader.load(self.filename), self.filename) + + return surf + + except Exception as e: + + if renpy.config.missing_image_callback: + im = renpy.config.missing_image_callback(self.filename) + if im is None: + raise e + + return im.load() + + raise + + def predict_files(self): + + if renpy.loader.loadable(self.filename): + return [ self.filename ] + else: + if renpy.config.missing_image_callback: + im = renpy.config.missing_image_callback(self.filename) + if im is not None: + return im.predict_files() + + return [ self.filename ] + +class ZipFileImage(ImageBase): + + def __init__(self, zipfilename, filename, mtime=0, **properties): + super(ZipFileImage, self).__init__(zipfilename, filename, mtime, **properties) + + self.zipfilename = zipfilename + self.filename = filename + + def load(self): + try: + zf = zipfile.ZipFile(self.zipfilename, 'r') + data = zf.read(self.filename) + sio = io.StringIO(data) + rv = renpy.display.pgrender.load_image(sio, self.filename) + zf.close() + return rv + except: + return renpy.display.pgrender.surface((2, 2), True) + + + + def predict_files(self): + return [ ] + + + +class Composite(ImageBase): + """ + :doc: im_im + + This image manipulator composites multiple images together to + form a single image. + + The `size` should be a (width, height) tuple giving the size + of the composed image. + + The remaining positional arguments are interpreted as groups of + two. The first argument in a group should be an (x, y) tuple, + while the second should be an image manipulator. The image + produced by the image manipulator is composited at the location + given by the tuple. + + :: + + image girl clothed happy = im.Composite( + (300, 600), + (0, 0), "girl_body.png", + (0, 0), "girl_clothes.png", + (100, 100), "girl_happy.png" + ) + + """ + + def __init__(self, size, *args, **properties): + + super(Composite, self).__init__(size, *args, **properties) + + if len(args) % 2 != 0: + raise Exception("Composite requires an odd number of arguments.") + + self.size = size + self.positions = args[0::2] + self.images = [ image(i) for i in args[1::2] ] + + def get_mtime(self): + return min(i.get_mtime() for i in self.images) + + def load(self): + + if self.size: + size = self.size + else: + size = cache.get(self.images[0]).get_size() + + rv = renpy.display.pgrender.surface(size, True) + + for pos, im in zip(self.positions, self.images): + rv.blit(cache.get(im), pos) + + return rv + + def predict_files(self): + + rv = [ ] + + for i in self.images: + rv.extend(i.predict_files()) + + return rv + +class Scale(ImageBase): + """ + :doc: im_im + + An image manipulator that scales `im` (an image manipulator) to + `width` and `height`. + + If `bilinear` is true, then bilinear interpolation is used for + the scaling. Otherwise, nearest neighbor interpolation is used. + + :: + + image logo scale = im.Scale("logo.png", 100, 150) + """ + + def __init__(self, im, width, height, bilinear=True, **properties): + + im = image(im) + super(Scale, self).__init__(im, width, height, bilinear, **properties) + + self.image = im + self.width = int(width) + self.height = int(height) + self.bilinear = bilinear + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + child = cache.get(self.image) + + if self.bilinear: + try: + renpy.display.render.blit_lock.acquire() + rv = renpy.display.scale.smoothscale(child, (self.width, self.height)) + finally: + renpy.display.render.blit_lock.release() + else: + try: + renpy.display.render.blit_lock.acquire() + rv = renpy.display.pgrender.transform_scale(child, (self.width, self.height)) + finally: + renpy.display.render.blit_lock.release() + + return rv + + def predict_files(self): + return self.image.predict_files() + +class FactorScale(ImageBase): + """ + :doc: im_im + + An image manipulator that scales `im` (a second image manipulator) + to `width` times its original `width`, and `height` times its + original height. If `height` is ommitted, it defaults to `width`. + + If `bilinear` is true, then bilinear interpolation is used for + the scaling. Otherwise, nearest neighbor interpolation is used. + + :: + + image logo doubled = im.FactorScale("logo.png", 1.5) + """ + + + def __init__(self, im, width, height=None, bilinear=True, **properties): + + if height is None: + height = width + + im = image(im) + super(FactorScale, self).__init__(im, width, height, bilinear, **properties) + + self.image = im + self.width = width + self.height = height + self.bilinear = bilinear + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + surf = cache.get(self.image) + width, height = surf.get_size() + + width = int(width * self.width) + height = int(height * self.height) + + if self.bilinear: + try: + renpy.display.render.blit_lock.acquire() + rv = renpy.display.scale.smoothscale(surf, (width, height)) + finally: + renpy.display.render.blit_lock.release() + + else: + try: + renpy.display.render.blit_lock.acquire() + rv = renpy.display.pgrender.transform_scale(surf, (width, height)) + finally: + renpy.display.render.blit_lock.release() + + return rv + + def predict_files(self): + return self.image.predict_files() + + +class Flip(ImageBase): + """ + :doc: im_im + + An image manipulator that flips `im` (an image manipulator) + vertically or horizontally. `vertical` and `horizontal` control + the directions in which the image is flipped. + + :: + + image eileen flip = im.Flip("eileen_happy.png", vertical=True) + """ + + def __init__(self, im, horizontal=False, vertical=False, **properties): + + if not (horizontal or vertical): + raise Exception("im.Flip must be called with a true value for horizontal or vertical.") + + im = image(im) + super(Flip, self).__init__(im, horizontal, vertical, **properties) + + self.image = im + self.horizontal = horizontal + self.vertical = vertical + + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + child = cache.get(self.image) + + try: + renpy.display.render.blit_lock.acquire() + rv = renpy.display.pgrender.flip(child, self.horizontal, self.vertical) + finally: + renpy.display.render.blit_lock.release() + + return rv + + + def predict_files(self): + return self.image.predict_files() + + + +class Rotozoom(ImageBase): + """ + This is an image manipulator that is a smooth rotation and zoom of another image manipulator. + """ + + def __init__(self, im, angle, zoom, **properties): + """ + @param im: The image to be rotozoomed. + + @param angle: The number of degrees counterclockwise the image is + to be rotated. + + @param zoom: The zoom factor. Numbers that are greater than 1.0 + lead to the image becoming larger. + """ + + im = image(im) + super(Rotozoom, self).__init__(im, angle, zoom, **properties) + + self.image = im + self.angle = angle + self.zoom = zoom + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + child = cache.get(self.image) + + try: + renpy.display.render.blit_lock.acquire() + rv = renpy.display.pgrender.rotozoom(child, self.angle, self.zoom) + finally: + renpy.display.render.blit_lock.release() + + return rv + + def predict_files(self): + return self.image.predict_files() + + + +class Crop(ImageBase): + """ + :doc: im_im + :args: (im, rect) + + An image manipulator that crops `rect`, a (x, y, width, height) tuple, + out of `im`, an image manipulator. + + :: + + image logo crop = im.Crop("logo.png", (0, 0, 100, 307)) + """ + + def __init__(self, im, x, y=None, w=None, h=None, **properties): + + im = image(im) + + if y is None: + (x, y, w, h) = x + + super(Crop, self).__init__(im, x, y, w, h, **properties) + + self.image = im + self.x = x + self.y = y + self.w = w + self.h = h + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + return cache.get(self.image).subsurface((self.x, self.y, + self.w, self.h)) + + def predict_files(self): + return self.image.predict_files() + + +ramp_cache = { } + + +def ramp(start, end): + """ + Returns a 256 character linear ramp, where the first character has + the value start and the last character has the value end. Such a + ramp can be used as a map argument of im.Map. + """ + + rv = ramp_cache.get((start, end), None) + if rv is None: + + chars = [ ] + + for i in range(0, 256): + i = i / 255.0 + chars.append(chr(int( end * i + start * (1.0 - i) ) ) ) + + rv = "".join(chars) + ramp_cache[start, end] = rv + + return rv + +identity = ramp(0, 255) + +class Map(ImageBase): + """ + This adjusts the colors of the image that is its child. It takes + as arguments 4 256 character strings. If a pixel channel has a + value of 192, then the value of the 192nd character in the string + is used for the mapped pixel component. + """ + + def __init__(self, im, rmap=identity, gmap=identity, bmap=identity, + amap=identity, force_alpha=False, **properties): + + im = image(im) + + super(Map, self).__init__(im, rmap, gmap, bmap, amap, force_alpha, **properties) + + self.image = im + self.rmap = rmap + self.gmap = gmap + self.bmap = bmap + self.amap = amap + + self.force_alpha = force_alpha + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + surf = cache.get(self.image) + + rv = renpy.display.pgrender.surface(surf.get_size(), True) + + renpy.display.module.map(surf, rv, + self.rmap, self.gmap, self.bmap, self.amap) + + return rv + + def predict_files(self): + return self.image.predict_files() + +class Twocolor(ImageBase): + """ + This takes as arguments two colors, white and black. The image is + mapped such that pixels in white have the white color, pixels in + black have the black color, and shades of gray are linearly + interpolated inbetween. The alpha channel is mapped linearly + between 0 and the alpha found in the white color, the black + color's alpha is ignored. + """ + + def __init__(self, im, white, black, force_alpha=False, **properties): + + white = renpy.easy.color(white) + black = renpy.easy.color(black) + + im = image(im) + + super(Twocolor, self).__init__(im, white, black, force_alpha, **properties) + + self.image = im + self.white = white + self.black = black + + self.force_alpha = force_alpha + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + surf = cache.get(self.image) + + rv = renpy.display.pgrender.surface(surf.get_size(), True) + + renpy.display.module.twomap(surf, rv, + self.white, self.black) + + return rv + + def predict_files(self): + return self.image.predict_files() + + +class Recolor(ImageBase): + """ + This adjusts the colors of the image that is its child. It takes as an + argument 4 numbers between 0 and 255, and maps each channel of the image + linearly between 0 and the supplied color. + """ + + def __init__(self, im, rmul=255, gmul=255, bmul=255, + amul=255, force_alpha=False, **properties): + + im = image(im) + + super(Recolor, self).__init__(im, rmul, gmul, bmul, amul, force_alpha, **properties) + + self.image = im + self.rmul = rmul + 1 + self.gmul = gmul + 1 + self.bmul = bmul + 1 + self.amul = amul + 1 + + self.force_alpha = force_alpha + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + surf = cache.get(self.image) + + rv = renpy.display.pgrender.surface(surf.get_size(), True) + + renpy.display.module.linmap(surf, rv, + self.rmul, self.gmul, self.bmul, self.amul) + + return rv + + def predict_files(self): + return self.image.predict_files() + +class MatrixColor(ImageBase): + """ + :doc: im_matrixcolor + + An image operator that uses `matrix` to linearly transform the + image manipulator `im`. + + `Matrix` should be a list, tuple, or :func:`im.matrix` that is 20 + or 25 elements long. If the object has 25 elements, then elements + past the 20th are ignored. + + When the four components of the source color are R, G, B, and A, + which range from 0.0 to 1.0; the four components of the transformed + color are R', G', B', and A', with the same range; and the elements + of the matrix are named:: + + [ a, b, c, d, e, + f, g, h, i, j, + k, l, m, n, o, + p, q, r, s, t ] + + the transformed colors can be computed with the formula:: + + R' = (a * R) + (b * G) + (c * B) + (d * A) + e + G' = (f * R) + (g * G) + (h * B) + (i * A) + j + B' = (k * R) + (l * G) + (m * B) + (n * A) + o + A' = (p * R) + (q * G) + (r * B) + (s * A) + t + + The components of the transformed color are clamped to the + range [0.0, 1.0]. + """ + + def __init__(self, im, matrix, **properties): + + im = image(im) + + if len(matrix) != 20 and len(matrix) != 25: + raise Exception("ColorMatrix expects a 20 or 25 element matrix, got %d elements." % len(matrix)) + + matrix = tuple(matrix) + super(MatrixColor, self).__init__(im, matrix, **properties) + + self.image = im + self.matrix = matrix + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + surf = cache.get(self.image) + + rv = renpy.display.pgrender.surface(surf.get_size(), True) + + renpy.display.module.colormatrix(surf, rv, self.matrix) + + return rv + + def predict_files(self): + return self.image.predict_files() + +class matrix(tuple): + """ + :doc: im_matrixcolor + + Constructs an im.matrix object from `matrix`. im.matrix objects + support The operations supported are matrix multiplication, scalar + multiplication, element-wise addition, and element-wise + subtraction. These operations are invoked using the standard + mathematical operators (\\*, \\*, +, and -, respectively). If two + im.matrix objects are multiplied, matrix multiplication is + performed, otherwise scalar multiplication is used. + + `matrix` is a 20 or 25 element list or tuple. If it is 20 elements + long, it is padded with (0, 0, 0, 0, 1) to make a 5x5 matrix, + suitable for multiplication. + """ + + def __new__(cls, *args): + + if len(args) == 1: + args = tuple(args[0]) + + if len(args) == 20: + args = args + (0, 0, 0, 0, 1) + + if len(args) != 25: + raise Exception("Matrix expects to be given 20 or 25 entries, not %d." % len(args)) + + return tuple.__new__(cls, args) + + def mul(self, a, b): + + if not isinstance(a, matrix): + a = matrix(a) + + if not isinstance(b, matrix): + b = matrix(b) + + result = [ 0 ] * 25 + for y in range(0, 5): + for x in range(0, 5): + for i in range(0, 5): + result[x + y * 5] += a[x + i * 5] * b[i + y * 5] + + return matrix(result) + + def scalar_mul(self, other): + other = float(other) + return matrix([ i * other for i in self ]) + + def vector_mul(self, o): + + return (o[0]*self[0] + o[1]*self[1] + o[2]*self[2] + o[3]*self[3] + self[4], + o[0]*self[5] + o[1]*self[6] + o[2]*self[7] + o[3]*self[8] + self[9], + o[0]*self[10] + o[1]*self[11] + o[2]*self[12] + o[3]*self[13] + self[14], + o[0]*self[15] + o[1]*self[16] + o[2]*self[17] + o[3]*self[18] + self[19], + 1) + + + def __add__(self, other): + if isinstance(other, (int, float)): + other = float(other) + return matrix([ i + other for i in self ]) + + other = matrix(other) + return matrix([ i + j for i, j in zip(self, other)]) + + __radd__ = __add__ + + def __sub__(self, other): + return self + other * -1 + + def __rsub__(self, other): + return self * -1 + other + + def __mul__(self, other): + if isinstance(other, (int, float)): + return self.scalar_mul(other) + + return self.mul(self, other) + + def __rmul__(self, other): + if isinstance(other, (int, float)): + return self.scalar_mul(other) + + return self.mul(other, self) + + def __repr__(self): + return """\ +im.matrix(%f, %f, %f, %f, %f. + %f, %f, %f, %f, %f, + %f, %f, %f, %f, %f, + %f, %f, %f, %f, %f, + %f, %f, %f, %f, %f)""" % self + + + @staticmethod + def identity(): + """ + :doc: im_matrixcolor + :name: im.matrix.identity + + Returns an identity matrix, one that does not change color or + alpha. + """ + + return matrix(1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, 1, 0) + @staticmethod + def saturation(level, desat=(0.2126, 0.7152, 0.0722)): + """ + :doc: im_matrixcolor + :name: im.matrix.saturation + + Returns an im.matrix that alters the saturation of an + image. The alpha channel is untouched. + + `level` + The amount of saturation in the resulting image. 1.0 is + the unaltered image, while 0.0 is grayscale. + + `desat` + This is a 3-element tuple that controls how much of the + red, green, and blue channels will be placed into all + three channels of a fully desaturated image. The default + is based on the constants used for the luminance channel + of an NTSC television signal. Since the human eye is + mostly sensitive to green, more of the green channel is + kept then the other two channels. + """ + + r, g, b = desat + + def I(a, b): + return a + (b - a) * level + + return matrix(I(r, 1), I(g, 0), I(b, 0), 0, 0, + I(r, 0), I(g, 1), I(b, 0), 0, 0, + I(r, 0), I(g, 0), I(b, 1), 0, 0, + 0, 0, 0, 1, 0) + + @staticmethod + def desaturate(): + """ + :doc: im_matrixcolor + :name: im.matrix.desaturate + + Returns an im.matrix that desaturates the image (makes it + grayscale). This is equivalent to calling + im.matrix.saturation(0). + """ + + return matrix.saturation(0.0) + + @staticmethod + def tint(r, g, b): + """ + :doc: im_matrixcolor + :name: im.matrix.tint + + Returns an im.matrix that tints an image, without changing + the alpha channel. `r`, `g`, and `b` should be numbers between + 0 and 1, and control what fraction of the given channel is + placed into the final image. (For example, if `r` is .5, and + the value of the red channel is 100, the transformed color + will have a red value of 50.) + """ + + return matrix(r, 0, 0, 0, 0, + 0, g, 0, 0, 0, + 0, 0, b, 0, 0, + 0, 0, 0, 1, 0) + + @staticmethod + def invert(): + """ + :doc: im_matrixcolor + :name: im.matrix.invert + + Returns an im.matrix that inverts the red, green, and blue + channels of the image without changing the alpha channel. + """ + + return matrix(-1, 0, 0, 0, 1, + 0, -1, 0, 0, 1, + 0, 0, -1, 0, 1, + 0, 0, 0, 1, 0) + + @staticmethod + def brightness(b): + """ + :doc: im_matrixcolor + :name: im.matrix.brightness + + Returns an im.matrix that alters the brightness of an image. + + `b` + The amount of change in image brightness. This should be + a number between -1 and 1, with -1 the darkest possible + image and 1 the brightest. + """ + + return matrix(1, 0, 0, 0, b, + 0, 1, 0, 0, b, + 0, 0, 1, 0, b, + 0, 0, 0, 1, 0) + + @staticmethod + def opacity(o): + """ + :doc: im_matrixcolor + :name: im.matrix.opacity + + Returns an im.matrix that alters the opacity of an image. An + `o` of 0.0 is fully transparent, while 1.0 is fully opaque. + """ + + return matrix(1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 0, o, 0) + + @staticmethod + def contrast(c): + """ + :doc: im_matrixcolor + :name: im.matrix.contrast + + Returns an im.matrix that alters the contrast of an image. `c` should + be greater than 0.0, with values between 0.0 and 1.0 decreasing contrast, and + values greater than 1.0 increasing contrast. + """ + + return matrix.brightness(-.5) * matrix.tint(c, c, c) * matrix.brightness(.5) + + # from http://www.gskinner.com/blog/archives/2005/09/flash_8_source.html + @staticmethod + def hue(h): + """ + :doc: im_matrixcolor + :name: im.matrix.hue + + Returns an im.matrix that rotates the hue by `h` degrees, while + preserving luminosity. + """ + + h = h * math.pi / 180 + cosVal = math.cos(h) + sinVal = math.sin(h) + lumR = 0.213 + lumG = 0.715 + lumB = 0.072 + return matrix( + lumR+cosVal*(1-lumR)+sinVal*(-lumR),lumG+cosVal*(-lumG)+sinVal*(-lumG),lumB+cosVal*(-lumB)+sinVal*(1-lumB),0,0, + lumR+cosVal*(-lumR)+sinVal*(0.143),lumG+cosVal*(1-lumG)+sinVal*(0.140),lumB+cosVal*(-lumB)+sinVal*(-0.283),0,0, + lumR+cosVal*(-lumR)+sinVal*(-(1-lumR)),lumG+cosVal*(-lumG)+sinVal*(lumG),lumB+cosVal*(1-lumB)+sinVal*(lumB),0,0, + 0,0,0,1,0, + 0,0,0,0,1 + ) + + @staticmethod + def colorize(black_color, white_color): + """ + :doc: im_matrixcolor + :name: im.matrix.colorize + + Returns an im.matrix that colorizes a black and white image. + `black_color` and `white_color` are Ren'Py style colors, so + they may be specfied as strings or tuples of (0-255) color + values. :: + + # This makes black colors red, and white colors blue. + image logo colored = im.MatrixColor( + "bwlogo.png", + im.matrix.colorize("#f00", "#00f")) + + """ + + (r0, g0, b0, _a0) = renpy.easy.color(black_color) + (r1, g1, b1, _a1) = renpy.easy.color(white_color) + + r0 /= 255.0 + g0 /= 255.0 + b0 /= 255.0 + r1 /= 255.0 + g1 /= 255.0 + b1 /= 255.0 + + return matrix((r1-r0), 0, 0, 0, r0, + 0, (g1-g0), 0, 0, g0, + 0, 0, (b1-b0), 0, b0, + 0, 0, 0, 1, 0) + + + +def Grayscale(im, desat=(0.2126, 0.7152, 0.0722), **properties): + """ + :doc: im_im + :args: (im, **properties) + + An image manipulator that creats a desaturated version of the image + manipulator `im`. + """ + + return MatrixColor(im, matrix.saturation(0.0, desat), **properties) + + +def Sepia(im, tint=(1.0, .94, .76), desat=(0.2126, 0.7152, 0.0722), **properties): + """ + :doc: im_im + :args: (im, **properties) + + An image manipulator that creates a sepia-toned version of the image + manipulator `im`. + """ + + return MatrixColor(im, matrix.saturation(0.0, desat) * matrix.tint(tint[0], tint[1], tint[2]), **properties) + + +def Color(im, color): + """ + This recolors the supplied image, mapping colors such that black is + black and white is the supplied color. + """ + + r, g, b, a = renpy.easy.color(color) + + return Recolor(im, r, g, b, a) + + +def Alpha(image, alpha, **properties): + """ + Returns an alpha-mapped version of the image. Alpha is the maximum + alpha that this image can have, a number between 0.0 (fully + transparent) and 1.0 (opaque). + + If an image already has an alpha channel, values in that alpha + channel are reduced as appropriate. + """ + + return Recolor(image, 255, 255, 255, int(255 * alpha), force_alpha=True, **properties) + +class Tile(ImageBase): + """ + :doc: im_im + + An image manipulator that tiles the image manipulator `im`, until + it is `size`. + + `size` + If not None, a (width, height) tuple. If None, this defaults to + (:var:`config.screen_width`, :var:`config.screen_height`). + """ + + def __init__(self, im, size=None, **properties): + + im = image(im) + + super(Tile, self).__init__(im, size, **properties) + self.image = im + self.size = size + + def get_mtime(self): + return self.image.get_mtime() + + def load(self): + + size = self.size + + if size is None: + size = (renpy.config.screen_width, renpy.config.screen_height) + + surf = cache.get(self.image) + + rv = renpy.display.pgrender.surface(size, True) + + width, height = size + sw, sh = surf.get_size() + + for y in range(0, height, sh): + for x in range(0, width, sw): + rv.blit(surf, (x, y)) + + return rv + + def predict_files(self): + return self.image.predict_files() + +class AlphaMask(ImageBase): + """ + :doc: im_im + + An image manipulator that takes two image manipulators, `base` and + `mask`, as arguments. It replaces the alpha channel of `base` with + the red channel of `mask`. + + This is used to provide an image's alpha channel in a second + image, like having one jpeg for color data, and a second one + for alpha. In some cases, two jpegs can be smaller than a + single png file. + """ + + def __init__(self, base, mask, **properties): + super(AlphaMask, self).__init__(base, mask, **properties) + + self.base = image(base) + self.mask = image(mask) + + def get_mtime(self): + return max(self.base.get_mtime(), self.image.get_mtime()) + + def load(self): + + basesurf = cache.get(self.base) + masksurf = cache.get(self.mask) + + if basesurf.get_size() != masksurf.get_size(): + raise Exception("AlphaMask surfaces must be the same size.") + + # Used to copy the surface. + rv = renpy.display.pgrender.copy_surface(basesurf) + renpy.display.module.alpha_munge(masksurf, rv, identity) + + return rv + + def predict_files(self): + return self.base.predict_files() + self.mask.predict_files() + +def image(arg, loose=False, **properties): + """ + :doc: im_image + :name: Image + :args: (filename, **properties) + + Loads an image from a file. `filename` is a + string giving the name of the file. + + `filename` should be a JPEG or PNG file with an appropriate + extension. + """ + + """ + (Actually, the user documentation is a bit misleading, as + this tries for compatibility with several older forms of + image specification.) + + If the loose argument is False, then this will report an error if an + arbitrary argument is given. If it's True, then the argument is passed + through unchanged. + """ + + if isinstance(arg, ImageBase): + return arg + + elif isinstance(arg, str): + return Image(arg, **properties) + + elif isinstance(arg, renpy.display.image.ImageReference): + arg.find_target() + return image(arg.target, loose=loose, **properties) + + elif isinstance(arg, tuple): + params = [ ] + + for i in arg: + params.append((0, 0)) + params.append(i) + + return Composite(None, *params) + + elif loose: + return arg + + if isinstance(arg, renpy.display.core.Displayable): + raise Exception("Expected an image, but got a general displayable.") + else: + raise Exception("Could not construct image from argument.") + + +def load_image(fn): + """ + This loads an image from the given filename, using the cache. + """ + + surf = cache.get(image(fn)) + return renpy.display.draw.load_texture(surf) -- cgit v1.2.3-54-g00ecf