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 class SaveLoadError(Exception):
23 """Expected errors on loading or saving, like missing permissions or non-existing files."""
28 This is the "model" in the Model-View-Controller design.
29 (The "view" would be ../gui/tileddrawwidget.py.)
30 It represents everything that the user would want to save.
33 The "controller" mostly in drawwindow.py.
34 It is possible to use it without any GUI attached (see ../tests/)
36 # Please note the following difficulty with the undo stack:
38 # Most of the time there is an unfinished (but already rendered)
39 # stroke pending, which has to be turned into a command.Action
40 # or discarded as empty before any other action is possible.
44 self.brush = brush.Brush()
46 self.canvas_observers = []
47 self.stroke_observers = [] # callback arguments: stroke, brush (brush is a temporary read-only convenience object)
48 self.doc_observers = []
51 def call_doc_observers(self):
52 for f in self.doc_observers:
56 def clear(self, init=False):
59 bbox = self.get_bbox()
60 # throw everything away, including undo stack
61 self.command_stack = command.CommandStack()
62 self.set_background((255, 255, 255))
66 # disallow undo of the first layer
67 self.command_stack.clear()
68 self.unsaved_painting_time = 0.0
71 for f in self.canvas_observers:
74 self.call_doc_observers()
76 def get_current_layer(self):
77 return self.layers[self.layer_idx]
78 layer = property(get_current_layer)
80 def split_stroke(self):
81 if not self.stroke: return
82 self.stroke.stop_recording()
83 if not self.stroke.empty:
84 self.command_stack.do(command.Stroke(self, self.stroke, self.snapshot_before_stroke))
85 del self.snapshot_before_stroke
86 self.unsaved_painting_time += self.stroke.total_painting_time
87 for f in self.stroke_observers:
88 f(self.stroke, self.brush)
91 def select_layer(self, idx):
92 self.do(command.SelectLayer(self, idx))
94 def move_layer(self, was_idx, new_idx):
95 self.do(command.MoveLayer(self, was_idx, new_idx))
97 def clear_layer(self):
98 if not self.layer.surface.is_empty():
99 self.do(command.ClearLayer(self))
101 def stroke_to(self, dtime, x, y, pressure, xtilt,ytilt):
103 self.stroke = stroke.Stroke()
104 self.stroke.start_recording(self.brush)
105 self.snapshot_before_stroke = self.layer.save_snapshot()
106 self.stroke.record_event(dtime, x, y, pressure, xtilt,ytilt)
109 l.surface.begin_atomic()
110 split = self.brush.stroke_to (l.surface, x, y, pressure, xtilt,ytilt, dtime)
111 l.surface.end_atomic()
116 def straight_line(self, src, dst):
118 # TODO: undo last stroke if it was very short... (but not at document level?)
119 real_brush = self.brush
120 self.brush = brush.Brush()
121 self.brush.copy_settings_from(real_brush)
126 x = numpy.linspace(src[0], dst[0], N)
127 y = numpy.linspace(src[1], dst[1], N)
128 # rest the brush in src for a minute, to avoid interpolation
129 # from the upper left corner (states are zero) (FIXME: the
130 # brush should handle this on its own, maybe?)
131 self.stroke_to(60.0, x[0], y[0], 0.0, 0.0, 0.0)
133 self.stroke_to(duration/N, x[i], y[i], pressure, 0.0, 0.0)
135 self.brush = real_brush
138 def layer_modified_cb(self, *args):
139 # for now, any layer modification is assumed to be visible
140 for f in self.canvas_observers:
143 def invalidate_all(self):
144 for f in self.canvas_observers:
150 cmd = self.command_stack.undo()
151 if not cmd or not cmd.automatic_undo:
157 cmd = self.command_stack.redo()
158 if not cmd or not cmd.automatic_undo:
163 self.command_stack.do(cmd)
165 def get_last_command(self):
167 return self.command_stack.get_last_command()
169 def set_brush(self, brush):
171 self.brush.copy_settings_from(brush)
175 for layer in self.layers:
176 # OPTIMIZE: only visible layers...
177 # careful: currently saving assumes that all layers are included
178 bbox = layer.surface.get_bbox()
179 res.expandToIncludeRect(bbox)
182 def blit_tile_into(self, dst_8bit, tx, ty, mipmap_level=0, layers=None, background=None):
185 if background is None:
186 background = self.background
188 assert dst_8bit.dtype == 'uint8'
189 dst = numpy.empty((N, N, 3), dtype='uint16')
191 background.blit_tile_into(dst, tx, ty, mipmap_level)
194 surface = layer.surface
195 surface.composite_tile_over(dst, tx, ty, mipmap_level=mipmap_level, opacity=layer.effective_opacity)
197 mypaintlib.tile_convert_rgb16_to_rgb8(dst, dst_8bit)
199 def add_layer(self, insert_idx=None, after=None, name=''):
200 self.do(command.AddLayer(self, insert_idx, after, name))
202 def remove_layer(self,layer=None):
203 if len(self.layers) > 1:
204 self.do(command.RemoveLayer(self,layer))
208 def merge_layer_down(self):
209 dst_idx = self.layer_idx - 1
212 self.do(command.MergeLayer(self, dst_idx))
215 def load_layer_from_pixbuf(self, pixbuf, x=0, y=0):
216 arr = helpers.gdkpixbuf2numpy(pixbuf)
217 self.do(command.LoadLayer(self, arr, x, y))
219 def set_layer_visibility(self, visible, layer):
220 cmd = self.get_last_command()
221 if isinstance(cmd, command.SetLayerVisibility) and cmd.layer is layer:
223 self.do(command.SetLayerVisibility(self, visible, layer))
225 def set_layer_opacity(self, opacity, layer=None):
226 """Sets the opacity of a layer. If layer=None, works on the current layer"""
227 cmd = self.get_last_command()
228 if isinstance(cmd, command.SetLayerOpacity):
230 self.do(command.SetLayerOpacity(self, opacity, layer))
232 def set_background(self, obj):
233 # This is not an undoable action. One reason is that dragging
234 # on the color chooser would get tons of undo steps.
236 if not isinstance(obj, backgroundsurface.Background):
237 obj = backgroundsurface.Background(obj)
238 self.background = obj
240 self.invalidate_all()
242 def load_from_pixbuf(self, pixbuf):
244 self.load_layer_from_pixbuf(pixbuf)
246 def is_layered(self):
248 for l in self.layers:
249 if not l.surface.is_empty():
254 return len(self.layers) == 1 and self.layer.surface.is_empty()
256 def save(self, filename, **kwargs):
258 trash, ext = os.path.splitext(filename)
259 ext = ext.lower().replace('.', '')
260 save = getattr(self, 'save_' + ext, self.unsupported)
262 save(filename, **kwargs)
263 except gobject.GError, e:
264 traceback.print_exc()
266 #add a hint due to a very consfusing error message when there is no space left on device
267 raise SaveLoadError, _('Unable to save: %s\nDo you have enough space left on the device?') % e.message
269 raise SaveLoadError, _('Unable to save: %s') % e.message
271 traceback.print_exc()
272 raise SaveLoadError, _('Unable to save: %s') % e.strerror
273 self.unsaved_painting_time = 0.0
275 def load(self, filename):
276 if not os.path.isfile(filename):
277 raise SaveLoadError, _('File does not exist: %s') % repr(filename)
278 if not os.access(filename,os.R_OK):
279 raise SaveLoadError, _('You do not have the necessary permissions to open file: %s') % repr(filename)
280 trash, ext = os.path.splitext(filename)
281 ext = ext.lower().replace('.', '')
282 load = getattr(self, 'load_' + ext, self.unsupported)
285 except gobject.GError, e:
286 traceback.print_exc()
287 raise SaveLoadError, _('Error while loading: GError %s') % e
289 traceback.print_exc()
290 raise SaveLoadError, _('Error while loading: IOError %s') % e
291 self.command_stack.clear()
292 self.unsaved_painting_time = 0.0
293 self.call_doc_observers()
295 def unsupported(self, filename):
296 raise SaveLoadError, _('Unknown file format extension: %s') % repr(filename)
298 def render_as_pixbuf(self, *args, **kwargs):
299 return pixbufsurface.render_as_pixbuf(self, *args, **kwargs)
301 def save_png(self, filename, alpha=False, multifile=False):
303 self.save_multifile_png(filename)
306 tmp_layer = layer.Layer()
307 for l in self.layers:
308 l.merge_into(tmp_layer)
309 tmp_layer.surface.save(filename)
311 pixbufsurface.save_as_png(self, filename, alpha=False)
313 def save_multifile_png(self, filename, alpha=False):
314 prefix, ext = os.path.splitext(filename)
315 # if we have a number already, strip it
316 l = prefix.rsplit('.', 1)
319 doc_bbox = self.get_bbox()
320 for i, l in enumerate(self.layers):
321 filename = '%s.%03d%s' % (prefix, i+1, ext)
322 l.surface.save(filename, *doc_bbox)
324 def load_png(self, filename):
325 self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
327 def load_jpg(self, filename):
328 self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
331 def save_jpg(self, filename, quality=90):
332 pixbuf = self.render_as_pixbuf()
333 pixbuf.save(filename, 'jpeg', options={'quality':str(quality)})
336 def save_ora(self, filename, options=None):
339 tempdir = tempfile.mkdtemp('mypaint')
340 # use .tmp extension, so we don't overwrite a valid file if there is an exception
341 z = zipfile.ZipFile(filename + '.tmpsave', 'w', compression=zipfile.ZIP_STORED)
342 # work around a permission bug in the zipfile library: http://bugs.python.org/issue3394
343 def write_file_str(filename, data):
344 zi = zipfile.ZipInfo(filename)
345 zi.external_attr = 0100644 << 16
347 write_file_str('mimetype', 'image/openraster') # must be the first file
348 image = ET.Element('image')
349 stack = ET.SubElement(image, 'stack')
350 x0, y0, w0, h0 = self.get_bbox()
355 def store_pixbuf(pixbuf, name):
356 tmp = join(tempdir, 'tmp.png')
358 pixbuf.save(tmp, 'png')
359 print ' %.3fs pixbuf saving %s' % (time.time() - t1, name)
363 def store_surface(surface, name, rect=[]):
364 tmp = join(tempdir, 'tmp.png')
366 surface.save(tmp, *rect)
367 print ' %.3fs surface saving %s' % (time.time() - t1, name)
371 def add_layer(x, y, opac, surface, name, layer_name, visible=True, rect=[]):
372 layer = ET.Element('layer')
374 store_surface(surface, name, rect)
377 a['name'] = layer_name
381 a['opacity'] = str(opac)
383 a['visibility'] = 'visible'
385 a['visibility'] = 'hidden'
388 for idx, l in enumerate(reversed(self.layers)):
389 if l.surface.is_empty():
392 x, y, w, h = l.surface.get_bbox()
393 el = add_layer(x-x0, y-y0, opac, l.surface, 'data/layer%03d.png' % idx, l.name, l.visible, rect=(x, y, w, h))
396 l.save_strokemap_to_file(sio, -x, -y)
397 data = sio.getvalue(); sio.close()
398 name = 'data/layer%03d_strokemap.dat' % idx
399 el.attrib['mypaint_strokemap_v2'] = name
400 write_file_str(name, data)
402 # save background as layer (solid color or tiled)
404 # save as fully rendered layer
405 l = add_layer(0, 0, 1.0, bg, 'data/background.png', 'background', rect=(x0, y0, w0, h0))
406 x, y, w, h = bg.get_pattern_bbox()
407 # save as single pattern (with corrected origin)
408 store_surface(bg, 'data/background_tile.png', rect=(x+x0, y+y0, w, h))
409 l.attrib['background_tile'] = 'data/background_tile.png'
413 print ' starting to render full image for thumbnail...'
415 x, y, w, h = x0, y0, w0, h0
417 while mipmap_level < tiledsurface.MAX_MIPMAP_LEVEL and max(w, h) >= 512:
419 x, y, w, h = x/2, y/2, w/2, h/2
421 pixbuf = self.render_as_pixbuf(x, y, w, h, mipmap_level=mipmap_level)
422 assert pixbuf.get_width() == w and pixbuf.get_height() == h
424 w, h = 256, max(h*256/w, 1)
426 w, h = max(w*256/h, 1), 256
428 pixbuf = pixbuf.scale_simple(w, h, gdk.INTERP_BILINEAR)
429 print ' %.3fs scaling thumbnail' % (time.time() - t1)
430 store_pixbuf(pixbuf, 'Thumbnails/thumbnail.png')
431 print ' total %.3fs spent on thumbnail' % (time.time() - t2)
433 helpers.indent_etree(image)
434 xml = ET.tostring(image, encoding='UTF-8')
436 write_file_str('stack.xml', xml)
439 if os.path.exists(filename):
440 os.remove(filename) # windows needs that
441 os.rename(filename + '.tmpsave', filename)
443 print '%.3fs save_ora total' % (time.time() - t0)
445 def load_ora(self, filename):
446 """Loads from an OpenRaster file"""
449 tempdir = tempfile.mkdtemp('mypaint')
450 z = zipfile.ZipFile(filename)
451 print 'mimetype:', z.read('mimetype').strip()
452 xml = z.read('stack.xml')
453 image = ET.fromstring(xml)
454 stack = image.find('stack')
456 def get_pixbuf(filename):
458 tmp = join(tempdir, 'tmp.png')
462 data = z.read(filename)
464 # support for bad zip files (saved by old versions of the GIMP ORA plugin)
465 data = z.read(filename.encode('utf-8'))
466 print 'WARNING: bad OpenRaster ZIP file. There is an utf-8 encoded filename that does not have the utf-8 flag set:', repr(filename)
470 res = gdk.pixbuf_new_from_file(tmp)
472 print ' %.3fs loading %s' % (time.time() - t1, filename)
475 def get_layers_list(root, x=0,y=0):
478 if item.tag == 'layer':
479 if 'x' in item.attrib:
480 item.attrib['x'] = int(item.attrib['x']) + x
481 if 'y' in item.attrib:
482 item.attrib['y'] = int(item.attrib['y']) + y
484 elif item.tag == 'stack':
485 stack_x = int( item.attrib.get('x', 0) )
486 stack_y = int( item.attrib.get('y', 0) )
487 res += get_layers_list(item, stack_x, stack_y)
489 print 'Warning: ignoring unsupported tag:', item.tag
492 self.clear() # this leaves one empty layer
494 for layer in get_layers_list(stack):
497 if 'background_tile' in a:
500 print a['background_tile']
501 self.set_background(get_pixbuf(a['background_tile']))
502 no_background = False
504 except backgroundsurface.BackgroundError, e:
505 print 'ORA background tile not usable:', e
507 src = a.get('src', '')
508 if not src.lower().endswith('.png'):
509 print 'Warning: ignoring non-png layer'
511 pixbuf = get_pixbuf(src)
512 name = a.get('name', '')
513 x = int(a.get('x', '0'))
514 y = int(a.get('y', '0'))
515 opac = float(a.get('opacity', '1.0'))
516 visible = not 'hidden' in a.get('visibility', 'visible')
517 self.add_layer(insert_idx=0, name=name)
520 self.load_layer_from_pixbuf(pixbuf, x, y)
521 layer = self.layers[0]
523 self.set_layer_opacity(helpers.clamp(opac, 0.0, 1.0), layer)
524 self.set_layer_visibility(visible, layer)
525 print ' %.3fs converting pixbuf to layer format' % (time.time() - t1)
527 fname = a.get('mypaint_strokemap_v2', None)
530 print 'Warning: dropping non-aligned strokemap'
532 sio = StringIO(z.read(fname))
533 layer.load_strokemap_from_file(sio, x, y)
538 if len(self.layers) == 1:
539 raise ValueError, 'Could not load any layer.'
542 # recognize solid or tiled background layers, at least those that mypaint <= 0.7.1 saves
545 if not p.get_has_alpha() and p.get_width() % N == 0 and p.get_height() % N == 0:
546 tiles = self.layers[0].surface.tiledict.values()
549 for tile in tiles[1:]:
550 if (tile.rgba != tiles[0].rgba).any():
554 arr = helpers.gdkpixbuf2numpy(p)
555 tile = arr[0:N,0:N,:]
556 self.set_background(tile.copy())
559 print ' %.3fs recognizing tiled background' % (time.time() - t1)
561 if len(self.layers) > 1:
562 # remove the still present initial empty top layer
563 self.select_layer(len(self.layers)-1)
565 # this leaves the topmost layer selected
567 print '%.3fs load_ora total' % (time.time() - t0)