# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
-import tiledsurface
+import struct, zlib
+from numpy import *
+from gettext import gettext as _
+
+import tiledsurface, strokemap
+
+COMPOSITE_OPS = [
+ # (internal-name, display-name)
+ ("svg:src-over", _("Normal")),
+ ("svg:multiply", _("Multiply")),
+ ("svg:color-burn", _("Burn")),
+ ("svg:color-dodge", _("Dodge")),
+ ("svg:screen", _("Screen")),
+ ]
+
+DEFAULT_COMPOSITE_OP = COMPOSITE_OPS[0][0]
+VALID_COMPOSITE_OPS = set([n for n, d in COMPOSITE_OPS])
class Layer:
- # A layer contains a list of strokes, and possibly a background
- # pixmap. There is also a surface with those strokes rendered.
- #
- # Some history from the time when each snapshot required a
- # larger-than-screen pixbuf: The undo system used to work by
- # finding the closest snapshot (there were only two or three of
- # them) and rerendering the missing strokes. There used to be
- # quite some generic code here for this. The last svn revision
- # including this code was r242.
- #
- # Now the strokes are here just for possible future features, like
- # "pick brush from stroke", or "repaint layer with different
- # brush" for brush previews.
- #
- # The strokes don't take much memory compared to the snapshots
- # IMO, but this should better be measured...
-
- def __init__(self):
- self.surface = tiledsurface.Surface()
- self.strokes = []
- self.background = None
+ """Representation of a layer in the document model.
+
+ The actual content of the layer is held by the surface implementation.
+ This is an internal detail that very few consumers should care about."""
+
+ def __init__(self, name="", compositeop=DEFAULT_COMPOSITE_OP):
+ self._surface = tiledsurface.Surface()
+ self.opacity = 1.0
+ self.name = name
+ self.visible = True
+ self.locked = False
+ self.compositeop = compositeop
+ # Called when contents of layer changed,
+ # with the bounding box of the changed region
+ self.content_observers = []
+
+ # Forward from surface implementation
+ self._surface.observers.append(self._notify_content_observers)
+
+ self.clear()
+
+ def _notify_content_observers(self, *args):
+ for f in self.content_observers:
+ f(*args)
+
+ def get_effective_opacity(self):
+ if self.visible:
+ return self.opacity
+ else:
+ return 0.0
+ effective_opacity = property(get_effective_opacity)
+
+ def get_alpha(self, x, y, radius):
+ return self._surface.get_alpha(x, y, radius)
+
+ def get_bbox(self):
+ return self._surface.get_bbox()
+
+ def is_empty(self):
+ return self._surface.is_empty()
+
+ def save_as_png(self, filename, *args, **kwargs):
+ self._surface.save_as_png(filename, *args, **kwargs)
+
+ def stroke_to(self, brush, x, y, pressure, xtilt, ytilt, dtime):
+ """Render a part of a stroke."""
+ self._surface.begin_atomic()
+ split = brush.stroke_to(self._surface, x, y,
+ pressure, xtilt, ytilt, dtime)
+ self._surface.end_atomic()
+ return split
def clear(self):
- self.background = None
- self.strokes = []
- self.surface.clear()
+ self.strokes = [] # contains StrokeShape instances (not stroke.Stroke)
+ self._surface.clear()
- def load_from_pixbuf(self, pixbuf):
+ def load_from_surface(self, surface):
self.strokes = []
- self.background = pixbuf
- self.surface.load_from_data(pixbuf)
+ self._surface.load_from_surface(surface)
+
+ def render_as_pixbuf(self, *rect, **kwargs):
+ return self._surface.render_as_pixbuf(*rect, **kwargs)
def save_snapshot(self):
- return (self.strokes[:], self.background, self.surface.save_snapshot())
+ return (self.strokes[:], self._surface.save_snapshot(), self.opacity)
def load_snapshot(self, data):
- strokes, background, data = data
+ strokes, data, self.opacity = data
self.strokes = strokes[:]
- self.background = background
- self.surface.load_snapshot(data)
+ self._surface.load_snapshot(data)
+
+
+ def translate(self, dx, dy):
+ """Translate a layer non-interactively.
+ """
+ move = self.get_move(0, 0)
+ move.update(dx, dy)
+ move.process(n=-1)
+ move.cleanup()
+
+
+ def get_move(self, x, y):
+ """Get a translation/move object for this layer.
+ """
+ return self._surface.get_move(x, y)
+
+
+ def add_stroke(self, stroke, snapshot_before):
+ before = snapshot_before[1] # extract surface snapshot
+ after = self._surface.save_snapshot()
+ shape = strokemap.StrokeShape()
+ shape.init_from_snapshots(before, after)
+ shape.brush_string = stroke.brush_settings
+ self.strokes.append(shape)
+
+
+ def save_strokemap_to_file(self, f, translate_x, translate_y):
+ brush2id = {}
+ for stroke in self.strokes:
+ s = stroke.brush_string
+ # save brush (if not already known)
+ if s not in brush2id:
+ brush2id[s] = len(brush2id)
+ s = zlib.compress(s)
+ f.write('b')
+ f.write(struct.pack('>I', len(s)))
+ f.write(s)
+ # save stroke
+ s = stroke.save_to_string(translate_x, translate_y)
+ f.write('s')
+ f.write(struct.pack('>II', brush2id[stroke.brush_string], len(s)))
+ f.write(s)
+ f.write('}')
+
+
+ def load_strokemap_from_file(self, f, translate_x, translate_y):
+ assert not self.strokes
+ brushes = []
+ while True:
+ t = f.read(1)
+ if t == 'b':
+ length, = struct.unpack('>I', f.read(4))
+ tmp = f.read(length)
+ brushes.append(zlib.decompress(tmp))
+ elif t == 's':
+ brush_id, length = struct.unpack('>II', f.read(2*4))
+ stroke = strokemap.StrokeShape()
+ tmp = f.read(length)
+ stroke.init_from_string(tmp, translate_x, translate_y)
+ stroke.brush_string = brushes[brush_id]
+ self.strokes.append(stroke)
+ elif t == '}':
+ break
+ else:
+ assert False, 'invalid strokemap'
def merge_into(self, dst):
"""
- Merges this layer into dst, modifying only dst.
+ Merge this layer into dst, modifying only dst.
"""
+ # We must respect layer visibility, because saving a
+ # transparent PNG just calls this function for each layer.
src = self
- dst.background = None # hm... this breaks "full-rerender" capability, but should work fine... FIXME: redesign needed?
- dst.strokes = [] # this one too...
- for tx, ty in src.surface.get_tiles():
- src.surface.composite_tile_over(dst.surface.get_tile_memory(tx, ty, readonly=False), tx, ty)
+ dst.strokes.extend(self.strokes)
+ for tx, ty in dst._surface.get_tiles():
+ surf = dst._surface.get_tile_memory(tx, ty, readonly=False)
+ surf[:,:,:] = dst.effective_opacity * surf[:,:,:]
+ for tx, ty in src._surface.get_tiles():
+ surf = dst._surface.get_tile_memory(tx, ty, readonly=False)
+ src._surface.composite_tile(surf, tx, ty,
+ opacity=self.effective_opacity,
+ mode=self.compositeop)
+ dst.opacity = 1.0
+
+ def get_stroke_info_at(self, x, y):
+ x, y = int(x), int(y)
+ for s in reversed(self.strokes):
+ if s.touches_pixel(x, y):
+ return s
+
+ def get_last_stroke_info(self):
+ if not self.strokes:
+ return None
+ return self.strokes[-1]