OSDN Git Service

layer move: make undoable, distance weighting etc
[mypaint-anime/master.git] / lib / tiledsurface.py
1 # This file is part of MyPaint.
2 # Copyright (C) 2007-2008 by Martin Renold <martinxyz@gmx.ch>
3 #
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.
8
9 # This module implements an unbounded tiled surface for painting.
10
11 from numpy import *
12 import time, sys
13 import mypaintlib, helpers
14 import math
15
16 TILE_SIZE = N = mypaintlib.TILE_SIZE
17 MAX_MIPMAP_LEVEL = mypaintlib.MAX_MIPMAP_LEVEL
18
19 from layer import DEFAULT_COMPOSITE_OP
20 import pixbufsurface
21
22
23 class Tile:
24     def __init__(self, copy_from=None):
25         # note: pixels are stored with premultiplied alpha
26         #       15bits are used, but fully opaque or white is stored as 2**15 (requiring 16 bits)
27         #       This is to allow many calcuations to divide by 2**15 instead of (2**16-1)
28         if copy_from is None:
29             self.rgba = zeros((N, N, 4), 'uint16')
30         else:
31             self.rgba = copy_from.rgba.copy()
32         self.readonly = False
33
34     def copy(self):
35         return Tile(copy_from=self)
36
37
38 def composite_array_src_over(dst, src, mipmap_level=0, opacity=1.0):
39     """The default "svg:src-over" layer composite op implementation.
40     """
41     if dst.shape[2] == 4 and dst.dtype == 'uint16':
42         # rarely used (for merging layers, also when exporting a transparent PNGs)
43         # src (premultiplied) OVER dst (premultiplied)
44         # cB = cA + (1.0 - aA) * cB
45         #  where B = dst, A = src
46         srcAlpha = src[:,:,3:4].astype('float') * opacity / (1 << 15)
47         dstAlpha = dst[:,:,3:4].astype('float') / (1 << 15)
48         src_premult = src[:,:,0:3].astype('float') * opacity / (1<<15)
49         dst_premult = dst[:,:,0:3].astype('float') / (1<<15)
50         dst_c = clip(src_premult + (1.0 - srcAlpha) * dst_premult, 0.0, 1.0)
51         dst_a = clip(srcAlpha + dstAlpha - srcAlpha * dstAlpha, 0.0, 1.0)
52         dst[:,:,0:3] = clip(dst_c * (1<<15), 0, (1<<15) - 1).astype('uint16')
53         dst[:,:,3:4] = clip(dst_a * (1<<15), 0, (1<<15) - 1).astype('uint16')
54     elif dst.shape[2] == 3 and dst.dtype == 'uint16':
55         mypaintlib.tile_composite_rgba16_over_rgb16(src, dst, opacity)
56     else:
57         raise NotImplementedError
58
59
60 def composite_array_multiply(dst, src, mipmap_level=0, opacity=1.0):
61     """The "svg:multiply" layer composite op implementation.
62     """
63     if dst.shape[2] == 4 and dst.dtype == 'uint16':
64         # rarely used (for merging layers, also when exporting a transparent PNGs)
65         # src (premultiplied) MULTIPLY dst (premultiplied)
66         # cA * cB +  cA * (1 - aB) + cB * (1 - aA)
67         srcAlpha = (opacity * src[:,:,3:4]).astype('float') / (1 << 15)
68         dstAlpha = (dst[:,:,3:4]).astype('float') / (1 << 15)
69         src_premult = src[:,:,0:3].astype('float') * opacity / (1<<15)
70         dst_premult = dst[:,:,0:3].astype('float') / (1<<15)
71         dst_c = clip(src_premult * dst_premult +  src_premult * (1.0 - dstAlpha) + dst_premult * (1.0 - srcAlpha),0.0, 1.0)
72         dst_a = clip(srcAlpha + dstAlpha - srcAlpha * dstAlpha, 0.0, 1.0)
73         dst[:,:,0:3] = clip(dst_c * (1<<15), 0, (1<<15) - 1).astype('uint16')
74         dst[:,:,3:4] = clip(dst_a * (1<<15), 0, (1<<15) - 1).astype('uint16')
75     elif dst.shape[2] == 3 and dst.dtype == 'uint16':
76         mypaintlib.tile_composite_rgba16_multiply_rgb16(src, dst, opacity)
77     else:
78         raise NotImplementedError
79
80
81 def composite_array_screen(dst, src, mipmap_level=0, opacity=1.0):
82     """The "svg:screen" layer composite op implementation.
83     """
84     if dst.shape[2] == 4 and dst.dtype == 'uint16':
85         # rarely used (for merging layers, also when exporting a transparent PNGs)
86         # src (premultiplied) SCREEN dst (premultiplied)
87         # cA + cB - cA * cB
88         srcAlpha = (opacity * src[:,:,3:4]).astype('float') / (1 << 15)
89         dstAlpha = (dst[:,:,3:4]).astype('float') / (1 << 15)
90         src_premult = src[:,:,0:3].astype('float') * opacity / (1<<15)
91         dst_premult = dst[:,:,0:3].astype('float') / (1<<15)
92         dst_c = clip(src_premult + dst_premult - src_premult * dst_premult, 0, 1)
93         dst_a = clip(srcAlpha + dstAlpha - srcAlpha * dstAlpha, 0, 1)
94         dst[:,:,0:3] = clip(dst_c * (1<<15),0, (1<<15) - 1).astype('uint16')
95         dst[:,:,3:4] = clip(dst_a * (1<<15),0, (1<<15) - 1).astype('uint16')
96     elif dst.shape[2] == 3 and dst.dtype == 'uint16':
97         mypaintlib.tile_composite_rgba16_screen_rgb16(src, dst, opacity)
98     else:
99         raise NotImplementedError
100
101
102 def composite_array_burn(dst, src, mipmap_level=0, opacity=1.0):
103     """The "svg:color-burn" layer composite op implementation.
104     """
105     if dst.shape[2] == 4 and dst.dtype == 'uint16':
106         # rarely used (for merging layers, also when exporting a transparent PNGs)
107         # src (premultiplied) OVER dst (premultiplied)
108         # if cA * aB + cB * aA <= aA * aB :
109         #   cA * (1 - aB) + cB * (1 - aA)
110         #   (cA == 0 ? 1 : (aA * (cA * aB + cB * aA - aA * aB) / cA) + cA * (1 - aB) + cB * (1 - aA))
111         #  where B = dst, A = src
112         aA = (opacity * src[:,:,3:4]).astype('float') / (1 << 15)
113         aB = (dst[:,:,3:4]).astype('float') / (1 << 15)
114         cA = src[:,:,0:3].astype('float') * opacity / (1<<15)
115         cB = dst[:,:,0:3].astype('float') / (1<<15)
116         dst_c = where(cA * aB + cB * aA <= aA * aB,
117                      cA * (1 - aB) + cB * (1 - aA),
118                      where(cA == 0,
119                            1.0,
120                            (aA * (cA * aB + cB * aA - aA * aB) / cA) + cA * (1.0 - aB) + cB * (1.0 - aA)))
121         dst_a = aA + aB - aA * aB
122         dst[:,:,0:3] = clip(dst_c * (1<<15), 0, (1<<15) - 1).astype('uint16')
123         dst[:,:,3:4] = clip(dst_a * (1<<15), 0, (1<<15) - 1).astype('uint16')
124     elif dst.shape[2] == 3 and dst.dtype == 'uint16':
125         mypaintlib.tile_composite_rgba16_burn_rgb16(src, dst, opacity)
126     else:
127         raise NotImplementedError
128
129
130 def composite_array_dodge(dst, src, mipmap_level=0, opacity=1.0):
131     """The "svg:color-dodge" layer composite op implementation.
132     """
133     if dst.shape[2] == 4 and dst.dtype == 'uint16':
134         # rarely used (for merging layers, also when exporting a transparent PNGs)
135         # src (premultiplied) OVER dst (premultiplied)
136         # if cA * aB + cB * aA >= aA * aB :
137         #   aA * aB + cA * (1 - aB) + cB * (1 - aA)
138         #   (cA == aA ? 1 : cB * aA / (aA == 0 ? 1 : 1 - cA / aA)) + cA * (1 - aB) + cB * (1 - aA)
139         #  where B = dst, A = src
140         aA = (opacity * src[:,:,3:4]).astype('float') / (1 << 15)
141         aB = (dst[:,:,3:4]).astype('float') / (1 << 15)
142         cA = src[:,:,0:3].astype('float') * opacity / (1<<15)
143         cB = dst[:,:,0:3].astype('float') / (1<<15)
144         dst_c = where(cA * aB + cB * aA >= aA * aB,
145                      aA * aB + cA * (1 - aB) + cB * (1 - aA),
146                      where(cA == aA, 1.0, cB * aA / where(aA == 0, 1.0, 1.0 - cA / aA)) + cA * (1.0 - aB) + cB * (1.0 - aA))
147         dst_a = (aA + aB - aA * aB)
148         dst[:,:,0:3] = clip(dst_c * (1<<15),0, (1<<15) - 1).astype('uint16')
149         dst[:,:,3:4] = clip(dst_a * (1<<15),0, (1<<15) - 1).astype('uint16')
150     elif dst.shape[2] == 3 and dst.dtype == 'uint16':
151         mypaintlib.tile_composite_rgba16_dodge_rgb16(src, dst, opacity)
152     else:
153         raise NotImplementedError
154
155
156 # tile for read-only operations on empty spots
157 transparent_tile = Tile()
158 transparent_tile.readonly = True
159
160 # tile with invalid pixel memory (needs refresh)
161 mipmap_dirty_tile = Tile()
162 del mipmap_dirty_tile.rgba
163
164 def get_tiles_bbox(tiles):
165     res = helpers.Rect()
166     for tx, ty in tiles:
167         res.expandToIncludeRect(helpers.Rect(N*tx, N*ty, N, N))
168     return res
169
170 class SurfaceSnapshot:
171     pass
172
173 class Surface(mypaintlib.TiledSurface):
174     # the C++ half of this class is in tiledsurface.hpp
175     def __init__(self, mipmap_level=0):
176         mypaintlib.TiledSurface.__init__(self, self)
177         self.tiledict = {}
178         self.observers = []
179
180         self.mipmap_level = mipmap_level
181         self.mipmap = None
182         self.parent = None
183
184         if mipmap_level < MAX_MIPMAP_LEVEL:
185             self.mipmap = Surface(mipmap_level+1)
186             self.mipmap.parent = self
187
188     def notify_observers(self, *args):
189         for f in self.observers:
190             f(*args)
191
192     def clear(self):
193         tiles = self.tiledict.keys()
194         self.tiledict = {}
195         self.notify_observers(*get_tiles_bbox(tiles))
196         if self.mipmap: self.mipmap.clear()
197
198     def get_tile_memory(self, tx, ty, readonly):
199         # OPTIMIZE: do some profiling to check if this function is a bottleneck
200         #           yes it is
201         # Note: we must return memory that stays valid for writing until the
202         # last end_atomic(), because of the caching in tiledsurface.hpp.
203         t = self.tiledict.get((tx, ty))
204         if t is None:
205             if readonly:
206                 t = transparent_tile
207             else:
208                 t = Tile()
209                 self.tiledict[(tx, ty)] = t
210         if t is mipmap_dirty_tile:
211             # regenerate mipmap
212             t = Tile()
213             self.tiledict[(tx, ty)] = t
214             empty = True
215             for x in xrange(2):
216                 for y in xrange(2):
217                     src = self.parent.get_tile_memory(tx*2 + x, ty*2 + y, True)
218                     mypaintlib.tile_downscale_rgba16(src, t.rgba, x*N/2, y*N/2)
219                     if src is not transparent_tile.rgba:
220                         empty = False
221             if empty:
222                 # rare case, no need to speed it up
223                 del self.tiledict[(tx, ty)]
224                 t = transparent_tile
225         if t.readonly and not readonly:
226             # shared memory, get a private copy for writing
227             t = t.copy()
228             self.tiledict[(tx, ty)] = t
229         if not readonly:
230             assert self.mipmap_level == 0
231             self._mark_mipmap_dirty(tx, ty)
232         return t.rgba
233
234     def _mark_mipmap_dirty(self, tx, ty):
235         if self.mipmap_level > 0:
236             self.tiledict[(tx, ty)] = mipmap_dirty_tile
237         if self.mipmap:
238             self.mipmap._mark_mipmap_dirty(tx/2, ty/2)
239
240     def blit_tile_into(self, dst, tx, ty, mipmap_level=0):
241         # used mainly for saving (transparent PNG)
242         if self.mipmap_level < mipmap_level:
243             return self.mipmap.blit_tile_into(dst, tx, ty, mipmap_level)
244         assert dst.shape[2] == 4
245         src = self.get_tile_memory(tx, ty, readonly=True)
246         if src is transparent_tile.rgba:
247             #dst[:] = 0 # <-- notably slower than memset()
248             mypaintlib.tile_clear(dst)
249         else:
250             mypaintlib.tile_convert_rgba16_to_rgba8(src, dst)
251
252
253     def composite_tile(self, dst, tx, ty, mipmap_level=0, opacity=1.0,
254                        mode=DEFAULT_COMPOSITE_OP):
255         """Composite one tile of this surface over a NumPy array.
256
257         Composite one tile of this surface over the array dst, modifying only dst.
258         """
259         if self.mipmap_level < mipmap_level:
260             return self.mipmap.composite_tile(dst, tx, ty, mipmap_level, opacity, mode)
261         if not (tx,ty) in self.tiledict:
262             return
263         src = self.get_tile_memory(tx, ty, readonly=True)
264
265         if mode == 'svg:src-over':
266             return composite_array_src_over(dst, src, mipmap_level, opacity)
267         elif mode == 'svg:multiply':
268             return composite_array_multiply(dst, src, mipmap_level, opacity)
269         elif mode == 'svg:screen':
270             return composite_array_screen(dst, src, mipmap_level, opacity)
271         elif mode == 'svg:color-burn':
272             return composite_array_burn(dst, src, mipmap_level, opacity)
273         elif mode == 'svg:color-dodge':
274             return composite_array_dodge(dst, src, mipmap_level, opacity)
275         else:
276             raise NotImplementedError, mode
277
278     def save_snapshot(self):
279         sshot = SurfaceSnapshot()
280         for t in self.tiledict.itervalues():
281             t.readonly = True
282         sshot.tiledict = self.tiledict.copy()
283         return sshot
284
285     def load_snapshot(self, sshot):
286         self._load_tiledict(sshot.tiledict)
287
288     def _load_tiledict(self, d):
289         if d == self.tiledict:
290             # common case optimization, called from split_stroke() via stroke.redo()
291             # testcase: comparison above (if equal) takes 0.6ms, code below 30ms
292             return
293         old = set(self.tiledict.iteritems())
294         self.tiledict = d.copy()
295         new = set(self.tiledict.iteritems())
296         dirty = old.symmetric_difference(new)
297         for pos, tile in dirty:
298             self._mark_mipmap_dirty(*pos)
299         bbox = get_tiles_bbox([pos for (pos, tile) in dirty])
300         if not bbox.empty():
301             self.notify_observers(*bbox)
302
303     def load_from_surface(self, other):
304         self.load_snapshot(other.save_snapshot())
305
306     def _load_from_pixbufsurface(self, s):
307         dirty_tiles = set(self.tiledict.keys())
308         self.tiledict = {}
309
310         for tx, ty in s.get_tiles():
311             dst = self.get_tile_memory(tx, ty, readonly=False)
312             s.blit_tile_into(dst, tx, ty)
313
314         dirty_tiles.update(self.tiledict.keys())
315         bbox = get_tiles_bbox(dirty_tiles)
316         self.notify_observers(*bbox)
317
318     def load_from_numpy(self, arr, x, y):
319         assert arr.dtype == 'uint8'
320         h, w, channels = arr.shape
321         s = pixbufsurface.Surface(x, y, w, h, alpha=True, data=arr)
322         self._load_from_pixbufsurface(s)
323
324     def load_from_png(self, filename, x, y, feedback_cb=None):
325         """Load from a PNG, one tilerow at a time, discarding empty tiles.
326         """
327         dirty_tiles = set(self.tiledict.keys())
328         self.tiledict = {}
329
330         state = {}
331         state['buf'] = None # array of height N, width depends on image
332         state['ty'] = y/N # current tile row being filled into buf
333
334         def get_buffer(png_w, png_h):
335             if feedback_cb:
336                 feedback_cb()
337             buf_x0 = x/N*N
338             buf_x1 = ((x+png_w-1)/N+1)*N
339             buf_y0 = state['ty']*N
340             buf_y1 = buf_y0+N
341             buf_w = buf_x1-buf_x0
342             buf_h = buf_y1-buf_y0
343             assert buf_w % N == 0
344             assert buf_h == N
345             if state['buf'] is not None:
346                 consume_buf()
347             else:
348                 state['buf'] = empty((buf_h, buf_w, 4), 'uint8')
349
350             png_x0 = x
351             png_x1 = x+png_w
352             subbuf = state['buf'][:,png_x0-buf_x0:png_x1-buf_x0]
353             if 1: # optimize: only needed for first and last
354                 state['buf'].fill(0)
355                 png_y0 = max(buf_y0, y)
356                 png_y1 = min(buf_y0+buf_h, y+png_h)
357                 assert png_y1 > png_y0
358                 subbuf = subbuf[png_y0-buf_y0:png_y1-buf_y0,:]
359
360             state['ty'] += 1
361             return subbuf
362
363         def consume_buf():
364             ty = state['ty']-1
365             for i in xrange(state['buf'].shape[1]/N):
366                 tx = x/N + i
367                 src = state['buf'][:,i*N:(i+1)*N,:]
368                 if src[:,:,3].any():
369                     dst = self.get_tile_memory(tx, ty, readonly=False)
370                     mypaintlib.tile_convert_rgba8_to_rgba16(src, dst)
371
372         filename_sys = filename.encode(sys.getfilesystemencoding()) # FIXME: should not do that, should use open(unicode_object)
373         mypaintlib.load_png_fast_progressive(filename_sys, get_buffer)
374         consume_buf() # also process the final chunk of data
375         
376         dirty_tiles.update(self.tiledict.keys())
377         bbox = get_tiles_bbox(dirty_tiles)
378         self.notify_observers(*bbox)
379
380     def render_as_pixbuf(self, *args, **kwargs):
381         if not self.tiledict:
382             print 'WARNING: empty surface'
383         t0 = time.time()
384         kwargs['alpha'] = True
385         res = pixbufsurface.render_as_pixbuf(self, *args, **kwargs)
386         print '  %.3fs rendering layer as pixbuf' % (time.time() - t0)
387         return res
388
389     def save_as_png(self, filename, *args, **kwargs):
390         assert 'alpha' not in kwargs
391         kwargs['alpha'] = True
392         pixbufsurface.save_as_png(self, filename, *args, **kwargs)
393
394     def get_tiles(self):
395         return self.tiledict
396
397     def get_bbox(self):
398         return get_tiles_bbox(self.tiledict)
399
400     def is_empty(self):
401         return not self.tiledict
402
403     def remove_empty_tiles(self):
404         # Only used in tests
405         for pos, data in self.tiledict.items():
406             if not data.rgba.any():
407                 self.tiledict.pop(pos)
408
409
410     def begin_interactive_move(self, x, y):
411         snapshot = self.save_snapshot()
412         chunks = snapshot.tiledict.keys()
413         tx = x // N
414         ty = y // N
415         #chebyshev = lambda p: max(abs(tx - p[0]), abs(ty - p[1]))
416         #euclidean = lambda p: math.sqrt((tx - p[0])**2 + (ty - p[1])**2)
417         def mandala(p):
418             # A sort of distance weighted, dithered effect favouring
419             # the area around the mouse pointer.
420             c = max(abs(tx - p[0]), abs(ty - p[1]))
421             for i in (17,13,11,7,5,3,2):
422                 if (p[0] % i == 0) and (p[1] % i == 0):
423                     return c // i
424             return c
425         chunks.sort(key=mandala)
426         return (snapshot, chunks)
427
428
429     def update_interactive_move(self, dx, dy):
430         # Blank everything currently in the layer
431         dirty_tiles = set(self.tiledict.keys())
432         self.tiledict = {}
433         for dirty_pos in dirty_tiles:
434             self._mark_mipmap_dirty(*dirty_pos)
435         bbox = get_tiles_bbox(dirty_tiles)
436         self.notify_observers(*bbox)
437         # Calculate offsets
438         dx = int(dx)
439         dy = int(dy)
440         slices_x = calc_translation_slices(dx)
441         slices_y = calc_translation_slices(dy)
442         return (slices_x, slices_y)
443
444
445     def process_interactive_move_queue(self, snapshot, chunks, offsets):
446         dirty = set()
447         slices_x, slices_y = offsets
448         for tile_pos in chunks:
449             src_tx, src_ty = tile_pos
450             src_tile = snapshot.tiledict[(src_tx, src_ty)]
451             is_integral = len(slices_x) == 1 and len(slices_y) == 1
452             for (src_x0, src_x1), (targ_tdx, targ_x0, targ_x1) in slices_x:
453                 for (src_y0, src_y1), (targ_tdy, targ_y0, targ_y1) in slices_y:
454                     targ_tx = src_tx + targ_tdx
455                     targ_ty = src_ty + targ_tdy
456                     if is_integral:
457                         self.tiledict[(targ_tx, targ_ty)] = src_tile.copy()
458                     else:
459                         targ_tile = self.tiledict.get((targ_tx, targ_ty), None)
460                         if targ_tile is None:
461                             targ_tile = Tile()
462                             self.tiledict[(targ_tx, targ_ty)] = targ_tile
463                         targ_tile.rgba[targ_y0:targ_y1, targ_x0:targ_x1] \
464                           = src_tile.rgba[src_y0:src_y1, src_x0:src_x1]
465                     dirty.add((targ_tx, targ_ty))
466         for dirty_pos in dirty:
467             self._mark_mipmap_dirty(*dirty_pos)
468         bbox = get_tiles_bbox(dirty) # hopefully relatively contiguous
469         self.notify_observers(*bbox)
470
471
472 def calc_translation_slices(dc):
473     """Returns a list of offsets and slice extents for a translation of `dc`.
474
475     The returned slice list's members are of the form
476
477         ((src_c0, src_c1), (targ_tdc, targ_c0, targ_c1))
478
479     where ``src_c0`` and ``src_c1`` determine the extents of the source slice
480     within a tile, their ``targ_`` equivalents specify where to put that slice
481     in the target tile, and ``targ_tdc`` is the tile offset.
482     """
483     dcr = dc % N
484     tdc = (dc // N)
485     if dcr == 0:
486         return [ ((0, N), (tdc, 0, N)) ]
487     else:
488         return [ ((0, N-dcr), (tdc, dcr, N)) ,
489                  ((N-dcr, N), (tdc+1, 0, dcr)) ]
490