PNGarray is a project I've been working on over the past few days. It's a "gem in the rough" that allows one to create and manipulate arrays of RGBA color values, and encode/save them as 8-bit PNG images with full alpha transparency. Right now the functionality is basic, but it can produce some interesting results.
After programming the basic shell of PNGarray, which uses the "png" gem to encode and save images, I wanted to quickly generate images with it to test its functionality. I first used an array of only 4 x 4 pixels. This allowed me to enter the image by hand, and do a fast check on the program. However, the small grid was limiting in terms of seeing what the program could do, so I wanted to create some other data sets to probe the ability of the tools
Considering that even a 16 x 16 pixel image (the size of a favicon) has 1024 pixels, hand entry was not an option. I realized I would have to find a way to programatically assess the color values of an image file and output those values as a nested array. The png gem that provides PNGarray's low level functionality has the ability to load an extant image as a canvas, but the documentation of how to interact with that data wasn't sufficient to come up with a way to format it for my use. Eventually I will have to address this problem in PNGarray, but for testing I ended up settling on using HTML5's canvas api to generate a pixel array.
First I created a 16 x 16 image in Photoshop:
To get the image's data, I created an HTML skeleton like this:
<html>
<head>
<title></title>
</head>
<body>
<img src="smile.png" id="smile" height="16" width="16"/>
<canvas id="smile-canvas" width="32" height="32">
</canvas>
</body>
</html>
And then ran the following script using Chrome's javascript console
var img = document.getElementById('smile'), //select the image element
can = document.getElementById('smile-canvas'), //select the canvas element
ctx = can.getContext("2d"), //select the canvas element's '2d context'
data = null, //set up empty variable to hold image data
arr = [], //set up empty array to hold pixel values
sl = null, //set up empty variable to hold slice of canvas data array
i = 3, //set up counter variable
ii = 0; //set up index variable
ctx.drawImage(img, 0, 0, 32, 32); //draw the image onto the 2d context, stretching it to 32 x 32
data = ctx.getImageData(0,0,32,32).data; //save the pixel data Uint8ClampedArray (a special type of array discussed below)
for(i; i < data.length; i = i + 4){ //iterate through the pixel data, stopping at every fourth item
sl=data.subarray((i - 3), i + 1); //load the current item, and the previous three items, into a sub array
arr[ii] = sl; //set 'pixel' of final array equal to the sub array
ii++; //increment pixel counter by 1
}
for(i = 0; i < arr.length; i++){ //iterate through the final array, printing out each pixel's value in a copyable format
console.log("[" + arr[i][0] + ", " + arr[i][1] + ", " + arr[i][2] + ", " + arr[i][3] +"],");
}
It took some time to arrive at this solution, after first attempting several solutions using Photoshop scripts and ruby. First I had to decipher the format of the output of the 2d context's getImageData
method. Eventually I learned that it is a one dimensional array in the format [r,g,b,a,r,g,b,a,r,g,b,a...], of the special type Uint8ClampedArray, which only accepts integers between 0 and 255 as element values, and has slightly different methods and attributes than a standard javascript array.
After figuring out how to use the counters to correctly cycle through the arrays values, I found that Chrome's javascript console splits the elements of long arrays up into several sections like [0..999][1000..1023]
, preventing easy copying of the array in a format usable by ruby. The final section of the script cycles through the array, "pretty printing" its contents.
Finally I ended up with the image represented by the array in this file. Using this array, I wrote the following test:
require_relative "../lib/png_from_array.rb"
png = PNGarray.new(32, 32, '~/Dev/array_to_png/test_images/test_1.png', img)
png.prime_canvas
png.save
png2 = PNGarray.new(32, 32, '~/Dev/array_to_png/test_images/test_2.png', img.shuffle)
png2.prime_canvas
png2.save
png3 = PNGarray.new(16, 64, '~/Dev/array_to_png/test_images/test_3.png', img.reverse)
png3.prime_canvas
png3.save
They're not the most astonishing image manipulations of all time but I think they're a solid first step. In working on this library I've learned an incredible ammount about image formats, png encoding, array manipulation, and even a little vector math. I will continue working on PNGarray until it becomes the tool I want it to be. Perhaps I will be able to incorporate a web interface for generating pixel arrays based on the javascript above.
When I started out, I thought I was going to be able to write my own png decoder/encoder from scratch but quickly realized that this was a far greater task than I had the time for. Besides, why start from scratch when someone else has given you scaffold? Don't let your hubris outweigh your laziness or your impatience, to paraphrase Larry Wall.