Skip to content

Instantly share code, notes, and snippets.

@ErikPeterson
Last active August 29, 2015 13:56
Show Gist options
  • Save ErikPeterson/9064042 to your computer and use it in GitHub Desktop.
Save ErikPeterson/9064042 to your computer and use it in GitHub Desktop.
PNGarray

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: https://github.com/ErikPeterson/github-images/blob/master/smile.png

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

Which produced these images: https://github.com/ErikPeterson/github-images/blob/master/test_1.png https://github.com/ErikPeterson/github-images/blob/master/test_2.png https://github.com/ErikPeterson/github-images/blob/master/test_3.png

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment