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
16 TILE_SIZE = N = mypaintlib.TILE_SIZE
17 MAX_MIPMAP_LEVEL = mypaintlib.MAX_MIPMAP_LEVEL
19 from layer import DEFAULT_COMPOSITE_OP
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)
29 self.rgba = zeros((N, N, 4), 'uint16')
31 self.rgba = copy_from.rgba.copy()
35 return Tile(copy_from=self)
38 def composite_array_src_over(dst, src, mipmap_level=0, opacity=1.0):
39 """The default "svg:src-over" layer composite op implementation.
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)
57 raise NotImplementedError
60 def composite_array_multiply(dst, src, mipmap_level=0, opacity=1.0):
61 """The "svg:multiply" layer composite op implementation.
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)
78 raise NotImplementedError
81 def composite_array_screen(dst, src, mipmap_level=0, opacity=1.0):
82 """The "svg:screen" layer composite op implementation.
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)
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)
99 raise NotImplementedError
102 def composite_array_burn(dst, src, mipmap_level=0, opacity=1.0):
103 """The "svg:color-burn" layer composite op implementation.
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),
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)
127 raise NotImplementedError
130 def composite_array_dodge(dst, src, mipmap_level=0, opacity=1.0):
131 """The "svg:color-dodge" layer composite op implementation.
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)
153 raise NotImplementedError
156 # tile for read-only operations on empty spots
157 transparent_tile = Tile()
158 transparent_tile.readonly = True
160 # tile with invalid pixel memory (needs refresh)
161 mipmap_dirty_tile = Tile()
162 del mipmap_dirty_tile.rgba
164 def get_tiles_bbox(tiles):
167 res.expandToIncludeRect(helpers.Rect(N*tx, N*ty, N, N))
170 class SurfaceSnapshot:
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)
180 self.mipmap_level = mipmap_level
184 if mipmap_level < MAX_MIPMAP_LEVEL:
185 self.mipmap = Surface(mipmap_level+1)
186 self.mipmap.parent = self
188 def notify_observers(self, *args):
189 for f in self.observers:
193 tiles = self.tiledict.keys()
195 self.notify_observers(*get_tiles_bbox(tiles))
196 if self.mipmap: self.mipmap.clear()
198 def get_tile_memory(self, tx, ty, readonly):
199 # OPTIMIZE: do some profiling to check if this function is a bottleneck
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))
209 self.tiledict[(tx, ty)] = t
210 if t is mipmap_dirty_tile:
213 self.tiledict[(tx, ty)] = t
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:
222 # rare case, no need to speed it up
223 del self.tiledict[(tx, ty)]
225 if t.readonly and not readonly:
226 # shared memory, get a private copy for writing
228 self.tiledict[(tx, ty)] = t
230 assert self.mipmap_level == 0
231 self._mark_mipmap_dirty(tx, ty)
234 def _mark_mipmap_dirty(self, tx, ty):
235 if self.mipmap_level > 0:
236 self.tiledict[(tx, ty)] = mipmap_dirty_tile
238 self.mipmap._mark_mipmap_dirty(tx/2, ty/2)
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)
250 mypaintlib.tile_convert_rgba16_to_rgba8(src, dst)
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.
257 Composite one tile of this surface over the array dst, modifying only dst.
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:
263 src = self.get_tile_memory(tx, ty, readonly=True)
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)
276 raise NotImplementedError, mode
278 def save_snapshot(self):
279 sshot = SurfaceSnapshot()
280 for t in self.tiledict.itervalues():
282 sshot.tiledict = self.tiledict.copy()
285 def load_snapshot(self, sshot):
286 self._load_tiledict(sshot.tiledict)
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
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])
301 self.notify_observers(*bbox)
303 def load_from_surface(self, other):
304 self.load_snapshot(other.save_snapshot())
306 def _load_from_pixbufsurface(self, s):
307 dirty_tiles = set(self.tiledict.keys())
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)
314 dirty_tiles.update(self.tiledict.keys())
315 bbox = get_tiles_bbox(dirty_tiles)
316 self.notify_observers(*bbox)
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)
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.
327 dirty_tiles = set(self.tiledict.keys())
331 state['buf'] = None # array of height N, width depends on image
332 state['ty'] = y/N # current tile row being filled into buf
334 def get_buffer(png_w, png_h):
338 buf_x1 = ((x+png_w-1)/N+1)*N
339 buf_y0 = state['ty']*N
341 buf_w = buf_x1-buf_x0
342 buf_h = buf_y1-buf_y0
343 assert buf_w % N == 0
345 if state['buf'] is not None:
348 state['buf'] = empty((buf_h, buf_w, 4), 'uint8')
352 subbuf = state['buf'][:,png_x0-buf_x0:png_x1-buf_x0]
353 if 1: # optimize: only needed for first and last
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,:]
365 for i in xrange(state['buf'].shape[1]/N):
367 src = state['buf'][:,i*N:(i+1)*N,:]
369 dst = self.get_tile_memory(tx, ty, readonly=False)
370 mypaintlib.tile_convert_rgba8_to_rgba16(src, dst)
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
376 dirty_tiles.update(self.tiledict.keys())
377 bbox = get_tiles_bbox(dirty_tiles)
378 self.notify_observers(*bbox)
380 def render_as_pixbuf(self, *args, **kwargs):
381 if not self.tiledict:
382 print 'WARNING: empty surface'
384 kwargs['alpha'] = True
385 res = pixbufsurface.render_as_pixbuf(self, *args, **kwargs)
386 print ' %.3fs rendering layer as pixbuf' % (time.time() - t0)
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)
398 return get_tiles_bbox(self.tiledict)
401 return not self.tiledict
403 def remove_empty_tiles(self):
405 for pos, data in self.tiledict.items():
406 if not data.rgba.any():
407 self.tiledict.pop(pos)
410 def begin_interactive_move(self, x, y):
411 snapshot = self.save_snapshot()
412 chunks = snapshot.tiledict.keys()
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)
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):
425 chunks.sort(key=mandala)
426 return (snapshot, chunks)
429 def update_interactive_move(self, dx, dy):
430 # Blank everything currently in the layer
431 dirty_tiles = set(self.tiledict.keys())
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)
440 slices_x = calc_translation_slices(dx)
441 slices_y = calc_translation_slices(dy)
442 return (slices_x, slices_y)
445 def process_interactive_move_queue(self, snapshot, chunks, offsets):
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
457 self.tiledict[(targ_tx, targ_ty)] = src_tile.copy()
459 targ_tile = self.tiledict.get((targ_tx, targ_ty), None)
460 if targ_tile is None:
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)
472 def calc_translation_slices(dc):
473 """Returns a list of offsets and slice extents for a translation of `dc`.
475 The returned slice list's members are of the form
477 ((src_c0, src_c1), (targ_tdc, targ_c0, targ_c1))
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.
486 return [ ((0, N), (tdc, 0, N)) ]
488 return [ ((0, N-dcr), (tdc, dcr, N)) ,
489 ((N-dcr, N), (tdc+1, 0, dcr)) ]