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);
Danny Shepherd
  • 363
  • 1
  • 3
  • 17

1 Answers1

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.

KIKO Software
  • 15,283
  • 3
  • 18
  • 33
  • 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. – Danny Shepherd Dec 13 '20 at 13:28
  • 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? – Danny Shepherd Dec 13 '20 at 13:40
  • @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. – KIKO Software Dec 13 '20 at 14:35
  • 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? [example](https://pastebin.com/1UAy7H31) – Danny Shepherd Dec 13 '20 at 14:41
  • @DannyShepherd You haven't changed the arguments, the `0` for the angle is still there, I removed that, because I don't use it. – KIKO Software Dec 13 '20 at 14:45
  • 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? https://pastebin.com/gwLZKYBd – Danny Shepherd Dec 13 '20 at 14:51
  • `imagettftextSpacing($im, $size2, $x, -$bbox[7], $textColourUse, $fontBold, $textToUse, $spacing);` is that still wrong? – Danny Shepherd Dec 13 '20 at 14:52
  • @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. – KIKO Software Dec 13 '20 at 14:57
  • 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)` – Danny Shepherd Dec 13 '20 at 15:05
  • @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. – KIKO Software Dec 13 '20 at 15:07
  • Ahh perfect, it works - you're amazing! Thank you so much for your time and help KIKO!! – Danny Shepherd Dec 13 '20 at 15:12