OSDN Git Service

fix wrong warning about unsaved painting time
[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 mypaintlib, helpers, tiledsurface, pixbufsurface
29 import command, stroke, layer, serialize
30 import brush # FIXME: the brush module depends on gtk and everything, but we only need brush_lowlevel
31 import gzip, os, zipfile, tempfile, numpy, time
32 join = os.path.join
33 import xml.etree.ElementTree as ET
34 from gtk import gdk
35
36 N = tiledsurface.N
37
38 class Document():
39     """
40     This is the "model" in the Model-View-Controller design.
41     (The "view" would be ../gui/tileddrawwidget.py.)
42     It represenst everything that the user would want to save.
43
44
45     The "controller" mostly in drawwindow.py.
46     It should be possible to use it without any GUI attached.
47     
48     Undo/redo is part of the model. The whole undo/redo stack can be
49     saved to disk (planned) and can be used to reconstruct
50     everything else.
51     """
52     # Please note the following difficulty:
53     #
54     #   Most of the time there is an unfinished (but already rendered)
55     #   stroke pending, which has to be turned into a command.Action
56     #   or discarded as empty before any other action is possible.
57     #
58     # TODO: the document should allow to "playback" (redo) a stroke
59     # partially and examine its timing (realtime playback / calculate
60     # total painting time) ?using half-done commands?
61
62     def __init__(self):
63         self.brush = brush.Brush_Lowlevel()
64         self.stroke = None
65         self.canvas_observers = []
66         self.layer_observers = []
67         self.unsaved_painting_time = 0.0
68
69         self.clear(True)
70
71     def clear(self, init=False):
72         self.split_stroke()
73         if not init:
74             bbox = self.get_bbox()
75         # throw everything away, including undo stack
76         self.command_stack = command.CommandStack()
77         self.set_background((255, 255, 255))
78         self.layers = []
79         self.layer_idx = None
80         self.add_layer(0)
81         # disallow undo of the first layer
82         self.command_stack.clear()
83
84         if not init:
85             for f in self.canvas_observers:
86                 f(*bbox)
87
88     def get_current_layer(self):
89         return self.layers[self.layer_idx]
90     layer = property(get_current_layer)
91
92     def split_stroke(self):
93         if not self.stroke: return
94         self.stroke.stop_recording()
95         if not self.stroke.empty:
96             self.layer.strokes.append(self.stroke)
97             before = self.snapshot_before_stroke
98             after = self.layer.save_snapshot()
99             self.command_stack.do(command.Stroke(self, self.stroke, before, after))
100             self.snapshot_before_stroke = after
101             self.unsaved_painting_time += self.stroke.total_painting_time
102         self.stroke = None
103
104     def select_layer(self, idx):
105         self.do(command.SelectLayer(self, idx))
106
107     def clear_layer(self):
108         self.do(command.ClearLayer(self))
109
110     def stroke_to(self, dtime, x, y, pressure):
111         if not self.stroke:
112             self.stroke = stroke.Stroke()
113             self.stroke.start_recording(self.brush)
114             self.snapshot_before_stroke = self.layer.save_snapshot()
115         self.stroke.record_event(dtime, x, y, pressure)
116
117         l = self.layer
118         l.surface.begin_atomic()
119         split = self.brush.stroke_to (l.surface, x, y, pressure, dtime)
120         l.surface.end_atomic()
121
122         if split:
123             self.split_stroke()
124
125     def layer_modified_cb(self, *args):
126         # for now, any layer modification is assumed to be visible
127         for f in self.canvas_observers:
128             f(*args)
129
130     def invalidate_all(self):
131         for f in self.canvas_observers:
132             f(0, 0, 0, 0)
133
134     def undo(self):
135         self.split_stroke()
136         while 1:
137             cmd = self.command_stack.undo()
138             if not cmd or not cmd.automatic_undo:
139                 return cmd
140
141     def redo(self):
142         self.split_stroke()
143         while 1:
144             cmd = self.command_stack.redo()
145             if not cmd or not cmd.automatic_undo:
146                 return cmd
147
148     def do(self, cmd):
149         self.split_stroke()
150         self.command_stack.do(cmd)
151
152     def set_brush(self, brush):
153         self.split_stroke()
154         self.brush.copy_settings_from(brush)
155
156     def get_bbox(self):
157         res = helpers.Rect()
158         for layer in self.layers:
159             # OPTIMIZE: only visible layers...
160             # careful: currently saving assumes that all layers are included
161             bbox = layer.surface.get_bbox()
162             res.expandToIncludeRect(bbox)
163         return res
164
165     def blit_tile_into(self, dst, tx, ty, layers=None):
166         if layers is None:
167             layers = self.layers
168
169         # render solid or tiled background
170         #dst[:] = self.background_memory # 13 times slower than below, with some bursts having the same speed as below (huh?)
171         # note: optimization for solid colors is not worth it any more now, even if it gives 2x speedup (at best)
172         mypaintlib.tile_blit_rgb8_into_rgb8(self.background_memory, dst)
173
174         for layer in layers:
175             surface = layer.surface
176             surface.composite_tile_over(dst, tx, ty)
177             
178     def add_layer(self, insert_idx):
179         self.do(command.AddLayer(self, insert_idx))
180
181     def remove_layer(self):
182         self.do(command.RemoveLayer(self))
183
184     def load_layer_from_pixbuf(self, pixbuf, x=0, y=0):
185         arr = helpers.gdkpixbuf2numpy(pixbuf)
186         self.do(command.LoadLayer(self, arr, x, y))
187
188     def set_background(self, obj):
189         # This is not an undoable action. One reason is that dragging
190         # on the color chooser would get tons of undo steps.
191         try:
192             obj = helpers.gdkpixbuf2numpy(obj)
193         except:
194             # it was already an array
195             pass
196         if len(obj) > 3:
197             # simplify single-color pixmaps
198             color = obj[0,0,:]
199             if (obj == color).all():
200                 obj = tuple(color)
201         self.background = obj
202
203         # optimization
204         self.background_memory = numpy.zeros((N, N, 3), dtype='uint8')
205         self.background_memory[:,:,:] = self.background
206
207         self.invalidate_all()
208
209     def get_background_pixbuf(self):
210         pixbuf = gdk.Pixbuf(gdk.COLORSPACE_RGB, False, 8, N, N)
211         arr = helpers.gdkpixbuf2numpy(pixbuf)
212         arr[:,:,:] = self.background
213         return pixbuf
214
215     def load_from_pixbuf(self, pixbuf):
216         self.clear()
217         self.load_layer_from_pixbuf(pixbuf)
218
219     def is_layered(self):
220         count = 0
221         for l in self.layers:
222             if not l.surface.is_empty():
223                 count += 1
224         return count > 1
225
226     def save(self, filename):
227         trash, ext = os.path.splitext(filename)
228         ext = ext.lower().replace('.', '')
229         print ext
230         save = getattr(self, 'save_' + ext, self.unsupported)
231         save(filename)
232         self.unsaved_painting_time = 0.0
233
234     def load(self, filename):
235         trash, ext = os.path.splitext(filename)
236         ext = ext.lower().replace('.', '')
237         load = getattr(self, 'load_' + ext, self.unsupported)
238         load(filename)
239         self.command_stack.clear()
240         self.unsaved_painting_time = 0.0
241
242     def unsupported(self, filename):
243         raise ValueError, 'Unkwnown file format extension: ' + repr(filename)
244
245     def render_as_pixbuf(self, *args):
246         return pixbufsurface.render_as_pixbuf(self, *args)
247
248     def save_png(self, filename):
249         pixbuf = self.render_as_pixbuf()
250         pixbuf.save(filename, 'png')
251
252     def load_png(self, filename):
253         self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
254
255     def load_jpg(self, filename):
256         self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
257
258     def save_jpg(self, filename):
259         pixbuf = self.render_as_pixbuf()
260         pixbuf.save(filename, 'jpeg', {'quality':'90'})
261
262     def save_ora(self, filename):
263         tempdir = tempfile.mkdtemp('mypaint')
264         z = zipfile.ZipFile(filename, 'w', compression=zipfile.ZIP_STORED)
265         # work around a permission bug in the zipfile library: http://bugs.python.org/issue3394
266         def write_file_str(filename, data):
267             zi = zipfile.ZipInfo(filename)
268             zi.external_attr = 0100644 << 16
269             z.writestr(zi, data)
270         write_file_str('mimetype', 'image/openraster') # must be the first file
271         image = ET.Element('image')
272         stack = ET.SubElement(image, 'stack')
273         x0, y0, w0, h0 = self.get_bbox()
274         a = image.attrib
275         a['x'] = str(0)
276         a['y'] = str(0)
277         a['w'] = str(w0)
278         a['h'] = str(h0)
279
280         def add_layer(x, y, pixbuf, name):
281             layer = ET.Element('layer')
282             stack.append(layer)
283
284             tmp = join(tempdir, 'tmp.png')
285             pixbuf.save(tmp, 'png')
286             z.write(tmp, name)
287             os.remove(tmp)
288
289             a = layer.attrib
290             a['src'] = name
291             a['x'] = str(x)
292             a['y'] = str(y)
293
294         for idx, l in enumerate(reversed(self.layers)):
295             if l.surface.is_empty():
296                 continue
297             x, y, w, h = l.surface.get_bbox()
298             pixbuf = l.surface.render_as_pixbuf()
299             add_layer(x-x0, y-y0, pixbuf, 'data/layer%03d.png' % idx)
300
301         # save background as layer (solid color or tiled)
302         s = pixbufsurface.Surface(0, 0, w0, h0)
303         s.fill(self.background)
304         add_layer(0, 0, s.pixbuf, 'data/background.png')
305
306         xml = ET.tostring(image, encoding='UTF-8')
307
308         write_file_str('stack.xml', xml)
309         z.close()
310         os.rmdir(tempdir)
311
312     def load_ora(self, filename):
313         tempdir = tempfile.mkdtemp('mypaint')
314         z = zipfile.ZipFile(filename)
315         print 'mimetype:', z.read('mimetype').strip()
316         xml = z.read('stack.xml')
317         image = ET.fromstring(xml)
318         stack = image.find('stack')
319
320         self.clear() # this leaves one empty layer
321         for layer in stack:
322             if layer.tag != 'layer':
323                 print 'Warning: ignoring unsupported tag:', layer.tag
324                 continue
325             a = layer.attrib
326             src = a.get('src', '')
327             if not src.lower().endswith('.png'):
328                 print 'Warning: ignoring non-png layer'
329                 continue
330
331             tmp = join(tempdir, 'tmp.png')
332             f = open(tmp, 'w')
333             f.write(z.read(src))
334             f.close()
335             pixbuf = gdk.pixbuf_new_from_file(tmp)
336             os.remove(tmp)
337
338             x = int(a.get('x', '0'))
339             y = int(a.get('y', '0'))
340             self.add_layer(insert_idx=0)
341             last_pixbuf = pixbuf
342             self.load_layer_from_pixbuf(pixbuf, x, y)
343
344         os.rmdir(tempdir)
345
346         if len(self.layers) == 1:
347             raise ValueError, 'Could not load any layer.'
348
349         # recognize solid or tiled background layers, at least those that mypaint saves
350         # (OpenRaster will probably get generator layers for this some day)
351         p = last_pixbuf
352         if not p.get_has_alpha() and p.get_width() % N == 0 and p.get_height() % N == 0:
353             tiles = self.layers[0].surface.tiledict.values()
354             if len(tiles) > 1:
355                 all_equal = True
356                 for tile in tiles[1:]:
357                     if (tile.rgba != tiles[0].rgba).any():
358                         all_equal = False
359                         break
360                 if all_equal:
361                     arr = helpers.gdkpixbuf2numpy(p)
362                     tile = arr[0:N,0:N,:]
363                     self.set_background(tile.copy())
364                     self.select_layer(0)
365                     self.remove_layer()
366
367         if len(self.layers) > 1:
368             # select the still present initial empty top layer
369             # hm, should this better be removed?
370             self.select_layer(len(self.layers)-1)
371