~ubuntu-branches/ubuntu/utopic/python-traitsui/utopic

« back to all changes in this revision

Viewing changes to traitsui/wx/image_slice.py

  • Committer: Bazaar Package Importer
  • Author(s): Varun Hiremath
  • Date: 2011-07-09 13:57:39 UTC
  • Revision ID: james.westby@ubuntu.com-20110709135739-x5u20q86huissmn1
Tags: upstream-4.0.0
ImportĀ upstreamĀ versionĀ 4.0.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#-------------------------------------------------------------------------------
 
2
#
 
3
#  Copyright (c) 2007, Enthought, Inc.
 
4
#  All rights reserved.
 
5
#
 
6
#  This software is provided without warranty under the terms of the BSD
 
7
#  license included in enthought/LICENSE.txt and may be redistributed only
 
8
#  under the conditions described in the aforementioned license.  The license
 
9
#  is also available online at http://www.enthought.com/licenses/BSD.txt
 
10
#
 
11
#  Thanks for using Enthought open source!
 
12
#
 
13
#  Author: David C. Morrill
 
14
#  Date:   06/06/2007
 
15
#
 
16
#-------------------------------------------------------------------------------
 
17
 
 
18
""" Class to aid in automatically computing the 'slice' points for a specified
 
19
    ImageResource and then drawing it that it can be 'stretched' to fit a larger
 
20
    region than the original image.
 
21
"""
 
22
 
 
23
#-------------------------------------------------------------------------------
 
24
#  Imports:
 
25
#-------------------------------------------------------------------------------
 
26
 
 
27
import wx
 
28
 
 
29
from colorsys \
 
30
    import rgb_to_hls
 
31
 
 
32
from numpy \
 
33
    import reshape, fromstring, uint8
 
34
 
 
35
from traits.api \
 
36
    import HasPrivateTraits, Instance, Int, List, Color, Enum, Bool
 
37
 
 
38
from pyface.image_resource \
 
39
    import ImageResource
 
40
 
 
41
from constants \
 
42
    import WindowColor
 
43
 
 
44
from constants import is_mac
 
45
import traitsui.wx.constants
 
46
 
 
47
#-------------------------------------------------------------------------------
 
48
#  Recursively paint the parent's background if they have an associated image
 
49
#  slice.
 
50
#-------------------------------------------------------------------------------
 
51
 
 
52
def paint_parent ( dc, window ):
 
53
    """ Recursively paint the parent's background if they have an associated
 
54
        image slice.
 
55
    """
 
56
    parent = window.GetParent()
 
57
    slice  = getattr( parent, '_image_slice', None )
 
58
    if slice is not None:
 
59
        x, y   = window.GetPositionTuple()
 
60
        dx, dy = parent.GetSizeTuple()
 
61
        slice.fill( dc, -x, -y, dx, dy )
 
62
    else:
 
63
        # Otherwise, just paint the normal window background color:
 
64
        dx, dy = window.GetClientSizeTuple()
 
65
        if is_mac and hasattr(window, '_border') and window._border:
 
66
            dc.SetBackgroundMode(wx.TRANSPARENT)
 
67
            dc.SetBrush( wx.Brush( wx.Colour(0, 0, 0, 0)))
 
68
        else:
 
69
            dc.SetBrush( wx.Brush( parent.GetBackgroundColour() ) )
 
70
        dc.SetPen( wx.TRANSPARENT_PEN )
 
71
        dc.DrawRectangle( 0, 0, dx, dy )
 
72
 
 
73
    return slice
 
74
 
 
75
#-------------------------------------------------------------------------------
 
76
#  'ImageSlice' class:
 
77
#-------------------------------------------------------------------------------
 
78
 
 
79
class ImageSlice ( HasPrivateTraits ):
 
80
 
 
81
    #-- Trait Definitions ------------------------------------------------------
 
82
 
 
83
    # The ImageResource to be sliced and drawn:
 
84
    image = Instance( ImageResource )
 
85
 
 
86
    # The minimum number of adjacent, identical rows/columns needed to identify
 
87
    # a repeatable section:
 
88
    threshold = Int( 10 )
 
89
 
 
90
    # The maximum number of 'stretchable' rows and columns:
 
91
    stretch_rows    = Enum( 1, 2 )
 
92
    stretch_columns = Enum( 1, 2 )
 
93
 
 
94
    # Width/height of the image borders:
 
95
    top    = Int
 
