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 class SaveLoadError(Exception):
25 """Expected errors on loading or saving, like missing permissions or non-existing files."""
30 This is the "model" in the Model-View-Controller design.
31 (The "view" would be ../gui/tileddrawwidget.py.)
32 It represents everything that the user would want to save.
35 The "controller" mostly in drawwindow.py.
36 It is possible to use it without any GUI attached (see ../tests/)
38 # Please note the following difficulty with the undo stack:
40 # Most of the time there is an unfinished (but already rendered)
41 # stroke pending, which has to be turned into a command.Action
42 # or discarded as empty before any other action is possible.
45 def __init__(self, brushinfo=None):
47 brushinfo = brush.BrushInfo()
48 brushinfo.load_defaults()
49 self.brush = brush.Brush(brushinfo)
51 self.canvas_observers = []
52 self.stroke_observers = [] # callback arguments: stroke, brush (brush is a temporary read-only convenience object)
53 self.doc_observers = []
54 self.frame_observers = []
57 self._frame = [0, 0, 0, 0]
58 self._frame_enabled = False
59 # Used by move_frame() to accumulate values
66 def move_frame(self, dx=0.0, dy=0.0):
67 """Move the frame. Accumulates changes and moves the frame once
68 the accumulated change reaches the minimum move step."""
69 # FIXME: Should be 1 (pixel aligned), not tile aligned
70 # This is due to PNG saving having to be tile aligned
73 def round_to_n(value, n):
74 return int(round(value/n)*n)
76 x, y, w, h = self.get_frame()
80 step_x = round_to_n(self._frame_dx, min_step)
81 step_y = round_to_n(self._frame_dy, min_step)
84 self.set_frame(x=x+step_x)
85 self._frame_dx -= step_x
88 self.set_frame(y=y+step_y)
89 self._frame_dy -= step_y
91 def set_frame(self, x=None, y=None, width=None, height=None):
92 """Set the size of the frame. Pass None to indicate no-change."""
94 for i, var in enumerate([x, y, width, height]):
96 # FIXME: must be aligned to tile size due to PNG saving
97 assert not var % N, "Frame size must be aligned to tile size"
100 for f in self.frame_observers: f()
102 def get_frame_enabled(self):
103 return self._frame_enabled
105 def set_frame_enabled(self, enabled):
106 self._frame_enabled = enabled
107 for f in self.frame_observers: f()
108 frame_enabled = property(get_frame_enabled)
110 def call_doc_observers(self):
111 for f in self.doc_observers:
115 def clear(self, init=False):
118 bbox = self.get_bbox()
119 # throw everything away, including undo stack
120 self.command_stack = command.CommandStack()
121 self.set_background((255, 255, 255))
123 self.layer_idx = None
125 # disallow undo of the first layer
126 self.command_stack.clear()
127 self.unsaved_painting_time = 0.0
130 for f in self.canvas_observers:
133 self.call_doc_observers()
135 def get_current_layer(self):
136 return self.layers[self.layer_idx]
137 layer = property(get_current_layer)
139 def split_stroke(self):
140 if not self.stroke: return
141 self.stroke.stop_recording()
142 if not self.stroke.empty:
143 self.command_stack.do(command.Stroke(self, self.stroke, self.snapshot_before_stroke))
144 del self.snapshot_before_stroke
145 self.unsaved_painting_time += self.stroke.total_painting_time
146 for f in self.stroke_observers:
147 f(self.stroke, self.brush)
150 def select_layer(self, idx):
151 self.do(command.SelectLayer(self, idx))
153 def move_layer(self, was_idx, new_idx):
154 self.do(command.MoveLayer(self, was_idx, new_idx))
156 def clear_layer(self):
157 if not self.layer.surface.is_empty():
158 self.do(command.ClearLayer(self))
160 def stroke_to(self, dtime, x, y, pressure, xtilt,ytilt):
162 self.stroke = stroke.Stroke()
163 self.stroke.start_recording(self.brush)
164 self.snapshot_before_stroke = self.layer.save_snapshot()
165 self.stroke.record_event(dtime, x, y, pressure, xtilt,ytilt)
168 l.surface.begin_atomic()
169 split = self.brush.stroke_to (l.surface, x, y, pressure, xtilt,ytilt, dtime)
170 l.surface.end_atomic()
175 def straight_line(self, src, dst):
177 self.brush.reset() # reset dynamic states (eg. filtered velocity)
182 x = numpy.linspace(src[0], dst[0], N)
183 y = numpy.linspace(src[1], dst[1], N)
184 # rest the brush in src for a minute, to avoid interpolation
185 # from the upper left corner (states are zero) (FIXME: the
186 # brush should handle this on its own, maybe?)
187 self.stroke_to(60.0, x[0], y[0], 0.0, 0.0, 0.0)
189 self.stroke_to(duration/N, x[i], y[i], pressure, 0.0, 0.0)
194 def layer_modified_cb(self, *args):
195 # for now, any layer modification is assumed to be visible
196 for f in self.canvas_observers:
199 def invalidate_all(self):
200 for f in self.canvas_observers:
206 cmd = self.command_stack.undo()
207 if not cmd or not cmd.automatic_undo:
213 cmd = self.command_stack.redo()
214 if not cmd or not cmd.automatic_undo:
219 self.command_stack.do(cmd)
221 def get_last_command(self):
223 return self.command_stack.get_last_command()
227 for layer in self.layers:
228 # OPTIMIZE: only visible layers...
229 # careful: currently saving assumes that all layers are included
230 bbox = layer.surface.get_bbox()
231 res.expandToIncludeRect(bbox)
234 def get_effective_bbox(self):
235 """Return the effective bounding box of the document.
236 If the frame is enabled, this is the bounding box of the frame,
237 else the (dynamic) bounding box of the document."""
238 return self.get_frame() if self.frame_enabled else self.get_bbox()
240 def blit_tile_into(self, dst_8bit, tx, ty, mipmap_level=0, layers=None, background=None):
243 if background is None:
244 background = self.background
246 assert dst_8bit.dtype == 'uint8'
247 dst = numpy.empty((N, N, 3), dtype='uint16')
249 background.blit_tile_into(dst, tx, ty, mipmap_level)
252 surface = layer.surface
253 surface.composite_tile_over(dst, tx, ty, mipmap_level=mipmap_level, opacity=layer.effective_opacity)
255 mypaintlib.tile_convert_rgb16_to_rgb8(dst, dst_8bit)
257 def add_layer(self, insert_idx=None, after=None, name=''):
258 self.do(command.AddLayer(self, insert_idx, after, name))
260 def remove_layer(self,layer=None):
261 if len(self.layers) > 1:
262 self.do(command.RemoveLayer(self,layer))
266 def merge_layer_down(self):
267 dst_idx = self.layer_idx - 1
270 self.do(command.MergeLayer(self, dst_idx))
273 def load_layer_from_pixbuf(self, pixbuf, x=0, y=0):
274 arr = helpers.gdkpixbuf2numpy(pixbuf)
275 self.do(command.LoadLayer(self, arr, x, y))
277 def set_layer_visibility(self, visible, layer):
278 cmd = self.get_last_command()
279 if isinstance(cmd, command.SetLayerVisibility) and cmd.layer is layer:
281 self.do(command.SetLayerVisibility(self, visible, layer))
283 def set_layer_locked(self, locked, layer):
284 cmd = self.get_last_command()
285 if isinstance(cmd, command.SetLayerLocked) and cmd.layer is layer:
287 self.do(command.SetLayerLocked(self, locked, layer))
289 def set_layer_opacity(self, opacity, layer=None):
290 """Sets the opacity of a layer. If layer=None, works on the current layer"""
291 cmd = self.get_last_command()
292 if isinstance(cmd, command.SetLayerOpacity):
294 self.do(command.SetLayerOpacity(self, opacity, layer))
296 def set_background(self, obj):
297 # This is not an undoable action. One reason is that dragging
298 # on the color chooser would get tons of undo steps.
300 if not isinstance(obj, backgroundsurface.Background):
301 obj = backgroundsurface.Background(obj)
302 self.background = obj
304 self.invalidate_all()
306 def load_from_pixbuf(self, pixbuf):
307 """Load a document from a pixbuf."""
309 self.load_layer_from_pixbuf(pixbuf)
310 self.set_frame(*self.get_bbox())
312 def is_layered(self):
314 for l in self.layers:
315 if not l.surface.is_empty():
320 return len(self.layers) == 1 and self.layer.surface.is_empty()
322 def save(self, filename, **kwargs):
324 trash, ext = os.path.splitext(filename)
325 ext = ext.lower().replace('.', '')
326 save = getattr(self, 'save_' + ext, self.unsupported)
328 save(filename, **kwargs)
329 except gobject.GError, e:
330 traceback.print_exc()
332 #add a hint due to a very consfusing error message when there is no space left on device
333 raise SaveLoadError, _('Unable to save: %s\nDo you have enough space left on the device?') % e.message
335 raise SaveLoadError, _('Unable to save: %s') % e.message
337 traceback.print_exc()
338 raise SaveLoadError, _('Unable to save: %s') % e.strerror
339 self.unsaved_painting_time = 0.0
341 def load(self, filename, **kwargs):
342 if not os.path.isfile(filename):
343 raise SaveLoadError, _('File does not exist: %s') % repr(filename)
344 if not os.access(filename,os.R_OK):
345 raise SaveLoadError, _('You do not have the necessary permissions to open file: %s') % repr(filename)
346 trash, ext = os.path.splitext(filename)
347 ext = ext.lower().replace('.', '')
348 load = getattr(self, 'load_' + ext, self.unsupported)
350 load(filename, **kwargs)
351 except gobject.GError, e:
352 traceback.print_exc()
353 raise SaveLoadError, _('Error while loading: GError %s') % e
355 traceback.print_exc()
356 raise SaveLoadError, _('Error while loading: IOError %s') % e
357 self.command_stack.clear()
358 self.unsaved_painting_time = 0.0
359 self.call_doc_observers()
361 def unsupported(self, filename, *args, **kwargs):
362 raise SaveLoadError, _('Unknown file format extension: %s') % repr(filename)
364 def render_as_pixbuf(self, *args, **kwargs):
365 return pixbufsurface.render_as_pixbuf(self, *args, **kwargs)
367 def render_thumbnail(self):
369 x, y, w, h = self.get_effective_bbox()
371 while mipmap_level < tiledsurface.MAX_MIPMAP_LEVEL and max(w, h) >= 512:
373 x, y, w, h = x/2, y/2, w/2, h/2
375 pixbuf = self.render_as_pixbuf(x, y, w, h, mipmap_level=mipmap_level)
376 assert pixbuf.get_width() == w and pixbuf.get_height() == h
377 pixbuf = helpers.scale_proportionally(pixbuf, 256, 256)
378 print 'Rendered thumbnail in', time.time() - t0, 'seconds.'
381 def save_png(self, filename, alpha=False, multifile=False, **kwargs):
382 doc_bbox = self.get_effective_bbox()
384 self.save_multifile_png(filename, **kwargs)
387 tmp_layer = layer.Layer()
388 for l in self.layers:
389 l.merge_into(tmp_layer)
390 tmp_layer.surface.save(filename, *doc_bbox)
392 pixbufsurface.save_as_png(self, filename, *doc_bbox, alpha=False, **kwargs)
394 def save_multifile_png(self, filename, alpha=False, **kwargs):
395 prefix, ext = os.path.splitext(filename)
396 # if we have a number already, strip it
397 l = prefix.rsplit('.', 1)
400 doc_bbox = self.get_effective_bbox()
401 for i, l in enumerate(self.layers):
402 filename = '%s.%03d%s' % (prefix, i+1, ext)
403 l.surface.save(filename, *doc_bbox, **kwargs)
406 def _pixbuf_from_stream(fp, feedback_cb=None):
407 loader = gdk.PixbufLoader()
409 if feedback_cb is not None:
411 buf = fp.read(LOAD_CHUNK_SIZE)
416 return loader.get_pixbuf()
418 def load_from_pixbuf_file(self, filename, feedback_cb=None):
419 fp = open(filename, 'rb')
420 pixbuf = self._pixbuf_from_stream(fp, feedback_cb)
422 self.load_from_pixbuf(pixbuf)
424 load_png = load_from_pixbuf_file
425 load_jpg = load_from_pixbuf_file
426 load_jpeg = load_from_pixbuf_file
428 def save_jpg(self, filename, quality=90, **kwargs):
429 doc_bbox = self.get_effective_bbox()
430 pixbuf = self.render_as_pixbuf(*doc_bbox, **kwargs)
431 pixbuf.save(filename, 'jpeg', options={'quality':str(quality)})
435 def save_ora(self, filename, options=None, **kwargs):
438 tempdir = tempfile.mkdtemp('mypaint')
439 # use .tmp extension, so we don't overwrite a valid file if there is an exception
440 z = zipfile.ZipFile(filename + '.tmpsave', 'w', compression=zipfile.ZIP_STORED)
441 # work around a permission bug in the zipfile library: http://bugs.python.org/issue3394
442 def write_file_str(filename, data):
443 zi = zipfile.ZipInfo(filename)
444 zi.external_attr = 0100644 << 16
446 write_file_str('mimetype', 'image/openraster') # must be the first file
447 image = ET.Element('image')
448 stack = ET.SubElement(image, 'stack')
449 x0, y0, w0, h0 = self.get_effective_bbox()
454 def store_pixbuf(pixbuf, name):
455 tmp = join(tempdir, 'tmp.png')
457 pixbuf.save(tmp, 'png')
458 print ' %.3fs pixbuf saving %s' % (time.time() - t1, name)
462 def store_surface(surface, name, rect=[]):
463 tmp = join(tempdir, 'tmp.png')
465 surface.save(tmp, *rect, **kwargs)
466 print ' %.3fs surface saving %s' % (time.time() - t1, name)
470 def add_layer(x, y, opac, surface, name, layer_name, visible=True, rect=[]):
471 layer = ET.Element('layer')
473 store_surface(surface, name, rect)
476 a['name'] = layer_name
480 a['opacity'] = str(opac)
482 a['visibility'] = 'visible'
484 a['visibility'] = 'hidden'
487 for idx, l in enumerate(reversed(self.layers)):
488 if l.surface.is_empty():
491 x, y, w, h = l.surface.get_bbox()
492 el = add_layer(x-x0, y-y0, opac, l.surface, 'data/layer%03d.png' % idx, l.name, l.visible, rect=(x, y, w, h))
495 l.save_strokemap_to_file(sio, -x, -y)
496 data = sio.getvalue(); sio.close()
497 name = 'data/layer%03d_strokemap.dat' % idx
498 el.attrib['mypaint_strokemap_v2'] = name
499 write_file_str(name, data)
501 # save background as layer (solid color or tiled)
503 # save as fully rendered layer
504 x, y, w, h = self.get_bbox()
505 l = add_layer(x-x0, y-y0, 1.0, bg, 'data/background.png', 'background', rect=(x,y,w,h))
506 x, y, w, h = bg.get_pattern_bbox()
507 # save as single pattern (with corrected origin)
508 store_surface(bg, 'data/background_tile.png', rect=(x+x0, y+y0, w, h))
509 l.attrib['background_tile'] = 'data/background_tile.png'
513 print ' starting to render full image for thumbnail...'
515 thumbnail_pixbuf = self.render_thumbnail()
516 store_pixbuf(thumbnail_pixbuf, 'Thumbnails/thumbnail.png')
517 print ' total %.3fs spent on thumbnail' % (time.time() - t2)
519 helpers.indent_etree(image)
520 xml = ET.tostring(image, encoding='UTF-8')
522 write_file_str('stack.xml', xml)
525 if os.path.exists(filename):
526 os.remove(filename) # windows needs that
527 os.rename(filename + '.tmpsave', filename)
529 print '%.3fs save_ora total' % (time.time() - t0)
531 return thumbnail_pixbuf
533 def load_ora(self, filename, feedback_cb=None):
534 """Loads from an OpenRaster file"""
537 z = zipfile.ZipFile(filename)
538 print 'mimetype:', z.read('mimetype').strip()
539 xml = z.read('stack.xml')
540 image = ET.fromstring(xml)
541 stack = image.find('stack')
543 w = int(image.attrib['w'])
544 h = int(image.attrib['h'])
546 def round_up_to_n(value, n):
547 assert value >= 0, "function undefined for negative numbers"
551 value = value - residual + n
554 def get_pixbuf(filename):
558 fp = z.open(filename, mode='r')
560 # support for bad zip files (saved by old versions of the GIMP ORA plugin)
561 fp = z.open(filename.encode('utf-8'), mode='r')
562 print 'WARNING: bad OpenRaster ZIP file. There is an utf-8 encoded filename that does not have the utf-8 flag set:', repr(filename)
564 res = self._pixbuf_from_stream(fp, feedback_cb)
566 print ' %.3fs loading %s' % (time.time() - t1, filename)
569 def get_layers_list(root, x=0,y=0):
572 if item.tag == 'layer':
573 if 'x' in item.attrib:
574 item.attrib['x'] = int(item.attrib['x']) + x
575 if 'y' in item.attrib:
576 item.attrib['y'] = int(item.attrib['y']) + y
578 elif item.tag == 'stack':
579 stack_x = int( item.attrib.get('x', 0) )
580 stack_y = int( item.attrib.get('y', 0) )
581 res += get_layers_list(item, stack_x, stack_y)
583 print 'Warning: ignoring unsupported tag:', item.tag
586 self.clear() # this leaves one empty layer
588 # FIXME: don't require tile alignment for frame
589 self.set_frame(width=round_up_to_n(w, N), height=round_up_to_n(h, N))
591 for layer in get_layers_list(stack):
594 if 'background_tile' in a:
597 print a['background_tile']
598 self.set_background(get_pixbuf(a['background_tile']))
599 no_background = False
601 except backgroundsurface.BackgroundError, e:
602 print 'ORA background tile not usable:', e
604 src = a.get('src', '')
605 if not src.lower().endswith('.png'):
606 print 'Warning: ignoring non-png layer'
608 pixbuf = get_pixbuf(src)
609 name = a.get('name', '')
610 x = int(a.get('x', '0'))
611 y = int(a.get('y', '0'))
612 opac = float(a.get('opacity', '1.0'))
613 visible = not 'hidden' in a.get('visibility', 'visible')
614 self.add_layer(insert_idx=0, name=name)
617 self.load_layer_from_pixbuf(pixbuf, x, y)
618 layer = self.layers[0]
620 self.set_layer_opacity(helpers.clamp(opac, 0.0, 1.0), layer)
621 self.set_layer_visibility(visible, layer)
622 print ' %.3fs converting pixbuf to layer format' % (time.time() - t1)
624 fname = a.get('mypaint_strokemap_v2', None)
627 print 'Warning: dropping non-aligned strokemap'
629 sio = StringIO(z.read(fname))
630 layer.load_strokemap_from_file(sio, x, y)
633 if len(self.layers) == 1:
634 raise ValueError, 'Could not load any layer.'
637 # recognize solid or tiled background layers, at least those that mypaint <= 0.7.1 saves
640 if not p.get_has_alpha() and p.get_width() % N == 0 and p.get_height() % N == 0:
641 tiles = self.layers[0].surface.tiledict.values()
644 for tile in tiles[1:]:
645 if (tile.rgba != tiles[0].rgba).any():
649 arr = helpers.gdkpixbuf2numpy(p)
650 tile = arr[0:N,0:N,:]
651 self.set_background(tile.copy())
654 print ' %.3fs recognizing tiled background' % (time.time() - t1)
656 if len(self.layers) > 1:
657 # remove the still present initial empty top layer
658 self.select_layer(len(self.layers)-1)
660 # this leaves the topmost layer selected
662 print '%.3fs load_ora total' % (time.time() - t0)