1 # This file is part of MyPaint.
2 # Copyright (C) 2007-2008 by Martin Renold <martinxyz@gmx.ch>
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.
9 import os, zipfile, tempfile, time
11 import xml.etree.ElementTree as ET
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
20 class SaveLoadError(Exception):
21 """Expected errors on loading or saving, like missing permissions or non-existing files."""
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.
31 The "controller" mostly in drawwindow.py.
32 It is possible to use it without any GUI attached (see ../tests/)
34 # Please note the following difficulty with the undo stack:
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.
42 self.brush = brush.Brush_Lowlevel()
44 self.canvas_observers = []
45 self.stroke_observers = [] # callback arguments: stroke, brush (brush is a temporary read-only convenience object)
48 def clear(self, init=False):
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))
58 # disallow undo of the first layer
59 self.command_stack.clear()
60 self.unsaved_painting_time = 0.0
63 for f in self.canvas_observers:
66 def get_current_layer(self):
67 return self.layers[self.layer_idx]
68 layer = property(get_current_layer)
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)
81 def select_layer(self, idx):
82 self.do(command.SelectLayer(self, idx))
84 def clear_layer(self):
85 self.do(command.ClearLayer(self))
87 def stroke_to(self, dtime, x, y, pressure):
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)
95 l.surface.begin_atomic()
96 split = self.brush.stroke_to (l.surface, x, y, pressure, dtime)
97 l.surface.end_atomic()
102 def straight_line(self, src, dst):
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)
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)
119 self.stroke_to(duration/N, x[i], y[i], pressure)
121 self.brush = real_brush
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:
129 def invalidate_all(self):
130 for f in self.canvas_observers:
136 cmd = self.command_stack.undo()
137 if not cmd or not cmd.automatic_undo:
143 cmd = self.command_stack.redo()
144 if not cmd or not cmd.automatic_undo:
149 self.command_stack.do(cmd)
151 def get_last_command(self):
153 return self.command_stack.get_last_command()
155 def set_brush(self, brush):
157 self.brush.copy_settings_from(brush)
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)
168 def blit_tile_into(self, dst_8bit, tx, ty, mipmap=0, layers=None, background=None):
171 if background is None:
172 background = self.background
174 assert dst_8bit.dtype == 'uint8'
175 dst = numpy.empty((N, N, 3), dtype='uint16')
177 background.blit_tile_into(dst, tx, ty, mipmap)
180 surface = layer.surface
181 surface.composite_tile_over(dst, tx, ty, mipmap_level=mipmap, opacity=layer.opacity)
183 mypaintlib.tile_convert_rgb16_to_rgb8(dst, dst_8bit)
185 def add_layer(self, insert_idx):
186 self.do(command.AddLayer(self, insert_idx))
188 def remove_layer(self):
189 self.do(command.RemoveLayer(self))
191 def merge_layer(self, dst_idx):
192 self.do(command.MergeLayer(self, dst_idx))
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))
198 def set_layer_opacity(self, opacity):
199 cmd = self.get_last_command()
200 if isinstance(cmd, command.SetLayerOpacity):
202 self.do(command.SetLayerOpacity(self, opacity))
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.
208 if not isinstance(obj, backgroundsurface.Background):
209 obj = backgroundsurface.Background(obj)
210 self.background = obj
212 self.invalidate_all()
214 def load_from_pixbuf(self, pixbuf):
216 self.load_layer_from_pixbuf(pixbuf)
218 def is_layered(self):
220 for l in self.layers:
221 if not l.surface.is_empty():
225 def save(self, filename, **kwargs):
227 trash, ext = os.path.splitext(filename)
228 ext = ext.lower().replace('.', '')
229 save = getattr(self, 'save_' + ext, self.unsupported)
231 save(filename, **kwargs)
232 except gobject.GError, e:
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?'
237 raise SaveLoadError, 'Unable to save: ' + e.message
239 raise SaveLoadError, 'Unable to save: ' + e.strerror
240 self.unsaved_painting_time = 0.0
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)
251 self.command_stack.clear()
252 self.unsaved_painting_time = 0.0
254 def unsupported(self, filename):
255 raise SaveLoadError, 'Unknown file format extension: ' + repr(filename)
257 def render_as_pixbuf(self, *args):
258 return pixbufsurface.render_as_pixbuf(self, *args)
260 def save_png(self, filename, compression=2, alpha=False, multifile=False):
262 self.save_multifile_png(filename, compression)
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()
270 pixbuf = self.render_as_pixbuf()
271 pixbuf.save(filename, 'png', {'compression':str(compression)})
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)
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)
284 def load_png(self, filename):
285 self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
287 def load_jpg(self, filename):
288 self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
291 def save_jpg(self, filename, quality=90):
292 pixbuf = self.render_as_pixbuf()
293 pixbuf.save(filename, 'jpeg', options={'quality':str(quality)})
296 def save_ora(self, filename, options=None):
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
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()
315 def store_pixbuf(pixbuf, name):
316 tmp = join(tempdir, 'tmp.png')
318 pixbuf.save(tmp, 'png', {'compression':'2'})
319 print ' %.3fs saving %s compression 2' % (time.time() - t1, name)
323 def add_layer(x, y, opac, pixbuf, name):
324 layer = ET.Element('layer')
326 store_pixbuf(pixbuf, name)
331 a['opacity'] = str(opac)
334 for idx, l in enumerate(reversed(self.layers)):
335 if l.surface.is_empty():
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)
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)
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')
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'
359 print ' starting to render image for thumbnail...'
360 pixbuf = self.render_as_pixbuf()
361 w, h = pixbuf.get_width(), pixbuf.get_height()
363 w, h = 256, max(h*256/w, 1)
365 w, h = max(w*256/h, 1), 256
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)
372 helpers.indent_etree(image)
373 xml = ET.tostring(image, encoding='UTF-8')
375 write_file_str('stack.xml', xml)
378 if os.path.exists(filename):
379 os.remove(filename) # windows needs that
380 os.rename(filename + '.tmpsave', filename)
382 print '%.3fs save_ora total' % (time.time() - t0)
384 def load_ora(self, filename):
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')
394 def get_pixbuf(filename):
396 tmp = join(tempdir, 'tmp.png')
398 f.write(z.read(filename))
400 res = gdk.pixbuf_new_from_file(tmp)
402 print ' %.3fs loading %s' % (time.time() - t1, filename)
405 def get_layers_list(root, x=0,y=0):
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
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)
419 print 'Warning: ignoring unsupported tag:', item.tag
422 self.clear() # this leaves one empty layer
424 for layer in get_layers_list(stack):
427 if 'background_tile' in a:
430 print a['background_tile']
431 self.set_background(get_pixbuf(a['background_tile']))
432 no_background = False
434 except backgroundsurface.BackgroundError, e:
435 print 'ORA background tile not usable:', e
437 src = a.get('src', '')
438 if not src.lower().endswith('.png'):
439 print 'Warning: ignoring non-png layer'
441 pixbuf = get_pixbuf(src)
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)
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)
453 fname = a.get('mypaint_strokemap', None)
456 print 'Warning: dropping non-aligned strokemap'
459 self.layers[0].load_strokemap_from_string(data, x, y)
463 if len(self.layers) == 1:
464 raise ValueError, 'Could not load any layer.'
467 # recognize solid or tiled background layers, at least those that mypaint <= 0.7.1 saves
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()
474 for tile in tiles[1:]:
475 if (tile.rgba != tiles[0].rgba).any():
479 arr = helpers.gdkpixbuf2numpy(p)
480 tile = arr[0:N,0:N,:]
481 self.set_background(tile.copy())
484 print ' %.3fs recognizing tiled background' % (time.time() - t1)
486 if len(self.layers) > 1:
487 # remove the still present initial empty top layer
488 self.select_layer(len(self.layers)-1)
490 # this leaves the topmost layer selected
492 print '%.3fs load_ora total' % (time.time() - t0)