96
    bottom = Int
 
97
    left   = Int
 
98
    right  = Int
 
99
 
 
100
    # Width/height of the extended image borders:
 
101
    xtop    = Int
 
102
    xbottom = Int
 
103
    xleft   = Int
 
104
    xright  = Int
 
105
 
 
106
    # The color to use for content text:
 
107
    content_color = Instance( wx.Colour )
 
108
 
 
109
    # The color to use for label text:
 
110
    label_color = Instance( wx.Colour )
 
111
 
 
112
    # The background color of the image:
 
113
    bg_color = Color
 
114
 
 
115
    # Should debugging slice lines be drawn?
 
116
    debug = Bool( False )
 
117
 
 
118
    #-- Private Traits ---------------------------------------------------------
 
119
 
 
120
    # The current image's opaque bitmap:
 
121
    opaque_bitmap = Instance( wx.Bitmap )
 
122
 
 
123
    # The current image's transparent bitmap:
 
124
    transparent_bitmap = Instance( wx.Bitmap )
 
125
 
 
126
    # Size of the current image:
 
127
    dx = Int
 
128
    dy = Int
 
129
 
 
130
    # Size of the current image's slices:
 
131
    dxs = List
 
132
    dys = List
 
133
 
 
134
    # Fixed minimum size of current image:
 
135
    fdx = Int
 
136
    fdy = Int
 
137
 
 
138
    #-- Public Methods ---------------------------------------------------------
 
139
 
 
140
    def fill ( self, dc, x, y, dx, dy, transparent = False ):
 
141
        """ 'Stretch fill' the specified region of a device context with the
 
142
            sliced image.
 
143
        """
 
144
        # Create the source image dc:
 
145
        idc = wx.MemoryDC()
 
146
        if transparent:
 
147
            idc.SelectObject( self.transparent_bitmap )
 
148
        else:
 
149
            idc.SelectObject( self.opaque_bitmap )
 
150
 
 
151
        # Set up the drawing parameters:
 
152
        sdx, sdy = self.dx, self.dx
 
153
        dxs, dys = self.dxs, self.dys
 
154
        tdx, tdy = dx - self.fdx, dy - self.fdy
 
155
 
 
156
        # Calculate vertical slice sizes to use for source and destination:
 
157
        n = len( dxs )
 
158
        if n == 1:
 
159
            pdxs = [ ( 0, 0 ), ( 1, max( 1, tdx/2 ) ), ( sdx - 2, sdx - 2 ),
 
160
                     ( 1, max( 1, tdx - (tdx/2) ) ), ( 0, 0 ) ]
 
161
        elif n == 3:
 
162
            pdxs = [ ( dxs[0], dxs[0] ), ( dxs[1], max( 0, tdx ) ), ( 0, 0 ),
 
163
                     ( 0, 0 ), ( dxs[2], dxs[2] ) ]
 
164
        else:
 
165
            pdxs = [ ( dxs[0], dxs[0] ), ( dxs[1], max( 0, tdx/2 ) ),
 
166
                     ( dxs[2], dxs[2] ), ( dxs[3], max( 0, tdx - (tdx/2) ) ),
 
167
                     ( dxs[4], dxs[4] ) ]
 
168
 
 
169
        # Calculate horizontal slice sizes to use for source and destination:
 
170
        n = len( dys )
 
171
        if n == 1:
 
172
            pdys = [ ( 0, 0 ), ( 1, max( 1, tdy/2 ) ), ( sdy - 2, sdy - 2 ),
 
173
                     ( 1, max( 1, tdy - (tdy/2) ) ), ( 0, 0 ) ]
 
174
        elif n == 3:
 
175
            pdys = [ ( dys[0], dys[0] ), ( dys[1], max( 0, tdy ) ), ( 0, 0 ),
 
176
                     ( 0, 0 ), ( dys[2], dys[2] ) ]
 
177
        else:
 
178
            pdys = [ ( dys[0], dys[0] ), ( dys[1], max( 0, tdy/2 ) ),
 
179
                     ( dys[2], dys[2] ), ( dys[3], max( 0, tdy - (tdy/2) ) ),
 
180
                     ( dys[4], dys[4] ) ]
 
181
 
 
182
        # Iterate over each cell, performing a stretch fill from the source
 
183
        # image to the destination window:
 
184
        last_x, last_y = x + dx, y + dy
 
185
        y0, iy0 = y, 0
 
