IronPython: Stealing Colors with System.Drawing.Bitmap (and yes Visio)
Monday, April 5, 2010 at 12:49AM I collect color sets for a hobby. Every time I see a page like this one about the traditional colors of Japan or this one about the of Crayola crayon colors I can’t help but capture them in a drawing so that I might be inspired later to use them in my own diagrams.
In this post, I’m going to show you how to use IronPython to help automate this task. Along the way we’ll learn a little about working with the System.Drawing namespace and of course I’ll do mix in some Visio interop.
FIRST THE COLORS
I found an interesting set of colors here: http://corte.si/posts/code/sortvis-fruitsalad/index.html
First step is to just save the image
I saved it as “D:\swatch.png”
Let’s start IronPython 2.6 with my favorite command-line options
"c:\Program Files (x86)\IronPython 2.6\ipy.exe" -X:TabCompletion -X:ColorfulConsole
Now import clr and System and add a reference to System.Drawing and then import System.Drawing
Now we are going to load the bitmap and fetch each pixel on the top row. Then we examine how many pixels we have (we should have 512 since that’s the width of the bitmap) and we’ll print out the contents of the first pixel
bmp = System.Drawing.Bitmap( "D:\\swatch.png" ) pixels = [ bmp.GetPixel(x,0) for x in xrange( bmp.Width ) ] print len(pixels) print pixels[0]
Now we’re going to do the Visio thing – we’ll launch it and get a reference to the active page
clr.AddReference("Microsoft.Office.Interop.Visio")
import Microsoft.Office.Interop.Visio
IVisio = Microsoft.Office.Interop.Visio
visapp = IVisio.ApplicationClass()
doc = visapp.Documents.Add("")
page = visapp.ActivePage
running those commands caused Visio to appear with a blank document …
Before we do the drawing let’s consider that we have 512 pixels
We could just draw a long line of squares, but that would make for an inconvenient drawing. We’d like something a bit more compact – place all the squares into a roughly rectangular area.
Thinking about this, I’ll approach the problem as one of mapping a series of numbers 0 to 511 to (x,y) coordinates
Now that means I have to figure out how many shapes I need on the edges … calling System.Math.Sqrt shows that I’ll need around 23 shapes for each side
So now I map the coordinates like this …
coords = [ (i%23,i/23) for i in xrange( len(pixels) ) ]
then I calculate the rectangle coordinates for all the shapes based on those coordinates
length = 0.25 rects = [ ( x*length, y*length, (x+1)*length, (y+1)*length ) for x,y in coords ]
then I draw the shapes
shapes = [ page.DrawRectangle( *rect ) for rect in rects ]
NOTE: there are better ways of drawing so many shapes (for example using the DropMany method) but to keep the example simple I’ll do it the simple way
What it rendered is this …
page.ResizeToFitContents()
Okay, that’s a bit easier to see.
So, now we get to the colors. We have a list of pixels. We have a list of shapes. what we want to do is set the FillForegnd cell of each shape to the appropiate rgb formula
for a single shape it would look like this…
shape1.Cells("FillForegnd").Formula = “=rgb(255,128,0)”
ok, so we calculate the formulas like this
formulas = [ "=RGB({0},{1},{2})".format(p.R, p.G, p.B) for p in pixels ]
and then we pair each formula with a shape and set the cell accordingly
for s,f in zip(shapes,formulas) : s.Cells("FillForegnd").Formula = f
And this yields…
And here’s the full script
import clr
import System
clr.AddReference("System.Drawing")
import System.Drawing
# Get the colors from the bitmap
bmp = System.Drawing.Bitmap( "D:\\swatch.png" )
pixels = [ bmp.GetPixel(x,0) for x in xrange( bmp.Width ) ]
# Start Visio
clr.AddReference("Microsoft.Office.Interop.Visio")
import Microsoft.Office.Interop.Visio
IVisio = Microsoft.Office.Interop.Visio
visapp = IVisio.ApplicationClass()
doc = visapp.Documents.Add("")
page = visapp.ActivePage
# Draw the shapes
coords = [ (i%23,i/23) for i in xrange( len(pixels) ) ]
length = 0.25
rects = [ ( x*length, y*length, (x+1)*length, (y+1)*length ) for x,y in coords ]
shapes = [ page.DrawRectangle( *rect ) for rect in rects ]
#Adjust the page
page.ResizeToFitContents()
# Set the colors
formulas = [ "=RGB({0},{1},{2})".format(p.R, p.G, p.B) for p in pixels ]
for s,f in zip(shapes,formulas) : s.Cells("FillForegnd").Formula = f
The full script running looks like this…
PARTING THOUGHTS
- I hope you noticed instead of a lot of for loops (there’s only one at the end) – I used a functional approach and mapped the input (the pixels) to the output (the shapes and colors) using Python’s list comprehension syntax.
- For the Pythonistas – yes I could have used generators instead of all those lists, but I thought it would be an easier example
- you can get the final visio diagram here: http://cid-19ec39cb500669d8.skydrive.live.com/self.aspx/Public/Blog%20Posts/2010/Hilbert-Colors-^52010-04-05^6/HilbertColors-^52010-04-05^6.vsd