OSDN Git Service

16bit compositing (step 1/2)
[mypaint-anime/master.git] / lib / document.py
index cee7b57..c86e89d 100644 (file)
@@ -6,32 +6,13 @@
 # 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
@@ -44,25 +25,18 @@ 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()
@@ -125,6 +99,28 @@ class Document():
         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:
@@ -169,7 +165,7 @@ class Document():
             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:
@@ -177,9 +173,15 @@ class Document():
 
         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))
@@ -210,12 +212,6 @@ class Document():
 
         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)
@@ -261,15 +257,29 @@ class Document():
     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))
@@ -319,6 +329,7 @@ class Document():
             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():
@@ -331,7 +342,12 @@ class Document():
         # 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()
@@ -370,25 +386,40 @@ class Document():
         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'))
@@ -405,25 +436,25 @@ class Document():
         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