OSDN Git Service

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