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 = []
49 self.frame_observers = []
52 self._frame = [0, 0, 0, 0]
53 self._frame_enabled = False
54 # Used by move_frame() to accumulate values
61 def move_frame(self, dx=0.0, dy=0.0):
62 """Move the frame. Accumulates changes and moves the frame once
63 the accumulated change reaches the minimum move step."""
64 # FIXME: Should be 1 (pixel aligned), not tile aligned
65 # This is due to PNG saving having to be tile aligned
68 def round_to_n(value, n):
69 return int(round(value/n)*n)
71 x, y, w, h = self.get_frame()
75 step_x = round_to_n(self._frame_dx, min_step)
76 step_y = round_to_n(self._frame_dy, min_step)
79 self.set_frame(x=x+step_x)
80 self._frame_dx -= step_x
83 self.set_frame(y=y+step_y)
84 self._frame_dy -= step_y
86 def set_frame(self, x=None, y=None, width=None, height=None):
87 """Set the size of the frame. Pass None to indicate no-change."""
89 for i, var in enumerate([x, y, width, height]):
91 # FIXME: must be aligned to tile size due to PNG saving
92 assert not var % N, "Frame size must be aligned to tile size"
95 for f in self.frame_observers: f()
97 def get_frame_enabled(self):
98 return self._frame_enabled
100 def set_frame_enabled(self, enabled):
101 self._frame_enabled = enabled
102 for f in self.frame_observers: f()
103 frame_enabled = property(get_frame_enabled)
105 def call_doc_observers(self):
106 for f in self.doc_observers:
110 def clear(self, init=False):
113 bbox = self.get_bbox()
114 # throw everything away, including undo stack
115 self.command_stack = command.CommandStack()
116 self.set_background((255, 255, 255))
118 self.layer_idx = None
120 # disallow undo of the first layer
121 self.command_stack.clear()
122 self.unsaved_painting_time = 0.0
125 for f in self.canvas_observers:
128 self.call_doc_observers()
130 def get_current_layer(self):
131 return self.layers[self.layer_idx]
132 layer = property(get_current_layer)
134 def split_stroke(self):
135 if not self.stroke: return
136 self.stroke.stop_recording()
137 if not self.stroke.empty:
138 self.command_stack.do(command.Stroke(self, self.stroke, self.snapshot_before_stroke))
139 del self.snapshot_before_stroke
140 self.unsaved_painting_time += self.stroke.total_painting_time
141 for f in self.stroke_observers:
142 f(self.stroke, self.brush)
145 def select_layer(self, idx):
146 self.do(command.SelectLayer(self, idx))
148 def move_layer(self, was_idx, new_idx):
149 self.do(command.MoveLayer(self, was_idx, new_idx))
151 def clear_layer(self):
152 if not self.layer.surface.is_empty():
153 self.do(command.ClearLayer(self))
155 def stroke_to(self, dtime, x, y, pressure, xtilt,ytilt):
157 self.stroke = stroke.Stroke()
158 self.stroke.start_recording(self.brush)
159 self.snapshot_before_stroke = self.layer.save_snapshot()
160 self.stroke.record_event(dtime, x, y, pressure, xtilt,ytilt)
163 l.surface.begin_atomic()
164 split = self.brush.stroke_to (l.surface, x, y, pressure, xtilt,ytilt, dtime)
165 l.surface.end_atomic()
170 def straight_line(self, src, dst):
172 # TODO: undo last stroke if it was very short... (but not at document level?)
173 real_brush = self.brush
174 self.brush = brush.Brush()
175 self.brush.copy_settings_from(real_brush)
180 x = numpy.linspace(src[0], dst[0], N)
181 y = numpy.linspace(src[1], dst[1], N)
182 # rest the brush in src for a minute, to avoid interpolation
183 # from the upper left corner (states are zero) (FIXME: the
184 # brush should handle this on its own, maybe?)
185 self.stroke_to(60.0, x[0], y[0], 0.0, 0.0, 0.0)
187 self.stroke_to(duration/N, x[i], y[i], pressure, 0.0, 0.0)
189 self.brush = real_brush
192 def layer_modified_cb(self, *args):
193 # for now, any layer modification is assumed to be visible
194 for f in self.canvas_observers:
197 def invalidate_all(self):
198 for f in self.canvas_observers:
204 cmd = self.command_stack.undo()
205 if not cmd or not cmd.automatic_undo:
211 cmd = self.command_stack.redo()
212 if not cmd or not cmd.automatic_undo:
217 self.command_stack.do(cmd)
219 def get_last_command(self):
221 return self.command_stack.get_last_command()
223 def set_brush(self, brush):
225 self.brush.copy_settings_from(brush)
229 for layer in self.layers:
230 # OPTIMIZE: only visible layers...
231 # careful: currently saving assumes that all layers are included
232 bbox = layer.surface.get_bbox()
233 res.expandToIncludeRect(bbox)
236 def get_effective_bbox(self):
237 """Return the effective bounding box of the document.
238 If the frame is enabled, this is the bounding box of the frame,
239 else the (dynamic) bounding box of the document."""
240 return self.get_frame() if self.frame_enabled else self.get_bbox()
242 def blit_tile_into(self, dst_8bit, tx, ty, mipmap_level=0, layers=None, background=None):
245 if background is None:
246 background = self.background
248 assert dst_8bit.dtype == 'uint8'
249 dst = numpy.empty((N, N, 3), dtype='uint16')
251 background.blit_tile_into(dst, tx, ty, mipmap_level)
254 surface = layer.surface
255 surface.composite_tile_over(dst, tx, ty, mipmap_level=mipmap_level, opacity=layer.effective_opacity)
257 mypaintlib.tile_convert_rgb16_to_rgb8(dst, dst_8bit)
259 def add_layer(self, insert_idx=None, after=None, name=''):
260 self.do(command.AddLayer(self, insert_idx, after, name))
262 def remove_layer(self,layer=None):
263 if len(self.layers) > 1:
264 self.do(command.RemoveLayer(self,layer))
268 def merge_layer_down(self):
269 dst_idx = self.layer_idx - 1
272 self.do(command.MergeLayer(self, dst_idx))
275 def load_layer_from_pixbuf(self, pixbuf, x=0, y=0):
276 arr = helpers.gdkpixbuf2numpy(pixbuf)
277 self.do(command.LoadLayer(self, arr, x, y))
279 def set_layer_visibility(self, visible, layer):
280 cmd = self.get_last_command()
281 if isinstance(cmd, command.SetLayerVisibility) and cmd.layer is layer:
283 self.do(command.SetLayerVisibility(self, visible, layer))
285 def set_layer_opacity(self, opacity, layer=None):
286 """Sets the opacity of a layer. If layer=None, works on the current layer"""
287 cmd = self.get_last_command()
288 if isinstance(cmd, command.SetLayerOpacity):
290 self.do(command.SetLayerOpacity(self, opacity, layer))
292 def set_background(self, obj):
293 # This is not an undoable action. One reason is that dragging
294 # on the color chooser would get tons of undo steps.
296 if not isinstance(obj, backgroundsurface.Background):
297 obj = backgroundsurface.Background(obj)
298 self.background = obj
300 self.invalidate_all()
302 def load_from_pixbuf(self, pixbuf):
303 """Load a document from a pixbuf."""
305 self.load_layer_from_pixbuf(pixbuf)
306 self.set_frame(*self.get_bbox())
308 def is_layered(self):
310 for l in self.layers:
311 if not l.surface.is_empty():
316 return len(self.layers) == 1 and self.layer.surface.is_empty()
318 def save(self, filename, **kwargs):
320 trash, ext = os.path.splitext(filename)
321 ext = ext.lower().replace('.', '')
322 save = getattr(self, 'save_' + ext, self.unsupported)
324 save(filename, **kwargs)
325 except gobject.GError, e:
326 traceback.print_exc()
328 #add a hint due to a very consfusing error message when there is no space left on device
329 raise SaveLoadError, _('Unable to save: %s\nDo you have enough space left on the device?') % e.message
331 raise SaveLoadError, _('Unable to save: %s') % e.message
333 traceback.print_exc()
334 raise SaveLoadError, _('Unable to save: %s') % e.strerror
335 self.unsaved_painting_time = 0.0
337 def load(self, filename):
338 if not os.path.isfile(filename):
339 raise SaveLoadError, _('File does not exist: %s') % repr(filename)
340 if not os.access(filename,os.R_OK):
341 raise SaveLoadError, _('You do not have the necessary permissions to open file: %s') % repr(filename)
342 trash, ext = os.path.splitext(filename)
343 ext = ext.lower().replace('.', '')
344 load = getattr(self, 'load_' + ext, self.unsupported)
347 except gobject.GError, e:
348 traceback.print_exc()
349 raise SaveLoadError, _('Error while loading: GError %s') % e
351 traceback.print_exc()
352 raise SaveLoadError, _('Error while loading: IOError %s') % e
353 self.command_stack.clear()
354 self.unsaved_painting_time = 0.0
355 self.call_doc_observers()
357 def unsupported(self, filename):
358 raise SaveLoadError, _('Unknown file format extension: %s') % repr(filename)
360 def render_as_pixbuf(self, *args, **kwargs):
361 return pixbufsurface.render_as_pixbuf(self, *args, **kwargs)
363 def render_thumbnail(self):
365 x, y, w, h = self.get_bbox()
367 while mipmap_level < tiledsurface.MAX_MIPMAP_LEVEL and max(w, h) >= 512:
369 x, y, w, h = x/2, y/2, w/2, h/2
371 pixbuf = self.render_as_pixbuf(x, y, w, h, mipmap_level=mipmap_level)
372 assert pixbuf.get_width() == w and pixbuf.get_height() == h
373 pixbuf = helpers.scale_proportionally(pixbuf, 256, 256)
374 print 'Rendered thumbnail in', time.time() - t0, 'seconds.'
377 def save_png(self, filename, alpha=False, multifile=False):
378 doc_bbox = self.get_effective_bbox()
380 self.save_multifile_png(filename)
383 tmp_layer = layer.Layer()
384 for l in self.layers:
385 l.merge_into(tmp_layer)
386 tmp_layer.surface.save(filename, *doc_bbox)
388 pixbufsurface.save_as_png(self, filename, *doc_bbox, alpha=False)
390 def save_multifile_png(self, filename, alpha=False):
391 prefix, ext = os.path.splitext(filename)
392 # if we have a number already, strip it
393 l = prefix.rsplit('.', 1)
396 doc_bbox = self.get_effective_bbox()
397 for i, l in enumerate(self.layers):
398 filename = '%s.%03d%s' % (prefix, i+1, ext)
399 l.surface.save(filename, *doc_bbox)
401 def load_png(self, filename):
402 self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
404 def load_jpg(self, filename):
405 self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
408 def save_jpg(self, filename, quality=90):
409 doc_bbox = self.get_effective_bbox()
410 pixbuf = self.render_as_pixbuf(*doc_bbox)
411 pixbuf.save(filename, 'jpeg', options={'quality':str(quality)})
414 def save_ora(self, filename, options=None):
417 tempdir = tempfile.mkdtemp('mypaint')
418 # use .tmp extension, so we don't overwrite a valid file if there is an exception
419 z = zipfile.ZipFile(filename + '.tmpsave', 'w', compression=zipfile.ZIP_STORED)
420 # work around a permission bug in the zipfile library: http://bugs.python.org/issue3394
421 def write_file_str(filename, data):
422 zi = zipfile.ZipInfo(filename)
423 zi.external_attr = 0100644 << 16
425 write_file_str('mimetype', 'image/openraster') # must be the first file
426 image = ET.Element('image')
427 stack = ET.SubElement(image, 'stack')
428 x0, y0, w0, h0 = self.get_effective_bbox()
433 def store_pixbuf(pixbuf, name):
434 tmp = join(tempdir, 'tmp.png')
436 pixbuf.save(tmp, 'png')
437 print ' %.3fs pixbuf saving %s' % (time.time() - t1, name)
441 def store_surface(surface, name, rect=[]):
442 tmp = join(tempdir, 'tmp.png')
444 surface.save(tmp, *rect)
445 print ' %.3fs surface saving %s' % (time.time() - t1, name)
449 def add_layer(x, y, opac, surface, name, layer_name, visible=True, rect=[]):
450 layer = ET.Element('layer')
452 store_surface(surface, name, rect)
455 a['name'] = layer_name
459 a['opacity'] = str(opac)
461 a['visibility'] = 'visible'
463 a['visibility'] = 'hidden'
466 for idx, l in enumerate(reversed(self.layers)):
467 if l.surface.is_empty():
470 x, y, w, h = l.surface.get_bbox()
471 el = add_layer(x-x0, y-y0, opac, l.surface, 'data/layer%03d.png' % idx, l.name, l.visible, rect=(x, y, w, h))
474 l.save_strokemap_to_file(sio, -x, -y)
475 data = sio.getvalue(); sio.close()
476 name = 'data/layer%03d_strokemap.dat' % idx
477 el.attrib['mypaint_strokemap_v2'] = name
478 write_file_str(name, data)
480 # save background as layer (solid color or tiled)
482 # save as fully rendered layer
483 x, y, w, h = self.get_bbox()
484 l = add_layer(x, y, 1.0, bg, 'data/background.png', 'background', rect=(x,y,w,h))
485 x, y, w, h = bg.get_pattern_bbox()
486 # save as single pattern (with corrected origin)
487 store_surface(bg, 'data/background_tile.png', rect=(x+x0, y+y0, w, h))
488 l.attrib['background_tile'] = 'data/background_tile.png'
492 print ' starting to render full image for thumbnail...'
494 thumbnail_pixbuf = self.render_thumbnail()
495 store_pixbuf(thumbnail_pixbuf, 'Thumbnails/thumbnail.png')
496 print ' total %.3fs spent on thumbnail' % (time.time() - t2)
498 helpers.indent_etree(image)
499 xml = ET.tostring(image, encoding='UTF-8')
501 write_file_str('stack.xml', xml)
504 if os.path.exists(filename):
505 os.remove(filename) # windows needs that
506 os.rename(filename + '.tmpsave', filename)
508 print '%.3fs save_ora total' % (time.time() - t0)
510 return thumbnail_pixbuf
512 def load_ora(self, filename):
513 """Loads from an OpenRaster file"""
516 tempdir = tempfile.mkdtemp('mypaint')
517 z = zipfile.ZipFile(filename)
518 print 'mimetype:', z.read('mimetype').strip()
519 xml = z.read('stack.xml')
520 image = ET.fromstring(xml)
521 stack = image.find('stack')
523 w = int(image.attrib['w'])
524 h = int(image.attrib['h'])
526 def round_up_to_n(value, n):
527 assert value >= 0, "function undefined for negative numbers"
531 value = value - residual + n
534 def get_pixbuf(filename):
536 tmp = join(tempdir, 'tmp.png')
540 data = z.read(filename)
542 # support for bad zip files (saved by old versions of the GIMP ORA plugin)
543 data = z.read(filename.encode('utf-8'))
544 print 'WARNING: bad OpenRaster ZIP file. There is an utf-8 encoded filename that does not have the utf-8 flag set:', repr(filename)
548 res = gdk.pixbuf_new_from_file(tmp)
550 print ' %.3fs loading %s' % (time.time() - t1, filename)
553 def get_layers_list(root, x=0,y=0):
556 if item.tag == 'layer':
557 if 'x' in item.attrib:
558 item.attrib['x'] = int(item.attrib['x']) + x
559 if 'y' in item.attrib:
560 item.attrib['y'] = int(item.attrib['y']) + y
562 elif item.tag == 'stack':
563 stack_x = int( item.attrib.get('x', 0) )
564 stack_y = int( item.attrib.get('y', 0) )
565 res += get_layers_list(item, stack_x, stack_y)
567 print 'Warning: ignoring unsupported tag:', item.tag
570 self.clear() # this leaves one empty layer
572 # FIXME: don't require tile alignment for frame
573 self.set_frame(width=round_up_to_n(w, N), height=round_up_to_n(h, N))
575 for layer in get_layers_list(stack):
578 if 'background_tile' in a:
581 print a['background_tile']
582 self.set_background(get_pixbuf(a['background_tile']))
583 no_background = False
585 except backgroundsurface.BackgroundError, e:
586 print 'ORA background tile not usable:', e
588 src = a.get('src', '')
589 if not src.lower().endswith('.png'):
590 print 'Warning: ignoring non-png layer'
592 pixbuf = get_pixbuf(src)
593 name = a.get('name', '')
594 x = int(a.get('x', '0'))
595 y = int(a.get('y', '0'))
596 opac = float(a.get('opacity', '1.0'))
597 visible = not 'hidden' in a.get('visibility', 'visible')
598 self.add_layer(insert_idx=0, name=name)
601 self.load_layer_from_pixbuf(pixbuf, x, y)
602 layer = self.layers[0]
604 self.set_layer_opacity(helpers.clamp(opac, 0.0, 1.0), layer)
605 self.set_layer_visibility(visible, layer)
606 print ' %.3fs converting pixbuf to layer format' % (time.time() - t1)
608 fname = a.get('mypaint_strokemap_v2', None)
611 print 'Warning: dropping non-aligned strokemap'
613 sio = StringIO(z.read(fname))
614 layer.load_strokemap_from_file(sio, x, y)
619 if len(self.layers) == 1:
620 raise ValueError, 'Could not load any layer.'
623 # recognize solid or tiled background layers, at least those that mypaint <= 0.7.1 saves
626 if not p.get_has_alpha() and p.get_width() % N == 0 and p.get_height() % N == 0:
627 tiles = self.layers[0].surface.tiledict.values()
630 for tile in tiles[1:]:
631 if (tile.rgba != tiles[0].rgba).any():
635 arr = helpers.gdkpixbuf2numpy(p)
636 tile = arr[0:N,0:N,:]
637 self.set_background(tile.copy())
640 print ' %.3fs recognizing tiled background' % (time.time() - t1)
642 if len(self.layers) > 1:
643 # remove the still present initial empty top layer
644 self.select_layer(len(self.layers)-1)
646 # this leaves the topmost layer selected
648 print '%.3fs load_ora total' % (time.time() - t0)