Skip to content

Instantly share code, notes, and snippets.

@sbrl
Last active July 29, 2018 22:46
Show Gist options
  • Save sbrl/c8b6f91ec00c65589bb41660085140a3 to your computer and use it in GitHub Desktop.
Save sbrl/c8b6f91ec00c65589bb41660085140a3 to your computer and use it in GitHub Desktop.
A lightweight placeholder image generator that's pending a tidy up. Only requires a font in it's current directory. #php
<?php
/*
* Starbeamrainbowlabs' Placeholder Image Generation Service script
*
* Licensed under the Mozilla Public License 2.0
* A copy of this license can be found here: https://www.mozilla.org/en-US/MPL/2.0/
*/
$exec_start = microtime(true);
if(isset($_GET["help"]))
{
header("content-type: text/plain");
exit("Starbeamrainbowlabs' Placeholder Service Help
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Welcome! You seem to have discovered my personal image placeholder service.
Please do not use this for production purposes - though you may use it
during development. Below is an explanation of the different GET paramters
and what they do.
Parameter Default Value Explanation
-------------------------------------------------------------------------------
width 640 The width of the image.
height 480 The height of the image.
angle 0 The angle of the text in degrees.
bg_colour aaaaaa The 6 digit hex code of the background colour
without the preceding hash. Note that 3 digit hex
codes will not work.
fg_colour 777777 The text colour in the same format as described
above.
text (undefined) The text to use for the placeholder image. Omitting
causes the image's dimensions to be used instead.
weight normal The font weight to use. Other supported values:
bold.
quality 95 The quality to use when rendering the resulting
jpeg.
hash anything Set to anything you like to cause the service to
generate a different etag for caching. Also useful
for forcing the browser (or other application) to
download a separate placeholder image if you want
multiple random ones.
Easter Eggs:
fg_colour and bg_colour support special values of 'random' and 'inverse'.
Links:
https://starbeamrainbowlabs.com/
Email: feedback@starbeamrainbowlabs.com
Twitter: @SBRLabs
Reddit: /u/Starbeamrainbowlabs
If you want a copy of this script, just ask! I can be contacted through any of
the above (or in person if you know me IRL).
It's licensed under the Mozilla Public License 2.0 (MPL-2.0) - see that license here: https://www.mozilla.org/en-US/MPL/2.0/
--Starbeamrainbowlabs
");
}
// Settings
/// Admin Settings ///
// The maximum size allowed for the returned image
define("MAX_SIZE", 5000);
// Whether we should utilise etags to aid caching
define("ENABLE_CACHING", true);
// The length of time that caches should cache the placeholder for
define("CACHE_LENGTH", 60 * 60 * 24 * 7); // 7 days
// Whether to enable debug drawing for the text drawing angle.
define('DEBUG_ANGLE', false);
/// Settings from GET ///
$settings = new stdClass();
$settings->width = intval($_GET["width"] ?? 640);
$settings->height = intval($_GET["height"] ?? 480);
$settings->angle = intval($_GET["angle"] ?? 0);
$settings->bg_colour = trim($_GET["bg_colour"] ?? "aaaaaa");
$settings->fg_colour = trim($_GET["fg_colour"] ?? "777777");
$settings->text = $_GET["text"] ?? "text";
$settings->weight = trim($_GET["weight"] ?? "normal");
$settings->quality = intval($_GET["quality"] ?? 95);
$settings->hash = $_GET["hash"] ?? "";
/// Polyfills ///
// From https://github.com/ralouphie/getallheaders
// PHP-FPM doesn't have getallheaders() - but it looks likeit'll be fixed in 7.3.
if (!function_exists('getallheaders')) {
/**
* Get all HTTP header key/values as an associative array for the current request.
*
* @return string[string] The HTTP header key/value pairs.
*/
function getallheaders()
{
$headers = [];
$copy_server = [
'CONTENT_TYPE' => 'Content-Type',
'CONTENT_LENGTH' => 'Content-Length',
'CONTENT_MD5' => 'Content-Md5',
];
foreach ($_SERVER as $key => $value) {
if (substr($key, 0, 5) === 'HTTP_') {
$key = substr($key, 5);
if (!isset($copy_server[$key]) || !isset($_SERVER[$key])) {
$key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key))));
$headers[$key] = $value;
}
} elseif (isset($copy_server[$key])) {
$headers[$copy_server[$key]] = $value;
}
}
if (!isset($headers['Authorization'])) {
if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) {
$headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION'];
} elseif (isset($_SERVER['PHP_AUTH_USER'])) {
$basic_pass = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : '';
$headers['Authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $basic_pass);
} elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) {
$headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST'];
}
}
return $headers;
}
}
/**
* Draws a cross on the image.
* @param image $image The image to draw on.
* @param int $x The x co-ordinate
* @param int $y The y co-ordinate
* @param int $size The radius of the cross.
* @param colour $colour The colour to draw it in. Should be from imagecolorallocate().
*/
function imagecross(&$image, $x, $y, $size, $colour) {
imageline($image, $x - $size, $y - $size, $x + $size, $y + $size, $colour);
imageline($image, $x + $size, $y - $size, $x - $size, $y + $size, $colour);
}
/**
* Put center-rotated ttf-text into image
* Has the same signature as imagettftext().
* @source https://secure.php.net/manual/it/function.imagettftext.php#48938
*/
function imagettftext_center_rotate(&$im, $size, $angle, $x, $y, $color, $fontfile, $text, $debug = false)
{
while($angle >= 360) $angle -= 360;
while($angle < 0) $angle += 360;
if($debug) {
$debug_col_a = imagecolorallocate($im, 0, 255, 0);
$debug_col_b = imagecolorallocate($im, 0, 0, 255);
$debug_col_c = imagecolorallocate($im, 255, 0, 0);
$debug_col_d = imagecolorallocate($im, 255, 255, 0);
imagettftext($im, $size, $angle, $x, $y, $debug_col_b, $fontfile, $text);
imagettftext($im, $size, 0, $x, $y, $debug_col_a, $fontfile, $text);
}
// retrieve boundingbox of the non rotated text
$bbox = imagettfbbox($size, 0, $fontfile, $text);
$orig_centre = [
($bbox[0] + $bbox[2] + $bbox[4] + $bbox[6]) / 4,
($bbox[1] + $bbox[3] + $bbox[5] + $bbox[7]) / 4
];
$text_width = $bbox[2] - $bbox[0];
$text_height = $bbox[3] - $bbox[5];
if($debug) header("x-text-rect: {$text_width} x {$text_height}");
// Retrieve boundingbox of the rotated text
$bbox = imagettfbbox($size, $angle, $fontfile, $text);
$rot_centre = [
($bbox[0] + $bbox[2] + $bbox[4] + $bbox[6]) / 4,
($bbox[1] + $bbox[3] + $bbox[5] + $bbox[7]) / 4
];
// Calculate the x and y shifting needed
$dx = $rot_centre[0] - $orig_centre[0];
$dy = $rot_centre[1] - $orig_centre[1];
// New pivot point
$px = $x - ($dx + $text_width/2);
// if($angle != 0) $px -= $text_width * 2 * $angle / 360;
$py = $y - $dy + $text_height/2;
header("x-centres: ($angle deg) ({$orig_centre[0]}, {$orig_centre[1]}), ({$rot_centre[0]}, {$rot_centre[1]}) -> ($px, $py)");
// Output
$result = imagettftext($im, $size, $angle, $px, $py, $color, $fontfile, $text);
if($debug) {
imagesetthickness($im, 8);
imagecross($im, $px, $py, 1000, $debug_col_d);
imagecross($im, $x, $y, 1000, $debug_col_c);
imagecross($im, $orig_centre[0] + $x, $orig_centre[1] + $y, 1000, $debug_col_a);
imagecross($im, $rot_centre[0] + $x, $rot_centre[1] + $y, 1000, $debug_col_b);
}
return $result;
}
/**
* Extracts a colour from a hex code
* @param string $string The hex code to extract from.
* @param int $index The index from which to extract.
* @return int The value of the channel at the specified index.
*/
function get_component($string, $index) {
return hexdec(substr($string, $index * 2, 2));
}
/**
* Returns a random hex colour.
* @return string The randomly generated hex colour.
*/
function generate_random_colour() {
$result = "";
for($i = 0; $i < 6; $i++) // Hex colours are 6 digits long
$result .= "0123456789abcdef"[random_int(0, 15)]; // Crypto random numbers are better
return $result;
}
/**
* Inverts the given hexadecimal colour.
* @param string $hex The hex colour to invert.
* @return string The inverted hex colour.
*/
function invert_colour($hex) {
return str_pad(dechex(255 - get_component($hex, 0)), 2, "0", STR_PAD_LEFT) .
str_pad(dechex(255 - get_component($hex, 1)), 2, "0", STR_PAD_LEFT) .
str_pad(dechex(255 - get_component($hex, 2)), 2, "0", STR_PAD_LEFT);
}
// Special colour handling
if($settings->bg_colour == "inverse" && $settings->fg_colour == "inverse")
$settings->bg_colour = "random";
if($settings->bg_colour == "random")
$settings->bg_colour = generate_random_colour();
if($settings->fg_colour == "random")
$settings->fg_colour = generate_random_colour();
if($settings->bg_colour == "inverse")
$settings->bg_colour = invert_colour($settings->fg_colour);
if($settings->fg_colour == "inverse")
$settings->fg_colour = invert_colour($settings->bg_colour);
header("x-parsed-colours: '$settings->bg_colour' / '$settings->fg_colour'");
// Limit the maximum size of the returned image
if($settings->width < 0) $settings->width = 1;
if($settings->height < 0) $settings->height = 1;
if($settings->width > MAX_SIZE) $settings->width = MAX_SIZE;
if($settings->height > MAX_SIZE) $settings->height = MAX_SIZE;
// Set the font filename
$font_types = [ "normal", "bold" ];
$font_map = [ "Regular", "Bold" ];
if(!in_array($settings->weight, $font_types))
$settings->weight = $font_types[0];
$settings->weight = str_replace($font_types, $font_map, strtolower($settings->weight));
$font_file = "./SourceSansPro-$settings->weight.ttf";
if(ENABLE_CACHING)
{
// Get the request headers and work out if the requested placeholder is the
// same as the one we are about to render
$headers = getallheaders();
$headers = array_change_key_case($headers, CASE_LOWER);
// header("content-type: text/plain");
$server_etag = sha1(json_encode($settings));
if(isset($headers["if-none-match"]))
{
$headers["if-none-match"] = preg_replace("/[^0-9a-f]/i", "", $headers["if-none-match"]);
if($headers["if-none-match"] == sha1(json_encode($settings)))
{
// The placeholder specified by the etag request is identical to the
// one we were about to render
http_response_code(304);
header("x-generation-time: " . (microtime(true) - $exec_start) . "s");
header("x-cache-status: if-none-match=foundmatched");
exit();
}
header("x-cache-status: if-none-match=foundnotmatched");
}
else
{
header("x-cache-status: if-none-match=notfound");
}
}
// Create the image
$image = imagecreatetruecolor($settings->width, $settings->height);
// Sort out the background colour
$bg_colour_parts = new stdClass();
$bg_colour_parts->r = get_component($settings->bg_colour, 0);
$bg_colour_parts->g = get_component($settings->bg_colour, 1);
$bg_colour_parts->b = get_component($settings->bg_colour, 2);
$bg_colour = imagecolorallocate($image, $bg_colour_parts->r, $bg_colour_parts->g, $bg_colour_parts->b);
imagefill($image, 0, 0, $bg_colour);
// Sort out the foreground colour
$fg_colour_parts = new stdClass();
$fg_colour_parts->r = get_component($settings->fg_colour, 0);
$fg_colour_parts->g = get_component($settings->fg_colour, 1);
$fg_colour_parts->b = get_component($settings->fg_colour, 2);
$fg_colour = imagecolorallocate($image, $fg_colour_parts->r, $fg_colour_parts->g, $fg_colour_parts->b);
// Determine the text we are going to draw
$text = $settings->text;
if(!isset($_GET["text"]))
$text = "$settings->width x $settings->height";
// Calculate the size of the text
$text_bounding_percent = 0.9;
$text_size = 20;
$text_box = imagettfbbox($text_size, 0, $font_file, $text);
$text_width = $text_box[2] - $text_box[0];
$text_height = $text_box[0] - $text_box[6];
$target_width = $settings->width * $text_bounding_percent;
$target_height = $settings->height * $text_bounding_percent;
$text_scale = $target_width / $text_width;
//header("content-type: text/plain");
$text_size *= $text_scale;
$text_box = imagettfbbox($text_size, $settings->angle, $font_file, $text);
$text_width = $text_box[2] - $text_box[0];
$text_height = $text_box[0] - $text_box[6];
// Calculate the position of the text
$text_x = ($settings->width / 2) - ($text_width / 2);
$text_y = ($settings->height / 2) + ((($text_size / 96) * 72) / 2); // point -> pixel
imagettftext_center_rotate(
$image,
$text_size, $settings->angle,
// $text_x, $text_y,
$settings->width / 2, $settings->height / 2,
$fg_colour, $font_file,
$text,
DEBUG_ANGLE
);
header("x-generation-time: " . (microtime(true) - $exec_start) . "s");
header("x-text-size: $text_size");
header("x-usage-help: Add 'help' GET param to request");
header("content-type: image/jpeg");
if(ENABLE_CACHING)
header("etag: " . sha1(json_encode($settings)));
imagejpeg($image, null, $settings->quality);
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment