OSDN Git Service

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