Generate a Letter Collage With PHP
by Jarrod posted 3 years agoToday I saw a picture my cousin, clever little cookie, had generated of letters randomly placed and tightly grouped, but not overlapping, to form an image which produced a pleasing result - curious to how he did, I thought I'd have a go.
Before we begin, we need to establish our rules for the collage we want to create. These being:
- Letters must be randomly placed
- Letters must not overlap - but can be placed one within the other.
Pretty simple. Let's take a look.
And how are we going to attempt this?
We'll, obviously, use PHP. We'll also make use of the GD library. And you'll need a TTF font (I got you covered on this).
That's pretty much it.
Step 1
We'll take an object oriented approach - as that's how I roll - so let's begin by creating two files:
- process.php
- collage_class.php
process.php
Here is the complete code for the process.php file. It's not much; it's basially "running" the code.
set_time_limit(3600);
require_once('collage_class.php');
$imageLibObj = new imageCollage('300', '300');
- line 1: Increases the time PHP has to run - you'll find out why we need this.
- line 2: Loads in the class file.
- line 3: Creates the object, passing in the size (width, height) of our image to generate.
collage_class.php
Let's lay down the ground work. This is our class skeleton. You can see I've added the $canvasWidth and $canvaseHeight parameters in the construct. And I've also gone ahead and added some setting variables that I'll explain later.
class imageCollage
{
private $_iterations = 1000;
private $_fontSizeMax = 35;
private $_fontSizeMin = 3;
private $_font = 'Qarmic_sans_Abridged.ttf';
private $_bgColorArray = array('r'=>0, 'g'=>0, 'b'=>0);
private $_charColor = array('r'=>255, 'g'=>255, 'b'=>255);
public function __construct($canvasWidth, $canvaseHeight)
{
}
}
Some prep
Working within the construct method, I'll go through chunks of code a time and explain.
putenv('GDFONTPATH=' . realpath('.'));
$charArray = range('A', 'Z');
- Line 1: This is needed for loading the font.
- Line 2: Define our character set. The range function will create an array of characters from A to Z for us.
Create the canvas
The next few lines will create our canvas. Starting with creating the background, we use the width and height we passed in to set the canvas size:
$im = @imagecreatetruecolor($canvasWidth, $canvaseHeight);
$backgroundColor = imagecolorallocate($im, $this->_bgColorArray['r'], $this->_bgColorArray['g'], $this->_bgColorArray['b']);
imagefilledrectangle($im, 0, 0, $canvasWidth, $canvasHeight, $backgroundColor);
- Line 1: Create the canvas with our defined width and height.
- Line 2: Create a color for our canvas background. The array variable "$this->_bgColorArray" has our color for each channel. In this case, it's black.
- Line 3: Apply the color to our canvas.
The loop
The next section of code is the loop. Also know as "The Loop". The loop is responsible for providing us with glorious iterations. Each iteration will attempt to place a letter on our canvas.
That's right, I said attempt. I never said this was scientific. In fact this is a pretty crude method - but hey, if you get the results then why not.
It's probably a good time to explain the process we're going to undertake.
- We're going to loop, say a thousand times.
- On each iteration we're going to take a random co-ordinate and check if there is an image.
- If so, move on. If not, place our image.
I have multiple version of this loop, each vastly different from each other, each controlling different settings such as when to decrease the font, changing the font color as the font gets smaller (so it look like it's in the distance - poetic, I know), different limits, etc. Some use while loops, some use for loops. This really is where you determine the "look" of your final image. Feel free to add your own flare here.
The loop I have included here is a budget one (read: less complex). But it's also not my most budget one (read: still produces a reasonable result).
$step = (100 / ($this->_fontSizeMax - $this->_fontSizeMin));
$step = $step/2.5;
$stepCounter = 0;
while($this->_fontSizeMax >= $this->_fontSizeMin) {
$this->_fontSizeMax--;
$falseCount = 0;
while ($falseCount < $this->_iterations) {
$result = $this->process($im, $charArray, $canvasWidth, $canvasHeight);
if (!$result) {
$falseCount++;
}
}
$stepCounter += $step;
}
This version of "The Loop" uses nested while loops. The outer loop keeps looping while the font is above a certain size, while the inner loop loops x number of times.
Remember some of those setting we set at the beginning. The ones beginning with public? Changing them will affect here.
The method, $this->process(), is where the actual work is done and returns true if we successfully placed a letter, otherwise false.
Save our image
Let's save our image to our folder. Make sure the path is writable! (We are actually calling a method here to save. I'll give you the code for that further down the page.)
$this->saveImage('collage.jpg', $im);
The real work
We've now finished with adding code to our construct. As I said before, this is more code than I'd like to have jammed in the construct method - but it is easier to follow.
We now need to create the aforementioned "process" method. This is where the fun part of the code begins. This is where we determine whether a letter should be placed or not.
Create the below method. Pass in the image resource, $im, our character array, $charArray, and the width and height.
private function process($im, $charArray, $canvasWidth, $canvasHeight)
{
}
Within the process method add the following code:
$charKey = array_rand($charArray);
$char = $charArray[$charKey];
- Line 1: Get the array key of a random character.
- Line 2: Return the value of that random character as $char.
In order to determine if our character will overlap another, we need to know the width and height of the character. This will vary depending on font size and font type.
I've created the method getTextSize to take care of this. You just need to pass in some information - like font size and font type. And of course the letter!
$textSizeArray = $this->getTextSize($this->_fontSizeMax, 0, $this->_font, $char);
$textWidth = $textSizeArray['width'];
$textHeight = $textSizeArray['height'];
Like I described somewhere above, we now need to select a random location on our canvas to test if it's an ok spot to place our letter. PHP rand function will do the trick. We simply pass in the min value, and the max value.
This satisfies our first rule: Letters must be randomly placed.
$x is from 0 (being the top-left corner), to the max width canvas, less the character length. We subtract the width of the letter to prevent the letter half appearing onthe canvas, half off. It's just a cosmetic thing. $y is the same but for the height.
$x = rand(0, $canvasWidth - $textWidth);
$y = rand(0, $canvasHeight - $textHeight);
Now that we have a location to test, let's test it!
Like all good magic tricks, the trick is ruined once you know how it's done. It's just never the same. This is probably no different - and I'm about to reveal the secret.
Starting at our random location we chop out a piece of our canvas the size of our letter. We save this to a temporary canvas.
$imTmp = imagecreatetruecolor($textWidth , $textHeight);
imagecopyresampled($imTmp, $im , 0, 0, $x, $y, $textWidth, $textHeight , $textWidth, $textHeight);
And how do we determine if an image is overlapping another? We could do some fancy collision detection? Na, we'll simply check if the piece of canvas we cut out has any other color on it, other than the background color.
Let me explain. The background color is black in this example. We've taken a piece of our canvas the same size as the letter we're going to place, and am now testing if it's got any other letter on it by checking if any part of the image is NOT black.
$result = $this->isColorVacant($imTmp, $this->_bgColorArray);
The method isColorVacant() is a method I've created (got from somewhere) that simply loops through every pixel on the cut out piece of canvas and checks it has the same color as our defined background color.
If so, we add the character. This is another custom method. We'll get to that.
if ($result) {
$this->addChar($im, $char, $x, $y, 0, $this->_charColor, $this->_fontSizeMax, 0, $this->_font);
}
To allow us to do some cooler stuff in our "The Loop", we return wheather the letter was successfully placed or not.
return $result;
The rest
That's the meat of it all. If you followed along you'd noticed I threw in the odd method call here and there without giving you the code. Well, this is where I do that.
I didn't explain these methods - but that's because this article is long enough and they're just the copy and paste material.
With that said, copy and paste these methods into your class to complete the code.
isColorVacant:
Checks if a color is present in the canvas.
private function isColorVacant($imTmp, $colorToTestArray)
{
$r1 = $colorToTestArray['r'];
$g1 = $colorToTestArray['g'];
$b1 = $colorToTestArray['b'];
for ($y=0; $y < imagesyx($imTmp); $y++) {
for ($x=0; $x < imagesx($imTmp); $x++) {
$rgb = ImageColorAt($imTmp, $x, $y);
$r = ($rgb >> 16) & 0xFF;
$g = ($rgb >> 8) & 0xFF;
$b = $rgb & 0xFF;
if (($r != $r1) && ($g != $g1) && ($b != $b1)) {
return false;
}
}
}
return true;
}
addChar:
Adds the character to the canvas.
public function addChar($im, $text, $x, $y, $padding = 0, $fontColor='#fff', $fontSize = 12, $angle = 0, $font = null)
{
// *** Split out the colors from the array
$r = $fontColor['r'];
$g = $fontColor['g'];
$b = $fontColor['b'];
// *** Get text size
$textSizeArray = $this->getTextSize($fontSize, $angle, $font, $text);
$textWidth = $textSizeArray['width'];
$textHeight = $textSizeArray['height'];
$y = $y + $textHeight; #invery
// *** Set text color
$fontColor = imagecolorallocate($im, $r, $g, $b);
// *** Add text
imagettftext($im, $fontSize, $angle, $x, $y, $fontColor, $font, $text);
}
getTextSize:
Gets the height and witdh of our character.
private function getTextSize($fontSize, $angle, $font, $text)
{
// *** Define box (so we can get the width)
$box = @imageTTFBbox($fontSize, $angle, $font, $text);
// *** Get width of text from dimensions
$textWidth = abs($box[4] - $box[0]);
// *** Get height of text from dimensions (should also be same as $fontSize)
$textHeight = abs($box[5] - $box[1]);
return array('height' => $textHeight, 'width' => $textWidth);
}
saveImage:
Save the canvas to file.
public function saveImage($savePath, $im, $imageQuality="100")
{
imagejpeg($im, $savePath, $imageQuality);
}
There are no comments. Would you like to be the first?