$size * 1.1) { [$lines, $size] = [$candidate, $fit]; } } $image = imagecreatetruecolor(self::WIDTH, self::HEIGHT); if ($image === false) { return null; } imagesavealpha($image, true); imagealphablending($image, false); imagefill($image, 0, 0, imagecolorallocatealpha($image, 0, 0, 0, 127)); imagealphablending($image, true); [$r, $g, $b] = sscanf(ltrim($hexColor, '#'), '%02x%02x%02x'); $color = imagecolorallocate($image, $r ?? 0, $g ?? 0, $b ?? 0); $lineHeight = $size * 1.18; $top = (self::HEIGHT - $lineHeight * count($lines)) / 2; foreach ($lines as $i => $line) { $box = imagettfbbox($size, 0, $font, $line); $lineWidth = $box[2] - $box[0]; $x = (int) round((self::WIDTH - $lineWidth) / 2 - $box[0]); // Baseline: the line's box, plus the font's ascent (about 0.93 of the size for Roboto) $y = (int) round($top + $i * $lineHeight + $size * 0.93 + ($lineHeight - $size * 1.17) / 2); imagettftext($image, $size, 0, $x, $y, $color, $font, $line); } ob_start(); imagepng($image); imagedestroy($image); return ob_get_clean() ?: null; } /** * The name on one line, then every split into two lines, then into three. Lines break at spaces * and after hyphens (Rotterdam-|Zuid keeps its hyphen at the end of the line). */ private static function layouts(string $name): array { // Parts and the glue that goes before each part when they stay on one line $parts = []; $glue = []; foreach (preg_split('/\s+/u', $name) as $w => $word) { foreach (preg_split('/(?<=-)(?=.)/u', $word) as $p => $part) { $glue[] = $p > 0 ? '' : ($w > 0 ? ' ' : ''); $parts[] = $part; } } // A long single word without hyphens: hyphenate it into two or three equal chunks as a last resort if (count($parts) === 1 && mb_strlen($name) > 16) { $layouts = [[$name]]; foreach ([2, 3] as $chunks) { $length = (int) ceil(mb_strlen($name) / $chunks); $lines = []; for ($c = 0; $c < $chunks; $c++) { $lines[] = mb_substr($name, $c * $length, $length) . ($c < $chunks - 1 ? '-' : ''); } $layouts[] = array_values(array_filter($lines, fn ($l) => $l !== '' && $l !== '-')); } return $layouts; } $count = count($parts); $join = function (int $from, int $to) use ($parts, $glue) { $line = ''; for ($k = $from; $k < $to; $k++) { $line .= ($k > $from ? $glue[$k] : '') . $parts[$k]; } return $line; }; $layouts = [[$name]]; for ($i = 1; $i < $count; $i++) { $layouts[] = [$join(0, $i), $join($i, $count)]; } for ($i = 1; $i < $count; $i++) { for ($j = $i + 1; $j < $count; $j++) { $layouts[] = [$join(0, $i), $join($i, $j), $join($j, $count)]; } } return $layouts; } /** The largest font size at which every line fits 84% of the width and all lines 64% of the height. */ private static function fittingSize(array $lines, string $font): int { for ($size = 260; $size > 20; $size -= 4) { [$width, $height] = self::measure($lines, $font, $size); if ($width <= self::WIDTH * 0.84 && $height <= self::HEIGHT * 0.64) { return $size; } } return 20; } /** Width of the widest line and the total height, at a font size. */ private static function measure(array $lines, string $font, int $size): array { $width = 0; foreach ($lines as $line) { $box = imagettfbbox($size, 0, $font, $line); $width = max($width, $box[2] - $box[0]); } return [$width, $size * 1.18 * count($lines)]; } }