186
        for idy, wdy in pdys:
 
187
            if y0 >= last_y:
 
188
                break
 
189
 
 
190
            if wdy != 0:
 
191
                x0, ix0 = x, 0
 
192
                for idx, wdx in pdxs:
 
193
                    if x0 >= last_x:
 
194
                        break
 
195
 
 
196
                    if wdx != 0:
 
197
                        self._fill( idc, ix0, iy0, idx, idy,
 
198
                                    dc,  x0,  y0,  wdx, wdy )
 
199
                        x0 += wdx
 
200
                    ix0 += idx
 
201
                y0 += wdy
 
202
            iy0 += idy
 
203
 
 
204
        if self.debug:
 
205
            dc.SetPen( wx.Pen( wx.RED ) )
 
206
            dc.DrawLine( x, y + self.top, last_x, y + self.top )
 
207
            dc.DrawLine( x, last_y - self.bottom - 1,
 
208
                         last_x, last_y - self.bottom - 1 )
 
209
            dc.DrawLine( x + self.left, y, x + self.left, last_y )
 
210
            dc.DrawLine( last_x - self.right - 1, y,
 
211
                         last_x - self.right - 1, last_y )
 
212
 
 
213
    #-- Event Handlers ---------------------------------------------------------
 
214
 
 
215
    def _image_changed ( self, image ):
 
216
        """ Handles the 'image' trait being changed.
 
217
        """
 
218
        # Save the original bitmap as the transparent version:
 
219
        self.transparent_bitmap = bitmap = \
 
220
            image.create_image().ConvertToBitmap()
 
221
 
 
222
        # Save the bitmap size information:
 
223
        self.dx = dx = bitmap.GetWidth()
 
224
        self.dy = dy = bitmap.GetHeight()
 
225
 
 
226
        # Create the opaque version of the bitmap:
 
227
        self.opaque_bitmap = wx.EmptyBitmap( dx, dy )
 
228
        mdc2 = wx.MemoryDC()
 
229
        mdc2.SelectObject( self.opaque_bitmap )
 
230
        mdc2.SetBrush( wx.Brush( WindowColor ) )
 
231
        mdc2.SetPen( wx.TRANSPARENT_PEN )
 
232
        mdc2.DrawRectangle( 0, 0, dx, dy )
 
233
        mdc = wx.MemoryDC()
 
234
        mdc.SelectObject( bitmap )
 
235
        mdc2.Blit( 0, 0, dx, dy, mdc, 0, 0, useMask = True )
 
236
        mdc.SelectObject(  wx.NullBitmap )
 
237
        mdc2.SelectObject( wx.NullBitmap )
 
238
 
 
239
        # Finally, analyze the image to find out its characteristics:
 
240
        self._analyze_bitmap()
 
241
 
 
242
    #-- Private Methods --------------------------------------------------------
 
243
 
 
244
    def _analyze_bitmap ( self ):
 
245
        """ Analyzes the bitmap.
 
246
        """
 
247
        # Get the image data:
 
248
        threshold = self.threshold
 
249
        bitmap    = self.opaque_bitmap
 
250
        dx, dy    = self.dx, self.dy
 
251
        image     = bitmap.ConvertToImage()
 
252
 
 
253
        # Convert the bitmap data to a numpy array for analysis:
 
254
        data = reshape( fromstring( image.GetData(), uint8 ), ( dy, dx, 3 ) )
 
255
 
 
256
        # Find the horizontal slices:
 
257
        matches  = []
 
258
        y, last  = 0, dy - 1
 
259
        max_diff = 0.10 * dx
 
260
        while y < last:
 
261
            y_data = data[y]
 
262
            for y2 in xrange( y + 1, dy ):
 
263
                if abs( y_data - data[y2] ).sum() > max_diff:
 
264
                    break
 
265
 
 
266
            n = y2 - y
 
267
            if n >= threshold:
 
268
                matches.append( ( y, n ) )
 
269
 
 
270
            y = y2
 
271
 
 
272
        n = len( matches )
 
273
        if n == 0:
 
274
            if dy > 50:
 
275
                matches = [ ( 0, dy ) ]
 
276
            else:
 
277
                matches = [ ( dy / 2, 1 ) ]
 
278
        elif n > self.stretch_rows:
 
279
            matches.sort( lambda l, r: cmp( r[1], l[1] ) )
 
280
            matches = matches[ : self.stretch_rows ]
 
