# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
+import struct, zlib
from numpy import *
-import time
+from gettext import gettext as _
-import tiledsurface
-N = tiledsurface.N
+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()
+ """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 = [] # contains StrokeInfo instances (not stroke.Stroke)
- 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[2] # extract surface snapshot
- after = self.surface.save_snapshot()
- self.strokes.append(StrokeInfo(stroke, before, after))
+ 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):
"""
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.extend(self.strokes) # TODO: test this codepath!
- for tx, ty in src.surface.get_tiles():
- src.surface.composite_tile_over(dst.surface.get_tile_memory(tx, ty, readonly=False), tx, ty)
-
- def get_brush_at(self, x, y):
+ 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.brush
-
-
-
-class StrokeInfo:
- def __init__(self, stroke, snapshot_before, snapshot_after):
- self.brush = stroke.brush_settings
- t0 = time.time()
- # extract the layer from each snapshot
- a, b = snapshot_before.tiledict, snapshot_after.tiledict
- # enumerate all tiles that have changed
- a_tiles = set(a.items())
- b_tiles = set(b.items())
- changes = a_tiles.symmetric_difference(b_tiles)
- tiles_modified = set([pos for pos, data in changes])
-
- # for each tile, calculate the exact difference and update our brushmap
- self.strokemap = {}
- for tx, ty in tiles_modified:
- # get the pixel data to compare
- a_data = a.get((tx, ty), tiledsurface.transparent_tile).rgba
- b_data = b.get((tx, ty), tiledsurface.transparent_tile).rgba
-
- # calculate the "perceptual" amount of difference
- absdiff = zeros((N, N), 'uint32')
- for i in range(4): # RGBA
- absdiff += abs(a_data[:,:,i].astype('uint32') - b_data[:,:,i])
- # ignore badly visible (parts of) strokes, eg. very faint strokes
- #
- # This is an arbitrary threshold. If it is too high, an
- # ink stroke with slightly different color than the one
- # below will not be pickable. If it is too high, barely
- # visible strokes will make things below unpickable.
- #
- threshold = (1<<15)*4 / 16 # require 1/16 of the max difference (also not bad: 1/8)
- is_different = absdiff > threshold
- # except if there is no previous stroke below it
- is_different |= (absdiff > 0) #& (brushmap_data == 0) --- FIXME: not possible any more
- self.strokemap[tx, ty] = is_different
-
- print 'brushmap update took %.3f seconds' % (time.time() - t0)
-
- def touches_pixel(self, x, y):
- tile = self.strokemap.get((x/N, y/N))
- if tile is not None:
- return tile[y%N, x%N]
+ return s
+
+ def get_last_stroke_info(self):
+ if not self.strokes:
+ return None
+ return self.strokes[-1]