PHP imageftbbox imagettftext - simple letter spacing/kerning?
Asked Answered
L

1

3

Does anyone know a simple way to do letter spacing/kerning using imagettftext? I have my script working just as I need now, but I could really do with the generated text having the CSS style

letter-spacing: -0.01em;

so it matches the standard text on the page, but I don't see any way to do this easily. I did find the following thread relating to this, but I've tried to fit the answers into my code and none of them had the desired effect.

php imagettftext letter spacing

My current code is as follows

<?php
include 'deliverytimes.php';

$date = new DateTime();
$now = date("Y-m-d H:i:s");
$h = date("H:i:s"); 

$days = explode(",", $businessDaysToAdd);
if (count($days) > 1) {
      
    $two_weekdays_later_1 = strtotime(date("Y-m-d H:i:s", strtotime($now)) . " +" . $days[0] . " weekdays $h");
    $date_1 = new DateTime("@$two_weekdays_later_1"); 
    $formattedDeliveryDate_1 =  $date_1->format('jS M');
    $formattedDeliveryDate_3 =  $date_1->format('jS \o\f F');
    
    $two_weekdays_later_2 = strtotime(date("Y-m-d H:i:s", strtotime($now)) . " +" . $days[1] . " weekdays $h");
    $date_2 = new DateTime("@$two_weekdays_later_2"); 
    $formattedDeliveryDate_2 =  $date_2->format('jS M.');
    $formattedDeliveryDate_4 =  $date_2->format('jS \o\f F');   

    $formattedDeliveryDate1 = $formattedDeliveryDate_3;
    $formattedDeliveryDate2 = $formattedDeliveryDate_4;

    $formattedDeliveryDate = "If ordered today we estimate delivery to be approximately between " . $formattedDeliveryDate_1 . " and " . $formattedDeliveryDate_2;
} else {
    $h = date("H:i:s");   
    $two_weekdays_later = strtotime(date("Y-m-d H:i:s", strtotime($now)) . " +" . $businessDaysToAdd . " weekdays $h");
    $date = new DateTime("@$two_weekdays_later"); 
    $formattedDeliveryDate = "If ordered today we estimate delivery approximately by " . $date->format('l, jS M.');
}

$defaultOutput = 'main';
$textMobile = isset($_REQUEST['mobile']) ? $_REQUEST['mobile'] : $defaultOutput;

switch($textMobile) {
    case "main":
        $textToUse = $formattedDeliveryDate;
        break;
    case "p1":
        $textToUse = $formattedDeliveryDate1;        
        break;
    case "p2":
        $textToUse = $formattedDeliveryDate2;        
        break;
}

// Path to our font file
$font = './Inter-SemiBold.ttf';
$fontBold = './Inter-Bold.ttf';
$size = 24;
$size2 = 83;
$bbox   = imageftbbox($size2, 0, $fontBold, $textToUse);
$width  = 1020;
$height = 110;
$im    = imagecreatetruecolor($width, $height);
$x = ($width - ($bbox[4] - $bbox [0])) / 2;
imagealphablending($im, false);
imagesavealpha($im, true);
$white = imagecolorallocate($im, 255, 255, 255);
$black = imagecolorallocate($im, 0, 0, 0);
$grey = imagecolorallocate($im, 161, 161, 168);
$trans = imagecolorallocatealpha($im, 255, 255, 255, 127);
imagefilledrectangle($im, 0, 0, $width, $height, $trans);

$defaultTextColour = 'white';
$textColour = isset($_REQUEST['colour']) ? $_REQUEST['colour'] : $defaultTextColour;

switch($textColour) {
    case "white":
        $textColourUse = $white;
        break;
    case "black":
        $textColourUse = $black;        
        break;
    case "grey":
        $textColourUse = $grey;        
        break;
}

// Write it
imagettftext($im, $size2, 0, $x, -$bbox[7], $textColourUse, $fontBold, $textToUse);

// Output to browser
header('Content-Type: image/png');
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
imagepng($im);
imagedestroy($im);
Lapland answered 10/12, 2020 at 17:53 Comment(0)
C
3

I can see what you mean. Without any kerning your output looks like this:

enter image description here

and when I take the accepted answer, which is good enough for your purposes, it looks like this for spacing set to -0.5:

enter image description here

Notice how the space between the f and e is still quite big whereas in other places the letters can even overlap.

What causes this, and is there a way to prevent it?

Let's start by drawing the box, returned by imagettfbbox() around the letters:

enter image description here

Clearly that routine doesn't quite work as planned. You would expect the horizontal overlap, if it is there, to be the same for all boxes.

To explain this is somewhat difficult, it has to do with font kerning. Kerning can subtract space before and after a letter:

enter image description here

