summaryrefslogtreecommitdiff
path: root/unrpyc/renpy/display/im.py
diff options
context:
space:
mode:
Diffstat (limited to 'unrpyc/renpy/display/im.py')
-rw-r--r--unrpyc/renpy/display/im.py1562
1 files changed, 1562 insertions, 0 deletions
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 <pytom@bishoujo.us>
+#
+# 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)