# 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 mypaintlib, helpers, tiledsurface, pixbufsurface
-import command, stroke, layer, serialize
-import brush # FIXME: the brush module depends on gtk and everything, but we only need brush_lowlevel
-import gzip, os, zipfile, tempfile, numpy, time
+import os, zipfile, tempfile, time, traceback
join = os.path.join
+from cStringIO import StringIO
import xml.etree.ElementTree as ET
from gtk import gdk
+import gobject, numpy
+from gettext import gettext as _
+
+import helpers, tiledsurface, pixbufsurface, backgroundsurface, mypaintlib
+import command, stroke, layer
+import brush
N = tiledsurface.N
+LOAD_CHUNK_SIZE = 64*1024
+
+from layer import DEFAULT_COMPOSITE_OP, VALID_COMPOSITE_OPS
+
+class SaveLoadError(Exception):
+ """Expected errors on loading or saving, like missing permissions or non-existing files."""
+ pass
class Document():
"""
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()
+ def __init__(self, brushinfo=None):
+ if not brushinfo:
+ brushinfo = brush.BrushInfo()
+ brushinfo.load_defaults()
+ self.brush = brush.Brush(brushinfo)
self.stroke = None
self.canvas_observers = []
- self.layer_observers = []
-
+ self.stroke_observers = [] # callback arguments: stroke, brush (brush is a temporary read-only convenience object)
+ self.doc_observers = []
+ self.frame_observers = []
+ self.command_stack_observers = []
self.clear(True)
+ self._frame = [0, 0, 0, 0]
+ self._frame_enabled = False
+ # Used by move_frame() to accumulate values
+ self._frame_dx = 0.0
+ self._frame_dy = 0.0
+
+ def get_frame(self):
+ return self._frame
+
+ def move_frame(self, dx=0.0, dy=0.0):
+ """Move the frame. Accumulates changes and moves the frame once
+ the accumulated change reaches the minimum move step."""
+ # FIXME: Should be 1 (pixel aligned), not tile aligned
+ # This is due to PNG saving having to be tile aligned
+ min_step = N
+
+ def round_to_n(value, n):
+ return int(round(value/n)*n)
+
+ x, y, w, h = self.get_frame()
+
+ self._frame_dx += dx
+ self._frame_dy += dy
+ step_x = round_to_n(self._frame_dx, min_step)
+ step_y = round_to_n(self._frame_dy, min_step)
+
+ if step_x:
+ self.set_frame(x=x+step_x)
+ self._frame_dx -= step_x
+
+ if step_y:
+ self.set_frame(y=y+step_y)
+ self._frame_dy -= step_y
+
+ def set_frame(self, x=None, y=None, width=None, height=None):
+ """Set the size of the frame. Pass None to indicate no-change."""
+
+ for i, var in enumerate([x, y, width, height]):
+ if not var is None:
+ # FIXME: must be aligned to tile size due to PNG saving
+ assert not var % N, "Frame size must be aligned to tile size"
+ self._frame[i] = var
+
+ for f in self.frame_observers: f()
+
+ def get_frame_enabled(self):
+ return self._frame_enabled
+
+ def set_frame_enabled(self, enabled):
+ self._frame_enabled = enabled
+ for f in self.frame_observers: f()
+ frame_enabled = property(get_frame_enabled)
+
+ def call_doc_observers(self):
+ for f in self.doc_observers:
+ f(self)
+ return True
+
def clear(self, init=False):
self.split_stroke()
if not init:
bbox = self.get_bbox()
# throw everything away, including undo stack
+
self.command_stack = command.CommandStack()
+ self.command_stack.stack_observers = self.command_stack_observers
self.set_background((255, 255, 255))
self.layers = []
self.layer_idx = None
for f in self.canvas_observers:
f(*bbox)
+ self.call_doc_observers()
+
def get_current_layer(self):
return self.layers[self.layer_idx]
layer = property(get_current_layer)
if not self.stroke: return
self.stroke.stop_recording()
if not self.stroke.empty:
- self.layer.strokes.append(self.stroke)
- before = self.snapshot_before_stroke
- after = self.layer.save_snapshot()
- self.command_stack.do(command.Stroke(self, self.stroke, before, after))
- self.snapshot_before_stroke = after
+ self.command_stack.do(command.Stroke(self, self.stroke, self.snapshot_before_stroke))
+ del self.snapshot_before_stroke
self.unsaved_painting_time += self.stroke.total_painting_time
+ for f in self.stroke_observers:
+ f(self.stroke, self.brush)
self.stroke = None
def select_layer(self, idx):
self.do(command.SelectLayer(self, idx))
+ def move_layer(self, was_idx, new_idx, select_new=False):
+ self.do(command.MoveLayer(self, was_idx, new_idx, select_new))
+
+ def duplicate_layer(self, insert_idx=None, name=''):
+ self.do(command.DuplicateLayer(self, insert_idx, name))
+
+ def reorder_layers(self, new_layers):
+ self.do(command.ReorderLayers(self, new_layers))
+
def clear_layer(self):
- self.do(command.ClearLayer(self))
+ if not self.layer.is_empty():
+ self.do(command.ClearLayer(self))
- def stroke_to(self, dtime, x, y, pressure):
+ def stroke_to(self, dtime, x, y, pressure, xtilt, ytilt):
if not self.stroke:
self.stroke = stroke.Stroke()
self.stroke.start_recording(self.brush)
self.snapshot_before_stroke = self.layer.save_snapshot()
- self.stroke.record_event(dtime, x, y, pressure)
+ self.stroke.record_event(dtime, x, y, pressure, xtilt, ytilt)
- l = self.layer
- l.surface.begin_atomic()
- split = self.brush.stroke_to (l.surface, x, y, pressure, dtime)
- l.surface.end_atomic()
+ split = self.layer.stroke_to(self.brush, x, y,
+ pressure, xtilt, ytilt, dtime)
if split:
self.split_stroke()
+ def redo_last_stroke_with_different_brush(self, brush):
+ cmd = self.get_last_command()
+ if not isinstance(cmd, command.Stroke):
+ return
+ cmd = self.undo()
+ assert isinstance(cmd, command.Stroke)
+ new_stroke = cmd.stroke.copy_using_different_brush(brush)
+ snapshot_before = self.layer.save_snapshot()
+ new_stroke.render(self.layer._surface)
+ self.do(command.Stroke(self, new_stroke, snapshot_before))
+
+ def straight_line(self, src, dst):
+ self.split_stroke()
+ self.brush.reset() # reset dynamic states (eg. filtered velocity)
+
+ 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, 0.0, 0.0)
+ for i in xrange(N):
+ self.stroke_to(duration/N, x[i], y[i], pressure, 0.0, 0.0)
+ self.split_stroke()
+ self.brush.reset()
+
+
def layer_modified_cb(self, *args):
# for now, any layer modification is assumed to be visible
for f in self.canvas_observers:
self.split_stroke()
self.command_stack.do(cmd)
- def set_brush(self, brush):
+ def get_last_command(self):
self.split_stroke()
- self.brush.copy_settings_from(brush)
+ return self.command_stack.get_last_command()
def get_bbox(self):
res = helpers.Rect()
for layer in self.layers:
# OPTIMIZE: only visible layers...
# careful: currently saving assumes that all layers are included
- bbox = layer.surface.get_bbox()
+ bbox = layer.get_bbox()
res.expandToIncludeRect(bbox)
return res
- def blit_tile_into(self, dst, tx, ty, layers=None):
+ def get_effective_bbox(self):
+ """Return the effective bounding box of the document.
+ If the frame is enabled, this is the bounding box of the frame,
+ else the (dynamic) bounding box of the document."""
+ return self.get_frame() if self.frame_enabled else self.get_bbox()
+
+ def blit_tile_into(self, dst_8bit, tx, ty, mipmap_level=0, layers=None, background=None):
if layers is None:
layers = self.layers
+ if background is None:
+ background = self.background
+
+ assert dst_8bit.dtype == 'uint8'
+ dst = numpy.empty((N, N, 3), dtype='uint16')
- # render solid or tiled background
- #dst[:] = self.background_memory # 13 times slower than below, with some bursts having the same speed as below (huh?)
- # note: optimization for solid colors is not worth it any more now, even if it gives 2x speedup (at best)
- mypaintlib.tile_blit_rgb8_into_rgb8(self.background_memory, dst)
+ background.blit_tile_into(dst, tx, ty, mipmap_level)
for layer in layers:
- surface = layer.surface
- surface.composite_tile_over(dst, tx, ty)
-
- def add_layer(self, insert_idx):
- self.do(command.AddLayer(self, insert_idx))
+ surface = layer._surface
+ surface.composite_tile(dst, tx, ty,
+ mipmap_level=mipmap_level,
+ opacity=layer.effective_opacity,
+ mode=layer.compositeop)
+
+ mypaintlib.tile_convert_rgb16_to_rgb8(dst, dst_8bit)
- def remove_layer(self):
- self.do(command.RemoveLayer(self))
+ def add_layer(self, insert_idx=None, after=None, name=''):
+ self.do(command.AddLayer(self, insert_idx, after, name))
- def merge_layer(self, dst_idx):
+ def remove_layer(self,layer=None):
+ if len(self.layers) > 1:
+ self.do(command.RemoveLayer(self,layer))
+ else:
+ self.clear_layer()
+
+ def merge_layer_down(self):
+ dst_idx = self.layer_idx - 1
+ if dst_idx < 0:
+ return False
self.do(command.MergeLayer(self, dst_idx))
+ return True
def load_layer_from_pixbuf(self, pixbuf, x=0, y=0):
arr = helpers.gdkpixbuf2numpy(pixbuf)
self.do(command.LoadLayer(self, arr, x, y))
+ def set_layer_visibility(self, visible, layer):
+ cmd = self.get_last_command()
+ if isinstance(cmd, command.SetLayerVisibility) and cmd.layer is layer:
+ self.undo()
+ self.do(command.SetLayerVisibility(self, visible, layer))
+
+ def set_layer_locked(self, locked, layer):
+ cmd = self.get_last_command()
+ if isinstance(cmd, command.SetLayerLocked) and cmd.layer is layer:
+ self.undo()
+ self.do(command.SetLayerLocked(self, locked, layer))
+
+ def set_layer_opacity(self, opacity, layer=None):
+ """Sets the opacity of a layer. If layer=None, works on the current layer"""
+ cmd = self.get_last_command()
+ if isinstance(cmd, command.SetLayerOpacity):
+ self.undo()
+ self.do(command.SetLayerOpacity(self, opacity, layer))
+
+ def set_layer_compositeop(self, compositeop, layer=None):
+ """Sets the composition-operation of a layer. If layer=None, works on the current layer"""
+ if compositeop not in VALID_COMPOSITE_OPS:
+ compositeop = DEFAULT_COMPOSITE_OP
+ cmd = self.get_last_command()
+ if isinstance(cmd, command.SetLayerCompositeOp):
+ self.undo()
+ self.do(command.SetLayerCompositeOp(self, compositeop, layer))
+
def set_background(self, obj):
# This is not an undoable action. One reason is that dragging
# on the color chooser would get tons of undo steps.
- try:
- obj = helpers.gdkpixbuf2numpy(obj)
- except:
- # it was already an array
- pass
- if len(obj) > 3:
- # simplify single-color pixmaps
- color = obj[0,0,:]
- if (obj == color).all():
- obj = tuple(color)
- self.background = obj
- # optimization
- self.background_memory = numpy.zeros((N, N, 3), dtype='uint8')
- self.background_memory[:,:,:] = self.background
+ if not isinstance(obj, backgroundsurface.Background):
+ obj = backgroundsurface.Background(obj)
+ self.background = obj
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):
+ """Load a document from a pixbuf."""
self.clear()
self.load_layer_from_pixbuf(pixbuf)
+ self.set_frame(*self.get_bbox())
def is_layered(self):
count = 0
for l in self.layers:
- if not l.surface.is_empty():
+ if not l.is_empty():
count += 1
return count > 1
- def save(self, filename):
- trash, ext = os.path.splitext(filename)
+ def is_empty(self):
+ return len(self.layers) == 1 and self.layer.is_empty()
+
+ def save(self, filename, **kwargs):
+ self.split_stroke()
+ junk, ext = os.path.splitext(filename)
ext = ext.lower().replace('.', '')
save = getattr(self, 'save_' + ext, self.unsupported)
- save(filename)
+ try:
+ save(filename, **kwargs)
+ except gobject.GError, e:
+ traceback.print_exc()
+ if e.code == 5:
+ #add a hint due to a very consfusing error message when there is no space left on device
+ raise SaveLoadError, _('Unable to save: %s\nDo you have enough space left on the device?') % e.message
+ else:
+ raise SaveLoadError, _('Unable to save: %s') % e.message
+ except IOError, e:
+ traceback.print_exc()
+ raise SaveLoadError, _('Unable to save: %s') % e.strerror
self.unsaved_painting_time = 0.0
- def load(self, filename):
- trash, ext = os.path.splitext(filename)
+ def load(self, filename, **kwargs):
+ if not os.path.isfile(filename):
+ raise SaveLoadError, _('File does not exist: %s') % repr(filename)
+ if not os.access(filename,os.R_OK):
+ raise SaveLoadError, _('You do not have the necessary permissions to open file: %s') % repr(filename)
+ junk, ext = os.path.splitext(filename)
ext = ext.lower().replace('.', '')
load = getattr(self, 'load_' + ext, self.unsupported)
- load(filename)
+ try:
+ load(filename, **kwargs)
+ except gobject.GError, e:
+ traceback.print_exc()
+ raise SaveLoadError, _('Error while loading: GError %s') % e
+ except IOError, e:
+ traceback.print_exc()
+ raise SaveLoadError, _('Error while loading: IOError %s') % e
self.command_stack.clear()
self.unsaved_painting_time = 0.0
+ self.call_doc_observers()
+
+ def unsupported(self, filename, *args, **kwargs):
+ raise SaveLoadError, _('Unknown file format extension: %s') % repr(filename)
+
+ def render_as_pixbuf(self, *args, **kwargs):
+ return pixbufsurface.render_as_pixbuf(self, *args, **kwargs)
+
+ def render_thumbnail(self):
+ t0 = time.time()
+ x, y, w, h = self.get_effective_bbox()
+ if w == 0 or h == 0:
+ # workaround to save empty documents
+ x, y, w, h = 0, 0, tiledsurface.N, tiledsurface.N
+ mipmap_level = 0
+ while mipmap_level < tiledsurface.MAX_MIPMAP_LEVEL and max(w, h) >= 512:
+ mipmap_level += 1
+ x, y, w, h = x/2, y/2, w/2, h/2
+
+ pixbuf = self.render_as_pixbuf(x, y, w, h, mipmap_level=mipmap_level)
+ assert pixbuf.get_width() == w and pixbuf.get_height() == h
+ pixbuf = helpers.scale_proportionally(pixbuf, 256, 256)
+ print 'Rendered thumbnail in', time.time() - t0, 'seconds.'
+ return pixbuf
- def unsupported(self, filename):
- raise ValueError, 'Unkwnown file format extension: ' + repr(filename)
-
- def render_as_pixbuf(self, *args):
- return pixbufsurface.render_as_pixbuf(self, *args)
-
- def save_png(self, filename):
- pixbuf = self.render_as_pixbuf()
- pixbuf.save(filename, 'png')
-
- def load_png(self, filename):
- self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
-
- def load_jpg(self, filename):
- self.load_from_pixbuf(gdk.pixbuf_new_from_file(filename))
- load_jpeg = load_jpg
+ def save_png(self, filename, alpha=False, multifile=False, **kwargs):
+ doc_bbox = self.get_effective_bbox()
+ if multifile:
+ self.save_multifile_png(filename, **kwargs)
+ else:
+ if alpha:
+ tmp_layer = layer.Layer()
+ for l in self.layers:
+ l.merge_into(tmp_layer)
+ tmp_layer.save_as_png(filename, *doc_bbox)
+ else:
+ pixbufsurface.save_as_png(self, filename, *doc_bbox, alpha=False, **kwargs)
+
+ def save_multifile_png(self, filename, alpha=False, **kwargs):
+ 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_effective_bbox()
+ for i, l in enumerate(self.layers):
+ filename = '%s.%03d%s' % (prefix, i+1, ext)
+ l.save_as_png(filename, *doc_bbox, **kwargs)
+
+ @staticmethod
+ def _pixbuf_from_stream(fp, feedback_cb=None):
+ loader = gdk.PixbufLoader()
+ while True:
+ if feedback_cb is not None:
+ feedback_cb()
+ buf = fp.read(LOAD_CHUNK_SIZE)
+ if buf == '':
+ break
+ loader.write(buf)
+ loader.close()
+ return loader.get_pixbuf()
+
+ def load_from_pixbuf_file(self, filename, feedback_cb=None):
+ fp = open(filename, 'rb')
+ pixbuf = self._pixbuf_from_stream(fp, feedback_cb)
+ fp.close()
+ self.load_from_pixbuf(pixbuf)
+
+ load_png = load_from_pixbuf_file
+ load_jpg = load_from_pixbuf_file
+ load_jpeg = load_from_pixbuf_file
+
+ def save_jpg(self, filename, quality=90, **kwargs):
+ x, y, w, h = self.get_effective_bbox()
+ if w == 0 or h == 0:
+ x, y, w, h = 0, 0, N, N # allow to save empty documents
+ pixbuf = self.render_as_pixbuf(x, y, w, h, **kwargs)
+ pixbuf.save(filename, 'jpeg', options={'quality':str(quality)})
- def save_jpg(self, filename):
- pixbuf = self.render_as_pixbuf()
- pixbuf.save(filename, 'jpeg', {'quality':'90'})
save_jpeg = save_jpg
- def save_ora(self, filename):
+ def save_ora(self, filename, options=None, **kwargs):
+ print 'save_ora:'
+ t0 = time.time()
tempdir = tempfile.mkdtemp('mypaint')
- z = zipfile.ZipFile(filename, 'w', compression=zipfile.ZIP_STORED)
+ # use .tmp extension, so we don't overwrite a valid file if there is an exception
+ z = zipfile.ZipFile(filename + '.tmpsave', 'w', compression=zipfile.ZIP_STORED)
# work around a permission bug in the zipfile library: http://bugs.python.org/issue3394
def write_file_str(filename, data):
zi = zipfile.ZipInfo(filename)
write_file_str('mimetype', 'image/openraster') # must be the first file
image = ET.Element('image')
stack = ET.SubElement(image, 'stack')
- x0, y0, w0, h0 = self.get_bbox()
+ x0, y0, w0, h0 = self.get_effective_bbox()
a = image.attrib
- a['x'] = str(0)
- a['y'] = str(0)
a['w'] = str(w0)
a['h'] = str(h0)
- def add_layer(x, y, pixbuf, name):
- layer = ET.Element('layer')
- stack.append(layer)
-
+ def store_pixbuf(pixbuf, name):
tmp = join(tempdir, 'tmp.png')
+ t1 = time.time()
pixbuf.save(tmp, 'png')
+ print ' %.3fs pixbuf saving %s' % (time.time() - t1, name)
+ z.write(tmp, name)
+ os.remove(tmp)
+
+ def store_surface(surface, name, rect=[]):
+ tmp = join(tempdir, 'tmp.png')
+ t1 = time.time()
+ surface.save_as_png(tmp, *rect, **kwargs)
+ print ' %.3fs surface saving %s' % (time.time() - t1, name)
z.write(tmp, name)
os.remove(tmp)
+ def add_layer(x, y, opac, surface, name, layer_name, visible=True, compositeop=DEFAULT_COMPOSITE_OP, rect=[]):
+ layer = ET.Element('layer')
+ stack.append(layer)
+ store_surface(surface, name, rect)
a = layer.attrib
+ if layer_name:
+ a['name'] = layer_name
a['src'] = name
a['x'] = str(x)
a['y'] = str(y)
+ a['opacity'] = str(opac)
+ if compositeop not in VALID_COMPOSITE_OPS:
+ compositeop = DEFAULT_COMPOSITE_OP
+ a['composite-op'] = compositeop
+ if visible:
+ a['visibility'] = 'visible'
+ else:
+ a['visibility'] = 'hidden'
+ return layer
for idx, l in enumerate(reversed(self.layers)):
- if l.surface.is_empty():
+ if l.is_empty():
continue
- x, y, w, h = l.surface.get_bbox()
- pixbuf = l.surface.render_as_pixbuf()
- add_layer(x-x0, y-y0, pixbuf, 'data/layer%03d.png' % idx)
+ opac = l.opacity
+ x, y, w, h = l.get_bbox()
+ el = add_layer(x-x0, y-y0, opac, l._surface, 'data/layer%03d.png' % idx, l.name, l.visible, l.compositeop, rect=(x, y, w, h))
+ # strokemap
+ sio = StringIO()
+ l.save_strokemap_to_file(sio, -x, -y)
+ data = sio.getvalue(); sio.close()
+ name = 'data/layer%03d_strokemap.dat' % idx
+ el.attrib['mypaint_strokemap_v2'] = name
+ write_file_str(name, data)
# save background as layer (solid color or tiled)
- s = pixbufsurface.Surface(0, 0, w0, h0)
- s.fill(self.background)
- add_layer(0, 0, s.pixbuf, 'data/background.png')
-
+ bg = self.background
+ # save as fully rendered layer
+ x, y, w, h = self.get_bbox()
+ l = add_layer(x-x0, y-y0, 1.0, bg, 'data/background.png', 'background',
+ DEFAULT_COMPOSITE_OP, rect=(x,y,w,h))
+ x, y, w, h = bg.get_pattern_bbox()
+ # save as single pattern (with corrected origin)
+ store_surface(bg, 'data/background_tile.png', rect=(x+x0, y+y0, w, h))
+ l.attrib['background_tile'] = 'data/background_tile.png'
+
+ # preview (256x256)
+ t2 = time.time()
+ print ' starting to render full image for thumbnail...'
+
+ thumbnail_pixbuf = self.render_thumbnail()
+ store_pixbuf(thumbnail_pixbuf, 'Thumbnails/thumbnail.png')
+ print ' total %.3fs spent on thumbnail' % (time.time() - t2)
+
+ helpers.indent_etree(image)
xml = ET.tostring(image, encoding='UTF-8')
write_file_str('stack.xml', xml)
z.close()
os.rmdir(tempdir)
+ if os.path.exists(filename):
+ os.remove(filename) # windows needs that
+ os.rename(filename + '.tmpsave', filename)
- def load_ora(self, filename):
- tempdir = tempfile.mkdtemp('mypaint')
+ print '%.3fs save_ora total' % (time.time() - t0)
+
+ return thumbnail_pixbuf
+
+ def load_ora(self, filename, feedback_cb=None):
+ """Loads from an OpenRaster file"""
+ print 'load_ora:'
+ t0 = time.time()
z = zipfile.ZipFile(filename)
print 'mimetype:', z.read('mimetype').strip()
xml = z.read('stack.xml')
image = ET.fromstring(xml)
stack = image.find('stack')
+ w = int(image.attrib['w'])
+ h = int(image.attrib['h'])
+
+ def round_up_to_n(value, n):
+ assert value >= 0, "function undefined for negative numbers"
+
+ residual = value % n
+ if residual:
+ value = value - residual + n
+ return int(value)
+
+ def get_pixbuf(filename):
+ t1 = time.time()
+
+ try:
+ fp = z.open(filename, mode='r')
+ except KeyError:
+ # support for bad zip files (saved by old versions of the GIMP ORA plugin)
+ fp = z.open(filename.encode('utf-8'), mode='r')
+ print 'WARNING: bad OpenRaster ZIP file. There is an utf-8 encoded filename that does not have the utf-8 flag set:', repr(filename)
+
+ res = self._pixbuf_from_stream(fp, feedback_cb)
+ fp.close()
+ print ' %.3fs loading %s' % (time.time() - t1, filename)
+ return res
+
+ def get_layers_list(root, x=0,y=0):
+ res = []
+ for item in root:
+ if item.tag == 'layer':
+ if 'x' in item.attrib:
+ item.attrib['x'] = int(item.attrib['x']) + x
+ if 'y' in item.attrib:
+ item.attrib['y'] = int(item.attrib['y']) + y
+ res.append(item)
+ elif item.tag == 'stack':
+ stack_x = int( item.attrib.get('x', 0) )
+ stack_y = int( item.attrib.get('y', 0) )
+ res += get_layers_list(item, stack_x, stack_y)
+ else:
+ print 'Warning: ignoring unsupported tag:', item.tag
+ return res
+
self.clear() # this leaves one empty layer
- for layer in stack:
- if layer.tag != 'layer':
- print 'Warning: ignoring unsupported tag:', layer.tag
- continue
+ no_background = True
+ # FIXME: don't require tile alignment for frame
+ self.set_frame(width=round_up_to_n(w, N), height=round_up_to_n(h, N))
+
+ for layer in get_layers_list(stack):
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, 'w')
- f.write(z.read(src))
- f.close()
- pixbuf = gdk.pixbuf_new_from_file(tmp)
- os.remove(tmp)
-
+ pixbuf = get_pixbuf(src)
+ name = a.get('name', '')
x = int(a.get('x', '0'))
y = int(a.get('y', '0'))
- self.add_layer(insert_idx=0)
+ opac = float(a.get('opacity', '1.0'))
+ compositeop = str(a.get('composite-op', DEFAULT_COMPOSITE_OP))
+ if compositeop not in VALID_COMPOSITE_OPS:
+ compositeop = DEFAULT_COMPOSITE_OP
+
+ visible = not 'hidden' in a.get('visibility', 'visible')
+ self.add_layer(insert_idx=0, name=name)
last_pixbuf = pixbuf
+ t1 = time.time()
self.load_layer_from_pixbuf(pixbuf, x, y)
-
- os.rmdir(tempdir)
+ layer = self.layers[0]
+
+ self.set_layer_opacity(helpers.clamp(opac, 0.0, 1.0), layer)
+ self.set_layer_compositeop(compositeop, layer)
+ self.set_layer_visibility(visible, layer)
+ print ' %.3fs converting pixbuf to layer format' % (time.time() - t1)
+ # strokemap
+ fname = a.get('mypaint_strokemap_v2', None)
+ if fname:
+ if x % N or y % N:
+ print 'Warning: dropping non-aligned strokemap'
+ else:
+ sio = StringIO(z.read(fname))
+ layer.load_strokemap_from_file(sio, x, y)
+ sio.close()
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)
- 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()
+ # no assertion (allow empty documents)
+ print 'Warning: Could not load any layer, document is empty.'
+
+ 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:
- # select the still present initial empty top layer
- # hm, should this better be removed?
+ # remove the still present initial empty top layer
self.select_layer(len(self.layers)-1)
+ self.remove_layer()
+ # this leaves the topmost layer selected
+ print '%.3fs load_ora total' % (time.time() - t0)