It's important to realize that the $x position you give to imagettftext() takes this kerning into account. So even though you say a letter should be draw at (x,y) that is not one of the coordinates returned by imagettfbbox(). That function returns the bounding box of the drawn letter, as shown by the red rectangles.

Now lets see if, with this knowledge, we can draw evenly spaced letters:

enter image description here

Clearly that is possible. The code I used was this:

function imagettftextSpacing($image, $size, $x, $y, $color, $font, $text, $spacing = 0)
{
    foreach (mb_str_split($text) as $char)
    {
        $frontBox = imagettfbbox($size, 0, $font, $char);
        $charBox  = imagettftext($image, $size, 0, $x - $frontBox[0], $y, $color, $font, $char);
        $charW    = $charBox[2] - $charBox[0];
        $x       += $charW + $spacing;
    }
}

This gets the horizontal offset of a character and corrects for that when drawing the character.

Although this is an improvement, it is not yet completely satisfactory. Some letters touch, and others do not. This is because we corrected for the kerning at the front of a character, but not for the kerning at the back. We somehow need to find out what the kerning at the back is, and then correct for it. I botched together this code:

function getBBoxW($bBox)
{
  return $bBox[2] - $bBox[0];
}


function imagettftextSpacing($image, $size, $x, $y, $color, $font, $text, $spacing = 0)
{
    $testStr = 'test';
    $testW   = getBBoxW(imagettfbbox($size, 0, $font, $testStr));
    foreach (mb_str_split($text) as $char)
    {
        $fullBox = imagettfbbox($size, 0, $font, $char . $testStr);
        imagettftext($image, $size, 0, $x - $fullBox[0], $y, $color, $font, $char);
        $x      += $spacing + getBBoxW($fullBox) - $testW;
    }
}

I made a separate function to get the width of a bounding box, because I compute it a lot, and the function name makes better clear what is being done. I use a test string, and check how wide that is, so I can later compute what a character in front of it does. I finally compensate for that. The result is this:

enter image description here

This is clearly better, you have a hard time spotting that this is spaced closer than the example at the beginning of this answer, but it is.

I have to admit this is rather complex, and I have no idea why you would want to do this. Using basic HTML and CSS could do the same job better.

Corriecorriedale answered 11/12, 2020 at 15:21 Comment(12)
Hi Kiko, once again thank you for your amazing and informative answer - I'll now have to try and put into my code. You're right it is complex and the reason why is because i'm using it for eBay listings where we have no javascript, no jquery, nothing but pure HTML and CSS - so to create a dynamic bit of info the only hack is to use PHP spitting out an image file. Definitely not idea - but working within very tight restrictions.Lapland
Actually KIKO i'm lost here - I was only hacking about at some code a friend did for me - how would I put the code you've done into the one I posted to create the desired out?Lapland
@DannyShepherd Just replace the imagettftext() with this new imagettftextSpacing() and note that two arguments have changed, the angle is gone, and I added the spacing. Centering the text is another matter, I haven't done that. You have to count the number of characters, and together with the spacing, you specified, you can recenter it.Corriecorriedale
i've tried the following, and i've updated the $ for the ones used in my script - but i'm getting no result in the image box, is that because it's disappearing or is something else wrong? exampleLapland
@DannyShepherd You haven't changed the arguments, the 0 for the angle is still there, I removed that, because I don't use it.Corriecorriedale
Sorry for being dumb, I'm not really understanding - i've changed it to this (remove the 0 from the result) and still don't get anything. Do I have remove 0 from everywhere in the script? pastebin.com/gwLZKYBdLapland
imagettftextSpacing($im, $size2, $x, -$bbox[7], $textColourUse, $fontBold, $textToUse, $spacing); is that still wrong?Lapland
@DannyShepherd I can't really test your code, missing the include 'deliverytimes.php';, but that looks good, it should work. If it doesn't try some debugging. Switch on error reporting and check the error log. Play a bit with it, this isn't overly complex code. One thing I have notice is that you're using the variable $textColourUse in my routine, whereas I don't have it, I call it $color. If the colors don't match you probably see nothing, and you should get a warning in your error log.Corriecorriedale
I'm gettting ` Fatal error: Call to undefined function mb_str_split() in /var/www/html/deecies/js/deliverym1c.php on line 96` which is foreach (mb_str_split($textToUse) as $char)Lapland
@DannyShepherd OK, you're using an older PHP version. This was the multibyte version, but you can probably use the normal version, so replace mb_str_split() by str_split() and it should work, providing you corrected the variable name of the color.Corriecorriedale
Ahh perfect, it works - you're amazing! Thank you so much for your time and help KIKO!!Lapland
The function works fine (with str_split version) but it give me the same visual result as standard imagettftext function – without the kerning. By kerning I mean the values included into the font file by the font developer.Farah

© 2022 - 2024 — McMap. All rights reserved.