1 # This file is part of MyPaint.
2 # Copyright (C) 2007-2008 by Martin Renold <martinxyz@gmx.ch>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 import os, zipfile, tempfile, time, traceback
11 from cStringIO import StringIO
12 import xml.etree.ElementTree as ET
15 from gettext import gettext as _
17 import helpers, tiledsurface, pixbufsurface, backgroundsurface, mypaintlib
18 import command, stroke, layer
22 LOAD_CHUNK_SIZE = 64*1024
24 from layer import DEFAULT_COMPOSITE_OP, VALID_COMPOSITE_OPS
26 class SaveLoadError(Exception):
27 """Expected errors on loading or saving, like missing permissions or non-existing files."""
32 This is the "model" in the Model-View-Controller design.
33 (The "view" would be ../gui/tileddrawwidget.py.)
34 It represents everything that the user would want to save.
37 The "controller" mostly in drawwindow.py.
38 It is possible to use it without any GUI attached (see ../tests/)
40 # Please note the following difficulty with the undo stack:
42 # Most of the time there is an unfinished (but already rendered)
43 # stroke pending, which has to be turned into a command.Action
44 # or discarded as empty before any other action is possible.
47 def __init__(self, brushinfo=None):
49 brushinfo = brush.BrushInfo()
50 brushinfo.load_defaults()
51 self.brush = brush.Brush(brushinfo)
53 self.canvas_observers = []
54 self.stroke_observers = [] # callback arguments: stroke, brush (brush is a temporary read-only convenience object)
55 self.doc_observers = []
56 self.frame_observers = []
57 self.command_stack_observers = []
60 self._frame = [0, 0, 0, 0]
61 self._frame_enabled = False
62 # Used by move_frame() to accumulate values
69 def move_frame(self, dx=0.0, dy=0.0):
70 """Move the frame. Accumulates changes and moves the frame once
71 the accumulated change reaches the minimum move step."""
72 # FIXME: Should be 1 (pixel aligned), not tile aligned
73 # This is due to PNG saving having to be tile aligned
76 def round_to_n(value, n):
77 return int(round(value/n)*n)
79 x, y, w, h = self.get_frame()
83 step_x = round_to_n(self._frame_dx, min_step)
84 step_y = round_to_n(self._frame_dy, min_step)
87 self.set_frame(x=x+step_x)
88 self._frame_dx -= step_x
91 self.set_frame(y=y+step_y)
92 self._frame_dy -= step_y
94 def set_frame(self, x=None, y=None, width=None, height=None):
95 """Set the size of the frame. Pass None to indicate no-change."""
97 for i, var in enumerate([x, y, width, height]):
99 # FIXME: must be aligned to tile size due to PNG saving
100 assert not var % N, "Frame size must be aligned to tile size"
103 for f in self.frame_observers: f()
105 def get_frame_enabled(self):
106 return self._frame_enabled
108 def set_frame_enabled(self, enabled):
109 self._frame_enabled = enabled
110 for f in self.frame_observers: f()
111 frame_enabled = property(get_frame_enabled)
113 def call_doc_observers(self):
114 for f in self.doc_observers:
118 def clear(self, init=False):
121 bbox = self.get_bbox()
122 # throw everything away, including undo stack
124 self.command_stack = command.CommandStack()
125 self.command_stack.stack_observers = self.command_stack_observers
126 self.set_background((255, 255, 255))
128 self.layer_idx = None
130 # disallow undo of the first layer
131 self.command_stack.clear()
132 self.unsaved_painting_time = 0.0
135 for f in self.canvas_observers:
138 self.call_doc_observers()
140 def get_current_layer(self):
141 return self.layers[self.layer_idx]
142 layer = property(get_current_layer)
144 def split_stroke(self):
145 if not self.stroke: return
146 self.stroke.stop_recording()
147 if not self.stroke.empty:
148 self.command_stack.do(command.Stroke(self, self.stroke, self.snapshot_before_stroke))
149 del self.snapshot_before_stroke
150 self.unsaved_painting_time += self.stroke.total_painting_time
151 for f in self.stroke_observers:
152 f(self.stroke, self.brush)
155 def select_layer(self, idx):
156 self.do(command.SelectLayer(self, idx))
158 def move_layer(self, was_idx, new_idx, select_new=False):
159 self.do(command.MoveLayer(self, was_idx, new_idx, select_new))
161 def duplicate_layer(self, insert_idx=None, name=''):
162 self.do(command.DuplicateLayer(self, insert_idx, name))
164 def reorder_layers(self, new_layers):
165 self.do(command.ReorderLayers(self, new_layers))
167 def clear_layer(self):
168 if not self.layer.is_empty():
169 self.do(command.ClearLayer(self))
171 def stroke_to(self, dtime, x, y, pressure, xtilt,ytilt):
173 self.stroke = stroke.Stroke()
174 self.stroke.start_recording(self.brush)
175 self.snapshot_before_stroke = self.layer.save_snapshot()
176 self.stroke.record_event(dtime, x, y, pressure, xtilt,ytilt)
179 l._surface.begin_atomic()
180 split = self.brush.stroke_to (l._surface, x, y, pressure, xtilt,ytilt, dtime)
181 l._surface.end_atomic()
186 def redo_last_stroke_with_different_brush(self, brush):
187 cmd = self.get_last_command()
188 if not isinstance(cmd, command.Stroke):
191 assert isinstance(cmd, command.Stroke)
192 new_stroke = cmd.stroke.copy_using_different_brush(brush)
193 snapshot_before = self.layer.save_snapshot()
194 new_stroke.render(self.layer._surface)
195 self.do(command.Stroke(self, new_stroke, snapshot_before))
197 def straight_line(self, src, dst):
199 self.brush.reset() # reset dynamic states (eg. filtered velocity)
204 x = numpy.linspace(src[0], dst[0], N)
205 y = numpy.linspace(src[1], dst[1], N)
206 # rest the brush in src for a minute, to avoid interpolation
207 # from the upper left corner (states are zero) (FIXME: the
208 # brush should handle this on its own, maybe?)
209 self.stroke_to(60.0, x[0], y[0], 0.0, 0.0, 0.0)
211 self.stroke_to(duration/N, x[i], y[i], pressure, 0.0, 0.0)
216 def layer_modified_cb(self, *args):
217 # for now, any layer modification is assumed to be visible
218 for f in self.canvas_observers:
221 def invalidate_all(self):
222 for f in self.canvas_observers:
228 cmd = self.command_stack.undo()
229 if not cmd or not cmd.automatic_undo:
235 cmd = self.command_stack.redo()
236 if not cmd or not cmd.automatic_undo:
241 self.command_stack.do(cmd)
243 def get_last_command(self):
245 return self.command_stack.get_last_command()
249 for layer in self.layers:
250 # OPTIMIZE: only visible layers...
251 # careful: currently saving assumes that all layers are included
252 bbox = layer.get_bbox()
253 res.expandToIncludeRect(bbox)
256 def get_effective_bbox(self):
257 """Return the effective bounding box of the document.
258 If the frame is enabled, this is the bounding box of the frame,
259 else the (dynamic) bounding box of the document."""
260 return self.get_frame() if self.frame_enabled else self.get_bbox()
262 def blit_tile_into(self, dst_8bit, tx, ty, mipmap_level=0, layers=None, background=None):
265 if background is None:
266 background = self.background
268 assert dst_8bit.dtype == 'uint8'
269 dst = numpy.empty((N, N, 3), dtype='uint16')
271 background.blit_tile_into(dst, tx, ty, mipmap_level)
274 surface = layer._surface
275 #surface.composite_tile_over(dst, tx, ty, mipmap_level=mipmap_level, opacity=layer.effective_opacity)
276 surface.composite_tile(dst, tx, ty, mipmap_level=mipmap_level, opacity=layer.effective_opacity, mode=layer.compositeop)
278 mypaintlib.tile_convert_rgb16_to_rgb8(dst, dst_8bit)
280 def add_layer(self, insert_idx=None, after=None, name=''):
281 self.do(command.AddLayer(self, insert_idx, after, name))
283 def remove_layer(self,layer=None):
284 if len(self.layers) > 1:
285 self.do(command.RemoveLayer(self,layer))
289 def merge_layer_down(self):
290 dst_idx = self.layer_idx - 1
293 self.do(command.MergeLayer(self, dst_idx))
296 def load_layer_from_pixbuf(self, pixbuf, x=0, y=0):
297 arr = helpers.gdkpixbuf2numpy(pixbuf)
298 self.do(command.LoadLayer(self, arr, x, y))
300 def set_layer_visibility(self, visible, layer):
301 cmd = self.get_last_command()
302 if isinstance(cmd, command.SetLayerVisibility) and cmd.layer is layer:
304 self.do(command.SetLayerVisibility(self, visible, layer))
306 def set_layer_locked(self, locked, layer):
307 cmd = self.get_last_command()
308 if isinstance(cmd, command.SetLayerLocked) and cmd.layer is layer:
310 self.do(command.SetLayerLocked(self, locked, layer))
312 def set_layer_opacity(self, opacity, layer=None):
313 """Sets the opacity of a layer. If layer=None, works on the current layer"""
314 cmd = self.get_last_command()
315 if isinstance(cmd, command.SetLayerOpacity):
317 self.do(command.SetLayerOpacity(self, opacity, layer))
319 def set_layer_compositeop(self, compositeop, layer=None):
320 """Sets the composition-operation of a layer. If layer=None, works on the current layer"""
321 if compositeop not in VALID_COMPOSITE_OPS:
322 compositeop = DEFAULT_COMPOSITE_OP
323 cmd = self.get_last_command()
324 if isinstance(cmd, command.SetLayerCompositeOp):
326 self.do(command.SetLayerCompositeOp(self, compositeop, layer))
328 def set_background(self, obj):
329 # This is not an undoable action. One reason is that dragging
330 # on the color chooser would get tons of undo steps.
332 if not isinstance(obj, backgroundsurface.Background):
333 obj = backgroundsurface.Background(obj)
334 self.background = obj
336 self.invalidate_all()
338 def load_from_pixbuf(self, pixbuf):
339 """Load a document from a pixbuf."""
341 self.load_layer_from_pixbuf(pixbuf)
342 self.set_frame(*self.get_bbox())
344 def is_layered(self):
346 for l in self.layers:
352 return len(self.layers) == 1 and self.layer.is_empty()
354 def save(self, filename, **kwargs):
356 junk, ext = os.path.splitext(filename)
357 ext = ext.lower().replace('.', '')
358 save = getattr(self, 'save_' + ext, self.unsupported)
360 save(filename, **kwargs)
361 except gobject.GError, e:
362 traceback.print_exc()
364 #add a hint due to a very consfusing error message when there is no space left on device
365 raise SaveLoadError, _('Unable to save: %s\nDo you have enough space left on the device?') % e.message
367 raise SaveLoadError, _('Unable to save: %s') % e.message
369 traceback.print_exc()
370 raise SaveLoadError, _('Unable to save: %s') % e.strerror
371 self.unsaved_painting_time = 0.0
373 def load(self, filename, **kwargs):
374 if not os.path.isfile(filename):
375 raise SaveLoadError, _('File does not exist: %s') % repr(filename)
376 if not os.access(filename,os.R_OK):
377 raise SaveLoadError, _('You do not have the necessary permissions to open file: %s') % repr(filename)
378 junk, ext = os.path.splitext(filename)
379 ext = ext.lower().replace('.', '')
380 load = getattr(self, 'load_' + ext, self.unsupported)
382 load(filename, **kwargs)
383 except gobject.GError, e:
384 traceback.print_exc()
385 raise SaveLoadError, _('Error while loading: GError %s') % e
387 traceback.print_exc()
388 raise SaveLoadError, _('Error while loading: IOError %s') % e
389 self.command_stack.clear()
390 self.unsaved_painting_time = 0.0
391 self.call_doc_observers()
393 def unsupported(self, filename, *args, **kwargs):
394 raise SaveLoadError, _('Unknown file format extension: %s') % repr(filename)
396 def render_as_pixbuf(self, *args, **kwargs):
397 return pixbufsurface.render_as_pixbuf(self, *args, **kwargs)
399 def render_thumbnail(self):
401 x, y, w, h = self.get_effective_bbox()
403 # workaround to save empty documents
404 x, y, w, h = 0, 0, tiledsurface.N, tiledsurface.N
406 while mipmap_level < tiledsurface.MAX_MIPMAP_LEVEL and max(w, h) >= 512:
408 x, y, w, h = x/2, y/2, w/2, h/2
410 pixbuf = self.render_as_pixbuf(x, y, w, h, mipmap_level=mipmap_level)
411 assert pixbuf.get_width() == w and pixbuf.get_height() == h
412 pixbuf = helpers.scale_proportionally(pixbuf, 256, 256)
413 print 'Rendered thumbnail in', time.time() - t0, 'seconds.'
416 def save_png(self, filename, alpha=False, multifile=False, **kwargs):
417 doc_bbox = self.get_effective_bbox()
419 self.save_multifile_png(filename, **kwargs)
422 tmp_layer = layer.Layer()
423 for l in self.layers:
424 l.merge_into(tmp_layer)
425 tmp_layer.save_as_png(filename, *doc_bbox)
427 pixbufsurface.save_as_png(self, filename, *doc_bbox, alpha=False, **kwargs)
429 def save_multifile_png(self, filename, alpha=False, **kwargs):
430 prefix, ext = os.path.splitext(filename)
431 # if we have a number already, strip it
432 l = prefix.rsplit('.', 1)
435 doc_bbox = self.get_effective_bbox()
436 for i, l in enumerate(self.layers):
437 filename = '%s.%03d%s' % (prefix, i+1, ext)
438 l.save_as_png(filename, *doc_bbox, **kwargs)
441 def _pixbuf_from_stream(fp, feedback_cb=None):
442 loader = gdk.PixbufLoader()
444 if feedback_cb is not None:
446 buf = fp.read(LOAD_CHUNK_SIZE)
451 return loader.get_pixbuf()
453 def load_from_pixbuf_file(self, filename, feedback_cb=None):
454 fp = open(filename, 'rb')
455 pixbuf = self._pixbuf_from_stream(fp, feedback_cb)
457 self.load_from_pixbuf(pixbuf)
459 load_png = load_from_pixbuf_file
460 load_jpg = load_from_pixbuf_file
461 load_jpeg = load_from_pixbuf_file
463 def save_jpg(self, filename, quality=90, **kwargs):
464 x, y, w, h = self.get_effective_bbox()
466 x, y, w, h = 0, 0, N, N # allow to save empty documents
467 pixbuf = self.render_as_pixbuf(x, y, w, h, **kwargs)
468 pixbuf.save(filename, 'jpeg', options={'quality':str(quality)})
472 def save_ora(self, filename, options=None, **kwargs):
475 tempdir = tempfile.mkdtemp('mypaint')
476 # use .tmp extension, so we don't overwrite a valid file if there is an exception
477 z = zipfile.ZipFile(filename + '.tmpsave', 'w', compression=zipfile.ZIP_STORED)
478 # work around a permission bug in the zipfile library: http://bugs.python.org/issue3394
479 def write_file_str(filename, data):
480 zi = zipfile.ZipInfo(filename)
481 zi.external_attr = 0100644 << 16
483 write_file_str('mimetype', 'image/openraster') # must be the first file
484 image = ET.Element('image')
485 stack = ET.SubElement(image, 'stack')
486 x0, y0, w0, h0 = self.get_effective_bbox()
491 def store_pixbuf(pixbuf, name):
492 tmp = join(tempdir, 'tmp.png')
494 pixbuf.save(tmp, 'png')
495 print ' %.3fs pixbuf saving %s' % (time.time() - t1, name)
499 def store_surface(surface, name, rect=[]):
500 tmp = join(tempdir, 'tmp.png')
502 surface.save_as_png(tmp, *rect, **kwargs)
503 print ' %.3fs surface saving %s' % (time.time() - t1, name)
507 def add_layer(x, y, opac, surface, name, layer_name, visible=True, compositeop=DEFAULT_COMPOSITE_OP, rect=[]):
508 layer = ET.Element('layer')
510 store_surface(surface, name, rect)
513 a['name'] = layer_name
517 a['opacity'] = str(opac)
518 if compositeop not in VALID_COMPOSITE_OPS:
519 compositeop = DEFAULT_COMPOSITE_OP
520 a['composite-op'] = compositeop
522 a['visibility'] = 'visible'
524 a['visibility'] = 'hidden'
527 for idx, l in enumerate(reversed(self.layers)):
531 x, y, w, h = l.get_bbox()
532 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))
535 l.save_strokemap_to_file(sio, -x, -y)
536 data = sio.getvalue(); sio.close()
537 name = 'data/layer%03d_strokemap.dat' % idx
538 el.attrib['mypaint_strokemap_v2'] = name
539 write_file_str(name, data)
541 # save background as layer (solid color or tiled)
543 # save as fully rendered layer
544 x, y, w, h = self.get_bbox()
545 l = add_layer(x-x0, y-y0, 1.0, bg, 'data/background.png', 'background',
546 DEFAULT_COMPOSITE_OP, rect=(x,y,w,h))
547 x, y, w, h = bg.get_pattern_bbox()
548 # save as single pattern (with corrected origin)
549 store_surface(bg, 'data/background_tile.png', rect=(x+x0, y+y0, w, h))
550 l.attrib['background_tile'] = 'data/background_tile.png'
554 print ' starting to render full image for thumbnail...'
556 thumbnail_pixbuf = self.render_thumbnail()
557 store_pixbuf(thumbnail_pixbuf, 'Thumbnails/thumbnail.png')
558 print ' total %.3fs spent on thumbnail' % (time.time() - t2)
560 helpers.indent_etree(image)
561 xml = ET.tostring(image, encoding='UTF-8')
563 write_file_str('stack.xml', xml)
566 if os.path.exists(filename):
567 os.remove(filename) # windows needs that
568 os.rename(filename + '.tmpsave', filename)
570 print '%.3fs save_ora total' % (time.time() - t0)
572 return thumbnail_pixbuf
574 def load_ora(self, filename, feedback_cb=None):
575 """Loads from an OpenRaster file"""
578 z = zipfile.ZipFile(filename)
579 print 'mimetype:', z.read('mimetype').strip()
580 xml = z.read('stack.xml')
581 image = ET.fromstring(xml)
582 stack = image.find('stack')
584 w = int(image.attrib['w'])
585 h = int(image.attrib['h'])
587 def round_up_to_n(value, n):
588 assert value >= 0, "function undefined for negative numbers"
592 value = value - residual + n
595 def get_pixbuf(filename):
599 fp = z.open(filename, mode='r')
601 # support for bad zip files (saved by old versions of the GIMP ORA plugin)
602 fp = z.open(filename.encode('utf-8'), mode='r')
603 print 'WARNING: bad OpenRaster ZIP file. There is an utf-8 encoded filename that does not have the utf-8 flag set:', repr(filename)
605 res = self._pixbuf_from_stream(fp, feedback_cb)
607 print ' %.3fs loading %s' % (time.time() - t1, filename)
610 def get_layers_list(root, x=0,y=0):
613 if item.tag == 'layer':
614 if 'x' in item.attrib:
615 item.attrib['x'] = int(item.attrib['x']) + x
616 if 'y' in item.attrib:
617 item.attrib['y'] = int(item.attrib['y']) + y
619 elif item.tag == 'stack':
620 stack_x = int( item.attrib.get('x', 0) )
621 stack_y = int( item.attrib.get('y', 0) )
622 res += get_layers_list(item, stack_x, stack_y)
624 print 'Warning: ignoring unsupported tag:', item.tag
627 self.clear() # this leaves one empty layer
629 # FIXME: don't require tile alignment for frame
630 self.set_frame(width=round_up_to_n(w, N), height=round_up_to_n(h, N))
632 for layer in get_layers_list(stack):
635 if 'background_tile' in a:
638 print a['background_tile']
639 self.set_background(get_pixbuf(a['background_tile']))
640 no_background = False
642 except backgroundsurface.BackgroundError, e:
643 print 'ORA background tile not usable:', e
645 src = a.get('src', '')
646 if not src.lower().endswith('.png'):
647 print 'Warning: ignoring non-png layer'
649 pixbuf = get_pixbuf(src)
650 name = a.get('name', '')
651 x = int(a.get('x', '0'))
652 y = int(a.get('y', '0'))
653 opac = float(a.get('opacity', '1.0'))
654 compositeop = str(a.get('composite-op', DEFAULT_COMPOSITE_OP))
655 if compositeop not in VALID_COMPOSITE_OPS:
656 compositeop = DEFAULT_COMPOSITE_OP
658 visible = not 'hidden' in a.get('visibility', 'visible')
659 self.add_layer(insert_idx=0, name=name)
662 self.load_layer_from_pixbuf(pixbuf, x, y)
663 layer = self.layers[0]
665 self.set_layer_opacity(helpers.clamp(opac, 0.0, 1.0), layer)
666 self.set_layer_compositeop(compositeop, layer)
667 self.set_layer_visibility(visible, layer)
668 print ' %.3fs converting pixbuf to layer format' % (time.time() - t1)
670 fname = a.get('mypaint_strokemap_v2', None)
673 print 'Warning: dropping non-aligned strokemap'
675 sio = StringIO(z.read(fname))
676 layer.load_strokemap_from_file(sio, x, y)
679 if len(self.layers) == 1:
680 # no assertion (allow empty documents)
681 print 'Warning: Could not load any layer, document is empty.'
684 # recognize solid or tiled background layers, at least those that mypaint <= 0.7.1 saves
687 if not p.get_has_alpha() and p.get_width() % N == 0 and p.get_height() % N == 0:
688 tiles = self.layers[0]._surface.tiledict.values()
691 for tile in tiles[1:]:
692 if (tile.rgba != tiles[0].rgba).any():
696 arr = helpers.gdkpixbuf2numpy(p)
697 tile = arr[0:N,0:N,:]
698 self.set_background(tile.copy())
701 print ' %.3fs recognizing tiled background' % (time.time() - t1)
703 if len(self.layers) > 1:
704 # remove the still present initial empty top layer
705 self.select_layer(len(self.layers)-1)
707 # this leaves the topmost layer selected
709 print '%.3fs load_ora total' % (time.time() - t0)