OSDN Git Service

load_ora: add feedback_cb
[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
15 TILE_SIZE = N = mypaintlib.TILE_SIZE
16 MAX_MIPMAP_LEVEL = mypaintlib.MAX_MIPMAP_LEVEL
17
18 from layer import DEFAULT_COMPOSITE_OP
19 import pixbufsurface
20
21
22 class Tile:
23     def __init__(self, copy_from=None):
24         # note: pixels are stored with premultiplied alpha
25         #       15bits are used, but fully opaque or white is stored as 2**15 (requiring 16 bits)
26         #       This is to allow many calcuations to divide by 2**15 instead of (2**16-1)
27         if copy_from is None:
28             self.rgba = zeros((N, N, 4), 'uint16')
29         else:
30             self.rgba = copy_from.rgba.copy()
31         self.readonly = False
32
33     def copy(self):
34         return Tile(copy_from=self)
35
36
37 def composite_array_src_over(dst, src, mipmap_level=0, opacity=1.0):
38     """The default "svg:src-over" layer composite op implementation.
39     """
40     if dst.shape[2] == 4 and dst.dtype == 'uint16':
41         # rarely used (for merging layers, also when exporting a transparent PNGs)
42         # src (premultiplied) OVER dst (premultiplied)
43         # cB = cA + (1.0 - aA) * cB
44         #  where B = dst, A = src
45         srcAlpha = src[:,:,3:4].astype('float') * opacity / (1 << 15)
46         dstAlpha = dst[:,:,3:4].astype('float') / (1 << 15)
47         src_premult = src[:,:,0:3].astype('float') * opacity / (1<<15)
48         dst_premult = dst[:,:,0:3].astype('float') / (1<<15)
49         dst_c = clip(src_premult + (1.0 - srcAlpha) * dst_premult, 0.0, 1.0)
50         dst_a = clip(srcAlpha + dstAlpha - srcAlpha * dstAlpha, 0.0, 1.0)
51         dst[:,:,0:3] = clip(dst_c * (1<<15), 0, (1<<15) - 1).astype('uint16')
52         dst[:,:,3:4] = clip(dst_a * (1<<15), 0, (1<<15) - 1).astype('uint16')
53     elif dst.shape[2] == 3 and dst.dtype == 'uint16':
54         mypaintlib.tile_composite_rgba16_over_rgb16(src, dst, opacity)
55     else:
56         raise NotImplementedError
57
58
59 def composite_array_multiply(dst, src, mipmap_level=0, opacity=1.0):
60     """The "svg:multiply" layer composite op implementation.
61     """
62     if dst.shape[2] == 4 and dst.dtype == 'uint16':
63         # rarely used (for merging layers, also when exporting a transparent PNGs)
64         # src (premultiplied) MULTIPLY dst (premultiplied)
65         # cA * cB +  cA * (1 - aB) + cB * (1 - aA)
66         srcAlpha = (opacity * src[:,:,3:4]).astype('float') / (1 << 15)
67         dstAlpha = (dst[:,:,3:4]).astype('float') / (1 << 15)
68         src_premult = src[:,:,0:3].astype('float') * opacity / (1<<15)
69         dst_premult = dst[:,:,0:3].astype('float') / (1<<15)
70         dst_c = clip(src_premult * dst_premult +  src_premult * (1.0 - dstAlpha) + dst_premult * (1.0 - srcAlpha),0.0, 1.0)
71         dst_a = clip(srcAlpha + dstAlpha - srcAlpha * dstAlpha, 0.0, 1.0)
72         dst[:,:,0:3] = clip(dst_c * (1<<15), 0, (1<<15) - 1).astype('uint16')
73         dst[:,:,3:4] = clip(dst_a * (1<<15), 0, (1<<15) - 1).astype('uint16')
74     elif dst.shape[2] == 3 and dst.dtype == 'uint16':
75         mypaintlib.tile_composite_rgba16_multiply_rgb16(src, dst, opacity)
76     else:
77         raise NotImplementedError
78
79
80 def composite_array_screen(dst, src, mipmap_level=0, opacity=1.0):
81     """The "svg:screen" layer composite op implementation.
82     """
83     if dst.shape[2] == 4 and dst.dtype == 'uint16':
84         # rarely used (for merging layers, also when exporting a transparent PNGs)
85         # src (premultiplied) SCREEN dst (premultiplied)
86         # cA + cB - cA * cB
87         srcAlpha = (opacity * src[:,:,3:4]).astype('float') / (1 << 15)
88         dstAlpha = (dst[:,:,3:4]).astype('float') / (1 << 15)
89         src_premult = src[:,:,0:3].astype('float') * opacity / (1<<15)
90         dst_premult = dst[:,:,0:3].astype('float') / (1<<15)
91         dst_c = clip(src_premult + dst_premult - src_premult * dst_premult, 0, 1)
92         dst_a = clip(srcAlpha + dstAlpha - srcAlpha * dstAlpha, 0, 1)
93         dst[:,:,0:3] = clip(dst_c * (1<<15),0, (1<<15) - 1).astype('uint16')
94         dst[:,:,3:4] = clip(dst_a * (1<<15),0, (1<<15) - 1).astype('uint16')
95     elif dst.shape[2] == 3 and dst.dtype == 'uint16':
96         mypaintlib.tile_composite_rgba16_screen_rgb16(src, dst, opacity)
97     else:
98         raise NotImplementedError
99
100
101 def composite_array_burn(dst, src, mipmap_level=0, opacity=1.0):
102     """The "svg:color-burn" layer composite op implementation.
103     """
104     if dst.shape[2] == 4 and dst.dtype == 'uint16':
105         # rarely used (for merging layers, also when exporting a transparent PNGs)
106         # src (premultiplied) OVER dst (premultiplied)
107         # if cA * aB + cB * aA <= aA * aB :
108         #   cA * (1 - aB) + cB * (1 - aA)
109         #   (cA == 0 ? 1 : (aA * (cA * aB + cB * aA - aA * aB) / cA) + cA * (1 - aB) + cB * (1 - aA))
110         #  where B = dst, A = src
111         aA = (opacity * src[:,:,3:4]).astype('float') / (1 << 15)
112         aB = (dst[:,:,3:4]).astype('float') / (1 << 15)
113         cA = src[:,:,0:3].astype('float') * opacity / (1<<15)
114         cB = dst[:,:,0:3].astype('float') / (1<<15)
115         dst_c = where(cA * aB + cB * aA <= aA * aB,
116                      cA * (1 - aB) + cB * (1 - aA),
117                      where(cA == 0,
118                            1.0,
119                            (aA * (cA * aB + cB * aA - aA * aB) / cA) + cA * (1.0 - aB) + cB * (1.0 - aA)))
120         dst_a = aA + aB - aA * aB
121         dst[:,:,0:3] = clip(dst_c * (1<<15), 0, (1<<15) - 1).astype('uint16')
122         dst[:,:,3:4] = clip(dst_a * (1<<15), 0, (1<<15) - 1).astype('uint16')
123     elif dst.shape[2] == 3 and dst.dtype == 'uint16':
124         mypaintlib.tile_composite_rgba16_burn_rgb16(src, dst, opacity)
125     else:
126         raise NotImplementedError
127
128
129 def composite_array_dodge(dst, src, mipmap_level=0, opacity=1.0):
130     """The "svg:color-dodge" layer composite op implementation.
131     """
132     if dst.shape[2] == 4 and dst.dtype == 'uint16':
133         # rarely used (for merging layers, also when exporting a transparent PNGs)
134         # src (premultiplied) OVER dst (premultiplied)
135         # if cA * aB + cB * aA >= aA * aB :
136         #   aA * aB + cA * (1 - aB) + cB * (1 - aA)
137         #   (cA == aA ? 1 : cB * aA / (aA == 0 ? 1 : 1 - cA / aA)) + cA * (1 - aB) + cB * (1 - aA)
138         #  where B = dst, A = src
139         aA = (opacity * src[:,:,3:4]).astype('float') / (1 << 15)
140         aB = (dst[:,:,3:4]).astype('float') / (1 << 15)
141         cA = src[:,:,0:3].astype('float') * opacity / (1<<15)
142         cB = dst[:,:,0:3].astype('float') / (1<<15)
143         dst_c = where(cA * aB + cB * aA >= aA * aB,
144                      aA * aB + cA * (1 - aB) + cB * (1 - aA),
145                      where(cA == aA, 1.0, cB * aA / where(aA == 0, 1.0, 1.0 - cA / aA)) + cA * (1.0 - aB) + cB * (1.0 - aA))
146         dst_a = (aA + aB - aA * aB)
147         dst[:,:,0:3] = clip(dst_c * (1<<15),0, (1<<15) - 1).astype('uint16')
148         dst[:,:,3:4] = clip(dst_a * (1<<15),0, (1<<15) - 1).astype('uint16')
149     elif dst.shape[2] == 3 and dst.dtype == 'uint16':
150         mypaintlib.tile_composite_rgba16_dodge_rgb16(src, dst, opacity)
151     else:
152         raise NotImplementedError
153
154
155 # tile for read-only operations on empty spots
156 transparent_tile = Tile()
157 transparent_tile.readonly = True
158
159 # tile with invalid pixel memory (needs refresh)
160 mipmap_dirty_tile = Tile()
161 del mipmap_dirty_tile.rgba
162
163 def get_tiles_bbox(tiles):
164     res = helpers.Rect()
165     for tx, ty in tiles:
166         res.expandToIncludeRect(helpers.Rect(N*tx, N*ty, N, N))
167     return res
168
169 class SurfaceSnapshot:
170     pass
171
172 class Surface(mypaintlib.TiledSurface):
173     # the C++ half of this class is in tiledsurface.hpp
174     def __init__(self, mipmap_level=0):
175         mypaintlib.TiledSurface.__init__(self, self)
176         self.tiledict = {}
177         self.observers = []
178
179         self.mipmap_level = mipmap_level
180         self.mipmap = None
181         self.parent = None
182
183         if mipmap_level < MAX_MIPMAP_LEVEL:
184             self.mipmap = Surface(mipmap_level+1)
185             self.mipmap.parent = self
186
187     def notify_observers(self, *args):
188         for f in self.observers:
189             f(*args)
190
191     def clear(self):
192         tiles = self.tiledict.keys()
193         self.tiledict = {}
194         self.notify_observers(*get_tiles_bbox(tiles))
195         if self.mipmap: self.mipmap.clear()
196
197     def get_tile_memory(self, tx, ty, readonly):
198         # OPTIMIZE: do some profiling to check if this function is a bottleneck
199         #           yes it is
200         # Note: we must return memory that stays valid for writing until the
201         # last end_atomic(), because of the caching in tiledsurface.hpp.
202         t = self.tiledict.get((tx, ty))
203         if t is None:
204             if readonly:
205                 t = transparent_tile
206             else:
207                 t = Tile()
208                 self.tiledict[(tx, ty)] = t
209         if t is mipmap_dirty_tile:
210             # regenerate mipmap
211             t = Tile()
212             self.tiledict[(tx, ty)] = t
213             empty = True
214             for x in xrange(2):
215                 for y in xrange(2):
216                     src = self.parent.get_tile_memory(tx*2 + x, ty*2 + y, True)
217                     mypaintlib.tile_downscale_rgba16(src, t.rgba, x*N/2, y*N/2)
218                     if src is not transparent_tile.rgba:
219                         empty = False
220             if empty:
221                 # rare case, no need to speed it up
222                 del self.tiledict[(tx, ty)]
223                 t = transparent_tile
224         if t.readonly and not readonly:
225             # shared memory, get a private copy for writing
226             t = t.copy()
227             self.tiledict[(tx, ty)] = t
228         if not readonly:
229             assert self.mipmap_level == 0
230             self._mark_mipmap_dirty(tx, ty)
231         return t.rgba
232
233     def _mark_mipmap_dirty(self, tx, ty):
234         if self.mipmap_level > 0:
235             self.tiledict[(tx, ty)] = mipmap_dirty_tile
236         if self.mipmap:
237             self.mipmap._mark_mipmap_dirty(tx/2, ty/2)
238
239     def blit_tile_into(self, dst, tx, ty, mipmap_level=0):
240         # used mainly for saving (transparent PNG)
241         if self.mipmap_level < mipmap_level:
242             return self.mipmap.blit_tile_into(dst, tx, ty, mipmap_level)
243         assert dst.shape[2] == 4
244         src = self.get_tile_memory(tx, ty, readonly=True)
245         if src is transparent_tile.rgba:
246             #dst[:] = 0 # <-- notably slower than memset()
247             mypaintlib.tile_clear(dst)
248         else:
249             mypaintlib.tile_convert_rgba16_to_rgba8(src, dst)
250
251
252     def composite_tile(self, dst, tx, ty, mipmap_level=0, opacity=1.0,
253                        mode=DEFAULT_COMPOSITE_OP):
254         """Composite one tile of this surface over a NumPy array.
255
256         Composite one tile of this surface over the array dst, modifying only dst.
257         """
258         if self.mipmap_level < mipmap_level:
259             return self.mipmap.composite_tile(dst, tx, ty, mipmap_level, opacity, mode)
260         if not (tx,ty) in self.tiledict:
261             return
262         src = self.get_tile_memory(tx, ty, readonly=True)
263
264         if mode == 'svg:src-over':
265             return composite_array_src_over(dst, src, mipmap_level, opacity)
266         elif mode == 'svg:multiply':
267             return composite_array_multiply(dst, src, mipmap_level, opacity)
268         elif mode == 'svg:screen':
269             return composite_array_screen(dst, src, mipmap_level, opacity)
270         elif mode == 'svg:color-burn':
271             return composite_array_burn(dst, src, mipmap_level, opacity)
272         elif mode == 'svg:color-dodge':
273             return composite_array_dodge(dst, src, mipmap_level, opacity)
274         else:
275             raise NotImplementedError, mode
276
277     def save_snapshot(self):
278         sshot = SurfaceSnapshot()
279         for t in self.tiledict.itervalues():
280             t.readonly = True
281         sshot.tiledict = self.tiledict.copy()
282         return sshot
283
284     def load_snapshot(self, sshot):
285         self._load_tiledict(sshot.tiledict)
286
287     def _load_tiledict(self, d):
288         if d == self.tiledict:
289             # common case optimization, called from split_stroke() via stroke.redo()
290             # testcase: comparison above (if equal) takes 0.6ms, code below 30ms
291             return
292         old = set(self.tiledict.iteritems())
293         self.tiledict = d.copy()
294         new = set(self.tiledict.iteritems())
295         dirty = old.symmetric_difference(new)
296         for pos, tile in dirty:
297             self._mark_mipmap_dirty(*pos)
298         bbox = get_tiles_bbox([pos for (pos, tile) in dirty])
299         if not bbox.empty():
300             self.notify_observers(*bbox)
301
302     def load_from_surface(self, other):
303         self.load_snapshot(other.save_snapshot())
304
305     def _load_from_pixbufsurface(self, s):
306         dirty_tiles = set(self.tiledict.keys())
307         self.tiledict = {}
308
309         for tx, ty in s.get_tiles():
310             dst = self.get_tile_memory(tx, ty, readonly=False)
311             s.blit_tile_into(dst, tx, ty)
312
313         dirty_tiles.update(self.tiledict.keys())
314         bbox = get_tiles_bbox(dirty_tiles)
315         self.notify_observers(*bbox)
316
317     def load_from_numpy(self, arr, x, y):
318         assert arr.dtype == 'uint8'
319         h, w, channels = arr.shape
320         s = pixbufsurface.Surface(x, y, w, h, alpha=True, data=arr)
321         self._load_from_pixbufsurface(s)
322
323     def load_from_png(self, filename, x, y, feedback_cb=None):
324         """Load from a PNG, one tilerow at a time, discarding empty tiles.
325         """
326         dirty_tiles = set(self.tiledict.keys())
327         self.tiledict = {}
328
329         state = {}
330         state['buf'] = None # array of height N, width depends on image
331         state['ty'] = y/N # current tile row being filled into buf
332
333         def get_buffer(png_w, png_h):
334             if feedback_cb:
335                 feedback_cb()
336             buf_x0 = x/N*N
337             buf_x1 = ((x+png_w-1)/N+1)*N
338             buf_y0 = state['ty']*N
339             buf_y1 = buf_y0+N
340             buf_w = buf_x1-buf_x0
341             buf_h = buf_y1-buf_y0
342             assert buf_w % N == 0
343             assert buf_h == N
344             if state['buf'] is not None:
345                 consume_buf()
346             else:
347                 state['buf'] = empty((buf_h, buf_w, 4), 'uint8')
348
349             png_x0 = x
350             png_x1 = x+png_w
351             subbuf = state['buf'][:,png_x0-buf_x0:png_x1-buf_x0]
352             if 1: # optimize: only needed for first and last
353                 state['buf'].fill(0)
354                 png_y0 = max(buf_y0, y)
355                 png_y1 = min(buf_y0+buf_h, y+png_h)
356                 assert png_y1 > png_y0
357                 subbuf = subbuf[png_y0-buf_y0:png_y1-buf_y0,:]
358
359             state['ty'] += 1
360             return subbuf
361
362         def consume_buf():
363             ty = state['ty']-1
364             for i in xrange(state['buf'].shape[1]/N):
365                 tx = x/N + i
366                 src = state['buf'][:,i*N:(i+1)*N,:]
367                 if src[:,:,3].any():
368                     dst = self.get_tile_memory(tx, ty, readonly=False)
369                     mypaintlib.tile_convert_rgba8_to_rgba16(src, dst)
370
371         filename_sys = filename.encode(sys.getfilesystemencoding()) # FIXME: should not do that, should use open(unicode_object)
372         mypaintlib.load_png_fast_progressive(filename_sys, get_buffer)
373         consume_buf() # also process the final chunk of data
374         
375         dirty_tiles.update(self.tiledict.keys())
376         bbox = get_tiles_bbox(dirty_tiles)
377         self.notify_observers(*bbox)
378
379     def render_as_pixbuf(self, *args, **kwargs):
380         if not self.tiledict:
381             print 'WARNING: empty surface'
382         t0 = time.time()
383         kwargs['alpha'] = True
384         res = pixbufsurface.render_as_pixbuf(self, *args, **kwargs)
385         print '  %.3fs rendering layer as pixbuf' % (time.time() - t0)
386         return res
387
388     def save_as_png(self, filename, *args, **kwargs):
389         assert 'alpha' not in kwargs
390         kwargs['alpha'] = True
391         pixbufsurface.save_as_png(self, filename, *args, **kwargs)
392
393     def get_tiles(self):
394         return self.tiledict
395
396     def get_bbox(self):
397         return get_tiles_bbox(self.tiledict)
398
399     def is_empty(self):
400         return not self.tiledict
401
402     def remove_empty_tiles(self):
403         # Only used in tests
404         for pos, data in self.tiledict.items():
405             if not data.rgba.any():
406                 self.tiledict.pop(pos)