# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
-"""
-Design thoughts:
-A stroke:
-- is a list of motion events
-- knows everything needed to draw itself (brush settings / initial brush state)
-- has fixed brush settings (only brush states can change during a stroke)
-
-A layer:
-- is a container of several strokes (strokes can be removed)
-- can be rendered as a whole
-- can contain cache bitmaps, so it doesn't have to retrace all strokes all the time
-
-A document:
-- contains several layers
-- knows the active layer and the current brush
-- manages the undo history
-- must be altered via undo/redo commands (except painting)
-"""
-
import os, zipfile, tempfile, time
join = os.path.join
import xml.etree.ElementTree as ET
from gtk import gdk
-import gobject
+import gobject, numpy
-import helpers, tiledsurface, pixbufsurface, backgroundsurface
+import helpers, tiledsurface, pixbufsurface, backgroundsurface, mypaintlib
import command, stroke, layer
import brush # FIXME: the brush module depends on gtk and everything, but we only need brush_lowlevel
N = tiledsurface.N
"""
This is the "model" in the Model-View-Controller design.
(The "view" would be ../gui/tileddrawwidget.py.)
- It represenst everything that the user would want to save.
+ It represents everything that the user would want to save.
The "controller" mostly in drawwindow.py.
- It should be possible to use it without any GUI attached.
-
- Undo/redo is part of the model. The whole undo/redo stack can be
- saved to disk (planned) and can be used to reconstruct
- everything else.
+ It is possible to use it without any GUI attached (see ../tests/)
"""
- # Please note the following difficulty:
+ # Please note the following difficulty with the undo stack:
#
# Most of the time there is an unfinished (but already rendered)
# stroke pending, which has to be turned into a command.Action
# or discarded as empty before any other action is possible.
- #
- # TODO: the document should allow to "playback" (redo) a stroke
- # partially and examine its timing (realtime playback / calculate
- # total painting time) ?using half-done commands?
+ # (split_stroke)
def __init__(self):
self.brush = brush.Brush_Lowlevel()
if split:
self.split_stroke()
+ def straight_line(self, src, dst):
+ self.split_stroke()
+ # TODO: undo last stroke if it was very short... (but not at document level?)
+ real_brush = self.brush
+ self.brush = brush.Brush_Lowlevel()
+ self.brush.copy_settings_from(real_brush)
+
+ duration = 3.0
+ pressure = 0.3
+ N = 1000
+ x = numpy.linspace(src[0], dst[0], N)
+ y = numpy.linspace(src[1], dst[1], N)
+ # rest the brush in src for a minute, to avoid interpolation
+ # from the upper left corner (states are zero) (FIXME: the
+ # brush should handle this on its own, maybe?)
+ self.stroke_to(60.0, x[0], y[0], 0.0)
+ for i in xrange(N):
+ self.stroke_to(duration/N, x[i], y[i], pressure)
+ self.split_stroke()
+ self.brush = real_brush
+
+
def layer_modified_cb(self, *args):
# for now, any layer modification is assumed to be visible
for f in self.canvas_observers:
res.expandToIncludeRect(bbox)
return res
- def blit_tile_into(self, dst, tx, ty, mipmap=1, layers=None, background=None):
+ def blit_tile_into(self, dst, tx, ty, mipmap=0, layers=None, background=None):
if layers is None:
layers = self.layers
if background is None:
background.blit_tile_into(dst, tx, ty, mipmap)
+ assert dst.dtype == 'uint8' # OPTIMIZE: rewrite background to hold 16bit directly
+ dst_16bit = (dst.astype('uint32') * (1<<15) / 255).astype('uint16')
+
for layer in layers:
surface = layer.surface
- surface.composite_tile_over(dst, tx, ty, mipmap_level=mipmap, opacity=layer.opacity)
+ surface.composite_tile_over(dst_16bit, tx, ty, mipmap_level=mipmap, opacity=layer.opacity)
+
+ if dst_16bit is not dst:
+ mypaintlib.tile_convert_rgb16_to_rgb8(dst_16bit, dst)
def add_layer(self, insert_idx):
self.do(command.AddLayer(self, insert_idx))
self.invalidate_all()
- def get_background_pixbuf(self):
- pixbuf = gdk.Pixbuf(gdk.COLORSPACE_RGB, False, 8, N, N)
- arr = helpers.gdkpixbuf2numpy(pixbuf)
- arr[:,:,:] = self.background
- return pixbuf
-
def load_from_pixbuf(self, pixbuf):
self.clear()
self.load_layer_from_pixbuf(pixbuf)
def render_as_pixbuf(self, *args):
return pixbufsurface.render_as_pixbuf(self, *args)
- def save_png(self, filename, compression=2, alpha=False):
- if alpha:
- tmp_layer = layer.Layer()
- for l in self.layers:
- l.merge_into(tmp_layer)
- pixbuf = tmp_layer.surface.render_as_pixbuf()
+ def save_png(self, filename, compression=2, alpha=False, multifile=False):
+ if multifile:
+ self.save_multifile_png(filename, compression)
else:
- pixbuf = self.render_as_pixbuf()
- pixbuf.save(filename, 'png', {'compression':str(compression)})
+ if alpha:
+ tmp_layer = layer.Layer()
+ for l in self.layers:
+ l.merge_into(tmp_layer)
+ pixbuf = tmp_layer.surface.render_as_pixbuf()
+ else:
+ pixbuf = self.render_as_pixbuf()
+ pixbuf.save(filename, 'png', {'compression':str(compression)})
+
+ def save_multifile_png(self, filename, compression=2, alpha=False):
+ prefix, ext = os.path.splitext(filename)
+ # if we have a number already, strip it
+ l = prefix.rsplit('.', 1)
+ if l[-1].isdigit():
+ prefix = l[0]
+ doc_bbox = self.get_bbox()
+ for i, l in enumerate(self.layers):
+ filename = '%s.%03d%s' % (prefix, i+1, ext)
+ l.surface.save(filename, *doc_bbox)
def load_png(self, filename):
self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
a['x'] = str(x)
a['y'] = str(y)
a['opacity'] = str(opac)
+ return layer
for idx, l in enumerate(reversed(self.layers)):
if l.surface.is_empty():
# save background as layer (solid color or tiled)
s = pixbufsurface.Surface(x0, y0, w0, h0)
s.fill(self.background)
- add_layer(0, 0, 1.0, s.pixbuf, 'data/background.png')
+ l = add_layer(0, 0, 1.0, s.pixbuf, 'data/background.png')
+ bg = self.background
+ x, y, w, h = bg.get_pattern_bbox()
+ pixbuf = pixbufsurface.render_as_pixbuf(bg, x, y, w, h, alpha=False)
+ store_pixbuf(pixbuf, 'data/background_tile.png')
+ l.attrib['background_tile'] = 'data/background_tile.png'
# preview
t2 = time.time()
image = ET.fromstring(xml)
stack = image.find('stack')
+ def get_pixbuf(filename):
+ t1 = time.time()
+ tmp = join(tempdir, 'tmp.png')
+ f = open(tmp, 'wb')
+ f.write(z.read(filename))
+ f.close()
+ res = gdk.pixbuf_new_from_file(tmp)
+ os.remove(tmp)
+ print ' %.3fs loading %s' % (time.time() - t1, filename)
+ return res
+
self.clear() # this leaves one empty layer
+ no_background = True
for layer in stack:
if layer.tag != 'layer':
print 'Warning: ignoring unsupported tag:', layer.tag
continue
a = layer.attrib
+
+ if 'background_tile' in a:
+ assert no_background
+ try:
+ print a['background_tile']
+ self.set_background(get_pixbuf(a['background_tile']))
+ no_background = False
+ continue
+ except backgroundsurface.BackgroundError, e:
+ print 'ORA background tile not usable:', e
+
src = a.get('src', '')
if not src.lower().endswith('.png'):
print 'Warning: ignoring non-png layer'
continue
-
- tmp = join(tempdir, 'tmp.png')
- f = open(tmp, 'wb')
- f.write(z.read(src))
- f.close()
- t1 = time.time()
- pixbuf = gdk.pixbuf_new_from_file(tmp)
- print ' %.3fs loading %s' % (time.time() - t1, src)
- os.remove(tmp)
+ pixbuf = get_pixbuf(src)
x = int(a.get('x', '0'))
y = int(a.get('y', '0'))
if len(self.layers) == 1:
raise ValueError, 'Could not load any layer.'
- # recognize solid or tiled background layers, at least those that mypaint saves
- # (OpenRaster will probably get generator layers for this some day)
- t1 = time.time()
- p = last_pixbuf
- if not p.get_has_alpha() and p.get_width() % N == 0 and p.get_height() % N == 0:
- tiles = self.layers[0].surface.tiledict.values()
- if len(tiles) > 1:
- all_equal = True
- for tile in tiles[1:]:
- if (tile.rgba != tiles[0].rgba).any():
- all_equal = False
- break
- if all_equal:
- arr = helpers.gdkpixbuf2numpy(p)
- tile = arr[0:N,0:N,:]
- self.set_background(tile.copy())
- self.select_layer(0)
- self.remove_layer()
- print ' %.3fs recognizing tiled background' % (time.time() - t1)
+ if no_background:
+ # recognize solid or tiled background layers, at least those that mypaint <= 0.7.1 saves
+ t1 = time.time()
+ p = last_pixbuf
+ if not p.get_has_alpha() and p.get_width() % N == 0 and p.get_height() % N == 0:
+ tiles = self.layers[0].surface.tiledict.values()
+ if len(tiles) > 1:
+ all_equal = True
+ for tile in tiles[1:]:
+ if (tile.rgba != tiles[0].rgba).any():
+ all_equal = False
+ break
+ if all_equal:
+ arr = helpers.gdkpixbuf2numpy(p)
+ tile = arr[0:N,0:N,:]
+ self.set_background(tile.copy())
+ self.select_layer(0)
+ self.remove_layer()
+ print ' %.3fs recognizing tiled background' % (time.time() - t1)
if len(self.layers) > 1:
# remove the still present initial empty top layer