phpfontsgd

Generated image with text always off-center


I have a simple PHP avatar generator that takes someone's username, and makes an image with their initials on it. This works well, but somehow the text is always off center and I cannot figure out have to always have the text properly centered.

This uses the Ubuntu font.

$im = imagecreatetruecolor(125, 125);

$str = 'dpoint';
if (str_word_count($str) > 1)
{
    $words = explode(' ', $str);
    $text = $words[0][0]. $words[1][0];
}
else
{
    $text = substr($str, 0, 2);
}

$font_size = '50';
$white = imagecolorallocate($im, 255, 255, 255);
$angle = 0;

// Get image dimensions
$width = imagesx($im);
$height = imagesy($im);
// Get center coordinates of image
$centerX = $width / 2;
$centerY = $height / 2;
// Get size of text
list($left, $bottom, $right, , , $top) = imageftbbox($font_size, $angle, './Ubuntu-L.ttf', $text);
// Determine offset of text
$left_offset = ($right - $left) / 2;
$top_offset = ($bottom - $top) / 2;
// Generate coordinates
$x = $centerX - $left_offset;
$y = $centerY + $top_offset;
// Add text to image
imagettftext($im, $font_size, $angle, $x, $y, $white, './Ubuntu-L.ttf', $text);

// Set the content type header
header('Content-Type: image/jpeg');

// Skip the file parameter using NULL, then set the quality
imagejpeg($im, NULL, 100);

// Free up memory
imagedestroy($im);

That for example ends up like this: enter image description here You can see it's much lower down that it should be. What positioning am I doing wrong here?

Ideally I just want it so no matter the text size or lower-case or upper-case, it just centers it vertically and horizontally properly inside the box.


Solution

  • The y parameter of imagettftext() is the position of the baseline, not of the bottom of the character:

    The y-ordinate. This sets the position of the font's baseline, not the very bottom of the character.

    See the illustration from the Wikipedia page (red line).

    There is no clean way to fix this issue, but here is a workaround:

    Example:

    function hasDescender(string $text): bool
    {
        // Only English letters are tested here
        return preg_match('/[gjpqyQ]/', $text);
    }
    
    function getDescenderHeight(float $font_size, string $font): int
    {
        $box_a = imageftbbox($font_size, 0, $font, 'a');
        $box_p = imageftbbox($font_size, 0, $font, 'p');
        return $box_p[1] - $box_a[1];
    }
    
    $width = 125;
    $height = 125;
    $font_size = 50;
    $font = './Ubuntu-L.ttf';
    $text = 'dp';
    
    $centerX = $width / 2;
    $centerY = $height / 2;
    list($left, $bottom, $right, , , $top) = imageftbbox($font_size, 0, $font, $text);
    $left_offset = ($right - $left) / 2;
    $top_offset = ($bottom - $top) / 2;
    $x = $centerX - $left_offset;
    $y = $centerY + $top_offset;
    if(hasDescender($text))
        $y -= getDescenderHeight($font_size, $font);
    
    $im = imagecreatetruecolor($width, $height);
    $white = imagecolorallocate($im, 255, 255, 255);
    imagettftext($im, $font_size, 0, (int)$x, (int)$y, $white, $font, $text);
    header('Content-Type: image/png');
    imagepng($im);
    imagedestroy($im);
    

    Output:

    enter image description here