OSDN Git Service

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