OSDN Git Service

Layer opacity and merging undo/redo
[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 """
10 Design thoughts:
11 A stroke:
12 - is a list of motion events
13 - knows everything needed to draw itself (brush settings / initial brush state)
14 - has fixed brush settings (only brush states can change during a stroke)
15
16 A layer:
17 - is a container of several strokes (strokes can be removed)
18 - can be rendered as a whole
19 - can contain cache bitmaps, so it doesn't have to retrace all strokes all the time
20
21 A document:
22 - contains several layers
23 - knows the active layer and the current brush
24 - manages the undo history
25 - must be altered via undo/redo commands (except painting)
26 """
27
28 import os, zipfile, tempfile, time
29 join = os.path.join
30 import xml.etree.ElementTree as ET
31 from gtk import gdk
32
33 import helpers, tiledsurface, pixbufsurface, backgroundsurface
34 import command, stroke, layer
35 import brush # FIXME: the brush module depends on gtk and everything, but we only need brush_lowlevel
36 N = tiledsurface.N
37
38 class SaveLoadError(Exception):
39     """Expected errors on loading or saving, like missing permissions or non-existing files."""
40     pass
41
42 class Document():
43     """
44     This is the "model" in the Model-View-Controller design.
45     (The "view" would be ../gui/tileddrawwidget.py.)
46     It represenst everything that the user would want to save.
47
48
49     The "controller" mostly in drawwindow.py.
50     It should be possible to use it without any GUI attached.
51     
52     Undo/redo is part of the model. The whole undo/redo stack can be
53     saved to disk (planned) and can be used to reconstruct
54     everything else.
55     """
56     # Please note the following difficulty:
57     #
58     #   Most of the time there is an unfinished (but already rendered)
59     #   stroke pending, which has to be turned into a command.Action
60     #   or discarded as empty before any other action is possible.
61     #
62     # TODO: the document should allow to "playback" (redo) a stroke
63     # partially and examine its timing (realtime playback / calculate
64     # total painting time) ?using half-done commands?
65
66     def __init__(self):
67         self.brush = brush.Brush_Lowlevel()
68         self.stroke = None
69         self.canvas_observers = []
70         self.stroke_observers = [] # callback arguments: stroke, brush (brush is a temporary read-only convenience object)
71         self.clear(True)
72
73     def clear(self, init=False):
74         self.split_stroke()
75         if not init:
76             bbox = self.get_bbox()
77         # throw everything away, including undo stack
78         self.command_stack = command.CommandStack()
79         self.set_background((255, 255, 255))
80         self.layers = []
81         self.layer_idx = None
82         self.add_layer(0)
83         # disallow undo of the first layer
84         self.command_stack.clear()
85         self.unsaved_painting_time = 0.0
86
87         if not init:
88             for f in self.canvas_observers:
89                 f(*bbox)
90
91     def get_current_layer(self):
92         return self.layers[self.layer_idx]
93     layer = property(get_current_layer)
94
95     def split_stroke(self):
96         if not self.stroke: return
97         self.stroke.stop_recording()
98         if not self.stroke.empty:
99             self.command_stack.do(command.Stroke(self, self.stroke, self.snapshot_before_stroke))
100             del self.snapshot_before_stroke
101             self.unsaved_painting_time += self.stroke.total_painting_time
102             for f in self.stroke_observers:
103                 f(self.stroke, self.brush)
104         self.stroke = None
105
106     def select_layer(self, idx):
107         self.do(command.SelectLayer(self, idx))
108
109     def clear_layer(self):
110         self.do(command.ClearLayer(self))
111
112     def stroke_to(self, dtime, x, y, pressure):
113         if not self.stroke:
114             self.stroke = stroke.Stroke()
115             self.stroke.start_recording(self.brush)
116             self.snapshot_before_stroke = self.layer.save_snapshot()
117         self.stroke.record_event(dtime, x, y, pressure)
118
119         l = self.layer
120         l.surface.begin_atomic()
121         split = self.brush.stroke_to (l.surface, x, y, pressure, dtime)
122         l.surface.end_atomic()
123
124         if split:
125             self.split_stroke()
126
127     def layer_modified_cb(self, *args):
128         # for now, any layer modification is assumed to be visible
129         for f in self.canvas_observers:
130             f(*args)
131
132     def invalidate_all(self):
133         for f in self.canvas_observers:
134             f(0, 0, 0, 0)
135
136     def undo(self):
137         self.split_stroke()
138         while 1:
139             cmd = self.command_stack.undo()
140             if not cmd or not cmd.automatic_undo:
141                 return cmd
142
143     def redo(self):
144         self.split_stroke()
145         while 1:
146             cmd = self.command_stack.redo()
147             if not cmd or not cmd.automatic_undo:
148                 return cmd
149
150     def do(self, cmd):
151         self.split_stroke()
152         self.command_stack.do(cmd)
153
154     def get_last_command(self):
155         self.split_stroke()
156         return self.command_stack.get_last_command()
157
158     def set_brush(self, brush):
159         self.split_stroke()
160         self.brush.copy_settings_from(brush)
161
162     def get_bbox(self):
163         res = helpers.Rect()
164         for layer in self.layers:
165             # OPTIMIZE: only visible layers...
166             # careful: currently saving assumes that all layers are included
167             bbox = layer.surface.get_bbox()
168             res.expandToIncludeRect(bbox)
169         return res
170
171     def blit_tile_into(self, dst, tx, ty, layers=None, background=None):
172         if layers is None:
173             layers = self.layers
174         if background is None:
175             background = self.background
176
177         background.blit_tile_into(dst, tx, ty)
178
179         for layer in layers:
180             surface = layer.surface
181             surface.composite_tile_over(dst, tx, ty, layer.opacity)
182             
183     def add_layer(self, insert_idx):
184         self.do(command.AddLayer(self, insert_idx))
185
186     def remove_layer(self):
187         self.do(command.RemoveLayer(self))
188
189     def merge_layer(self, dst_idx):
190         self.do(command.MergeLayer(self, dst_idx))
191
192     def load_layer_from_pixbuf(self, pixbuf, x=0, y=0):
193         arr = helpers.gdkpixbuf2numpy(pixbuf)
194         self.do(command.LoadLayer(self, arr, x, y))
195
196     def set_layer_opacity(self, opacity):
197         cmd = self.get_last_command()
198         if isinstance(cmd, command.SetLayerOpacity):
199             self.undo()
200         self.do(command.SetLayerOpacity(self, opacity))
201
202     def set_background(self, obj):
203         # This is not an undoable action. One reason is that dragging
204         # on the color chooser would get tons of undo steps.
205
206         if not isinstance(obj, backgroundsurface.Background):
207             obj = backgroundsurface.Background(obj)
208         self.background = obj
209
210         self.invalidate_all()
211
212     def get_background_pixbuf(self):
213         pixbuf = gdk.Pixbuf(gdk.COLORSPACE_RGB, False, 8, N, N)
214         arr = helpers.gdkpixbuf2numpy(pixbuf)
215         arr[:,:,:] = self.background
216         return pixbuf
217
218     def load_from_pixbuf(self, pixbuf):
219         self.clear()
220         self.load_layer_from_pixbuf(pixbuf)
221
222     def is_layered(self):
223         count = 0
224         for l in self.layers:
225             if not l.surface.is_empty():
226                 count += 1
227         return count > 1
228
229     def save(self, filename, **kwargs):
230         #if the file doesnt allready exist, we need to check permissions on the directory
231         if os.path.isfile(filename):
232             path = filename
233         else:
234             path = os.path.dirname(filename)
235             if not path:
236                 path = '.'
237         if not os.access(path,os.W_OK):
238             raise SaveLoadError, 'You do not have the necessary permissions to save file: ' + repr(filename)
239         trash, ext = os.path.splitext(filename)
240         ext = ext.lower().replace('.', '')
241         save = getattr(self, 'save_' + ext, self.unsupported)
242         save(filename, **kwargs)
243         self.unsaved_painting_time = 0.0
244
245     def load(self, filename):
246         if not os.path.isfile(filename):
247             raise SaveLoadError, 'File does not exist: ' + repr(filename)
248         if not os.access(filename,os.R_OK):
249             raise SaveLoadError, 'You do not have the necessary permissions to open file: ' + repr(filename)
250         trash, ext = os.path.splitext(filename)
251         ext = ext.lower().replace('.', '')
252         load = getattr(self, 'load_' + ext, self.unsupported)
253         load(filename)
254         self.command_stack.clear()
255         self.unsaved_painting_time = 0.0
256
257     def unsupported(self, filename):
258         raise SaveLoadError, 'Unknown file format extension: ' + repr(filename)
259
260     def render_as_pixbuf(self, *args):
261         return pixbufsurface.render_as_pixbuf(self, *args)
262
263     def save_png(self, filename, compression=2, alpha=False):
264         if alpha:
265             tmp_layer = layer.Layer()
266             for l in self.layers:
267                 l.merge_into(tmp_layer)
268             pixbuf = tmp_layer.surface.render_as_pixbuf()
269         else:
270             pixbuf = self.render_as_pixbuf()
271         pixbuf.save(filename, 'png', {'compression':str(compression)})
272
273     def load_png(self, filename):
274         self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
275
276     def load_jpg(self, filename):
277         self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
278     load_jpeg = load_jpg
279
280     def save_jpg(self, filename, quality=90):
281         pixbuf = self.render_as_pixbuf()
282         pixbuf.save(filename, 'jpeg', options={'quality':str(quality)})
283     save_jpeg = save_jpg
284
285     def save_ora(self, filename, options=None):
286         print 'save_ora:'
287         t0 = time.time()
288         tempdir = tempfile.mkdtemp('mypaint')
289         # use .tmp extension, so we don't overwrite a valid file if there is an exception
290         z = zipfile.ZipFile(filename + '.tmpsave', 'w', compression=zipfile.ZIP_STORED)
291         # work around a permission bug in the zipfile library: http://bugs.python.org/issue3394
292         def write_file_str(filename, data):
293             zi = zipfile.ZipInfo(filename)
294             zi.external_attr = 0100644 << 16
295             z.writestr(zi, data)
296         write_file_str('mimetype', 'image/openraster') # must be the first file
297         image = ET.Element('image')
298         stack = ET.SubElement(image, 'stack')
299         x0, y0, w0, h0 = self.get_bbox()
300         a = image.attrib
301         a['w'] = str(w0)
302         a['h'] = str(h0)
303
304         def store_pixbuf(pixbuf, name):
305             tmp = join(tempdir, 'tmp.png')
306             t1 = time.time()
307             pixbuf.save(tmp, 'png', {'compression':'2'})
308             print '  %.3fs saving %s compression 2' % (time.time() - t1, name)
309             z.write(tmp, name)
310             os.remove(tmp)
311
312         def add_layer(x, y, opac, pixbuf, name):
313             layer = ET.Element('layer')
314             stack.append(layer)
315             store_pixbuf(pixbuf, name)
316             a = layer.attrib
317             a['src'] = name
318             a['x'] = str(x)
319             a['y'] = str(y)
320             a['opacity'] = str(opac)
321
322         for idx, l in enumerate(reversed(self.layers)):
323             if l.surface.is_empty():
324                 continue
325             opac = l.opacity
326             x, y, w, h = l.surface.get_bbox()
327             pixbuf = l.surface.render_as_pixbuf()
328             add_layer(x-x0, y-y0, opac, pixbuf, 'data/layer%03d.png' % idx)
329
330         # save background as layer (solid color or tiled)
331         s = pixbufsurface.Surface(x0, y0, w0, h0)
332         s.fill(self.background)
333         add_layer(0, 0, 1.0, s.pixbuf, 'data/background.png')
334
335         # preview
336         t2 = time.time()
337         print '  starting to render image for thumbnail...'
338         pixbuf = self.render_as_pixbuf()
339         w, h = pixbuf.get_width(), pixbuf.get_height()
340         if w > h:
341             w, h = 256, max(h*256/w, 1)
342         else:
343             w, h = max(w*256/h, 1), 256
344         t1 = time.time()
345         pixbuf = pixbuf.scale_simple(w, h, gdk.INTERP_BILINEAR)
346         print '  %.3fs scaling thumbnail' % (time.time() - t1)
347         store_pixbuf(pixbuf, 'Thumbnails/thumbnail.png')
348         print '  total %.3fs spent on thumbnail' % (time.time() - t2)
349
350         helpers.indent_etree(image)
351         xml = ET.tostring(image, encoding='UTF-8')
352
353         write_file_str('stack.xml', xml)
354         z.close()
355         os.rmdir(tempdir)
356         os.rename(filename + '.tmpsave', filename)
357
358         print '%.3fs save_ora total' % (time.time() - t0)
359
360     def load_ora(self, filename):
361         print 'load_ora:'
362         t0 = time.time()
363         tempdir = tempfile.mkdtemp('mypaint')
364         z = zipfile.ZipFile(filename)
365         print 'mimetype:', z.read('mimetype').strip()
366         xml = z.read('stack.xml')
367         image = ET.fromstring(xml)
368         stack = image.find('stack')
369
370         self.clear() # this leaves one empty layer
371         for layer in stack:
372             if layer.tag != 'layer':
373                 print 'Warning: ignoring unsupported tag:', layer.tag
374                 continue
375             a = layer.attrib
376             src = a.get('src', '')
377             if not src.lower().endswith('.png'):
378                 print 'Warning: ignoring non-png layer'
379                 continue
380
381             tmp = join(tempdir, 'tmp.png')
382             f = open(tmp, 'wb')
383             f.write(z.read(src))
384             f.close()
385             t1 = time.time()
386             pixbuf = gdk.pixbuf_new_from_file(tmp)
387             print '  %.3fs loading %s' % (time.time() - t1, src)
388             os.remove(tmp)
389
390             x = int(a.get('x', '0'))
391             y = int(a.get('y', '0'))
392             opac = float(a.get('opacity', '1.0'))
393             self.add_layer(insert_idx=0)
394             last_pixbuf = pixbuf
395             t1 = time.time()
396             self.load_layer_from_pixbuf(pixbuf, x, y)
397             self.layers[0].opacity = helpers.clamp(opac, 0.0, 1.0)
398             print '  %.3fs converting pixbuf to layer format' % (time.time() - t1)
399
400         os.rmdir(tempdir)
401
402         if len(self.layers) == 1:
403             raise ValueError, 'Could not load any layer.'
404
405         # recognize solid or tiled background layers, at least those that mypaint saves
406         # (OpenRaster will probably get generator layers for this some day)
407         t1 = time.time()
408         p = last_pixbuf
409         if not p.get_has_alpha() and p.get_width() % N == 0 and p.get_height() % N == 0:
410             tiles = self.layers[0].surface.tiledict.values()
411             if len(tiles) > 1:
412                 all_equal = True
413                 for tile in tiles[1:]:
414                     if (tile.rgba != tiles[0].rgba).any():
415                         all_equal = False
416                         break
417                 if all_equal:
418                     arr = helpers.gdkpixbuf2numpy(p)
419                     tile = arr[0:N,0:N,:]
420                     self.set_background(tile.copy())
421                     self.select_layer(0)
422                     self.remove_layer()
423         print '  %.3fs recognizing tiled background' % (time.time() - t1)
424
425         if len(self.layers) > 1:
426             # remove the still present initial empty top layer
427             self.select_layer(len(self.layers)-1)
428             self.remove_layer()
429             # this leaves the topmost layer selected
430
431         print '%.3fs load_ora total' % (time.time() - t0)