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.
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)
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
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)
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
33 import xml.etree.ElementTree as ET
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.
45 The "controller" mostly in drawwindow.py.
46 It should be possible to use it without any GUI attached.
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
52 # Please note the following difficulty:
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.
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?
63 self.brush = brush.Brush_Lowlevel()
65 self.canvas_observers = []
66 self.layer_observers = []
67 self.unsaved_painting_time = 0.0
71 def clear(self, init=False):
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))
81 # disallow undo of the first layer
82 self.command_stack.clear()
85 for f in self.canvas_observers:
88 def get_current_layer(self):
89 return self.layers[self.layer_idx]
90 layer = property(get_current_layer)
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
104 def select_layer(self, idx):
105 self.do(command.SelectLayer(self, idx))
107 def clear_layer(self):
108 self.do(command.ClearLayer(self))
110 def stroke_to(self, dtime, x, y, pressure):
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)
118 l.surface.begin_atomic()
119 split = self.brush.stroke_to (l.surface, x, y, pressure, dtime)
120 l.surface.end_atomic()
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:
130 def invalidate_all(self):
131 for f in self.canvas_observers:
137 cmd = self.command_stack.undo()
138 if not cmd or not cmd.automatic_undo:
144 cmd = self.command_stack.redo()
145 if not cmd or not cmd.automatic_undo:
150 self.command_stack.do(cmd)
152 def set_brush(self, brush):
154 self.brush.copy_settings_from(brush)
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)
165 def blit_tile_into(self, dst, tx, ty, layers=None):
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)
175 surface = layer.surface
176 surface.composite_tile_over(dst, tx, ty)
178 def add_layer(self, insert_idx):
179 self.do(command.AddLayer(self, insert_idx))
181 def remove_layer(self):
182 self.do(command.RemoveLayer(self))
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))
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.
192 obj = helpers.gdkpixbuf2numpy(obj)
194 # it was already an array
197 # simplify single-color pixmaps
199 if (obj == color).all():
201 self.background = obj
204 self.background_memory = numpy.zeros((N, N, 3), dtype='uint8')
205 self.background_memory[:,:,:] = self.background
207 self.invalidate_all()
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
215 def load_from_pixbuf(self, pixbuf):
217 self.load_layer_from_pixbuf(pixbuf)
219 def is_layered(self):
221 for l in self.layers:
222 if not l.surface.is_empty():
226 def save(self, filename):
227 trash, ext = os.path.splitext(filename)
228 ext = ext.lower().replace('.', '')
230 save = getattr(self, 'save_' + ext, self.unsupported)
232 self.unsaved_painting_time = 0.0
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)
239 self.command_stack.clear()
240 self.unsaved_painting_time = 0.0
242 def unsupported(self, filename):
243 raise ValueError, 'Unkwnown file format extension: ' + repr(filename)
245 def render_as_pixbuf(self, *args):
246 return pixbufsurface.render_as_pixbuf(self, *args)
248 def save_png(self, filename):
249 pixbuf = self.render_as_pixbuf()
250 pixbuf.save(filename, 'png')
252 def load_png(self, filename):
253 self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
255 def load_jpg(self, filename):
256 self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
258 def save_jpg(self, filename):
259 pixbuf = self.render_as_pixbuf()
260 pixbuf.save(filename, 'jpeg', {'quality':'90'})
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
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()
280 def add_layer(x, y, pixbuf, name):
281 layer = ET.Element('layer')
284 tmp = join(tempdir, 'tmp.png')
285 pixbuf.save(tmp, 'png')
294 for idx, l in enumerate(reversed(self.layers)):
295 if l.surface.is_empty():
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)
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')
306 xml = ET.tostring(image, encoding='UTF-8')
308 write_file_str('stack.xml', xml)
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')
320 self.clear() # this leaves one empty layer
322 if layer.tag != 'layer':
323 print 'Warning: ignoring unsupported tag:', layer.tag
326 src = a.get('src', '')
327 if not src.lower().endswith('.png'):
328 print 'Warning: ignoring non-png layer'
331 tmp = join(tempdir, 'tmp.png')
335 pixbuf = gdk.pixbuf_new_from_file(tmp)
338 x = int(a.get('x', '0'))
339 y = int(a.get('y', '0'))
340 self.add_layer(insert_idx=0)
342 self.load_layer_from_pixbuf(pixbuf, x, y)
346 if len(self.layers) == 1:
347 raise ValueError, 'Could not load any layer.'
349 # recognize solid or tiled background layers, at least those that mypaint saves
350 # (OpenRaster will probably get generator layers for this some day)
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()
356 for tile in tiles[1:]:
357 if (tile.rgba != tiles[0].rgba).any():
361 arr = helpers.gdkpixbuf2numpy(p)
362 tile = arr[0:N,0:N,:]
363 self.set_background(tile.copy())
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)