281
 
 
282
        # Calculate and save the horizontal slice sizes:
 
283
        self.fdy, self.dys = self._calculate_dxy( dy, matches )
 
284
 
 
285
        # Find the vertical slices:
 
286
        matches  = []
 
287
        x, last  = 0, dx - 1
 
288
        max_diff = 0.10 * dy
 
289
        while x < last:
 
290
            x_data = data[:,x]
 
291
            for x2 in xrange( x + 1, dx ):
 
292
                if abs( x_data - data[:,x2] ).sum() > max_diff:
 
293
                    break
 
294
 
 
295
            n = x2 - x
 
296
            if n >= threshold:
 
297
                matches.append( ( x, n ) )
 
298
 
 
299
            x = x2
 
300
 
 
301
        n = len( matches )
 
302
        if n == 0:
 
303
            if dx > 50:
 
304
                matches = [ ( 0, dx ) ]
 
305
            else:
 
306
                matches = [ ( dx / 2, 1 ) ]
 
307
        elif n > self.stretch_columns:
 
308
            matches.sort( lambda l, r: cmp( r[1], l[1] ) )
 
309
            matches = matches[ : self.stretch_columns ]
 
310
 
 
311
        # Calculate and save the vertical slice sizes:
 
312
        self.fdx, self.dxs = self._calculate_dxy( dx, matches )
 
313
 
 
314
        # Save the border size information:
 
315
        self.top    = min( dy / 2, self.dys[0] )
 
316
        self.bottom = min( dy / 2, self.dys[-1] )
 
317
        self.left   = min( dx / 2, self.dxs[0] )
 
318
        self.right  = min( dx / 2, self.dxs[-1] )
 
319
 
 
320
        # Find the optimal size for the borders (i.e. xleft, xright, ... ):
 
321
        self._find_best_borders( data )
 
322
 
 
323
        # Save the background color:
 
324
        x, y          = (dx / 2), (dy / 2)
 
325
        r, g, b       = data[ y, x ]
 
326
        self.bg_color = (0x10000 * r) + (0x100 * g) + b
 
327
 
 
328
        # Find the best contrasting text color (black or white):
 
329
        self.content_color = self._find_best_color( data, x, y )
 
330
 
 
331
        # Find the best contrasting label color:
 
332
        if self.xtop >= self.xbottom:
 
333
            self.label_color = self._find_best_color( data, x, self.xtop / 2 )
 
334
        else:
 
335
            self.label_color = self._find_best_color(
 
336
                                        data, x, dy - (self.xbottom / 2) - 1 )
 
337
 
 
338
    def _fill ( self, idc, ix, iy, idx, idy, dc, x, y, dx, dy ):
 
339
        """ Performs a stretch fill of a region of an image into a region of a
 
340
            window device context.
 
341
        """
 
342
        last_x, last_y = x + dx, y + dy
 
343
        while y < last_y:
 
344
            ddy = min( idy, last_y - y )
 
345
            x0  = x
 
346
            while x0 < last_x:
 
347
                ddx = min( idx, last_x - x0 )
 
348
                dc.Blit( x0, y, ddx, ddy, idc, ix, iy, useMask = True )
 
349
                x0 += ddx
 
350
            y += ddy
 
351
 
 
352
    def _calculate_dxy ( self, d, matches ):
 
353
        """ Calculate the size of all image slices for a specified set of
 
354
            matches.
 
355
        """
 
356
        if len( matches ) == 1:
 
357
            d1, d2 = matches[0]
 
358
 
 
359
            return ( d - d2, [ d1, d2, d - d1 - d2 ] )
 
360
 
 
361
        d1, d2 = matches[0]
 
362
        d3, d4 = matches[1]
 
363
 
 
364
        return ( d - d2 - d4, [ d1, d2, d3 - d1 - d2, d4, d - d3 - d4 ] )
 
365
 
 
366
    def _find_best_borders ( self, data ):
 
367
        """ Find the best set of image slice border sizes (e.g. for images with
 
368
            rounded corners, there should exist a better set of borders than
 
369
            the ones computed by the image slice algorithm.
 
370
        """
 
371
        # Make sure the image size is worth bothering about:
 
372
        dx, dy = self.dx, self.dy
 
373
        if (dx < 5) or (dy < 5):
 
374
            return
 
375
 
 
376
        # Calculate the starting point:
 
377
        left = right  = dx / 2
 
378
        top  = bottom = dy / 2
 
379
 
 
380
        # Calculate the end points:
 
381
        last_y = dy - 1
 
382
        last_x = dx - 1
 
383
 
 
384
        # Mark which edges as 'scanning':
 
385
        t = b = l = r = True
 
386
 
 
387
        # Keep looping while at last one edge is still 'scanning':
 
388
        while l or r or t or b:
 
389
 
 
390
            # Calculate the current core area size:
 
391
            height = bottom - top + 1
 
392
            width  = right - left + 1
 
393
 
 
394
            # Try to extend all edges that are still 'scanning':
 
395
            nl = (l and (left > 0) and
 
396
                  self._is_equal( data, left - 1, top, left, top, 1, height ))
 
397
 
 
398
            nr = (r and (right < last_x) and
 
399
                  self._is_equal( data, right + 1, top, right, top, 1, height ))
 
400
 
 
401
            nt = (t and (top > 0) and
 
402
                 self._is_equal( data, left, top - 1, left, top, width, 1 ))
 
403
 
 
404
            nb = (b and (bottom < last_y) and
 
405
                  self._is_equal( data, left, bottom + 1, left, bottom,
 
406
                                  width, 1 ))
 
407
 
 
408
            # Now check the corners of the edges:
 
409
            tl = ((not nl) or (not nt) or
 
410
                  self._is_equal( data, left - 1, top - 1, left, top, 1, 1 ))
 
411
 
 
412
            tr = ((not nr) or (not nt) or
 
413
                  self._is_equal( data, right + 1, top - 1, right, top, 1, 1 ))
 
414
 
 
415
            bl = ((not nl) or (not nb) or
 
416
                  self._is_equal( data, left - 1, bottom + 1, left, bottom,
 
417
                                  1, 1 ))
 
418
 
 
419
            br = ((not nr) or (not nb) or
 
420
                  self._is_equal( data, right + 1, bottom + 1, right, bottom,
 
421
                                  1, 1 ))
 
422
 
 
423
            # Calculate the new edge 'scanning' values:
 
424
            l = nl and tl and bl
 
425
            r = nr and tr and br
 
426
            t = nt and tl and tr
 
427
            b = nb and bl and br
 
428
 
 
429
            # Adjust the coordinate of an edge if it is still 'scanning':
 
430
            left   -= l
 
431
            right  += r
 
432
            top    -= t
 
433
            bottom += b
 
434
 
 
435
        # Now compute the best set of image border sizes using the current set
 
436
        # and the ones we just calculated:
 
437
        self.xleft   = min( self.left,   left )
 
438
        self.xright  = min( self.right,  dx - right - 1 )
 
439
        self.xtop    = min( self.top,    top )
 
440
        self.xbottom = min( self.bottom, dy - bottom - 1 )
 
441
 
 
442
    def _find_best_color ( self, data, x, y ):
 
443
        """ Find the best contrasting text color for a specified pixel
 
444
            coordinate.
 
445
        """
 
446
        r, g, b = data[ y, x ]
 
447
        h, l, s = rgb_to_hls( r / 255.0, g / 255.0, b / 255.0 )
 
448
        text_color = wx.BLACK
 
449
        if l < 0.50:
 
450
            text_color = wx.WHITE
 
451
 
 
452
        return text_color
 
453
 
 
454
    def _is_equal ( self, data, x0, y0, x1, y1, dx, dy ):
 
455
        """ Determines if two identically sized regions of an image array are
 
456
            'the same' (i.e. within some slight color variance of each other).
 
457
        """
 
458
        return (abs( data[ y0: y0 + dy, x0: x0 + dx ] -
 
459
                     data[ y1: y1 + dy, x1: x1 + dx ] ).sum() < 0.10 * dx * dy)
 
460
 
 
461
 
 
462
#-------------------------------------------------------------------------------
 
463
#  Returns a (possibly cached) ImageSlice:
 
464
#-------------------------------------------------------------------------------
 
465
 
 
466
image_slice_cache = {}
 
467
 
 
468
def image_slice_for ( image ):
 
469
    """ Returns a (possibly cached) ImageSlice.
 
470
    """
 
471
    global image_slice_cache
 
472
 
 
473
    result = image_slice_cache.get( image )
 
474
    if result is None:
 
475
        image_slice_cache[ image ] = result = ImageSlice( image = image )
 
476
 
 
477
    return result