def render_to_image(self): img = self._get_backing_store()[0] if self.alpha != 255: blended_img = imagelib.new( img.size ) blended_img.blend(img, alpha = self.alpha) img = blended_img return CanvasImage(img)
def new(self, size): """ Initialize a blank image to the given size, a tuple containing the new width and height. """ self.image = imagelib.new(size) self.filename = None self._image_changed()
def _get_backing_store(self, update = False, use_cached = False, update_object = None, preserve_alpha = True, clip = False): """ Render the container and all of its children to an image. This image is cached as the container's backing store and subsequent calls to _get_backing_store() will only update those regions of the backing store that require it. If 'update_object' is not None, it specifies an object somewhere in the container's hierarchy that is to be updated on the backing store. It may be that all objects in the container are dirty, but if we only want one object to be updated on the backing store, we can specify it in update_object. This is useful for BitmapCanvas, as it translates child.update() to self._get_backing_store(update_object = child). If 'update' is True, unqueue_paint() is called on dirty children. This is intended specifically for canvas implementations that rely on _get_backing_store(), like BitmapCanvas. If 'use_cached' is True and a backing store exists for this container (that is, _get_backing_store() has been called before), the old image will be returned; dirty children will not be updated. If 'preserve_alpha' is True, the canvas backing store image has the appropriate alpha channel (relative to its children). If it is False, the backing store image's pixels are fully opaque and the alpha is applied is if the backing store's backround is black. This is useful for BitmapCanvas-based canvases that will be blitting to a screen. This function returns a tuple whose first item is an imagelib.Image representing the rendered container, and whose second item is a list of rectangles that have been updated since the last call to _get_backing_store(). This is useful for subclasses of BitmapCanvas that want to blit only changed regions to the display. Rectangles are in the form ( (left, top), (width, height) ). """ # WARNING: This code is scary. :) # # # Part of the reason the code below is sticky is that is solves a # a problem with sticky requirements, particularly the fact that # canvas objects can have negative coordinates, so coords must # be continually translated. # Another requirement is that if 'update_object' is specified, # all parent or sibling objects to 'update_object' shouldn't be # touched. But because we may need blit an object that isn't to # be updated (if it is underneath an image that _is_ to be updated, # for instance), we need to know its position. If the position has # changed, we need to use the old position, so this means we must # maintain a cache of old values. We use the _backing_store_info # dict for this. # # HOW THIS FUNCTION WORKS # ======================= # # The work is divided into three steps: # # 1. Update the backing stores of all dirty children and obtain a # list of rectangles that are considered dirty. (Rectangles # of all dirty children are updated if 'update_object' is None.) # 2. Clear the dirty rectangles in the backing store. # 3. For each child in the container, create a list of rectangles # that intersect with all dirty rectangles. Then blend the # intersection rectangle of that child to the backing store. # # It's a bit more complicated than that (read the code below for # the real truth :)), but that's the general approach. # width, height = self.get_size() left, top = self.get_pos() if 0 in (width, height): return None, [] if not self._backing_store_dirty or \ (use_cached and self._backing_store): return self._backing_store, [] # Offset of container image relative to container. Because container # children can have negative positions, (0,0) of the container may # not be (0,0) of the backing store image. offset_x, offset_y = self._get_child_min_pos() # List of dirty rectangles relative to the container. This means # a rect of ((0, 0), (100, 100)) may not map directly to the backing # store if offset_x or offset_y are non-zero. dirty_rects = [] if self._backing_store_info["dirty-rects"]: dirty_rects += self._backing_store_info["dirty-rects"] self._backing_store_info["dirty-rects"] = [] # If the alpha of the container has changed then we need to # invalidate the whole thing. if "alpha" not in self._backing_store_info or \ self._backing_store_info["alpha"] != self.alpha: self._backing_store_info["alpha"] = self.alpha # Optimization: if alpha has changed but none of the children # are dirty, and we already have a backing store, we just return # the backing store as is. It's up to the caller to blend us at # our alpha. dirty_rects.append(((offset_x, offset_y), (width, height))) if len(self.dirty_children) == 0 and self._backing_store: if update_object == self and update: self.unqueue_paint() return self._backing_store, dirty_rects if "pos" not in self._backing_store_info: # First time _get_backing_store() is called on this container, so # we initialize pos with the present position. self._backing_store_info["pos"] = self.get_pos() # 'uo' is used in the children loop below. If update_object is this # container (self), then all children must be updated too, so as far # as chlidren are concerned, update_object is None. if self == update_object: uo = None else: uo = update_object # STEP 1 # ====== # Iterate through all children and update the backing stories of all # dirty children (or rather, those dirty children according to 'ua'), # and collect a list of dirty rectangles relative to this container. # (Note rectangles are relative to the container, not the backing # store, so negative coordinates are ok; they will be translated # later). # Sort children in order of their z-index. self.children.sort(lambda a, b: cmp(a.zindex, b.zindex)) for child in self.children: if not child.dirty: continue child_x, child_y = child.get_pos() child_w, child_h = child.get_size() child_offset_x = child_offset_y = 0 # If update_object isn't this child, then we use the child's old # backing store position (if one exists). if uo and uo != child and hasattr(child, "_backing_store_info") \ and "pos" in child._backing_store_info: child_x, child_y = child._backing_store_info["pos"] # If child is a container, we need to recurse. if isinstance(child, CanvasContainer): img, child_dirty_rects\ = child._get_backing_store(update, update_object = uo) # For each of the dirty rectangles returned by the above call, # translate them so they are relative to this container, # rather than the child container. dirty_rects += rect.offset_list(child_dirty_rects, (child_x, child_y)) # Get the container's min coordinates; they are used below. child_offset_x, child_offset_y = child._get_child_min_pos() elif not hasattr(child, "_backing_store_info"): # First time for this non-container child. child._backing_store_info = {} # If this child is not the requested update object, continue on. # Note that we traverse child containers because the update_object # may be one of our grand children. if uo and uo != child: continue # 'bsi' for convenience. bsi = child._backing_store_info # If the child is invisible (either hidden or alpha of 0) then # we don't bother with this child. # ***************************************** # FIXME: # Dischi to Tack: Isn't that a bad idea? We just made it not # dirty and by calling continue now, we will never set # the child to dirty=false # ***************************************** # if ("visible" in bsi and not child.visible and \ # not bsi["visible"]) or \ # ("alpha" in bsi and child.alpha == 0 and bsi["alpha"] == 0): # continue # If the child has either changed positions or changed sizes (or # both), we add the child's old rectangle and the new one to the # list of dirty rectangles. if "size" in bsi and (bsi["pos"] != (child_x, child_y) or \ bsi["size"] != (child_w, child_h)): # Positions get offsetted by child_offset_x/y which are both 0 # for non-containers, but may be non-zero for containers. We # need to translate child containers with children with # negative coordinates. # Old rectangle. dirty_rects.append(((bsi["pos"][0] + child_offset_x, bsi["pos"][1] + child_offset_y), bsi["size"])) # New rectangle. dirty_rects.append(((child_x + child_offset_x, child_y + child_offset_y), (child_w, child_h) )) # If visibility or zindex has changed, entire child region gets # invalidated. elif ("visible" in bsi and bsi["visible"] != child.visible) or \ ("z-index" in bsi and bsi["z-index"] != child.zindex): dirty_rects.append(( (child_x + child_offset_x, child_y + child_offset_y), (child_w, child_h) )) # If we've gotten this far and the child is an image, then we # assume the whole image needs blitting. elif isinstance(child, CanvasImage): dirty_rects.append(( (child_x, child_y), (child_w, child_h) )) # Remember the current values. bsi["pos"] = child_x, child_y bsi["size"] = child_w, child_h bsi["alpha"] = child.alpha bsi["z-index"] = child.zindex bsi["visible"] = child.visible if update: child.unqueue_paint() if isinstance(child, CanvasImage): child.needs_blitting(False) # If we've found the child whose updated was requested, there's # no need to loop any further. if uo == child: break # END CHILDREN LOOP # Unqueue paint for this container if necesary. if update_object == self and update: self.unqueue_paint() # So now we have a list of dirty rectangles. We optimize the # list for rendering by removing redundant rectangles (if A is fully # contained in B, remove A), and removing interections. # FIXME: Tacl doesm't like this hack, but it is faster if len(dirty_rects) > 10: # more changes than children, reduce the dirty_rects # by making all a big union. This is not correct, but will # speed up the later blitting. dirty_rects = [ reduce(lambda x,y: rect.union(x,y), dirty_rects) ] else: dirty_rects = rect.optimize_for_rendering(dirty_rects) # STEP 2 # ====== # Clear all dirty rectangles on the backing store. "Clear" in this # context means setting those pixels to (0, 0, 0, 0). bs = self._backing_store if not bs: # First time calling _get_backing_store() on this container or it # has resized, so create backing store image. bs = imagelib.new( (width, height) ) if not preserve_alpha: bs.draw_rectangle((0, 0), (width, height), (0, 0, 0, 255), fill=True) self._backing_store = bs else: # If the container has resized, we create a backing store of the # new size and copy the old contents over. Dirty regions are # still valid, and they are relative to the container's new # dimensions. if bs and (width, height) != bs.size: bs_width, bs_height = bs.size old_offset_x, old_offset_y = self._backing_store_info["offset"] move_x = old_offset_x - offset_x move_y = old_offset_y - offset_y # Grow the backing store if needed. if width > bs_width or height > bs_height: img = imagelib.new( (width, height) ) img.blend(bs, (move_x, move_y), alpha = 256) self._backing_store = bs = img else: # We're not growing the backing store, but we still need # to shift its contents. So we do that, and then dirty # the edges that are stale. bs.copy_rect((0, 0), bs.size, (move_x, move_y)) if move_x < 0: dirty_rects.append(((width + move_x, 0), (bs_width-width, bs_height))) if move_y < 0: dirty_rects.append(((0, height + move_y), (bs_width, bs_height-height))) dirty_rects = rect.remove_intersections(dirty_rects) for r in dirty_rects: # Clip rect to backing store image boundary. r = rect.clip(r, ((offset_x, offset_y), bs.size) ) if preserve_alpha: bs.clear( (r[0][0] - offset_x, r[0][1] - offset_y), r[1] ) else: bs.draw_rectangle((r[0][0] - offset_x, r[0][1] - offset_y), r[1] , (0, 0, 0, 255), fill=True) self._backing_store_info["offset"] = (offset_x, offset_y) self._backing_store_info["size"] = (width, height) # STEP 3 # ====== # Render the intersections between all children and all dirty # rectangles. Children are already sorted in order of z-index. for child in self.children: if not child.visible or child.alpha <= 0 or not dirty_rects: continue child_size = child.get_size() if child_size == (0,0): # ignore this child, it is empty continue # Use the backing store position if it exists. # Note: I don't know why a child can't have one, but it # happens :( if hasattr(child, "_backing_store_info") and \ "pos" in child._backing_store_info: child_x, child_y = child._backing_store_info["pos"] else: child_x, child_y = child.get_pos() if isinstance(child, CanvasContainer): r = child._get_drawing_rect() if not r: # ignore this child, it is empty continue child_size = (r[0]+r[2], r[1]+r[3]) geometry = ((r[0], r[1]), child_size) child_offset_y = r[1] - child_y else: geometry = ((child_x, child_y), child_size) # Get all regions this child intersects with. intersects = [] for r in dirty_rects: intersect = rect.intersect( geometry, r) if intersect != rect.empty: intersects.append(intersect) # We must remove all intersections from the list because drawing # over a rectangle multiple times with an image whose opacity is # less than 255 will cause incorrect results. (Intersections are # collapsed into unions.) remove_intersections() could be more # intelligent: see rect.py for details. draw_regions = rect.remove_intersections(intersects) if len(draw_regions) == 0: continue # FIXME: image objects don't have backing stores; we just use the # underlying image directly. This is broken because it may happen # that update_object is a sibling or "uncle" to an image that has # been modified, we should blit the old image before the update, # not the new one. IN PRACTICE this likely isn't a problem, # however it is not correct according to how the canvas should # behave. if isinstance(child, CanvasContainer): img = child._get_backing_store(use_cached = True)[0] else: img = child.image for r in draw_regions: rx, ry = r[0] dst_x, dst_y = rx - offset_x, ry - offset_y src_x, src_y = rx - child_x, ry - child_y src_w, src_h = dst_w, dst_h = r[1] self._backing_store.blend(img, (dst_x, dst_y), (dst_w, dst_h), (src_x, src_y), (src_w, src_h), alpha = child.alpha, merge_alpha = preserve_alpha) if not update_object or update_object == self: self._backing_store_dirty = False return self._backing_store, dirty_rects