| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786 |
- <?php
- require_once 'telemetry_db.php';
- error_reporting(0);
- putenv('GDFONTPATH='.realpath('.'));
- /**
- * Everything is drawn at SS times the final size and scaled down at the end.
- * GD has no antialiasing for filled shapes, so circles, rounded corners and
- * arcs would otherwise come out stepped; oversampling gives all of them smooth
- * edges from one mechanism.
- */
- const SS = 3;
- const WIDTH = 800;
- const HEIGHT = 480;
- // Palette taken from frontend/styling/colors.css, so the image matches the
- // modern frontend rather than inventing a second look for the same project.
- const C_CARD = [14, 7, 32]; // --background-backup-color
- const C_PANEL = [23, 16, 42];
- const C_PANEL_EDGE = [42, 32, 64];
- const C_TEXT = [255, 255, 255]; // --primary-text-color
- const C_TEXT_DIM = [137, 133, 145]; // --secondary-text-color
- const C_DOWNLOAD = [92, 249, 253]; // --theme-green
- const C_UPLOAD = [214, 59, 198]; // --theme-pink
- /**
- * @param string $name
- *
- * @return string|null
- */
- function tryFont($name)
- {
- if (is_array(imageftbbox(12, 0, $name, 'M'))) {
- return $name;
- }
- $fullPathToFont = realpath('.').'/'.$name.'.ttf';
- if (is_array(imageftbbox(12, 0, $fullPathToFont, 'M'))) {
- return $fullPathToFont;
- }
- return null;
- }
- /**
- * @param int|float $d
- *
- * @return string
- */
- function format($d)
- {
- if ($d < 10) {
- return number_format($d, 2, '.', '');
- }
- if ($d < 100) {
- return number_format($d, 1, '.', '');
- }
- return number_format($d, 0, '.', '');
- }
- /**
- * Drops the fractional seconds a database may keep on the timestamp column.
- *
- * How much precision that column carries is decided by the backend, and the
- * three schemas shipped here disagree: MySQL declares `timestamp`, which keeps
- * none, PostgreSQL uses `timestamp without time zone DEFAULT now()`, which
- * keeps microseconds, and MSSQL uses `datetime`, which keeps milliseconds.
- *
- * The fraction is matched against the seconds field it belongs to rather than
- * the end of the string, so a value carrying a zone offset after it, such as
- * "2026-08-10 02:04:13.957789+00", is handled as well.
- *
- * @param string $timestamp
- *
- * @return string
- */
- function formatTimestamp($timestamp)
- {
- return preg_replace('/(:\d{2})\.\d+/', '$1', (string) $timestamp);
- }
- /**
- * Splits a timestamp into a date and a time meant to be read rather than
- * parsed.
- *
- * The whole value is returned as the date with an empty time when it cannot be
- * parsed, which keeps a backend nobody here has seen legible rather than blank.
- *
- * @param string $timestamp
- *
- * @return array{0: string, 1: string}
- */
- function splitTimestamp($timestamp)
- {
- $timestamp = formatTimestamp($timestamp);
- $t = strtotime($timestamp);
- if (false === $t) {
- return [$timestamp, ''];
- }
- return [date('j M Y', $t), date('H:i', $t)];
- }
- /**
- * Names the address family a result was measured over, without disclosing the
- * address itself.
- *
- * Returns an empty string when the deployment redacts client addresses, since
- * the placeholder it stores would otherwise be reported as IPv4.
- *
- * @param string $ip
- *
- * @return string
- */
- function addressFamily($ip)
- {
- if ('' === $ip || '0.0.0.0' === $ip) {
- return '';
- }
- return false === strpos($ip, ':') ? 'IPv4' : 'IPv6';
- }
- /**
- * @param resource|GdImage $im
- * @param int[] $rgb
- * @param int $alpha 0 opaque, 127 transparent
- *
- * @return int
- */
- function col($im, $rgb, $alpha = 0)
- {
- return imagecolorallocatealpha($im, $rgb[0], $rgb[1], $rgb[2], $alpha);
- }
- /**
- * The horizontal span of a rounded rectangle on a given row.
- *
- * Filling and shading both need to respect the same corners, so both ask this
- * rather than each approximating the curve on its own.
- *
- * @return array{0: float, 1: float}|null
- */
- function roundedSpan($y, $x1, $y1, $x2, $y2, $r)
- {
- if ($y < $y1 || $y > $y2) {
- return null;
- }
- $inset = 0;
- if ($y < $y1 + $r) {
- $dy = $r - ($y - $y1);
- $inset = $r - sqrt(max(0, $r * $r - $dy * $dy));
- } elseif ($y > $y2 - $r) {
- $dy = $r - ($y2 - $y);
- $inset = $r - sqrt(max(0, $r * $r - $dy * $dy));
- }
- return [$x1 + $inset, $x2 - $inset];
- }
- /**
- * @param resource|GdImage $im
- *
- * @return void
- */
- function roundedRect($im, $x1, $y1, $x2, $y2, $r, $color)
- {
- for ($y = (int) $y1; $y <= (int) $y2; ++$y) {
- $span = roundedSpan($y, $x1, $y1, $x2, $y2, $r);
- if (null !== $span) {
- imageline($im, (int) round($span[0]), $y, (int) round($span[1]), $y, $color);
- }
- }
- }
- /**
- * A panel: a one-pixel edge with a darker fill inside it.
- *
- * @param resource|GdImage $im
- *
- * @return void
- */
- function panel($im, $x1, $y1, $x2, $y2, $r, $fill, $edge)
- {
- roundedRect($im, $x1, $y1, $x2, $y2, $r, $edge);
- roundedRect($im, $x1 + SS, $y1 + SS, $x2 - SS, $y2 - SS, $r - SS, $fill);
- }
- /**
- * Washes an accent colour into the bottom of a panel.
- *
- * This is deliberately a plain gradient and not a line chart. A curve inside a
- * speed test result reads as measured data, and no per-second samples are
- * stored for a shared result, so any curve drawn here would be invented.
- *
- * @param resource|GdImage $im
- * @param int[] $rgb
- *
- * @return void
- */
- function wash($im, $x1, $y1, $x2, $y2, $r, $rgb, $height, $maxAlpha = 88)
- {
- $start = $y2 - $height;
- for ($y = (int) $start; $y <= (int) $y2; ++$y) {
- $span = roundedSpan($y, $x1, $y1, $x2, $y2, $r);
- if (null === $span) {
- continue;
- }
- $t = ($y - $start) / max(1, $height);
- $alpha = (int) round(127 - (127 - $maxAlpha) * ($t * $t));
- imageline($im, (int) round($span[0]), $y, (int) round($span[1]), $y, col($im, $rgb, $alpha));
- }
- }
- /**
- * @param string $text
- *
- * @return float
- */
- function textWidth($text, $font, $size)
- {
- $bbox = imageftbbox($size, 0, $font, $text);
- return is_array($bbox) ? $bbox[2] - $bbox[0] : 0;
- }
- /**
- * Draws text, anchored left, centre or right on x.
- *
- * @param resource|GdImage $im
- *
- * @return float the width drawn
- */
- function text($im, $x, $y, $size, $font, $color, $string, $align = 'left')
- {
- $w = textWidth($string, $font, $size);
- if ('center' === $align) {
- $x -= $w / 2;
- } elseif ('right' === $align) {
- $x -= $w;
- }
- imagefttext($im, $size, 0, (int) round($x), (int) round($y), $color, $font, $string);
- return $w;
- }
- /**
- * The width trackedText() would draw.
- *
- * @return float
- */
- function trackedWidth($string, $font, $size, $tracking)
- {
- $total = -$tracking;
- foreach (preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY) as $ch) {
- $total += textWidth($ch, $font, $size) + $tracking;
- }
- return $total;
- }
- /**
- * The same as text(), with the letters spaced apart.
- *
- * GD has no notion of tracking, so each character is placed on its own. The
- * small uppercase labels are unreadable without it at this size.
- *
- * @param resource|GdImage $im
- *
- * @return float
- */
- function trackedText($im, $x, $y, $size, $font, $color, $string, $tracking, $align = 'left')
- {
- $chars = preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY);
- $total = trackedWidth($string, $font, $size, $tracking);
- if ('center' === $align) {
- $x -= $total / 2;
- } elseif ('right' === $align) {
- $x -= $total;
- }
- foreach ($chars as $ch) {
- imagefttext($im, $size, 0, (int) round($x), (int) round($y), $color, $font, $ch);
- $x += textWidth($ch, $font, $size) + $tracking;
- }
- return $total;
- }
- /**
- * Fills a polygon on both supported PHP majors.
- *
- * imagefilledpolygon() took a point count until PHP 8.0, which deprecated it
- * and made the parameter optional. The backend already relies on the null
- * coalescing operator, so PHP 7 is the floor to keep working here.
- *
- * @param resource|GdImage $im
- * @param int[] $points
- *
- * @return void
- */
- function filledPolygon($im, $points, $color)
- {
- if (PHP_VERSION_ID >= 80000) {
- imagefilledpolygon($im, $points, $color);
- return;
- }
- imagefilledpolygon($im, $points, count($points) / 2, $color);
- }
- /**
- * A ring with an arrow in it, marking which direction a panel measures.
- *
- * @param resource|GdImage $im
- * @param int[] $rgb
- *
- * @return void
- */
- function directionBadge($im, $cx, $cy, $r, $rgb, $pointsDown)
- {
- // ring
- imagefilledellipse($im, (int) $cx, (int) $cy, (int) (2 * $r), (int) (2 * $r), col($im, $rgb, 108));
- imagefilledellipse($im, (int) $cx, (int) $cy, (int) (2 * $r - 2 * SS), (int) (2 * $r - 2 * SS), col($im, C_PANEL));
- $color = col($im, $rgb);
- $shaft = $r * 0.52;
- $head = $r * 0.34;
- $dir = $pointsDown ? 1 : -1;
- imagesetthickness($im, (int) max(1, SS * 0.7));
- imageline($im, (int) $cx, (int) ($cy - $dir * $shaft), (int) $cx, (int) ($cy + $dir * $shaft), $color);
- imagesetthickness($im, 1);
- $tipY = $cy + $dir * $shaft;
- filledPolygon($im, [
- (int) $cx, (int) ($tipY + $dir * $head * 0.5),
- (int) ($cx - $head), (int) ($tipY - $dir * $head * 0.55),
- (int) ($cx + $head), (int) ($tipY - $dir * $head * 0.55),
- ], $color);
- }
- /**
- * A ring with a trace in it: a spike for ping, a wave for jitter.
- *
- * @param resource|GdImage $im
- * @param int[] $rgb
- *
- * @return void
- */
- function traceBadge($im, $cx, $cy, $r, $rgb, $wave)
- {
- imagefilledellipse($im, (int) $cx, (int) $cy, (int) (2 * $r), (int) (2 * $r), col($im, $rgb, 112));
- imagefilledellipse($im, (int) $cx, (int) $cy, (int) (2 * $r - 2 * SS), (int) (2 * $r - 2 * SS), col($im, C_PANEL));
- $color = col($im, $rgb);
- imagesetthickness($im, (int) max(1, SS * 0.8));
- $w = $r * 1.05;
- $prev = null;
- for ($i = 0; $i <= 48; ++$i) {
- $t = $i / 48;
- $x = $cx - $w + 2 * $w * $t;
- if ($wave) {
- $y = $cy - sin($t * 2 * M_PI) * $r * 0.42;
- } else {
- // a flat line with one spike in the middle
- $d = abs($t - 0.5);
- $y = $cy - ($d < 0.16 ? (0.16 - $d) / 0.16 * $r * 0.62 : 0) + ($d > 0.16 && $d < 0.26 ? $r * 0.2 : 0);
- }
- if (null !== $prev) {
- imageline($im, (int) $prev[0], (int) $prev[1], (int) $x, (int) $y, $color);
- }
- $prev = [$x, $y];
- }
- imagesetthickness($im, 1);
- }
- /**
- * Shortens text until it fits, marking it as shortened.
- *
- * User agents in particular are unbounded: a browser sends a far longer string
- * than a command line client, and drawn as-is it would run off the panel.
- *
- * Characters are dropped rather than bytes. A provider name or user agent may
- * well carry multibyte characters, and cutting one in half leaves a sequence
- * that renders as a replacement glyph rather than shortened text. The input is
- * capped first so that the measuring below is bounded no matter what is stored.
- *
- * @return string
- */
- function fitText($text, $font, $size, $maxWidth)
- {
- if (textWidth($text, $font, $size) <= $maxWidth) {
- return $text;
- }
- $chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY);
- if (!is_array($chars)) {
- return '…';
- }
- $chars = array_slice($chars, 0, 256);
- while (count($chars) > 0 && textWidth(implode('', $chars).'…', $font, $size) > $maxWidth) {
- array_pop($chars);
- }
- return implode('', $chars).'…';
- }
- /**
- * @param array $speedtest
- *
- * @return array
- */
- function formatSpeedtestDataForImage($speedtest)
- {
- $speedtest['dl'] = format($speedtest['dl']);
- $speedtest['ul'] = format($speedtest['ul']);
- $speedtest['ping'] = format($speedtest['ping']);
- $speedtest['jitter'] = format($speedtest['jitter']);
- $speedtest['family'] = addressFamily($speedtest['ip']);
- list($speedtest['date'], $speedtest['time']) = splitTimestamp($speedtest['timestamp']);
- // The classic design prints the timestamp whole rather than split in two.
- $speedtest['timestamp'] = formatTimestamp($speedtest['timestamp']);
- $ispinfo = json_decode($speedtest['ispinfo'], true)['processedString'];
- $dash = strpos($ispinfo, '-');
- if ($dash !== false) {
- $ispinfo = substr($ispinfo, $dash + 2);
- $par = strrpos($ispinfo, '(');
- if ($par !== false) {
- $ispinfo = substr($ispinfo, 0, $par);
- }
- } else {
- $ispinfo = '';
- }
- $speedtest['ispinfo'] = trim($ispinfo);
- return $speedtest;
- }
- /**
- * Draws the result in the modern dark design, matching index-modern.html.
- *
- * @param array $speedtest
- *
- * @return void
- */
- function drawModernImage($speedtest)
- {
- $data = formatSpeedtestDataForImage($speedtest);
- $W = WIDTH * SS;
- $H = HEIGHT * SS;
- $im = imagecreatetruecolor($W, $H);
- imagealphablending($im, false);
- imagesavealpha($im, true);
- imagefilledrectangle($im, 0, 0, $W, $H, imagecolorallocatealpha($im, 0, 0, 0, 127));
- imagealphablending($im, true);
- $FONT_BOLD = tryFont('OpenSans-Semibold');
- $FONT_LIGHT = tryFont('OpenSans-Light');
- $card = col($im, C_CARD);
- $panelFill = col($im, C_PANEL);
- $panelEdge = col($im, C_PANEL_EDGE);
- $white = col($im, C_TEXT);
- $dim = col($im, C_TEXT_DIM);
- // the whole canvas is the card, with rounded corners over transparency so
- // it sits on any page background
- roundedRect($im, 0, 0, $W - 1, $H - 1, 18 * SS, $card);
- $M = 24 * SS;
- // ---- header -------------------------------------------------------
- $headerBaseline = 46 * SS;
- text($im, $M, $headerBaseline, 21 * SS, $FONT_BOLD, $white, 'LibreSpeed');
- trackedText($im, $W / 2, $headerBaseline - 4 * SS, 10 * SS, $FONT_BOLD, $dim, 'INTERNET SPEED TEST', 3 * SS, 'center');
- $family = $data['family'];
- if ('' !== $family) {
- $fw = textWidth($family, $FONT_BOLD, 11 * SS);
- $chipW = $fw + 22 * SS;
- $chipH = 26 * SS;
- $chipX = $W - $M - $chipW;
- $chipY = $headerBaseline - 19 * SS;
- roundedRect($im, $chipX, $chipY, $chipX + $chipW, $chipY + $chipH, $chipH / 2, col($im, C_DOWNLOAD, 100));
- roundedRect($im, $chipX + SS, $chipY + SS, $chipX + $chipW - SS, $chipY + $chipH - SS, $chipH / 2 - SS, $card);
- text($im, $chipX + $chipW / 2, $chipY + 18 * SS, 11 * SS, $FONT_BOLD, col($im, C_DOWNLOAD), $family, 'center');
- }
- // ---- download and upload -----------------------------------------
- $panelTop = 72 * SS;
- $panelBottom = 286 * SS;
- $gap = 16 * SS;
- $panelW = ($W - 2 * $M - $gap) / 2;
- $radius = 14 * SS;
- $columns = [
- [$M, $data['dl'], 'DOWNLOAD', C_DOWNLOAD, true],
- [$M + $panelW + $gap, $data['ul'], 'UPLOAD', C_UPLOAD, false],
- ];
- foreach ($columns as list($x1, $value, $label, $rgb, $down)) {
- $x2 = $x1 + $panelW;
- panel($im, $x1, $panelTop, $x2, $panelBottom, $radius, $panelFill, $panelEdge);
- wash($im, $x1 + SS, $panelTop + SS, $x2 - SS, $panelBottom - SS, $radius - SS, $rgb, 78 * SS);
- $badgeR = 19 * SS;
- $labelW = trackedWidth($label, $FONT_BOLD, 13 * SS, 2.5 * SS);
- $groupW = 2 * $badgeR + 14 * SS + $labelW;
- $groupX = $x1 + ($panelW - $groupW) / 2;
- directionBadge($im, $groupX + $badgeR, $panelTop + 46 * SS, $badgeR, $rgb, $down);
- trackedText($im, $groupX + 2 * $badgeR + 14 * SS, $panelTop + 52 * SS, 13 * SS, $FONT_BOLD, col($im, $rgb), $label, 2.5 * SS);
- text($im, $x1 + $panelW / 2, $panelTop + 140 * SS, 58 * SS, $FONT_LIGHT, $white, $value, 'center');
- text($im, $x1 + $panelW / 2, $panelTop + 178 * SS, 19 * SS, $FONT_LIGHT, $dim, 'Mbps', 'center');
- }
- // ---- ping and jitter ----------------------------------------------
- $pjTop = 298 * SS;
- $pjBottom = 376 * SS;
- panel($im, $M, $pjTop, $W - $M, $pjBottom, $radius, $panelFill, $panelEdge);
- $mid = $W / 2;
- imageline($im, (int) $mid, (int) ($pjTop + 18 * SS), (int) $mid, (int) ($pjBottom - 18 * SS), $panelEdge);
- $metrics = [
- [$M, $data['ping'], 'PING', C_DOWNLOAD, false],
- [$mid, $data['jitter'], 'JITTER', C_UPLOAD, true],
- ];
- foreach ($metrics as list($half, $value, $label, $rgb, $wave)) {
- $badgeR = 19 * SS;
- $cx = $half + 58 * SS;
- $cy = ($pjTop + $pjBottom) / 2;
- traceBadge($im, $cx, $cy, $badgeR, $rgb, $wave);
- $tx = $cx + $badgeR + 18 * SS;
- trackedText($im, $tx, $cy - 12 * SS, 11 * SS, $FONT_BOLD, col($im, $rgb), $label, 2.5 * SS);
- $vw = text($im, $tx, $cy + 25 * SS, 29 * SS, $FONT_LIGHT, $white, $value);
- text($im, $tx + $vw + 7 * SS, $cy + 25 * SS, 14 * SS, $FONT_LIGHT, $dim, 'ms');
- }
- // ---- footer --------------------------------------------------------
- $fTop = 390 * SS;
- $fBottom = 456 * SS;
- panel($im, $M, $fTop, $W - $M, $fBottom, $radius, $panelFill, $panelEdge);
- $fx = $M + 22 * SS;
- $wmW = textWidth('LibreSpeed', $FONT_BOLD, 15 * SS);
- $wmX = $W - $M - 22 * SS;
- $isp = fitText($data['ispinfo'], $FONT_BOLD, 15 * SS, $wmX - $wmW - $fx - 20 * SS);
- text($im, $fx, $fTop + 30 * SS, 15 * SS, $FONT_BOLD, $white, $isp);
- $stamp = trim($data['date'].('' === $data['time'] ? '' : ' '.$data['time']));
- $sw = text($im, $fx, $fTop + 56 * SS, 12 * SS, $FONT_LIGHT, $dim, $stamp);
- $client = trim((string) $data['ua']);
- if ('' !== $client) {
- $cx = $fx + $sw + 18 * SS;
- $client = fitText($client, $FONT_LIGHT, 12 * SS, $wmX - $wmW - $cx - 20 * SS);
- text($im, $cx, $fTop + 56 * SS, 12 * SS, $FONT_LIGHT, $dim, $client);
- }
- text($im, $wmX, $fTop + 48 * SS, 15 * SS, $FONT_BOLD, col($im, C_TEXT_DIM), 'LibreSpeed', 'right');
- // ---- scale down ----------------------------------------------------
- $out = imagecreatetruecolor(WIDTH, HEIGHT);
- imagealphablending($out, false);
- imagesavealpha($out, true);
- imagefilledrectangle($out, 0, 0, WIDTH, HEIGHT, imagecolorallocatealpha($out, 0, 0, 0, 127));
- imagecopyresampled($out, $im, 0, 0, 0, 0, WIDTH, HEIGHT, $W, $H);
- imagedestroy($im);
- header('Content-Type: image/png');
- imagepng($out);
- }
- /**
- * Draws the result in the classic light design, as index-classic.html uses.
- *
- * Kept as it was drawn before the modern design existed: a deployment serving
- * the classic frontend would otherwise get an image that matches nothing on
- * its own page.
- *
- * @param array $speedtest
- *
- * @return void
- */
- function drawClassicImage($speedtest)
- {
- // format values for the image
- $data = formatSpeedtestDataForImage($speedtest);
- $dl = $data['dl'];
- $ul = $data['ul'];
- $ping = $data['ping'];
- $jit = $data['jitter'];
- $ispinfo = $data['ispinfo'];
- $timestamp = $data['timestamp'];
- // initialize the image
- $SCALE = 1.25;
- $SMALL_SEP = 8 * $SCALE;
- $WIDTH = 400 * $SCALE;
- $HEIGHT = 229 * $SCALE;
- $im = imagecreatetruecolor($WIDTH, $HEIGHT);
- $BACKGROUND_COLOR = imagecolorallocate($im, 255, 255, 255);
- // configure fonts
- $FONT_LABEL = tryFont('OpenSans-Semibold');
- $FONT_LABEL_SIZE = 14 * $SCALE;
- $FONT_LABEL_SIZE_BIG = 16 * $SCALE;
- $FONT_METER = tryFont('OpenSans-Light');
- $FONT_METER_SIZE = 20 * $SCALE;
- $FONT_METER_SIZE_BIG = 22 * $SCALE;
- $FONT_MEASURE = tryFont('OpenSans-Semibold');
- $FONT_MEASURE_SIZE = 12 * $SCALE;
- $FONT_MEASURE_SIZE_BIG = 12 * $SCALE;
- $FONT_ISP = tryFont('OpenSans-Semibold');
- $FONT_ISP_SIZE = 9 * $SCALE;
- $FONT_TIMESTAMP = tryFont("OpenSans-Light");
- $FONT_TIMESTAMP_SIZE = 8 * $SCALE;
- $FONT_WATERMARK = tryFont('OpenSans-Light');
- $FONT_WATERMARK_SIZE = 8 * $SCALE;
- // configure text colors
- $TEXT_COLOR_LABEL = imagecolorallocate($im, 40, 40, 40);
- $TEXT_COLOR_PING_METER = imagecolorallocate($im, 170, 96, 96);
- $TEXT_COLOR_JIT_METER = imagecolorallocate($im, 170, 96, 96);
- $TEXT_COLOR_DL_METER = imagecolorallocate($im, 96, 96, 170);
- $TEXT_COLOR_UL_METER = imagecolorallocate($im, 96, 96, 96);
- $TEXT_COLOR_MEASURE = imagecolorallocate($im, 40, 40, 40);
- $TEXT_COLOR_ISP = imagecolorallocate($im, 40, 40, 40);
- $SEPARATOR_COLOR = imagecolorallocate($im, 192, 192, 192);
- $TEXT_COLOR_TIMESTAMP = imagecolorallocate($im, 160, 160, 160);
- $TEXT_COLOR_WATERMARK = imagecolorallocate($im, 160, 160, 160);
- // configure positioning or the different parts on the image
- $POSITION_X_PING = 125 * $SCALE;
- $POSITION_Y_PING_LABEL = 24 * $SCALE;
- $POSITION_Y_PING_METER = 60 * $SCALE;
- $POSITION_Y_PING_MEASURE = 60 * $SCALE;
- $POSITION_X_JIT = 275 * $SCALE;
- $POSITION_Y_JIT_LABEL = 24 * $SCALE;
- $POSITION_Y_JIT_METER = 60 * $SCALE;
- $POSITION_Y_JIT_MEASURE = 60 * $SCALE;
- $POSITION_X_DL = 120 * $SCALE;
- $POSITION_Y_DL_LABEL = 105 * $SCALE;
- $POSITION_Y_DL_METER = 143 * $SCALE;
- $POSITION_Y_DL_MEASURE = 169 * $SCALE;
- $POSITION_X_UL = 280 * $SCALE;
- $POSITION_Y_UL_LABEL = 105 * $SCALE;
- $POSITION_Y_UL_METER = 143 * $SCALE;
- $POSITION_Y_UL_MEASURE = 169 * $SCALE;
- $POSITION_X_ISP = 4 * $SCALE;
- $POSITION_Y_ISP = 205 * $SCALE;
- $SEPARATOR_Y = 211 * $SCALE;
- $POSITION_X_TIMESTAMP= 4 * $SCALE;
- $POSITION_Y_TIMESTAMP = 223 * $SCALE;
- $POSITION_Y_WATERMARK = 223 * $SCALE;
- // configure labels
- $MBPS_TEXT = 'Mbit/s';
- $MS_TEXT = 'ms';
- $PING_TEXT = 'Ping';
- $JIT_TEXT = 'Jitter';
- $DL_TEXT = 'Download';
- $UL_TEXT = 'Upload';
- $WATERMARK_TEXT = 'LibreSpeed';
- // create text boxes for each part of the image
- $mbpsBbox = imageftbbox($FONT_MEASURE_SIZE_BIG, 0, $FONT_MEASURE, $MBPS_TEXT);
- $msBbox = imageftbbox($FONT_MEASURE_SIZE, 0, $FONT_MEASURE, $MS_TEXT);
- $pingBbox = imageftbbox($FONT_LABEL_SIZE, 0, $FONT_LABEL, $PING_TEXT);
- $pingMeterBbox = imageftbbox($FONT_METER_SIZE, 0, $FONT_METER, $ping);
- $jitBbox = imageftbbox($FONT_LABEL_SIZE, 0, $FONT_LABEL, $JIT_TEXT);
- $jitMeterBbox = imageftbbox($FONT_METER_SIZE, 0, $FONT_METER, $jit);
- $dlBbox = imageftbbox($FONT_LABEL_SIZE_BIG, 0, $FONT_LABEL, $DL_TEXT);
- $dlMeterBbox = imageftbbox($FONT_METER_SIZE_BIG, 0, $FONT_METER, $dl);
- $ulBbox = imageftbbox($FONT_LABEL_SIZE_BIG, 0, $FONT_LABEL, $UL_TEXT);
- $ulMeterBbox = imageftbbox($FONT_METER_SIZE_BIG, 0, $FONT_METER, $ul);
- $watermarkBbox = imageftbbox($FONT_WATERMARK_SIZE, 0, $FONT_WATERMARK, $WATERMARK_TEXT);
- $POSITION_X_WATERMARK = $WIDTH - $watermarkBbox[4] - 4 * $SCALE;
- // put the parts together to draw the image
- imagefilledrectangle($im, 0, 0, $WIDTH, $HEIGHT, $BACKGROUND_COLOR);
- // ping
- imagefttext($im, $FONT_LABEL_SIZE, 0, $POSITION_X_PING - $pingBbox[4] / 2, $POSITION_Y_PING_LABEL, $TEXT_COLOR_LABEL, $FONT_LABEL, $PING_TEXT);
- imagefttext($im, $FONT_METER_SIZE, 0, $POSITION_X_PING - $pingMeterBbox[4] / 2 - $msBbox[4] / 2 - $SMALL_SEP / 2, $POSITION_Y_PING_METER, $TEXT_COLOR_PING_METER, $FONT_METER, $ping);
- imagefttext($im, $FONT_MEASURE_SIZE, 0, $POSITION_X_PING + $pingMeterBbox[4] / 2 + $SMALL_SEP / 2 - $msBbox[4] / 2, $POSITION_Y_PING_MEASURE, $TEXT_COLOR_MEASURE, $FONT_MEASURE, $MS_TEXT);
- // jitter
- imagefttext($im, $FONT_LABEL_SIZE, 0, $POSITION_X_JIT - $jitBbox[4] / 2, $POSITION_Y_JIT_LABEL, $TEXT_COLOR_LABEL, $FONT_LABEL, $JIT_TEXT);
- imagefttext($im, $FONT_METER_SIZE, 0, $POSITION_X_JIT - $jitMeterBbox[4] / 2 - $msBbox[4] / 2 - $SMALL_SEP / 2, $POSITION_Y_JIT_METER, $TEXT_COLOR_JIT_METER, $FONT_METER, $jit);
- imagefttext($im, $FONT_MEASURE_SIZE, 0, $POSITION_X_JIT + $jitMeterBbox[4] / 2 + $SMALL_SEP / 2 - $msBbox[4] / 2, $POSITION_Y_JIT_MEASURE, $TEXT_COLOR_MEASURE, $FONT_MEASURE, $MS_TEXT);
- // dl
- imagefttext($im, $FONT_LABEL_SIZE_BIG, 0, $POSITION_X_DL - $dlBbox[4] / 2, $POSITION_Y_DL_LABEL, $TEXT_COLOR_LABEL, $FONT_LABEL, $DL_TEXT);
- imagefttext($im, $FONT_METER_SIZE_BIG, 0, $POSITION_X_DL - $dlMeterBbox[4] / 2, $POSITION_Y_DL_METER, $TEXT_COLOR_DL_METER, $FONT_METER, $dl);
- imagefttext($im, $FONT_MEASURE_SIZE_BIG, 0, $POSITION_X_DL - $mbpsBbox[4] / 2, $POSITION_Y_DL_MEASURE, $TEXT_COLOR_MEASURE, $FONT_MEASURE, $MBPS_TEXT);
- // ul
- imagefttext($im, $FONT_LABEL_SIZE_BIG, 0, $POSITION_X_UL - $ulBbox[4] / 2, $POSITION_Y_UL_LABEL, $TEXT_COLOR_LABEL, $FONT_LABEL, $UL_TEXT);
- imagefttext($im, $FONT_METER_SIZE_BIG, 0, $POSITION_X_UL - $ulMeterBbox[4] / 2, $POSITION_Y_UL_METER, $TEXT_COLOR_UL_METER, $FONT_METER, $ul);
- imagefttext($im, $FONT_MEASURE_SIZE_BIG, 0, $POSITION_X_UL - $mbpsBbox[4] / 2, $POSITION_Y_UL_MEASURE, $TEXT_COLOR_MEASURE, $FONT_MEASURE, $MBPS_TEXT);
- // isp
- imagefttext($im, $FONT_ISP_SIZE, 0, $POSITION_X_ISP, $POSITION_Y_ISP, $TEXT_COLOR_ISP, $FONT_ISP, $ispinfo);
- // separator
- imagefilledrectangle($im, 0, $SEPARATOR_Y, $WIDTH, $SEPARATOR_Y, $SEPARATOR_COLOR);
- // timestamp
- imagefttext($im, $FONT_TIMESTAMP_SIZE, 0, $POSITION_X_TIMESTAMP, $POSITION_Y_TIMESTAMP, $TEXT_COLOR_TIMESTAMP, $FONT_TIMESTAMP, $timestamp);
- // watermark
- imagefttext($im, $FONT_WATERMARK_SIZE, 0, $POSITION_X_WATERMARK, $POSITION_Y_WATERMARK, $TEXT_COLOR_WATERMARK, $FONT_WATERMARK, $WATERMARK_TEXT);
- // send the image to the browser
- header('Content-Type: image/png');
- imagepng($im);
- }
- /**
- * Which of the two designs to draw.
- *
- * The frontends both link the same URL, so the image cannot tell which page a
- * result came from. An explicit style parameter decides, which is what lets the
- * modern frontend ask for the design it matches and what makes an existing
- * share link render either way on demand.
- *
- * Without one the image follows useNewDesign, the same switch the frontend
- * already reads, so a deployment has one setting rather than two that can
- * disagree. A missing or unreadable config.json means classic, which is what
- * design-switch.js falls back to as well.
- *
- * @return string
- */
- function resultImageStyle()
- {
- if (isset($_GET['style'])) {
- $style = strtolower(trim((string) $_GET['style']));
- if ('modern' === $style) {
- return 'modern';
- }
- if ('classic' === $style) {
- return 'classic';
- }
- }
- $config = json_decode((string) @file_get_contents(__DIR__.'/../config.json'), true);
- if (is_array($config) && isset($config['useNewDesign']) && true === $config['useNewDesign']) {
- return 'modern';
- }
- return 'classic';
- }
- $speedtest = getSpeedtestUserById($_GET['id']);
- if (!is_array($speedtest)) {
- exit(1);
- }
- if ('modern' === resultImageStyle()) {
- drawModernImage($speedtest);
- } else {
- drawClassicImage($speedtest);
- }
|