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).

    Here is a way to fix this issue:

    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

    EDIT: it turns out there is a better solution, see Hakre's answer.