# 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 handles imagemap caching. import pygame import renpy.display from renpy.display.render import render import hashlib import os # A list of cache images we've already written. cached = set() class ImageMapCrop(renpy.display.core.Displayable): """ This handles the cropping of uncached imagemap components. """ def __init__(self, child, rect): super(ImageMapCrop, self).__init__() self.child = child self.rect = rect def visit(self): return [ self.child ] def render(self, width, height, st, at): cr = render(self.child, width, height, st, at) return cr.subsurface(self.rect) class ImageCacheCrop(renpy.display.core.Displayable): """ This handles the cropping of an imagemap component. """ def __init__(self, cache, index): super(ImageCacheCrop, self).__init__() # The cache object we're associated with. self.cache = cache # The index of self.index = index def visit(self): return self.cache.visit(self.index) def render(self, width, height, st, at): return self.cache.render(self.index, width, height, st, at) class ImageMapCache(renpy.object.Object): def __init__(self, enable): self.md5 = hashlib.md5() # A list of (image, rect) tuples. The index in this list is used # as a unique identifier for an ImageCacheCrop object. self.imagerect = [ ] # A map from (image, rect) to ImageCacheCrop object. self.hotspots = { } # A list of (width, height, index) tuples. self.areas = [ ] # The image containing our children. self.cache = None # A list that, for each hotspot, gives the rectangle in the cache # image corresponding to that hotspot. self.cache_rect = None # The size of the cache. self.cache_width = None self.cache_height = None self.enable = enable def visit(self, index): if self.cache is not None: return [ self.cache ] else: return [ self.imagerect[index][0] ] def crop(self, d, rect): if not isinstance(d, renpy.display.im.ImageBase) or \ not renpy.config.imagemap_cache or \ not self.enable: return ImageMapCrop(d, rect) key = (d, rect) rv = self.hotspots.get(key, None) if rv is not None: return rv self.md5.update(repr(d.identity)) self.md5.update(repr(d.identity)) index = len(self.imagerect) rv = ImageCacheCrop(self, index) self.imagerect.append(key) self.hotspots[key] = rv self.areas.append((rect[2] + 2, rect[3] + 2, index)) return rv def layout(self): self.areas.sort() self.areas.reverse() self.cache_rect = [ None ] * len(self.areas) # The width of the cache image. width = self.areas[0][0] x = 0 y = 0 line_height = 0 for w, h, i in self.areas: if x + w > width: y += line_height line_height = 0 x = 0 self.cache_rect[i] = (x+1, y+1, w-2, h-2) x += w if line_height < h: line_height = h self.cache_width = width self.cache_height = y + line_height def write_cache(self, filename): if filename in cached: return cached.add(filename) # If all of our dependencies are of the same age or less, # we don't need to rebuild the cache. if renpy.loader.loadable(filename): d_set = set() mtime = 0 for i, rect in self.imagerect: if i in d_set: continue d_set.add(i) mtime = max(i.get_mtime(), mtime) if renpy.loader.get_mtime(filename) >= mtime: return fn = os.path.join(renpy.config.gamedir, filename) dir = os.path.dirname(fn) #@ReservedAssignment if not os.path.exists(dir): os.makedirs(dir) cache = pygame.Surface((self.cache_width, self.cache_height), pygame.SRCALPHA, 32) for i, (d, rect) in enumerate(self.imagerect): x, y, _w, _h = self.cache_rect[i] surf = renpy.display.im.cache.get(d).subsurface(rect) cache.blit(surf, (x, y)) pygame.image.save(cache, renpy.exports.fsencode(fn)) def finish(self): if not self.areas: return filename = "im-%s.png" % (self.md5.hexdigest()) if renpy.game.preferences.language: filename = renpy.game.preferences.language + "-" + filename filename = "cache/" + filename self.md5 = None self.layout() if renpy.config.developer: try: self.write_cache(filename) except: pass if renpy.loader.loadable(filename): self.cache = renpy.display.im.Image(filename) def render(self, index, width, height, st, at): if self.cache is None: d, rect = self.imagerect[index] return render(d, width, height, st, at).subsurface(rect) return render(self.cache, width, height, st, at).subsurface(self.cache_rect[index])