1
0

index.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786
  1. <?php
  2. require_once 'telemetry_db.php';
  3. error_reporting(0);
  4. putenv('GDFONTPATH='.realpath('.'));
  5. /**
  6. * Everything is drawn at SS times the final size and scaled down at the end.
  7. * GD has no antialiasing for filled shapes, so circles, rounded corners and
  8. * arcs would otherwise come out stepped; oversampling gives all of them smooth
  9. * edges from one mechanism.
  10. */
  11. const SS = 3;
  12. const WIDTH = 800;
  13. const HEIGHT = 480;
  14. // Palette taken from frontend/styling/colors.css, so the image matches the
  15. // modern frontend rather than inventing a second look for the same project.
  16. const C_CARD = [14, 7, 32]; // --background-backup-color
  17. const C_PANEL = [23, 16, 42];
  18. const C_PANEL_EDGE = [42, 32, 64];
  19. const C_TEXT = [255, 255, 255]; // --primary-text-color
  20. const C_TEXT_DIM = [137, 133, 145]; // --secondary-text-color
  21. const C_DOWNLOAD = [92, 249, 253]; // --theme-green
  22. const C_UPLOAD = [214, 59, 198]; // --theme-pink
  23. /**
  24. * @param string $name
  25. *
  26. * @return string|null
  27. */
  28. function tryFont($name)
  29. {
  30. if (is_array(imageftbbox(12, 0, $name, 'M'))) {
  31. return $name;
  32. }
  33. $fullPathToFont = realpath('.').'/'.$name.'.ttf';
  34. if (is_array(imageftbbox(12, 0, $fullPathToFont, 'M'))) {
  35. return $fullPathToFont;
  36. }
  37. return null;
  38. }
  39. /**
  40. * @param int|float $d
  41. *
  42. * @return string
  43. */
  44. function format($d)
  45. {
  46. if ($d < 10) {
  47. return number_format($d, 2, '.', '');
  48. }
  49. if ($d < 100) {
  50. return number_format($d, 1, '.', '');
  51. }
  52. return number_format($d, 0, '.', '');
  53. }
  54. /**
  55. * Drops the fractional seconds a database may keep on the timestamp column.
  56. *
  57. * How much precision that column carries is decided by the backend, and the
  58. * three schemas shipped here disagree: MySQL declares `timestamp`, which keeps
  59. * none, PostgreSQL uses `timestamp without time zone DEFAULT now()`, which
  60. * keeps microseconds, and MSSQL uses `datetime`, which keeps milliseconds.
  61. *
  62. * The fraction is matched against the seconds field it belongs to rather than
  63. * the end of the string, so a value carrying a zone offset after it, such as
  64. * "2026-08-10 02:04:13.957789+00", is handled as well.
  65. *
  66. * @param string $timestamp
  67. *
  68. * @return string
  69. */
  70. function formatTimestamp($timestamp)
  71. {
  72. return preg_replace('/(:\d{2})\.\d+/', '$1', (string) $timestamp);
  73. }
  74. /**
  75. * Splits a timestamp into a date and a time meant to be read rather than
  76. * parsed.
  77. *
  78. * The whole value is returned as the date with an empty time when it cannot be
  79. * parsed, which keeps a backend nobody here has seen legible rather than blank.
  80. *
  81. * @param string $timestamp
  82. *
  83. * @return array{0: string, 1: string}
  84. */
  85. function splitTimestamp($timestamp)
  86. {
  87. $timestamp = formatTimestamp($timestamp);
  88. $t = strtotime($timestamp);
  89. if (false === $t) {
  90. return [$timestamp, ''];
  91. }
  92. return [date('j M Y', $t), date('H:i', $t)];
  93. }
  94. /**
  95. * Names the address family a result was measured over, without disclosing the
  96. * address itself.
  97. *
  98. * Returns an empty string when the deployment redacts client addresses, since
  99. * the placeholder it stores would otherwise be reported as IPv4.
  100. *
  101. * @param string $ip
  102. *
  103. * @return string
  104. */
  105. function addressFamily($ip)
  106. {
  107. if ('' === $ip || '0.0.0.0' === $ip) {
  108. return '';
  109. }
  110. return false === strpos($ip, ':') ? 'IPv4' : 'IPv6';
  111. }
  112. /**
  113. * @param resource|GdImage $im
  114. * @param int[] $rgb
  115. * @param int $alpha 0 opaque, 127 transparent
  116. *
  117. * @return int
  118. */
  119. function col($im, $rgb, $alpha = 0)
  120. {
  121. return imagecolorallocatealpha($im, $rgb[0], $rgb[1], $rgb[2], $alpha);
  122. }
  123. /**
  124. * The horizontal span of a rounded rectangle on a given row.
  125. *
  126. * Filling and shading both need to respect the same corners, so both ask this
  127. * rather than each approximating the curve on its own.
  128. *
  129. * @return array{0: float, 1: float}|null
  130. */
  131. function roundedSpan($y, $x1, $y1, $x2, $y2, $r)
  132. {
  133. if ($y < $y1 || $y > $y2) {
  134. return null;
  135. }
  136. $inset = 0;
  137. if ($y < $y1 + $r) {
  138. $dy = $r - ($y - $y1);
  139. $inset = $r - sqrt(max(0, $r * $r - $dy * $dy));
  140. } elseif ($y > $y2 - $r) {
  141. $dy = $r - ($y2 - $y);
  142. $inset = $r - sqrt(max(0, $r * $r - $dy * $dy));
  143. }
  144. return [$x1 + $inset, $x2 - $inset];
  145. }
  146. /**
  147. * @param resource|GdImage $im
  148. *
  149. * @return void
  150. */
  151. function roundedRect($im, $x1, $y1, $x2, $y2, $r, $color)
  152. {
  153. for ($y = (int) $y1; $y <= (int) $y2; ++$y) {
  154. $span = roundedSpan($y, $x1, $y1, $x2, $y2, $r);
  155. if (null !== $span) {
  156. imageline($im, (int) round($span[0]), $y, (int) round($span[1]), $y, $color);
  157. }
  158. }
  159. }
  160. /**
  161. * A panel: a one-pixel edge with a darker fill inside it.
  162. *
  163. * @param resource|GdImage $im
  164. *
  165. * @return void
  166. */
  167. function panel($im, $x1, $y1, $x2, $y2, $r, $fill, $edge)
  168. {
  169. roundedRect($im, $x1, $y1, $x2, $y2, $r, $edge);
  170. roundedRect($im, $x1 + SS, $y1 + SS, $x2 - SS, $y2 - SS, $r - SS, $fill);
  171. }
  172. /**
  173. * Washes an accent colour into the bottom of a panel.
  174. *
  175. * This is deliberately a plain gradient and not a line chart. A curve inside a
  176. * speed test result reads as measured data, and no per-second samples are
  177. * stored for a shared result, so any curve drawn here would be invented.
  178. *
  179. * @param resource|GdImage $im
  180. * @param int[] $rgb
  181. *
  182. * @return void
  183. */
  184. function wash($im, $x1, $y1, $x2, $y2, $r, $rgb, $height, $maxAlpha = 88)
  185. {
  186. $start = $y2 - $height;
  187. for ($y = (int) $start; $y <= (int) $y2; ++$y) {
  188. $span = roundedSpan($y, $x1, $y1, $x2, $y2, $r);
  189. if (null === $span) {
  190. continue;
  191. }
  192. $t = ($y - $start) / max(1, $height);
  193. $alpha = (int) round(127 - (127 - $maxAlpha) * ($t * $t));
  194. imageline($im, (int) round($span[0]), $y, (int) round($span[1]), $y, col($im, $rgb, $alpha));
  195. }
  196. }
  197. /**
  198. * @param string $text
  199. *
  200. * @return float
  201. */
  202. function textWidth($text, $font, $size)
  203. {
  204. $bbox = imageftbbox($size, 0, $font, $text);
  205. return is_array($bbox) ? $bbox[2] - $bbox[0] : 0;
  206. }
  207. /**
  208. * Draws text, anchored left, centre or right on x.
  209. *
  210. * @param resource|GdImage $im
  211. *
  212. * @return float the width drawn
  213. */
  214. function text($im, $x, $y, $size, $font, $color, $string, $align = 'left')
  215. {
  216. $w = textWidth($string, $font, $size);
  217. if ('center' === $align) {
  218. $x -= $w / 2;
  219. } elseif ('right' === $align) {
  220. $x -= $w;
  221. }
  222. imagefttext($im, $size, 0, (int) round($x), (int) round($y), $color, $font, $string);
  223. return $w;
  224. }
  225. /**
  226. * The width trackedText() would draw.
  227. *
  228. * @return float
  229. */
  230. function trackedWidth($string, $font, $size, $tracking)
  231. {
  232. $total = -$tracking;
  233. foreach (preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY) as $ch) {
  234. $total += textWidth($ch, $font, $size) + $tracking;
  235. }
  236. return $total;
  237. }
  238. /**
  239. * The same as text(), with the letters spaced apart.
  240. *
  241. * GD has no notion of tracking, so each character is placed on its own. The
  242. * small uppercase labels are unreadable without it at this size.
  243. *
  244. * @param resource|GdImage $im
  245. *
  246. * @return float
  247. */
  248. function trackedText($im, $x, $y, $size, $font, $color, $string, $tracking, $align = 'left')
  249. {
  250. $chars = preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY);
  251. $total = trackedWidth($string, $font, $size, $tracking);
  252. if ('center' === $align) {
  253. $x -= $total / 2;
  254. } elseif ('right' === $align) {
  255. $x -= $total;
  256. }
  257. foreach ($chars as $ch) {
  258. imagefttext($im, $size, 0, (int) round($x), (int) round($y), $color, $font, $ch);
  259. $x += textWidth($ch, $font, $size) + $tracking;
  260. }
  261. return $total;
  262. }
  263. /**
  264. * Fills a polygon on both supported PHP majors.
  265. *
  266. * imagefilledpolygon() took a point count until PHP 8.0, which deprecated it
  267. * and made the parameter optional. The backend already relies on the null
  268. * coalescing operator, so PHP 7 is the floor to keep working here.
  269. *
  270. * @param resource|GdImage $im
  271. * @param int[] $points
  272. *
  273. * @return void
  274. */
  275. function filledPolygon($im, $points, $color)
  276. {
  277. if (PHP_VERSION_ID >= 80000) {
  278. imagefilledpolygon($im, $points, $color);
  279. return;
  280. }
  281. imagefilledpolygon($im, $points, count($points) / 2, $color);
  282. }
  283. /**
  284. * A ring with an arrow in it, marking which direction a panel measures.
  285. *
  286. * @param resource|GdImage $im
  287. * @param int[] $rgb
  288. *
  289. * @return void
  290. */
  291. function directionBadge($im, $cx, $cy, $r, $rgb, $pointsDown)
  292. {
  293. // ring
  294. imagefilledellipse($im, (int) $cx, (int) $cy, (int) (2 * $r), (int) (2 * $r), col($im, $rgb, 108));
  295. imagefilledellipse($im, (int) $cx, (int) $cy, (int) (2 * $r - 2 * SS), (int) (2 * $r - 2 * SS), col($im, C_PANEL));
  296. $color = col($im, $rgb);
  297. $shaft = $r * 0.52;
  298. $head = $r * 0.34;
  299. $dir = $pointsDown ? 1 : -1;
  300. imagesetthickness($im, (int) max(1, SS * 0.7));
  301. imageline($im, (int) $cx, (int) ($cy - $dir * $shaft), (int) $cx, (int) ($cy + $dir * $shaft), $color);
  302. imagesetthickness($im, 1);
  303. $tipY = $cy + $dir * $shaft;
  304. filledPolygon($im, [
  305. (int) $cx, (int) ($tipY + $dir * $head * 0.5),
  306. (int) ($cx - $head), (int) ($tipY - $dir * $head * 0.55),
  307. (int) ($cx + $head), (int) ($tipY - $dir * $head * 0.55),
  308. ], $color);
  309. }
  310. /**
  311. * A ring with a trace in it: a spike for ping, a wave for jitter.
  312. *
  313. * @param resource|GdImage $im
  314. * @param int[] $rgb
  315. *
  316. * @return void
  317. */
  318. function traceBadge($im, $cx, $cy, $r, $rgb, $wave)
  319. {
  320. imagefilledellipse($im, (int) $cx, (int) $cy, (int) (2 * $r), (int) (2 * $r), col($im, $rgb, 112));
  321. imagefilledellipse($im, (int) $cx, (int) $cy, (int) (2 * $r - 2 * SS), (int) (2 * $r - 2 * SS), col($im, C_PANEL));
  322. $color = col($im, $rgb);
  323. imagesetthickness($im, (int) max(1, SS * 0.8));
  324. $w = $r * 1.05;
  325. $prev = null;
  326. for ($i = 0; $i <= 48; ++$i) {
  327. $t = $i / 48;
  328. $x = $cx - $w + 2 * $w * $t;
  329. if ($wave) {
  330. $y = $cy - sin($t * 2 * M_PI) * $r * 0.42;
  331. } else {
  332. // a flat line with one spike in the middle
  333. $d = abs($t - 0.5);
  334. $y = $cy - ($d < 0.16 ? (0.16 - $d) / 0.16 * $r * 0.62 : 0) + ($d > 0.16 && $d < 0.26 ? $r * 0.2 : 0);
  335. }
  336. if (null !== $prev) {
  337. imageline($im, (int) $prev[0], (int) $prev[1], (int) $x, (int) $y, $color);
  338. }
  339. $prev = [$x, $y];
  340. }
  341. imagesetthickness($im, 1);
  342. }
  343. /**
  344. * Shortens text until it fits, marking it as shortened.
  345. *
  346. * User agents in particular are unbounded: a browser sends a far longer string
  347. * than a command line client, and drawn as-is it would run off the panel.
  348. *
  349. * Characters are dropped rather than bytes. A provider name or user agent may
  350. * well carry multibyte characters, and cutting one in half leaves a sequence
  351. * that renders as a replacement glyph rather than shortened text. The input is
  352. * capped first so that the measuring below is bounded no matter what is stored.
  353. *
  354. * @return string
  355. */
  356. function fitText($text, $font, $size, $maxWidth)
  357. {
  358. if (textWidth($text, $font, $size) <= $maxWidth) {
  359. return $text;
  360. }
  361. $chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY);
  362. if (!is_array($chars)) {
  363. return '…';
  364. }
  365. $chars = array_slice($chars, 0, 256);
  366. while (count($chars) > 0 && textWidth(implode('', $chars).'…', $font, $size) > $maxWidth) {
  367. array_pop($chars);
  368. }
  369. return implode('', $chars).'…';
  370. }
  371. /**
  372. * @param array $speedtest
  373. *
  374. * @return array
  375. */
  376. function formatSpeedtestDataForImage($speedtest)
  377. {
  378. $speedtest['dl'] = format($speedtest['dl']);
  379. $speedtest['ul'] = format($speedtest['ul']);
  380. $speedtest['ping'] = format($speedtest['ping']);
  381. $speedtest['jitter'] = format($speedtest['jitter']);
  382. $speedtest['family'] = addressFamily($speedtest['ip']);
  383. list($speedtest['date'], $speedtest['time']) = splitTimestamp($speedtest['timestamp']);
  384. // The classic design prints the timestamp whole rather than split in two.
  385. $speedtest['timestamp'] = formatTimestamp($speedtest['timestamp']);
  386. $ispinfo = json_decode($speedtest['ispinfo'], true)['processedString'];
  387. $dash = strpos($ispinfo, '-');
  388. if ($dash !== false) {
  389. $ispinfo = substr($ispinfo, $dash + 2);
  390. $par = strrpos($ispinfo, '(');
  391. if ($par !== false) {
  392. $ispinfo = substr($ispinfo, 0, $par);
  393. }
  394. } else {
  395. $ispinfo = '';
  396. }
  397. $speedtest['ispinfo'] = trim($ispinfo);
  398. return $speedtest;
  399. }
  400. /**
  401. * Draws the result in the modern dark design, matching index-modern.html.
  402. *
  403. * @param array $speedtest
  404. *
  405. * @return void
  406. */
  407. function drawModernImage($speedtest)
  408. {
  409. $data = formatSpeedtestDataForImage($speedtest);
  410. $W = WIDTH * SS;
  411. $H = HEIGHT * SS;
  412. $im = imagecreatetruecolor($W, $H);
  413. imagealphablending($im, false);
  414. imagesavealpha($im, true);
  415. imagefilledrectangle($im, 0, 0, $W, $H, imagecolorallocatealpha($im, 0, 0, 0, 127));
  416. imagealphablending($im, true);
  417. $FONT_BOLD = tryFont('OpenSans-Semibold');
  418. $FONT_LIGHT = tryFont('OpenSans-Light');
  419. $card = col($im, C_CARD);
  420. $panelFill = col($im, C_PANEL);
  421. $panelEdge = col($im, C_PANEL_EDGE);
  422. $white = col($im, C_TEXT);
  423. $dim = col($im, C_TEXT_DIM);
  424. // the whole canvas is the card, with rounded corners over transparency so
  425. // it sits on any page background
  426. roundedRect($im, 0, 0, $W - 1, $H - 1, 18 * SS, $card);
  427. $M = 24 * SS;
  428. // ---- header -------------------------------------------------------
  429. $headerBaseline = 46 * SS;
  430. text($im, $M, $headerBaseline, 21 * SS, $FONT_BOLD, $white, 'LibreSpeed');
  431. trackedText($im, $W / 2, $headerBaseline - 4 * SS, 10 * SS, $FONT_BOLD, $dim, 'INTERNET SPEED TEST', 3 * SS, 'center');
  432. $family = $data['family'];
  433. if ('' !== $family) {
  434. $fw = textWidth($family, $FONT_BOLD, 11 * SS);
  435. $chipW = $fw + 22 * SS;
  436. $chipH = 26 * SS;
  437. $chipX = $W - $M - $chipW;
  438. $chipY = $headerBaseline - 19 * SS;
  439. roundedRect($im, $chipX, $chipY, $chipX + $chipW, $chipY + $chipH, $chipH / 2, col($im, C_DOWNLOAD, 100));
  440. roundedRect($im, $chipX + SS, $chipY + SS, $chipX + $chipW - SS, $chipY + $chipH - SS, $chipH / 2 - SS, $card);
  441. text($im, $chipX + $chipW / 2, $chipY + 18 * SS, 11 * SS, $FONT_BOLD, col($im, C_DOWNLOAD), $family, 'center');
  442. }
  443. // ---- download and upload -----------------------------------------
  444. $panelTop = 72 * SS;
  445. $panelBottom = 286 * SS;
  446. $gap = 16 * SS;
  447. $panelW = ($W - 2 * $M - $gap) / 2;
  448. $radius = 14 * SS;
  449. $columns = [
  450. [$M, $data['dl'], 'DOWNLOAD', C_DOWNLOAD, true],
  451. [$M + $panelW + $gap, $data['ul'], 'UPLOAD', C_UPLOAD, false],
  452. ];
  453. foreach ($columns as list($x1, $value, $label, $rgb, $down)) {
  454. $x2 = $x1 + $panelW;
  455. panel($im, $x1, $panelTop, $x2, $panelBottom, $radius, $panelFill, $panelEdge);
  456. wash($im, $x1 + SS, $panelTop + SS, $x2 - SS, $panelBottom - SS, $radius - SS, $rgb, 78 * SS);
  457. $badgeR = 19 * SS;
  458. $labelW = trackedWidth($label, $FONT_BOLD, 13 * SS, 2.5 * SS);
  459. $groupW = 2 * $badgeR + 14 * SS + $labelW;
  460. $groupX = $x1 + ($panelW - $groupW) / 2;
  461. directionBadge($im, $groupX + $badgeR, $panelTop + 46 * SS, $badgeR, $rgb, $down);
  462. trackedText($im, $groupX + 2 * $badgeR + 14 * SS, $panelTop + 52 * SS, 13 * SS, $FONT_BOLD, col($im, $rgb), $label, 2.5 * SS);
  463. text($im, $x1 + $panelW / 2, $panelTop + 140 * SS, 58 * SS, $FONT_LIGHT, $white, $value, 'center');
  464. text($im, $x1 + $panelW / 2, $panelTop + 178 * SS, 19 * SS, $FONT_LIGHT, $dim, 'Mbps', 'center');
  465. }
  466. // ---- ping and jitter ----------------------------------------------
  467. $pjTop = 298 * SS;
  468. $pjBottom = 376 * SS;
  469. panel($im, $M, $pjTop, $W - $M, $pjBottom, $radius, $panelFill, $panelEdge);
  470. $mid = $W / 2;
  471. imageline($im, (int) $mid, (int) ($pjTop + 18 * SS), (int) $mid, (int) ($pjBottom - 18 * SS), $panelEdge);
  472. $metrics = [
  473. [$M, $data['ping'], 'PING', C_DOWNLOAD, false],
  474. [$mid, $data['jitter'], 'JITTER', C_UPLOAD, true],
  475. ];
  476. foreach ($metrics as list($half, $value, $label, $rgb, $wave)) {
  477. $badgeR = 19 * SS;
  478. $cx = $half + 58 * SS;
  479. $cy = ($pjTop + $pjBottom) / 2;
  480. traceBadge($im, $cx, $cy, $badgeR, $rgb, $wave);
  481. $tx = $cx + $badgeR + 18 * SS;
  482. trackedText($im, $tx, $cy - 12 * SS, 11 * SS, $FONT_BOLD, col($im, $rgb), $label, 2.5 * SS);
  483. $vw = text($im, $tx, $cy + 25 * SS, 29 * SS, $FONT_LIGHT, $white, $value);
  484. text($im, $tx + $vw + 7 * SS, $cy + 25 * SS, 14 * SS, $FONT_LIGHT, $dim, 'ms');
  485. }
  486. // ---- footer --------------------------------------------------------
  487. $fTop = 390 * SS;
  488. $fBottom = 456 * SS;
  489. panel($im, $M, $fTop, $W - $M, $fBottom, $radius, $panelFill, $panelEdge);
  490. $fx = $M + 22 * SS;
  491. $wmW = textWidth('LibreSpeed', $FONT_BOLD, 15 * SS);
  492. $wmX = $W - $M - 22 * SS;
  493. $isp = fitText($data['ispinfo'], $FONT_BOLD, 15 * SS, $wmX - $wmW - $fx - 20 * SS);
  494. text($im, $fx, $fTop + 30 * SS, 15 * SS, $FONT_BOLD, $white, $isp);
  495. $stamp = trim($data['date'].('' === $data['time'] ? '' : ' '.$data['time']));
  496. $sw = text($im, $fx, $fTop + 56 * SS, 12 * SS, $FONT_LIGHT, $dim, $stamp);
  497. $client = trim((string) $data['ua']);
  498. if ('' !== $client) {
  499. $cx = $fx + $sw + 18 * SS;
  500. $client = fitText($client, $FONT_LIGHT, 12 * SS, $wmX - $wmW - $cx - 20 * SS);
  501. text($im, $cx, $fTop + 56 * SS, 12 * SS, $FONT_LIGHT, $dim, $client);
  502. }
  503. text($im, $wmX, $fTop + 48 * SS, 15 * SS, $FONT_BOLD, col($im, C_TEXT_DIM), 'LibreSpeed', 'right');
  504. // ---- scale down ----------------------------------------------------
  505. $out = imagecreatetruecolor(WIDTH, HEIGHT);
  506. imagealphablending($out, false);
  507. imagesavealpha($out, true);
  508. imagefilledrectangle($out, 0, 0, WIDTH, HEIGHT, imagecolorallocatealpha($out, 0, 0, 0, 127));
  509. imagecopyresampled($out, $im, 0, 0, 0, 0, WIDTH, HEIGHT, $W, $H);
  510. imagedestroy($im);
  511. header('Content-Type: image/png');
  512. imagepng($out);
  513. }
  514. /**
  515. * Draws the result in the classic light design, as index-classic.html uses.
  516. *
  517. * Kept as it was drawn before the modern design existed: a deployment serving
  518. * the classic frontend would otherwise get an image that matches nothing on
  519. * its own page.
  520. *
  521. * @param array $speedtest
  522. *
  523. * @return void
  524. */
  525. function drawClassicImage($speedtest)
  526. {
  527. // format values for the image
  528. $data = formatSpeedtestDataForImage($speedtest);
  529. $dl = $data['dl'];
  530. $ul = $data['ul'];
  531. $ping = $data['ping'];
  532. $jit = $data['jitter'];
  533. $ispinfo = $data['ispinfo'];
  534. $timestamp = $data['timestamp'];
  535. // initialize the image
  536. $SCALE = 1.25;
  537. $SMALL_SEP = 8 * $SCALE;
  538. $WIDTH = 400 * $SCALE;
  539. $HEIGHT = 229 * $SCALE;
  540. $im = imagecreatetruecolor($WIDTH, $HEIGHT);
  541. $BACKGROUND_COLOR = imagecolorallocate($im, 255, 255, 255);
  542. // configure fonts
  543. $FONT_LABEL = tryFont('OpenSans-Semibold');
  544. $FONT_LABEL_SIZE = 14 * $SCALE;
  545. $FONT_LABEL_SIZE_BIG = 16 * $SCALE;
  546. $FONT_METER = tryFont('OpenSans-Light');
  547. $FONT_METER_SIZE = 20 * $SCALE;
  548. $FONT_METER_SIZE_BIG = 22 * $SCALE;
  549. $FONT_MEASURE = tryFont('OpenSans-Semibold');
  550. $FONT_MEASURE_SIZE = 12 * $SCALE;
  551. $FONT_MEASURE_SIZE_BIG = 12 * $SCALE;
  552. $FONT_ISP = tryFont('OpenSans-Semibold');
  553. $FONT_ISP_SIZE = 9 * $SCALE;
  554. $FONT_TIMESTAMP = tryFont("OpenSans-Light");
  555. $FONT_TIMESTAMP_SIZE = 8 * $SCALE;
  556. $FONT_WATERMARK = tryFont('OpenSans-Light');
  557. $FONT_WATERMARK_SIZE = 8 * $SCALE;
  558. // configure text colors
  559. $TEXT_COLOR_LABEL = imagecolorallocate($im, 40, 40, 40);
  560. $TEXT_COLOR_PING_METER = imagecolorallocate($im, 170, 96, 96);
  561. $TEXT_COLOR_JIT_METER = imagecolorallocate($im, 170, 96, 96);
  562. $TEXT_COLOR_DL_METER = imagecolorallocate($im, 96, 96, 170);
  563. $TEXT_COLOR_UL_METER = imagecolorallocate($im, 96, 96, 96);
  564. $TEXT_COLOR_MEASURE = imagecolorallocate($im, 40, 40, 40);
  565. $TEXT_COLOR_ISP = imagecolorallocate($im, 40, 40, 40);
  566. $SEPARATOR_COLOR = imagecolorallocate($im, 192, 192, 192);
  567. $TEXT_COLOR_TIMESTAMP = imagecolorallocate($im, 160, 160, 160);
  568. $TEXT_COLOR_WATERMARK = imagecolorallocate($im, 160, 160, 160);
  569. // configure positioning or the different parts on the image
  570. $POSITION_X_PING = 125 * $SCALE;
  571. $POSITION_Y_PING_LABEL = 24 * $SCALE;
  572. $POSITION_Y_PING_METER = 60 * $SCALE;
  573. $POSITION_Y_PING_MEASURE = 60 * $SCALE;
  574. $POSITION_X_JIT = 275 * $SCALE;
  575. $POSITION_Y_JIT_LABEL = 24 * $SCALE;
  576. $POSITION_Y_JIT_METER = 60 * $SCALE;
  577. $POSITION_Y_JIT_MEASURE = 60 * $SCALE;
  578. $POSITION_X_DL = 120 * $SCALE;
  579. $POSITION_Y_DL_LABEL = 105 * $SCALE;
  580. $POSITION_Y_DL_METER = 143 * $SCALE;
  581. $POSITION_Y_DL_MEASURE = 169 * $SCALE;
  582. $POSITION_X_UL = 280 * $SCALE;
  583. $POSITION_Y_UL_LABEL = 105 * $SCALE;
  584. $POSITION_Y_UL_METER = 143 * $SCALE;
  585. $POSITION_Y_UL_MEASURE = 169 * $SCALE;
  586. $POSITION_X_ISP = 4 * $SCALE;
  587. $POSITION_Y_ISP = 205 * $SCALE;
  588. $SEPARATOR_Y = 211 * $SCALE;
  589. $POSITION_X_TIMESTAMP= 4 * $SCALE;
  590. $POSITION_Y_TIMESTAMP = 223 * $SCALE;
  591. $POSITION_Y_WATERMARK = 223 * $SCALE;
  592. // configure labels
  593. $MBPS_TEXT = 'Mbit/s';
  594. $MS_TEXT = 'ms';
  595. $PING_TEXT = 'Ping';
  596. $JIT_TEXT = 'Jitter';
  597. $DL_TEXT = 'Download';
  598. $UL_TEXT = 'Upload';
  599. $WATERMARK_TEXT = 'LibreSpeed';
  600. // create text boxes for each part of the image
  601. $mbpsBbox = imageftbbox($FONT_MEASURE_SIZE_BIG, 0, $FONT_MEASURE, $MBPS_TEXT);
  602. $msBbox = imageftbbox($FONT_MEASURE_SIZE, 0, $FONT_MEASURE, $MS_TEXT);
  603. $pingBbox = imageftbbox($FONT_LABEL_SIZE, 0, $FONT_LABEL, $PING_TEXT);
  604. $pingMeterBbox = imageftbbox($FONT_METER_SIZE, 0, $FONT_METER, $ping);
  605. $jitBbox = imageftbbox($FONT_LABEL_SIZE, 0, $FONT_LABEL, $JIT_TEXT);
  606. $jitMeterBbox = imageftbbox($FONT_METER_SIZE, 0, $FONT_METER, $jit);
  607. $dlBbox = imageftbbox($FONT_LABEL_SIZE_BIG, 0, $FONT_LABEL, $DL_TEXT);
  608. $dlMeterBbox = imageftbbox($FONT_METER_SIZE_BIG, 0, $FONT_METER, $dl);
  609. $ulBbox = imageftbbox($FONT_LABEL_SIZE_BIG, 0, $FONT_LABEL, $UL_TEXT);
  610. $ulMeterBbox = imageftbbox($FONT_METER_SIZE_BIG, 0, $FONT_METER, $ul);
  611. $watermarkBbox = imageftbbox($FONT_WATERMARK_SIZE, 0, $FONT_WATERMARK, $WATERMARK_TEXT);
  612. $POSITION_X_WATERMARK = $WIDTH - $watermarkBbox[4] - 4 * $SCALE;
  613. // put the parts together to draw the image
  614. imagefilledrectangle($im, 0, 0, $WIDTH, $HEIGHT, $BACKGROUND_COLOR);
  615. // ping
  616. imagefttext($im, $FONT_LABEL_SIZE, 0, $POSITION_X_PING - $pingBbox[4] / 2, $POSITION_Y_PING_LABEL, $TEXT_COLOR_LABEL, $FONT_LABEL, $PING_TEXT);
  617. 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);
  618. 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);
  619. // jitter
  620. imagefttext($im, $FONT_LABEL_SIZE, 0, $POSITION_X_JIT - $jitBbox[4] / 2, $POSITION_Y_JIT_LABEL, $TEXT_COLOR_LABEL, $FONT_LABEL, $JIT_TEXT);
  621. 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);
  622. 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);
  623. // dl
  624. imagefttext($im, $FONT_LABEL_SIZE_BIG, 0, $POSITION_X_DL - $dlBbox[4] / 2, $POSITION_Y_DL_LABEL, $TEXT_COLOR_LABEL, $FONT_LABEL, $DL_TEXT);
  625. imagefttext($im, $FONT_METER_SIZE_BIG, 0, $POSITION_X_DL - $dlMeterBbox[4] / 2, $POSITION_Y_DL_METER, $TEXT_COLOR_DL_METER, $FONT_METER, $dl);
  626. imagefttext($im, $FONT_MEASURE_SIZE_BIG, 0, $POSITION_X_DL - $mbpsBbox[4] / 2, $POSITION_Y_DL_MEASURE, $TEXT_COLOR_MEASURE, $FONT_MEASURE, $MBPS_TEXT);
  627. // ul
  628. imagefttext($im, $FONT_LABEL_SIZE_BIG, 0, $POSITION_X_UL - $ulBbox[4] / 2, $POSITION_Y_UL_LABEL, $TEXT_COLOR_LABEL, $FONT_LABEL, $UL_TEXT);
  629. imagefttext($im, $FONT_METER_SIZE_BIG, 0, $POSITION_X_UL - $ulMeterBbox[4] / 2, $POSITION_Y_UL_METER, $TEXT_COLOR_UL_METER, $FONT_METER, $ul);
  630. imagefttext($im, $FONT_MEASURE_SIZE_BIG, 0, $POSITION_X_UL - $mbpsBbox[4] / 2, $POSITION_Y_UL_MEASURE, $TEXT_COLOR_MEASURE, $FONT_MEASURE, $MBPS_TEXT);
  631. // isp
  632. imagefttext($im, $FONT_ISP_SIZE, 0, $POSITION_X_ISP, $POSITION_Y_ISP, $TEXT_COLOR_ISP, $FONT_ISP, $ispinfo);
  633. // separator
  634. imagefilledrectangle($im, 0, $SEPARATOR_Y, $WIDTH, $SEPARATOR_Y, $SEPARATOR_COLOR);
  635. // timestamp
  636. imagefttext($im, $FONT_TIMESTAMP_SIZE, 0, $POSITION_X_TIMESTAMP, $POSITION_Y_TIMESTAMP, $TEXT_COLOR_TIMESTAMP, $FONT_TIMESTAMP, $timestamp);
  637. // watermark
  638. imagefttext($im, $FONT_WATERMARK_SIZE, 0, $POSITION_X_WATERMARK, $POSITION_Y_WATERMARK, $TEXT_COLOR_WATERMARK, $FONT_WATERMARK, $WATERMARK_TEXT);
  639. // send the image to the browser
  640. header('Content-Type: image/png');
  641. imagepng($im);
  642. }
  643. /**
  644. * Which of the two designs to draw.
  645. *
  646. * The frontends both link the same URL, so the image cannot tell which page a
  647. * result came from. An explicit style parameter decides, which is what lets the
  648. * modern frontend ask for the design it matches and what makes an existing
  649. * share link render either way on demand.
  650. *
  651. * Without one the image follows useNewDesign, the same switch the frontend
  652. * already reads, so a deployment has one setting rather than two that can
  653. * disagree. A missing or unreadable config.json means classic, which is what
  654. * design-switch.js falls back to as well.
  655. *
  656. * @return string
  657. */
  658. function resultImageStyle()
  659. {
  660. if (isset($_GET['style'])) {
  661. $style = strtolower(trim((string) $_GET['style']));
  662. if ('modern' === $style) {
  663. return 'modern';
  664. }
  665. if ('classic' === $style) {
  666. return 'classic';
  667. }
  668. }
  669. $config = json_decode((string) @file_get_contents(__DIR__.'/../config.json'), true);
  670. if (is_array($config) && isset($config['useNewDesign']) && true === $config['useNewDesign']) {
  671. return 'modern';
  672. }
  673. return 'classic';
  674. }
  675. $speedtest = getSpeedtestUserById($_GET['id']);
  676. if (!is_array($speedtest)) {
  677. exit(1);
  678. }
  679. if ('modern' === resultImageStyle()) {
  680. drawModernImage($speedtest);
  681. } else {
  682. drawClassicImage($speedtest);
  683. }