1 # This file is part of MyPaint.
2 # Copyright (C) 2007-2008 by Martin Renold <martinxyz@gmx.ch>
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.
9 # This module implements an unbounded tiled surface for painting.
13 import mypaintlib, helpers
15 TILE_SIZE = N = mypaintlib.TILE_SIZE
16 MAX_MIPMAP_LEVEL = mypaintlib.MAX_MIPMAP_LEVEL
18 from layer import DEFAULT_COMPOSITE_OP
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)
28 self.rgba = zeros((N, N, 4), 'uint16')
30 self.rgba = copy_from.rgba.copy()
34 return Tile(copy_from=self)
37 def composite_array_src_over(dst, src, mipmap_level=0, opacity=1.0):
38 """The default "svg:src-over" layer composite op implementation.
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)
56 raise NotImplementedError
59 def composite_array_multiply(dst, src, mipmap_level=0, opacity=1.0):
60 """The "svg:multiply" layer composite op implementation.
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)
77 raise NotImplementedError
80 def composite_array_screen(dst, src, mipmap_level=0, opacity=1.0):
81 """The "svg:screen" layer composite op implementation.
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)
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)
98 raise NotImplementedError
101 def composite_array_burn(dst, src, mipmap_level=0, opacity=1.0):
102 """The "svg:color-burn" layer composite op implementation.
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),
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)
126 raise NotImplementedError
129 def composite_array_dodge(dst, src, mipmap_level=0, opacity=1.0):
130 """The "svg:color-dodge" layer composite op implementation.
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)
152 raise NotImplementedError
155 # tile for read-only operations on empty spots
156 transparent_tile = Tile()
157 transparent_tile.readonly = True
159 # tile with invalid pixel memory (needs refresh)
160 mipmap_dirty_tile = Tile()
161 del mipmap_dirty_tile.rgba
163 def get_tiles_bbox(tiles):
166 res.expandToIncludeRect(helpers.Rect(N*tx, N*ty, N, N))
169 class SurfaceSnapshot:
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)
179 self.mipmap_level = mipmap_level
183 if mipmap_level < MAX_MIPMAP_LEVEL:
184 self.mipmap = Surface(mipmap_level+1)
185 self.mipmap.parent = self
187 def notify_observers(self, *args):
188 for f in self.observers:
192 tiles = self.tiledict.keys()
194 self.notify_observers(*get_tiles_bbox(tiles))
195 if self.mipmap: self.mipmap.clear()
197 def get_tile_memory(self, tx, ty, readonly):
198 # OPTIMIZE: do some profiling to check if this function is a bottleneck
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))
208 self.tiledict[(tx, ty)] = t
209 if t is mipmap_dirty_tile:
212 self.tiledict[(tx, ty)] = t
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:
221 # rare case, no need to speed it up
222 del self.tiledict[(tx, ty)]
224 if t.readonly and not readonly:
225 # shared memory, get a private copy for writing
227 self.tiledict[(tx, ty)] = t
229 assert self.mipmap_level == 0
230 self._mark_mipmap_dirty(tx, ty)
233 def _mark_mipmap_dirty(self, tx, ty):
234 if self.mipmap_level > 0:
235 self.tiledict[(tx, ty)] = mipmap_dirty_tile
237 self.mipmap._mark_mipmap_dirty(tx/2, ty/2)
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)
249 mypaintlib.tile_convert_rgba16_to_rgba8(src, dst)
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.
256 Composite one tile of this surface over the array dst, modifying only dst.
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:
262 src = self.get_tile_memory(tx, ty, readonly=True)
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)
275 raise NotImplementedError, mode
277 def save_snapshot(self):
278 sshot = SurfaceSnapshot()
279 for t in self.tiledict.itervalues():
281 sshot.tiledict = self.tiledict.copy()
284 def load_snapshot(self, sshot):
285 self._load_tiledict(sshot.tiledict)
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
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])
300 self.notify_observers(*bbox)
302 def load_from_surface(self, other):
303 self.load_snapshot(other.save_snapshot())
305 def _load_from_pixbufsurface(self, s):
306 dirty_tiles = set(self.tiledict.keys())
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)
313 dirty_tiles.update(self.tiledict.keys())
314 bbox = get_tiles_bbox(dirty_tiles)
315 self.notify_observers(*bbox)
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)
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.
326 dirty_tiles = set(self.tiledict.keys())
330 state['buf'] = None # array of height N, width depends on image
331 state['ty'] = y/N # current tile row being filled into buf
333 def get_buffer(png_w, png_h):
337 buf_x1 = ((x+png_w-1)/N+1)*N
338 buf_y0 = state['ty']*N
340 buf_w = buf_x1-buf_x0
341 buf_h = buf_y1-buf_y0
342 assert buf_w % N == 0
344 if state['buf'] is not None:
347 state['buf'] = empty((buf_h, buf_w, 4), 'uint8')
351 subbuf = state['buf'][:,png_x0-buf_x0:png_x1-buf_x0]
352 if 1: # optimize: only needed for first and last
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,:]
364 for i in xrange(state['buf'].shape[1]/N):
366 src = state['buf'][:,i*N:(i+1)*N,:]
368 dst = self.get_tile_memory(tx, ty, readonly=False)
369 mypaintlib.tile_convert_rgba8_to_rgba16(src, dst)
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
375 dirty_tiles.update(self.tiledict.keys())
376 bbox = get_tiles_bbox(dirty_tiles)
377 self.notify_observers(*bbox)
379 def render_as_pixbuf(self, *args, **kwargs):
380 if not self.tiledict:
381 print 'WARNING: empty surface'
383 kwargs['alpha'] = True
384 res = pixbufsurface.render_as_pixbuf(self, *args, **kwargs)
385 print ' %.3fs rendering layer as pixbuf' % (time.time() - t0)
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)
397 return get_tiles_bbox(self.tiledict)
400 return not self.tiledict
402 def remove_empty_tiles(self):
404 for pos, data in self.tiledict.items():
405 if not data.rgba.any():
406 self.tiledict.pop(pos)