-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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