1
#-------------------------------------------------------------------------------
3
# Copyright (c) 2007, Enthought, Inc.
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
11
# Thanks for using Enthought open source!
13
# Author: David C. Morrill
16
#-------------------------------------------------------------------------------
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.
23
#-------------------------------------------------------------------------------
25
#-------------------------------------------------------------------------------
33
import reshape, fromstring, uint8
36
import HasPrivateTraits, Instance, Int, List, Color, Enum, Bool
38
from pyface.image_resource \
44
from constants import is_mac
45
import traitsui.wx.constants
47
#-------------------------------------------------------------------------------
48
# Recursively paint the parent's background if they have an associated image
50
#-------------------------------------------------------------------------------
52
def paint_parent ( dc, window ):
53
""" Recursively paint the parent's background if they have an associated
56
parent = window.GetParent()
57
slice = getattr( parent, '_image_slice', None )
59
x, y = window.GetPositionTuple()
60
dx, dy = parent.GetSizeTuple()
61
slice.fill( dc, -x, -y, dx, dy )
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)))
69
dc.SetBrush( wx.Brush( parent.GetBackgroundColour() ) )
70
dc.SetPen( wx.TRANSPARENT_PEN )
71
dc.DrawRectangle( 0, 0, dx, dy )
75
#-------------------------------------------------------------------------------
77
#-------------------------------------------------------------------------------
79
class ImageSlice ( HasPrivateTraits ):
81
#-- Trait Definitions ------------------------------------------------------
83
# The ImageResource to be sliced and drawn:
84
image = Instance( ImageResource )
86
# The minimum number of adjacent, identical rows/columns needed to identify
87
# a repeatable section:
90
# The maximum number of 'stretchable' rows and columns:
91
stretch_rows = Enum( 1, 2 )
92
stretch_columns = Enum( 1, 2 )
94
# Width/height of the image borders:
100
# Width/height of the extended image borders:
106
# The color to use for content text:
107
content_color = Instance( wx.Colour )
109
# The color to use for label text:
110
label_color = Instance( wx.Colour )
112
# The background color of the image:
115
# Should debugging slice lines be drawn?
116
debug = Bool( False )
118
#-- Private Traits ---------------------------------------------------------
120
# The current image's opaque bitmap:
121
opaque_bitmap = Instance( wx.Bitmap )
123
# The current image's transparent bitmap:
124
transparent_bitmap = Instance( wx.Bitmap )
126
# Size of the current image:
130
# Size of the current image's slices:
134
# Fixed minimum size of current image:
138
#-- Public Methods ---------------------------------------------------------
140
def fill ( self, dc, x, y, dx, dy, transparent = False ):
141
""" 'Stretch fill' the specified region of a device context with the
144
# Create the source image dc:
147
idc.SelectObject( self.transparent_bitmap )
149
idc.SelectObject( self.opaque_bitmap )
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
156
# Calculate vertical slice sizes to use for source and destination:
159
pdxs = [ ( 0, 0 ), ( 1, max( 1, tdx/2 ) ), ( sdx - 2, sdx - 2 ),
160
( 1, max( 1, tdx - (tdx/2) ) ), ( 0, 0 ) ]
162
pdxs = [ ( dxs[0], dxs[0] ), ( dxs[1], max( 0, tdx ) ), ( 0, 0 ),
163
( 0, 0 ), ( dxs[2], dxs[2] ) ]
165
pdxs = [ ( dxs[0], dxs[0] ), ( dxs[1], max( 0, tdx/2 ) ),
166
( dxs[2], dxs[2] ), ( dxs[3], max( 0, tdx - (tdx/2) ) ),
169
# Calculate horizontal slice sizes to use for source and destination:
172
pdys = [ ( 0, 0 ), ( 1, max( 1, tdy/2 ) ), ( sdy - 2, sdy - 2 ),
173
( 1, max( 1, tdy - (tdy/2) ) ), ( 0, 0 ) ]
175
pdys = [ ( dys[0], dys[0] ), ( dys[1], max( 0, tdy ) ), ( 0, 0 ),
176
( 0, 0 ), ( dys[2], dys[2] ) ]
178
pdys = [ ( dys[0], dys[0] ), ( dys[1], max( 0, tdy/2 ) ),
179
( dys[2], dys[2] ), ( dys[3], max( 0, tdy - (tdy/2) ) ),
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
186
for idy, wdy in pdys:
192
for idx, wdx in pdxs:
197
self._fill( idc, ix0, iy0, idx, idy,
198
dc, x0, y0, wdx, wdy )
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 )
213
#-- Event Handlers ---------------------------------------------------------
215
def _image_changed ( self, image ):
216
""" Handles the 'image' trait being changed.
218
# Save the original bitmap as the transparent version:
219
self.transparent_bitmap = bitmap = \
220
image.create_image().ConvertToBitmap()
222
# Save the bitmap size information:
223
self.dx = dx = bitmap.GetWidth()
224
self.dy = dy = bitmap.GetHeight()
226
# Create the opaque version of the bitmap:
227
self.opaque_bitmap = wx.EmptyBitmap( dx, dy )
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 )
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 )
239
# Finally, analyze the image to find out its characteristics:
240
self._analyze_bitmap()
242
#-- Private Methods --------------------------------------------------------
244
def _analyze_bitmap ( self ):
245
""" Analyzes the bitmap.
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()
253
# Convert the bitmap data to a numpy array for analysis:
254
data = reshape( fromstring( image.GetData(), uint8 ), ( dy, dx, 3 ) )
256
# Find the horizontal slices:
262
for y2 in xrange( y + 1, dy ):
263
if abs( y_data - data[y2] ).sum() > max_diff:
268
matches.append( ( y, n ) )
275
matches = [ ( 0, dy ) ]
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 ]
282
# Calculate and save the horizontal slice sizes:
283
self.fdy, self.dys = self._calculate_dxy( dy, matches )
285
# Find the vertical slices:
291
for x2 in xrange( x + 1, dx ):
292
if abs( x_data - data[:,x2] ).sum() > max_diff:
297
matches.append( ( x, n ) )
304
matches = [ ( 0, dx ) ]
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 ]
311
# Calculate and save the vertical slice sizes:
312
self.fdx, self.dxs = self._calculate_dxy( dx, matches )
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] )
320
# Find the optimal size for the borders (i.e. xleft, xright, ... ):
321
self._find_best_borders( data )
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
328
# Find the best contrasting text color (black or white):
329
self.content_color = self._find_best_color( data, x, y )
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 )
335
self.label_color = self._find_best_color(
336
data, x, dy - (self.xbottom / 2) - 1 )
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.
342
last_x, last_y = x + dx, y + dy
344
ddy = min( idy, last_y - y )
347
ddx = min( idx, last_x - x0 )
348
dc.Blit( x0, y, ddx, ddy, idc, ix, iy, useMask = True )
352
def _calculate_dxy ( self, d, matches ):
353
""" Calculate the size of all image slices for a specified set of
356
if len( matches ) == 1:
359
return ( d - d2, [ d1, d2, d - d1 - d2 ] )
364
return ( d - d2 - d4, [ d1, d2, d3 - d1 - d2, d4, d - d3 - d4 ] )
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.
371
# Make sure the image size is worth bothering about:
372
dx, dy = self.dx, self.dy
373
if (dx < 5) or (dy < 5):
376
# Calculate the starting point:
377
left = right = dx / 2
378
top = bottom = dy / 2
380
# Calculate the end points:
384
# Mark which edges as 'scanning':
387
# Keep looping while at last one edge is still 'scanning':
388
while l or r or t or b:
390
# Calculate the current core area size:
391
height = bottom - top + 1
392
width = right - left + 1
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 ))
398
nr = (r and (right < last_x) and
399
self._is_equal( data, right + 1, top, right, top, 1, height ))
401
nt = (t and (top > 0) and
402
self._is_equal( data, left, top - 1, left, top, width, 1 ))
404
nb = (b and (bottom < last_y) and
405
self._is_equal( data, left, bottom + 1, left, bottom,
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 ))
412
tr = ((not nr) or (not nt) or
413
self._is_equal( data, right + 1, top - 1, right, top, 1, 1 ))
415
bl = ((not nl) or (not nb) or
416
self._is_equal( data, left - 1, bottom + 1, left, bottom,
419
br = ((not nr) or (not nb) or
420
self._is_equal( data, right + 1, bottom + 1, right, bottom,
423
# Calculate the new edge 'scanning' values:
429
# Adjust the coordinate of an edge if it is still 'scanning':
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 )
442
def _find_best_color ( self, data, x, y ):
443
""" Find the best contrasting text color for a specified pixel
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
450
text_color = wx.WHITE
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).
458
return (abs( data[ y0: y0 + dy, x0: x0 + dx ] -
459
data[ y1: y1 + dy, x1: x1 + dx ] ).sum() < 0.10 * dx * dy)
462
#-------------------------------------------------------------------------------
463
# Returns a (possibly cached) ImageSlice:
464
#-------------------------------------------------------------------------------
466
image_slice_cache = {}
468
def image_slice_for ( image ):
469
""" Returns a (possibly cached) ImageSlice.
471
global image_slice_cache
473
result = image_slice_cache.get( image )
475
image_slice_cache[ image ] = result = ImageSlice( image = image )