OSDN Git Service

frame: State and visualization
[mypaint-anime/master.git] / lib / document.py
1 # This file is part of MyPaint.
2 # Copyright (C) 2007-2008 by Martin Renold <martinxyz@gmx.ch>
3 #
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.
8
9 import os, zipfile, tempfile, time, traceback
10 join = os.path.join
11 from cStringIO import StringIO
12 import xml.etree.ElementTree as ET
13 from gtk import gdk
14 import gobject, numpy
15 from gettext import gettext as _
16
17 import helpers, tiledsurface, pixbufsurface, backgroundsurface, mypaintlib
18 import command, stroke, layer
19 import brush
20 N = tiledsurface.N
21
22 class SaveLoadError(Exception):
23     """Expected errors on loading or saving, like missing permissions or non-existing files."""
24     pass
25
26 class Document():
27     """
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.
31
32
33     The "controller" mostly in drawwindow.py.
34     It is possible to use it without any GUI attached (see ../tests/)
35     """
36     # Please note the following difficulty with the undo stack:
37     #
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.
41     #   (split_stroke)
42
43     def __init__(self):
44         self.brush = brush.Brush()
45         self.stroke = None
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 = []
50         self.clear(True)
51
52         self._frame = [0, 0, 0, 0]
53         self._frame_enabled = False
54
55     def get_frame(self):
56         return self._frame
57
58     def set_frame(self, x=None, y=None, width=None, height=None):
59         """Set the size of the frame. Pass None to indicate no-change."""
60
61         for i, var in enumerate([x, y, width, height]):
62             if not var is None:
63                 # FIXME: must be aligned to tile size due to PNG saving
64                 assert not var % N, "Frame size must be aligned to tile size"
65                 self._frame[i] = var
66
67         for f in self.frame_observers: f()
68
69     def get_frame_enabled(self):
70         return self._frame_enabled
71
72     def set_frame_enabled(self, enabled):
73         self._frame_enabled = enabled
74         for f in self.frame_observers: f()
75     frame_enabled = property(get_frame_enabled)
76
77     def call_doc_observers(self):
78         for f in self.doc_observers:
79             f(self)
80         return True
81
82     def clear(self, init=False):
83         self.split_stroke()
84         if not init:
85             bbox = self.get_bbox()
86         # throw everything away, including undo stack
87         self.command_stack = command.CommandStack()
88         self.set_background((255, 255, 255))
89         self.layers = []
90         self.layer_idx = None
91         self.add_layer(0)
92         # disallow undo of the first layer
93         self.command_stack.clear()
94         self.unsaved_painting_time = 0.0
95
96         if not init:
97             for f in self.canvas_observers:
98                 f(*bbox)
99
100         self.call_doc_observers()
101
102     def get_current_layer(self):
103         return self.layers[self.layer_idx]
104     layer = property(get_current_layer)
105
106     def split_stroke(self):
107         if not self.stroke: return
108         self.stroke.stop_recording()
109         if not self.stroke.empty:
110             self.command_stack.do(command.Stroke(self, self.stroke, self.snapshot_before_stroke))
111             del self.snapshot_before_stroke
112             self.unsaved_painting_time += self.stroke.total_painting_time
113             for f in self.stroke_observers:
114                 f(self.stroke, self.brush)
115         self.stroke = None
116
117     def select_layer(self, idx):
118         self.do(command.SelectLayer(self, idx))
119
120     def move_layer(self, was_idx, new_idx):
121         self.do(command.MoveLayer(self, was_idx, new_idx))
122
123     def clear_layer(self):
124         if not self.layer.surface.is_empty():
125             self.do(command.ClearLayer(self))
126
127     def stroke_to(self, dtime, x, y, pressure, xtilt,ytilt):
128         if not self.stroke:
129             self.stroke = stroke.Stroke()
130             self.stroke.start_recording(self.brush)
131             self.snapshot_before_stroke = self.layer.save_snapshot()
132         self.stroke.record_event(dtime, x, y, pressure, xtilt,ytilt)
133
134         l = self.layer
135         l.surface.begin_atomic()
136         split = self.brush.stroke_to (l.surface, x, y, pressure, xtilt,ytilt, dtime)
137         l.surface.end_atomic()
138
139         if split:
140             self.split_stroke()
141
142     def straight_line(self, src, dst):
143         self.split_stroke()
144         # TODO: undo last stroke if it was very short... (but not at document level?)
145         real_brush = self.brush
146         self.brush = brush.Brush()
147         self.brush.copy_settings_from(real_brush)
148
149         duration = 3.0
150         pressure = 0.3
151         N = 1000
152         x = numpy.linspace(src[0], dst[0], N)
153         y = numpy.linspace(src[1], dst[1], N)
154         # rest the brush in src for a minute, to avoid interpolation
155         # from the upper left corner (states are zero) (FIXME: the
156         # brush should handle this on its own, maybe?)
157         self.stroke_to(60.0, x[0], y[0], 0.0, 0.0, 0.0)
158         for i in xrange(N):
159             self.stroke_to(duration/N, x[i], y[i], pressure, 0.0, 0.0)
160         self.split_stroke()
161         self.brush = real_brush
162
163
164     def layer_modified_cb(self, *args):
165         # for now, any layer modification is assumed to be visible
166         for f in self.canvas_observers:
167             f(*args)
168
169     def invalidate_all(self):
170         for f in self.canvas_observers:
171             f(0, 0, 0, 0)
172
173     def undo(self):
174         self.split_stroke()
175         while 1:
176             cmd = self.command_stack.undo()
177             if not cmd or not cmd.automatic_undo:
178                 return cmd
179
180     def redo(self):
181         self.split_stroke()
182         while 1:
183             cmd = self.command_stack.redo()
184             if not cmd or not cmd.automatic_undo:
185                 return cmd
186
187     def do(self, cmd):
188         self.split_stroke()
189         self.command_stack.do(cmd)
190
191     def get_last_command(self):
192         self.split_stroke()
193         return self.command_stack.get_last_command()
194
195     def set_brush(self, brush):
196         self.split_stroke()
197         self.brush.copy_settings_from(brush)
198
199     def get_bbox(self):
200         res = helpers.Rect()
201         for layer in self.layers:
202             # OPTIMIZE: only visible layers...
203             # careful: currently saving assumes that all layers are included
204             bbox = layer.surface.get_bbox()
205             res.expandToIncludeRect(bbox)
206         return res
207
208     def blit_tile_into(self, dst_8bit, tx, ty, mipmap_level=0, layers=None, background=None):
209         if layers is None:
210             layers = self.layers
211         if background is None:
212             background = self.background
213
214         assert dst_8bit.dtype == 'uint8'
215         dst = numpy.empty((N, N, 3), dtype='uint16')
216
217         background.blit_tile_into(dst, tx, ty, mipmap_level)
218
219         for layer in layers:
220             surface = layer.surface
221             surface.composite_tile_over(dst, tx, ty, mipmap_level=mipmap_level, opacity=layer.effective_opacity)
222
223         mypaintlib.tile_convert_rgb16_to_rgb8(dst, dst_8bit)
224
225     def add_layer(self, insert_idx=None, after=None, name=''):
226         self.do(command.AddLayer(self, insert_idx, after, name))
227
228     def remove_layer(self,layer=None):
229         if len(self.layers) > 1:
230             self.do(command.RemoveLayer(self,layer))
231         else:
232             self.clear_layer()
233
234     def merge_layer_down(self):
235         dst_idx = self.layer_idx - 1
236         if dst_idx < 0:
237             return False
238         self.do(command.MergeLayer(self, dst_idx))
239         return True
240
241     def load_layer_from_pixbuf(self, pixbuf, x=0, y=0):
242         arr = helpers.gdkpixbuf2numpy(pixbuf)
243         self.do(command.LoadLayer(self, arr, x, y))
244
245     def set_layer_visibility(self, visible, layer):
246         cmd = self.get_last_command()
247         if isinstance(cmd, command.SetLayerVisibility) and cmd.layer is layer:
248             self.undo()
249         self.do(command.SetLayerVisibility(self, visible, layer))
250
251     def set_layer_opacity(self, opacity, layer=None):
252         """Sets the opacity of a layer. If layer=None, works on the current layer"""
253         cmd = self.get_last_command()
254         if isinstance(cmd, command.SetLayerOpacity):
255             self.undo()
256         self.do(command.SetLayerOpacity(self, opacity, layer))
257
258     def set_background(self, obj):
259         # This is not an undoable action. One reason is that dragging
260         # on the color chooser would get tons of undo steps.
261
262         if not isinstance(obj, backgroundsurface.Background):
263             obj = backgroundsurface.Background(obj)
264         self.background = obj
265
266         self.invalidate_all()
267
268     def load_from_pixbuf(self, pixbuf):
269         self.clear()
270         self.load_layer_from_pixbuf(pixbuf)
271
272     def is_layered(self):
273         count = 0
274         for l in self.layers:
275             if not l.surface.is_empty():
276                 count += 1
277         return count > 1
278
279     def is_empty(self):
280         return len(self.layers) == 1 and self.layer.surface.is_empty()
281
282     def save(self, filename, **kwargs):
283         self.split_stroke()
284         trash, ext = os.path.splitext(filename)
285         ext = ext.lower().replace('.', '')
286         save = getattr(self, 'save_' + ext, self.unsupported)
287         try:        
288             save(filename, **kwargs)
289         except gobject.GError, e:
290             traceback.print_exc()
291             if e.code == 5:
292                 #add a hint due to a very consfusing error message when there is no space left on device
293                 raise SaveLoadError, _('Unable to save: %s\nDo you have enough space left on the device?') % e.message
294             else:
295                 raise SaveLoadError, _('Unable to save: %s') % e.message
296         except IOError, e:
297             traceback.print_exc()
298             raise SaveLoadError, _('Unable to save: %s') % e.strerror
299         self.unsaved_painting_time = 0.0
300
301     def load(self, filename):
302         if not os.path.isfile(filename):
303             raise SaveLoadError, _('File does not exist: %s') % repr(filename)
304         if not os.access(filename,os.R_OK):
305             raise SaveLoadError, _('You do not have the necessary permissions to open file: %s') % repr(filename)
306         trash, ext = os.path.splitext(filename)
307         ext = ext.lower().replace('.', '')
308         load = getattr(self, 'load_' + ext, self.unsupported)
309         try:
310             load(filename)
311         except gobject.GError, e:
312             traceback.print_exc()
313             raise SaveLoadError, _('Error while loading: GError %s') % e
314         except IOError, e:
315             traceback.print_exc()
316             raise SaveLoadError, _('Error while loading: IOError %s') % e
317         self.command_stack.clear()
318         self.unsaved_painting_time = 0.0
319         self.call_doc_observers()
320
321     def unsupported(self, filename):
322         raise SaveLoadError, _('Unknown file format extension: %s') % repr(filename)
323
324     def render_as_pixbuf(self, *args, **kwargs):
325         return pixbufsurface.render_as_pixbuf(self, *args, **kwargs)
326
327     def render_thumbnail(self):
328         t0 = time.time()
329         x, y, w, h = self.get_bbox()
330         mipmap_level = 0
331         while mipmap_level < tiledsurface.MAX_MIPMAP_LEVEL and max(w, h) >= 512:
332             mipmap_level += 1
333             x, y, w, h = x/2, y/2, w/2, h/2
334
335         pixbuf = self.render_as_pixbuf(x, y, w, h, mipmap_level=mipmap_level)
336         assert pixbuf.get_width() == w and pixbuf.get_height() == h
337         pixbuf = helpers.scale_proportionally(pixbuf, 256, 256)
338         print 'Rendered thumbnail in', time.time() - t0, 'seconds.'
339         return pixbuf
340
341     def save_png(self, filename, alpha=False, multifile=False):
342         if multifile:
343             self.save_multifile_png(filename)
344         else:
345             if alpha:
346                 tmp_layer = layer.Layer()
347                 for l in self.layers:
348                     l.merge_into(tmp_layer)
349                 tmp_layer.surface.save(filename)
350             else:
351                 pixbufsurface.save_as_png(self, filename, alpha=False)
352
353     def save_multifile_png(self, filename, alpha=False):
354         prefix, ext = os.path.splitext(filename)
355         # if we have a number already, strip it
356         l = prefix.rsplit('.', 1)
357         if l[-1].isdigit():
358             prefix = l[0]
359         doc_bbox = self.get_bbox()
360         for i, l in enumerate(self.layers):
361             filename = '%s.%03d%s' % (prefix, i+1, ext)
362             l.surface.save(filename, *doc_bbox)
363
364     def load_png(self, filename):
365         self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
366
367     def load_jpg(self, filename):
368         self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
369     load_jpeg = load_jpg
370
371     def save_jpg(self, filename, quality=90):
372         pixbuf = self.render_as_pixbuf()
373         pixbuf.save(filename, 'jpeg', options={'quality':str(quality)})
374     save_jpeg = save_jpg
375
376     def save_ora(self, filename, options=None):
377         print 'save_ora:'
378         t0 = time.time()
379         tempdir = tempfile.mkdtemp('mypaint')
380         # use .tmp extension, so we don't overwrite a valid file if there is an exception
381         z = zipfile.ZipFile(filename + '.tmpsave', 'w', compression=zipfile.ZIP_STORED)
382         # work around a permission bug in the zipfile library: http://bugs.python.org/issue3394
383         def write_file_str(filename, data):
384             zi = zipfile.ZipInfo(filename)
385             zi.external_attr = 0100644 << 16
386             z.writestr(zi, data)
387         write_file_str('mimetype', 'image/openraster') # must be the first file
388         image = ET.Element('image')
389         stack = ET.SubElement(image, 'stack')
390         x0, y0, w0, h0 = self.get_bbox()
391         a = image.attrib
392         a['w'] = str(w0)
393         a['h'] = str(h0)
394
395         def store_pixbuf(pixbuf, name):
396             tmp = join(tempdir, 'tmp.png')
397             t1 = time.time()
398             pixbuf.save(tmp, 'png')
399             print '  %.3fs pixbuf saving %s' % (time.time() - t1, name)
400             z.write(tmp, name)
401             os.remove(tmp)
402
403         def store_surface(surface, name, rect=[]):
404             tmp = join(tempdir, 'tmp.png')
405             t1 = time.time()
406             surface.save(tmp, *rect)
407             print '  %.3fs surface saving %s' % (time.time() - t1, name)
408             z.write(tmp, name)
409             os.remove(tmp)
410
411         def add_layer(x, y, opac, surface, name, layer_name, visible=True, rect=[]):
412             layer = ET.Element('layer')
413             stack.append(layer)
414             store_surface(surface, name, rect)
415             a = layer.attrib
416             if layer_name:
417                 a['name'] = layer_name
418             a['src'] = name
419             a['x'] = str(x)
420             a['y'] = str(y)
421             a['opacity'] = str(opac)
422             if visible:
423                 a['visibility'] = 'visible'
424             else:
425                 a['visibility'] = 'hidden'
426             return layer
427
428         for idx, l in enumerate(reversed(self.layers)):
429             if l.surface.is_empty():
430                 continue
431             opac = l.opacity
432             x, y, w, h = l.surface.get_bbox()
433             el = add_layer(x-x0, y-y0, opac, l.surface, 'data/layer%03d.png' % idx, l.name, l.visible, rect=(x, y, w, h))
434             # strokemap
435             sio = StringIO()
436             l.save_strokemap_to_file(sio, -x, -y)
437             data = sio.getvalue(); sio.close()
438             name = 'data/layer%03d_strokemap.dat' % idx
439             el.attrib['mypaint_strokemap_v2'] = name
440             write_file_str(name, data)
441
442         # save background as layer (solid color or tiled)
443         bg = self.background
444         # save as fully rendered layer
445         l = add_layer(0, 0, 1.0, bg, 'data/background.png', 'background', rect=(x0, y0, w0, h0))
446         x, y, w, h = bg.get_pattern_bbox()
447         # save as single pattern (with corrected origin)
448         store_surface(bg, 'data/background_tile.png', rect=(x+x0, y+y0, w, h))
449         l.attrib['background_tile'] = 'data/background_tile.png'
450
451         # preview (256x256)
452         t2 = time.time()
453         print '  starting to render full image for thumbnail...'
454
455         thumbnail_pixbuf = self.render_thumbnail()
456         store_pixbuf(thumbnail_pixbuf, 'Thumbnails/thumbnail.png')
457         print '  total %.3fs spent on thumbnail' % (time.time() - t2)
458
459         helpers.indent_etree(image)
460         xml = ET.tostring(image, encoding='UTF-8')
461
462         write_file_str('stack.xml', xml)
463         z.close()
464         os.rmdir(tempdir)
465         if os.path.exists(filename):
466             os.remove(filename) # windows needs that
467         os.rename(filename + '.tmpsave', filename)
468
469         print '%.3fs save_ora total' % (time.time() - t0)
470
471         return thumbnail_pixbuf
472
473     def load_ora(self, filename):
474         """Loads from an OpenRaster file"""
475         print 'load_ora:'
476         t0 = time.time()
477         tempdir = tempfile.mkdtemp('mypaint')
478         z = zipfile.ZipFile(filename)
479         print 'mimetype:', z.read('mimetype').strip()
480         xml = z.read('stack.xml')
481         image = ET.fromstring(xml)
482         stack = image.find('stack')
483
484         def get_pixbuf(filename):
485             t1 = time.time()
486             tmp = join(tempdir, 'tmp.png')
487             f = open(tmp, 'wb')
488
489             try:
490                 data = z.read(filename)
491             except KeyError:
492                 # support for bad zip files (saved by old versions of the GIMP ORA plugin)
493                 data = z.read(filename.encode('utf-8'))
494                 print 'WARNING: bad OpenRaster ZIP file. There is an utf-8 encoded filename that does not have the utf-8 flag set:', repr(filename)
495
496             f.write(data)
497             f.close()
498             res = gdk.pixbuf_new_from_file(tmp)
499             os.remove(tmp)
500             print '  %.3fs loading %s' % (time.time() - t1, filename)
501             return res
502
503         def get_layers_list(root, x=0,y=0):
504             res = []
505             for item in root:
506                 if item.tag == 'layer':
507                     if 'x' in item.attrib:
508                         item.attrib['x'] = int(item.attrib['x']) + x
509                     if 'y' in item.attrib:
510                         item.attrib['y'] = int(item.attrib['y']) + y
511                     res.append(item)
512                 elif item.tag == 'stack':
513                     stack_x = int( item.attrib.get('x', 0) )
514                     stack_y = int( item.attrib.get('y', 0) )
515                     res += get_layers_list(item, stack_x, stack_y)
516                 else:
517                     print 'Warning: ignoring unsupported tag:', item.tag
518             return res
519
520         self.clear() # this leaves one empty layer
521         no_background = True
522         for layer in get_layers_list(stack):
523             a = layer.attrib
524
525             if 'background_tile' in a:
526                 assert no_background
527                 try:
528                     print a['background_tile']
529                     self.set_background(get_pixbuf(a['background_tile']))
530                     no_background = False
531                     continue
532                 except backgroundsurface.BackgroundError, e:
533                     print 'ORA background tile not usable:', e
534
535             src = a.get('src', '')
536             if not src.lower().endswith('.png'):
537                 print 'Warning: ignoring non-png layer'
538                 continue
539             pixbuf = get_pixbuf(src)
540             name = a.get('name', '')
541             x = int(a.get('x', '0'))
542             y = int(a.get('y', '0'))
543             opac = float(a.get('opacity', '1.0'))
544             visible = not 'hidden' in a.get('visibility', 'visible')
545             self.add_layer(insert_idx=0, name=name)
546             last_pixbuf = pixbuf
547             t1 = time.time()
548             self.load_layer_from_pixbuf(pixbuf, x, y)
549             layer = self.layers[0]
550
551             self.set_layer_opacity(helpers.clamp(opac, 0.0, 1.0), layer)
552             self.set_layer_visibility(visible, layer)
553             print '  %.3fs converting pixbuf to layer format' % (time.time() - t1)
554             # strokemap
555             fname = a.get('mypaint_strokemap_v2', None)
556             if fname:
557                 if x % N or y % N:
558                     print 'Warning: dropping non-aligned strokemap'
559                 else:
560                     sio = StringIO(z.read(fname))
561                     layer.load_strokemap_from_file(sio, x, y)
562                     sio.close()
563
564         os.rmdir(tempdir)
565
566         if len(self.layers) == 1:
567             raise ValueError, 'Could not load any layer.'
568
569         if no_background:
570             # recognize solid or tiled background layers, at least those that mypaint <= 0.7.1 saves
571             t1 = time.time()
572             p = last_pixbuf
573             if not p.get_has_alpha() and p.get_width() % N == 0 and p.get_height() % N == 0:
574                 tiles = self.layers[0].surface.tiledict.values()
575                 if len(tiles) > 1:
576                     all_equal = True
577                     for tile in tiles[1:]:
578                         if (tile.rgba != tiles[0].rgba).any():
579                             all_equal = False
580                             break
581                     if all_equal:
582                         arr = helpers.gdkpixbuf2numpy(p)
583                         tile = arr[0:N,0:N,:]
584                         self.set_background(tile.copy())
585                         self.select_layer(0)
586                         self.remove_layer()
587             print '  %.3fs recognizing tiled background' % (time.time() - t1)
588
589         if len(self.layers) > 1:
590             # remove the still present initial empty top layer
591             self.select_layer(len(self.layers)-1)
592             self.remove_layer()
593             # this leaves the topmost layer selected
594
595         print '%.3fs load_ora total' % (time.time() - t0)