N = tiledsurface.N
LOAD_CHUNK_SIZE = 64*1024
+from layer import DEFAULT_COMPOSITE_OP, VALID_COMPOSITE_OPS
+
class SaveLoadError(Exception):
"""Expected errors on loading or saving, like missing permissions or non-existing files."""
pass
# or discarded as empty before any other action is possible.
# (split_stroke)
- def __init__(self):
- self.brush = brush.Brush()
+ def __init__(self, brushinfo=None):
+ if not brushinfo:
+ brushinfo = brush.BrushInfo()
+ brushinfo.load_defaults()
+ self.brush = brush.Brush(brushinfo)
self.stroke = None
self.canvas_observers = []
self.stroke_observers = [] # callback arguments: stroke, brush (brush is a temporary read-only convenience object)
self.doc_observers = []
self.frame_observers = []
+ self.command_stack_observers = []
self.clear(True)
self._frame = [0, 0, 0, 0]
if not init:
bbox = self.get_bbox()
# throw everything away, including undo stack
+
self.command_stack = command.CommandStack()
+ self.command_stack.stack_observers = self.command_stack_observers
self.set_background((255, 255, 255))
self.layers = []
self.layer_idx = None
def select_layer(self, idx):
self.do(command.SelectLayer(self, idx))
- def move_layer(self, was_idx, new_idx):
- self.do(command.MoveLayer(self, was_idx, new_idx))
+ def move_layer(self, was_idx, new_idx, select_new=False):
+ self.do(command.MoveLayer(self, was_idx, new_idx, select_new))
+
+ def duplicate_layer(self, insert_idx=None, name=''):
+ self.do(command.DuplicateLayer(self, insert_idx, name))
+
+ def reorder_layers(self, new_layers):
+ self.do(command.ReorderLayers(self, new_layers))
def clear_layer(self):
- if not self.layer.surface.is_empty():
+ if not self.layer.is_empty():
self.do(command.ClearLayer(self))
- def stroke_to(self, dtime, x, y, pressure, xtilt,ytilt):
+ def stroke_to(self, dtime, x, y, pressure, xtilt, ytilt):
if not self.stroke:
self.stroke = stroke.Stroke()
self.stroke.start_recording(self.brush)
self.snapshot_before_stroke = self.layer.save_snapshot()
- self.stroke.record_event(dtime, x, y, pressure, xtilt,ytilt)
+ self.stroke.record_event(dtime, x, y, pressure, xtilt, ytilt)
- l = self.layer
- l.surface.begin_atomic()
- split = self.brush.stroke_to (l.surface, x, y, pressure, xtilt,ytilt, dtime)
- l.surface.end_atomic()
+ split = self.layer.stroke_to(self.brush, x, y,
+ pressure, xtilt, ytilt, dtime)
if split:
self.split_stroke()
+ def redo_last_stroke_with_different_brush(self, brush):
+ cmd = self.get_last_command()
+ if not isinstance(cmd, command.Stroke):
+ return
+ cmd = self.undo()
+ assert isinstance(cmd, command.Stroke)
+ new_stroke = cmd.stroke.copy_using_different_brush(brush)
+ snapshot_before = self.layer.save_snapshot()
+ new_stroke.render(self.layer._surface)
+ self.do(command.Stroke(self, new_stroke, snapshot_before))
+
def straight_line(self, src, dst):
self.split_stroke()
- # TODO: undo last stroke if it was very short... (but not at document level?)
- real_brush = self.brush
- self.brush = brush.Brush()
- self.brush.copy_settings_from(real_brush)
+ self.brush.reset() # reset dynamic states (eg. filtered velocity)
duration = 3.0
pressure = 0.3
for i in xrange(N):
self.stroke_to(duration/N, x[i], y[i], pressure, 0.0, 0.0)
self.split_stroke()
- self.brush = real_brush
+ self.brush.reset()
def layer_modified_cb(self, *args):
self.split_stroke()
return self.command_stack.get_last_command()
- def set_brush(self, brush):
- self.split_stroke()
- self.brush.copy_settings_from(brush)
-
def get_bbox(self):
res = helpers.Rect()
for layer in self.layers:
# OPTIMIZE: only visible layers...
# careful: currently saving assumes that all layers are included
- bbox = layer.surface.get_bbox()
+ bbox = layer.get_bbox()
res.expandToIncludeRect(bbox)
return res
background.blit_tile_into(dst, tx, ty, mipmap_level)
for layer in layers:
- surface = layer.surface
- surface.composite_tile_over(dst, tx, ty, mipmap_level=mipmap_level, opacity=layer.effective_opacity)
+ surface = layer._surface
+ surface.composite_tile(dst, tx, ty,
+ mipmap_level=mipmap_level,
+ opacity=layer.effective_opacity,
+ mode=layer.compositeop)
mypaintlib.tile_convert_rgb16_to_rgb8(dst, dst_8bit)
self.undo()
self.do(command.SetLayerOpacity(self, opacity, layer))
+ def set_layer_compositeop(self, compositeop, layer=None):
+ """Sets the composition-operation of a layer. If layer=None, works on the current layer"""
+ if compositeop not in VALID_COMPOSITE_OPS:
+ compositeop = DEFAULT_COMPOSITE_OP
+ cmd = self.get_last_command()
+ if isinstance(cmd, command.SetLayerCompositeOp):
+ self.undo()
+ self.do(command.SetLayerCompositeOp(self, compositeop, layer))
+
def set_background(self, obj):
# This is not an undoable action. One reason is that dragging
# on the color chooser would get tons of undo steps.
def is_layered(self):
count = 0
for l in self.layers:
- if not l.surface.is_empty():
+ if not l.is_empty():
count += 1
return count > 1
def is_empty(self):
- return len(self.layers) == 1 and self.layer.surface.is_empty()
+ return len(self.layers) == 1 and self.layer.is_empty()
def save(self, filename, **kwargs):
self.split_stroke()
- trash, ext = os.path.splitext(filename)
+ junk, ext = os.path.splitext(filename)
ext = ext.lower().replace('.', '')
save = getattr(self, 'save_' + ext, self.unsupported)
- try:
+ try:
save(filename, **kwargs)
except gobject.GError, e:
traceback.print_exc()
raise SaveLoadError, _('File does not exist: %s') % repr(filename)
if not os.access(filename,os.R_OK):
raise SaveLoadError, _('You do not have the necessary permissions to open file: %s') % repr(filename)
- trash, ext = os.path.splitext(filename)
+ junk, ext = os.path.splitext(filename)
ext = ext.lower().replace('.', '')
load = getattr(self, 'load_' + ext, self.unsupported)
try:
def render_thumbnail(self):
t0 = time.time()
x, y, w, h = self.get_effective_bbox()
+ if w == 0 or h == 0:
+ # workaround to save empty documents
+ x, y, w, h = 0, 0, tiledsurface.N, tiledsurface.N
mipmap_level = 0
while mipmap_level < tiledsurface.MAX_MIPMAP_LEVEL and max(w, h) >= 512:
mipmap_level += 1
tmp_layer = layer.Layer()
for l in self.layers:
l.merge_into(tmp_layer)
- tmp_layer.surface.save(filename, *doc_bbox)
+ tmp_layer.save_as_png(filename, *doc_bbox)
else:
pixbufsurface.save_as_png(self, filename, *doc_bbox, alpha=False, **kwargs)
doc_bbox = self.get_effective_bbox()
for i, l in enumerate(self.layers):
filename = '%s.%03d%s' % (prefix, i+1, ext)
- l.surface.save(filename, *doc_bbox, **kwargs)
+ l.save_as_png(filename, *doc_bbox, **kwargs)
@staticmethod
def _pixbuf_from_stream(fp, feedback_cb=None):
load_jpeg = load_from_pixbuf_file
def save_jpg(self, filename, quality=90, **kwargs):
- doc_bbox = self.get_effective_bbox()
- pixbuf = self.render_as_pixbuf(*doc_bbox, **kwargs)
+ x, y, w, h = self.get_effective_bbox()
+ if w == 0 or h == 0:
+ x, y, w, h = 0, 0, N, N # allow to save empty documents
+ pixbuf = self.render_as_pixbuf(x, y, w, h, **kwargs)
pixbuf.save(filename, 'jpeg', options={'quality':str(quality)})
save_jpeg = save_jpg
def store_surface(surface, name, rect=[]):
tmp = join(tempdir, 'tmp.png')
t1 = time.time()
- surface.save(tmp, *rect, **kwargs)
+ surface.save_as_png(tmp, *rect, **kwargs)
print ' %.3fs surface saving %s' % (time.time() - t1, name)
z.write(tmp, name)
os.remove(tmp)
- def add_layer(x, y, opac, surface, name, layer_name, visible=True, rect=[]):
+ def add_layer(x, y, opac, surface, name, layer_name, visible=True, compositeop=DEFAULT_COMPOSITE_OP, rect=[]):
layer = ET.Element('layer')
stack.append(layer)
store_surface(surface, name, rect)
a['x'] = str(x)
a['y'] = str(y)
a['opacity'] = str(opac)
+ if compositeop not in VALID_COMPOSITE_OPS:
+ compositeop = DEFAULT_COMPOSITE_OP
+ a['composite-op'] = compositeop
if visible:
a['visibility'] = 'visible'
else:
return layer
for idx, l in enumerate(reversed(self.layers)):
- if l.surface.is_empty():
+ if l.is_empty():
continue
opac = l.opacity
- x, y, w, h = l.surface.get_bbox()
- el = add_layer(x-x0, y-y0, opac, l.surface, 'data/layer%03d.png' % idx, l.name, l.visible, rect=(x, y, w, h))
+ x, y, w, h = l.get_bbox()
+ el = add_layer(x-x0, y-y0, opac, l._surface, 'data/layer%03d.png' % idx, l.name, l.visible, l.compositeop, rect=(x, y, w, h))
# strokemap
sio = StringIO()
l.save_strokemap_to_file(sio, -x, -y)
bg = self.background
# save as fully rendered layer
x, y, w, h = self.get_bbox()
- l = add_layer(x, y, 1.0, bg, 'data/background.png', 'background', rect=(x,y,w,h))
+ l = add_layer(x-x0, y-y0, 1.0, bg, 'data/background.png', 'background',
+ DEFAULT_COMPOSITE_OP, rect=(x,y,w,h))
x, y, w, h = bg.get_pattern_bbox()
# save as single pattern (with corrected origin)
store_surface(bg, 'data/background_tile.png', rect=(x+x0, y+y0, w, h))
x = int(a.get('x', '0'))
y = int(a.get('y', '0'))
opac = float(a.get('opacity', '1.0'))
+ compositeop = str(a.get('composite-op', DEFAULT_COMPOSITE_OP))
+ if compositeop not in VALID_COMPOSITE_OPS:
+ compositeop = DEFAULT_COMPOSITE_OP
+
visible = not 'hidden' in a.get('visibility', 'visible')
self.add_layer(insert_idx=0, name=name)
last_pixbuf = pixbuf
layer = self.layers[0]
self.set_layer_opacity(helpers.clamp(opac, 0.0, 1.0), layer)
+ self.set_layer_compositeop(compositeop, layer)
self.set_layer_visibility(visible, layer)
print ' %.3fs converting pixbuf to layer format' % (time.time() - t1)
# strokemap
sio.close()
if len(self.layers) == 1:
- raise ValueError, 'Could not load any layer.'
+ # no assertion (allow empty documents)
+ print 'Warning: Could not load any layer, document is empty.'
if no_background:
# recognize solid or tiled background layers, at least those that mypaint <= 0.7.1 saves
t1 = time.time()
p = last_pixbuf
if not p.get_has_alpha() and p.get_width() % N == 0 and p.get_height() % N == 0:
- tiles = self.layers[0].surface.tiledict.values()
+ tiles = self.layers[0]._surface.tiledict.values()
if len(tiles) > 1:
all_equal = True
for tile in tiles[1:]: