A new day, a new riddle to solve! Today’s task involves two images: one background image (in any size) and one smaller puzzle piece (provided as a PNG with an alpha channel). The goal? To place the puzzle piece randomly on the background. Simple, right? But wait — here’s where the fun begins. You need to generate two new images:
- The background image where the puzzle piece has been "cut out" or darkened.
- The puzzle piece, now containing the corresponding part of the background image it covered.
Sounds easy enough, right? Let’s dive in!
Now, for those familiar with ImageMagick, this won't be a major challenge. But since PHP was the tool of choice for this particular project (yes, PHP, the nostalgic knight of the web), I decided to add a fallback using GDImage. Yes, GD. The "I can barely keep up with PNG transparency" library.
Let's have some fun with this.
1. The Easy ImageMagick (Imagick) Way
Let’s kick things off with ImageMagick. If you know Imagick, you know it’s a dream. Just a few lines of code, and you’re done. That’s how it feels, anyway. This function accepts four arguments: the background and puzzle piece image files, as well as the desired output width and height.
function imagickPuzzle(
string $imageFile,
string $puzzleFile,
int $outputWidth,
int $outputHeight
): array {
list($imageWidth, $imageHeight) = getimagesize($imageFile);
list($puzzleWidth, $puzzleHeight) = getimagesize($puzzleFile);
// Get random position
$positionX = rand(20, $outputWidth - $puzzleWidth - 20);
$positionY = rand(20, $outputHeight - $puzzleHeight - 20);
// Create main image
$image = new \Imagick($imageFile);
$image->setImageFormat('png');
// Downsize main image to output size
$imageRatio = $imageWidth / $imageHeight;
$fixedImageHeight = intval(floor($outputWidth / $imageRatio));
$image->resizeImage(
$outputWidth,
$fixedImageHeight,
\Imagick::FILTER_POINT,
0
);
$image->cropImage($outputWidth, $outputHeight, 0, 0);
// Create and composite puzzle image
$puzzle = new \Imagick($puzzleFile);
$puzzle->compositeImage(
$image,
\Imagick::COMPOSITE_ATOP,
-$positionX,
-$positionY
);
// Extract and composite puzzle from main image
$extractPuzzle = new \Imagick($puzzleFile);
$extractPuzzle->transparentPaintImage('black', 0.8, 100, false);
$drawer = new \ImagickDraw();
$drawer->composite(
\Imagick::COMPOSITE_DARKEN,
$positionX,
$positionY,
$puzzleWidth,
$puzzleHeight,
$extractPuzzle
);
$image->drawImage($drawer);
// Return Blobs
return [
'image' => $image->getImageBlob(),
'puzzle' => $puzzle->getImageBlob()
];
}
How this Works
1. Dimensions - First, we get the dimensions of the background and puzzle images. Nothing fancy here — just like measuring how big your coffee mug is before programming.
list($imageWidth, $imageHeight) = getimagesize($imageFile);
list($puzzleWidth, $puzzleHeight) = getimagesize($puzzleFile);
2. Random Position - We then randomly calculate the X and Y positions for the puzzle piece. We make sure it doesn't go too close to the edges — don’t want it looking like it’s trying to escape!
$positionX = rand(20, $outputWidth - $puzzleWidth - 20);
$positionY = rand(20, $outputHeight - $puzzleHeight - 20);
3. Create the Background - We load the background image, set the format to PNG (because we’re good like that), resize it, and crop it to fit the output dimensions. It’s all about getting that perfect crop without cutting off anything important (like your favorite coffee mug in the background).
$image = new \Imagick($imageFile);
$image->setImageFormat('png');
$imageRatio = $imageWidth / $imageHeight;
$fixedImageHeight = intval(floor($outputWidth / $imageRatio));
$image->resizeImage(
$outputWidth,
$fixedImageHeight,
\Imagick::FILTER_POINT,
0
);
$image->cropImage($outputWidth, $outputHeight, 0, 0);
4. Place the Puzzle Piece - We composite the puzzle piece onto the image at the calculated position. Think of this as putting the last piece in the jigsaw puzzle. Victory is close!
$puzzle = new \Imagick($puzzleFile);
$puzzle->compositeImage(
$image,
\Imagick::COMPOSITE_ATOP,
-$positionX,
-$positionY
);
5. Cut Out the Puzzle Piece - We make the background image look like the puzzle piece has been "cut out". This is where the magic happens. It's like opening your favorite IDE and finally seeing your code actually work. Feels good.
$extractPuzzle = new \Imagick($puzzleFile);
$extractPuzzle->transparentPaintImage('black', 0.8, 100, false);
$drawer = new \ImagickDraw();
$drawer->composite(
\Imagick::COMPOSITE_DARKEN,
$positionX,
$positionY,
$puzzleWidth,
$puzzleHeight,
$extractPuzzle
);
$image->drawImage($drawer);
6. Return Blobs - We wrap everything up nicely by returning the image blobs. Work done.
return [
'image' => $image->getImageBlob(),
'puzzle' => $puzzle->getImageBlob()
];
Examples
2. The Not-So-Easy GDImage (gd) Way
Now, let’s get a bit adventurous! We already know GDImage isn’t the first choice for handling PNG transparency (it’s more like the old-school library you use when your teacher forces you to do so), but let’s give it a shot.
Here's the code. Just buckle up — this one gets messy, and we’re about to dive deep into the pixel-level madness. Warning: 8 hours of coding may ensue.
function gdPuzzle(
string $imageFile,
string $puzzleFile,
int $outputWidth,
int $outputHeight
): array {
list($imageWidth, $imageHeight) = getimagesize($imageFile);
list($puzzleWidth, $puzzleHeight) = getimagesize($puzzleFile);
// Get random position
$positionX = rand(20, $outputWidth - $puzzleWidth - 20);
$positionY = rand(20, $outputHeight - $puzzleHeight - 20);
// Create main image
$image = imagecreatetruecolor($outputWidth, $outputHeight);
imagefilledrectangle(
$image,
0,
0,
$outputWidth,
$outputHeight,
imagecolorallocate($image, 255, 255, 255)
);
// Downsize main image to output size
$imageRatio = $imageWidth / $imageHeight;
$fixedImageHeight = intval(floor($outputWidth / $imageRatio));
$temp = imagecreatefrompng($imageFile);
$cropped = imagecreatetruecolor($outputWidth, $outputHeight);
imagecopyresampled(
$cropped,
$temp,
0, 0, 0, 0,
$outputWidth,
$fixedImageHeight,
$imageWidth,
$imageHeight
);
imagecrop(
$cropped,
[
'x' => 0,
'y' => 0,
'width' => $outputWidth,
'height' => $outputHeight
]
);
imagecopymerge(
$image,
$cropped,
0, 0, 0, 0,
$outputWidth,
$outputHeight,
100
);
// Exclude Piece
$tempPiece = imagecreatefrompng($puzzleFile);
imagealphablending($tempPiece, false);
imagesavealpha($tempPiece, true);
imagecopyresampled(
$image,
$tempPiece,
$positionX,
$positionY,
0, 0,
$puzzleWidth,
$puzzleHeight,
$puzzleWidth,
$puzzleHeight
);
// Piece Image
$puzzle = imagecreatetruecolor($puzzleWidth, $puzzleHeight);
imagealphablending($puzzle, false);
imagesavealpha($puzzle, true);
imagecopyresampled(
$puzzle,
$cropped,
0, 0,
$positionX,
$positionY,
$puzzleWidth,
$puzzleHeight,
$puzzleWidth,
$puzzleHeight
);
// Copy alpha channels from puzzle piece to cropped source image
for ($i = 0; $i < $puzzleWidth; $i++) {
for ($j = 0; $j < $puzzleHeight; $j++) {
$alpha = imagecolorsforindex(
$tempPiece,
imagecolorat($tempPiece, $i, $j)
)['alpha'];
$original = array_values(
imagecolorsforindex(
$puzzle,
imagecolorat($puzzle, $i, $j)
)
);
$edited = imagecolorallocatealpha(
$puzzle,
$original[0],
$original[1],
$original[2],
$alpha ?? 0
);
imagesetpixel($puzzle, $i, $j, $edited);
}
}
// Get BLOBs
ob_start();
imagepng($image, null);
$imageBlob = ob_get_clean();
ob_start();
imagepng($puzzle, null);
$puzzleBlob = ob_get_clean();
// Destroy Images & Return
imagedestroy($image);
imagedestroy($temp);
imagedestroy($cropped);
imagedestroy($puzzle);
imagedestroy($tempPiece);
return [
'image' => $imageBlob,
'puzzle' => $puzzleBlob
];
}
How this works (probably)
Yes, this may look like a mess, and trust me, I’m 100% sure there's a cleaner way to do it — probably involving magic, and possibly a wizard named Refactorio. But, you know what? It works! And sometimes, that’s all we can hope for. Let's break it down, shall we? (Note: the first and second parts are identical to the ImageMagick solution, so we’ll just pick up from 3.)
3. Main Image Creation (with a Side of Rectangle Love) - First, we create the main image at the desired output dimensions and draw a full-sized rectangle (because who doesn’t love rectangles? I mean, they are the building blocks of the universe, right?).
$image = imagecreatetruecolor($outputWidth, $outputHeight);
imagefilledrectangle(
$image,
0,
0,
$outputWidth,
$outputHeight,
imagecolorallocate($image, 255, 255, 255)
);
4. Resizing the Background Image (Because We’re Not Monsters) - Next, we downsize the main background image, just like in the ImageMagick solution... except this time we do it the good old GD way. Is there a better way to do this? Probably, but who has time for that? Let’s embrace the madness and make it work!
$imageRatio = $imageWidth / $imageHeight;
$fixedImageHeight = intval(floor($outputWidth / $imageRatio));
$temp = imagecreatefrompng($imageFile);
$cropped = imagecreatetruecolor($outputWidth, $outputHeight);
imagecopyresampled(
$cropped,
$temp,
0, 0, 0, 0,
$outputWidth,
$fixedImageHeight,
$imageWidth,
$imageHeight
);
imagecrop(
$cropped,
[
'x' => 0,
'y' => 0,
'width' => $outputWidth,
'height' => $outputHeight
]
);
imagecopymerge(
$image,
$cropped,
0, 0, 0, 0,
$outputWidth,
$outputHeight,
100
);
5. "Cutting Out" the Puzzle Piece (No, Not with Scissors) - Let’s take the puzzle piece out of the image like a magician pulling a rabbit out of a hat — if that hat were made of pixelated backgrounds and slightly questionable logic. If you have questions about transparency... don't worry. I do too. It's a GD thing. Someday I’ll crack it.
$tempPiece = imagecreatefrompng($puzzleFile);
imagealphablending($tempPiece, false);
imagesavealpha($tempPiece, true);
imagecopyresampled(
$image,
$tempPiece,
$positionX,
$positionY,
0, 0,
$puzzleWidth,
$puzzleHeight,
$puzzleWidth,
$puzzleHeight
);
6. The Alpha Channel Strikes Back (A Pixel by Pixel Journey) - Here’s where things get messy. GD doesn’t exactly love keeping alpha channels alive. So, I went full DIY mode. You know, "If the library can’t do it, just do it manually" — classic solution to all your problems. No worries about performance here! Memory? What memory?
$puzzle = imagecreatetruecolor($puzzleWidth, $puzzleHeight);
imagealphablending($puzzle, false);
imagesavealpha($puzzle, true);
imagecopyresampled(
$puzzle,
$cropped,
0, 0,
$positionX,
$positionY,
$puzzleWidth,
$puzzleHeight,
$puzzleWidth,
$puzzleHeight
);
// Copy alpha channels from puzzle piece to cropped source image
for ($i = 0; $i < $puzzleWidth; $i++) {
for ($j = 0; $j < $puzzleHeight; $j++) {
$alpha = imagecolorsforindex(
$tempPiece,
imagecolorat($tempPiece, $i, $j)
)['alpha'];
$original = array_values(
imagecolorsforindex(
$puzzle,
imagecolorat($puzzle, $i, $j)
)
);
$edited = imagecolorallocatealpha(
$puzzle,
$original[0],
$original[1],
$original[2],
$alpha ?? 0
);
imagesetpixel($puzzle, $i, $j, $edited);
}
}
At this point, I’ve manually processed every pixel like a true hero of the digital realm. I can almost hear the "pixel by pixel" music playing in the background. It’s like the ending of The Shawshank Redemption, but with more coffee and less hope.
7. Blobbing and Clean-Up (The Real Hero of the Story) - Finally, we start cleaning up. No, we’re not cleaning the office... just the images. But cleaning up memory in GD-PHP is like cleaning your room: you have to throw away all the random junk you don’t need anymore. In this case, it’s your images.
ob_start();
imagepng($image, null);
$imageBlob = ob_get_clean();
ob_start();
imagepng($puzzle, null);
$puzzleBlob = ob_get_clean();
imagedestroy($image);
imagedestroy($temp);
imagedestroy($cropped);
imagedestroy($puzzle);
imagedestroy($tempPiece);
return [
'image' => $imageBlob,
'puzzle' => $puzzleBlob
];
All done. The blobs are returned, memory is freed (sort of), and you can pat yourself on the back for completing yet another “simple” task that turned into an odyssey. You've now written some GD code that sort of works, like a slightly messy masterpiece!
Examples
Job done. Notice the difference? GDImage has a more neatless cutout , while you see a small border around the puzzle piece on ImageMagick. But hey, this isn’t a design critique!
While ImageMagick is the undisputed heavyweight champion here, GD has its own quirky charm. If you know a better way to handle this, please let me know — I’m always up for a good challenge!
Top comments (0)