Question

Question

How do I appropriately match kerning from GD library in PHP (5.2.17) with SVG (1.1)?

Problem

PNG and SVG file text are not identical. Kerning seems to be off.

Situation

I have a web application which creates SVG files and image files (PNGs) from some text and positional data.

Currently, both implementations "work", in as much as everything is being created. The SVG files work appropriately, the PNGs work appropriately. The issue that I'm having is that the text doesn't line up. The kerning seems to be off.

The PNG is going to be our baseline (what we're trying to replicate).

What I Want

I want for the 'l' in "line", to line up with the last 'L' of the second line. Visualization of what is wrong

What I've Tried

  • I have made sure that I'm using the same font for both SVG and PNG files.
  • I have base64 encoded the fonts with the SVG, to make SURE that it's the same font
  • I have sacrificed a goat.

Extra

  • The PHP will be boosted to 5.3, on all PHP 5.3 servers I've tried, I have the same output.
  • The SVG version can change too.
  • The SVG can use groupings and viewBoxes. I just don't have a firm grasp on them yet.

Demo

http://resurrectionwebdesign.com/demo/demo_text.php

Source
Okay, there's a good amount of source here. I tried to simplify it as much as possible; however, what's left is necessary (at least for making it practically accessible for everyone).

<?php
   // Prepare Background Image
   $image_width  =  300;
   $image_height =  200;
   $bg           = imagecreatetruecolor($image_width, $image_height);

   // Make it transparent
   imagesavealpha($bg, true);
   $transparent_color = imagecolorallocatealpha($bg, 0, 0, 0, 127);
   imagefill($bg, 0, 0, imagecolorallocatealpha($bg, 0, 0, 0, 127));

   // Prepare black for the color to use
   $black = imagecolorallocate($bg, 0x00, 0x00, 0x00);

   // Prepare two lines of text.
   $line1['text']     = "Multiple words in a line.";
   $line1['height']   = 22;
   $line1['width']    = 233;
   $line1['pt']       = 17;
   $line1['font']     = "ARIAL.TTF";
   $line1['font_fam'] = 'Arial';
   $line1['x']        = 24;
   $line1['y']        = 94;
   $line1['angle']    = 0;

   $line2['text']     = "Last up last L";
   $line2['height']   = 25;
   $line2['width']    = 160;
   $line2['pt']       = 20.25;
   $line2['font']     = "ARIAL.TTF";
   $line2['font_fam'] = 'Arial';
   $line2['x']        = 68;
   $line2['y']        = 118;

   imagettftext( $bg, 
                 $line1['pt'], 
                 $line1['angle'], 
                 $line1['x'],
                 $line1['y'],
                 $line1['color'],
                 $line1['font'],
                 $line1['text']);

   imagettftext( $bg, 
                 $line2['pt'], 
                 $line2['angle'], 
                 $line2['x'],
                 $line2['y'],
                 $line2['color'],
                 $line2['font'],
                 $line2['text']);

   // Save the PNG file.
   $im = imagepng($bg, 'text_align.png');
   imagedestroy($bg);

   // Prepare SVG
   $svg = <<< EOT
       <svg xmlns="http://www.w3.org/2000/svg"
         xmlns:xlink="http://www.w3.org/1999/xlink"
         xmlns:ev="http://www.w3.org/2001/xml-events" 
         width="{$image_width}px" 
         height="{$image_height}px"
         baseProfile="full"
         version="1.1">
EOT;

   // Line 1
   $svg .= "\r\n\t<text x='{$line1['x']}' y='{$line1['y']}'";
   $svg .= "font-size='{$line1['pt']}pt'  font-family='{$line1['font_fam']}'";
   $svg .= "fill='rgb(0,0,0)'>{$line1['text']}</text>";
   // Line 2
   $svg .= "\r\n\t<text x='{$line2['x']}' y='{$line2['y']}'";
   $svg .= "font-size='{$line2['pt']}pt'  font-family='{$line2['font_fam']}'";
   $svg .= "fill='rgb(0,0,0)'>{$line2['text']}</text>";

   // Close SVG
   $svg .= "\r\n</svg>";

   $file = fopen('text_align.svg', 'w');
   fwrite($file, $svg);
   fclose($file);   

   if(isset($_GET['download']) && !empty($_GET['download']))
   {
      if($_GET['download'] == 'SVG')
      {
         $file   = 'text_align.svg';
         $header = "Content-type: image/svg+xml";
      }
      elseif($_GET['download'] == 'IMAGE')
      {
         $file   = 'text_align.png';
         $header = "Content-type: image/png";
      }
      if(isset($header))
      {
         header($header);
         header('Content-Disposition: attachment; filename="'.$file.'"');
         echo file_get_contents($file);
      }
   }
   else
   {
?>   
<!doctype html>
<html>
   <body>
      Download Image: <a href="demo_text.php?download=IMAGE">Text Align PNG</a>
      <br /><img src='text_align.png' /><br />
      Download SVG: <a href="demo_text.php?download=SVG">Text Align SVG</a>
      <div style="border: 1px dotted blue;"><?echo $svg ?></div>
   </body>
</html>
<?
   }
?>

Updates / Progress

Update 1

I'm not sure if it has anything to do with the image being generated on the server, and the text being rendered on the client. I'm currently running a Windows build, but if I have to I'll throw up a CentOS VM, and test the image on the VM as well as what the image looks like from a browser within the VM.

Update 2

I've tried embedding fonts every way that I know how. So far, the results are all the same. I'll look at trying a transform to see if that works.

Update 3

Okay! Some progress! I found that the people from the SVG WG committee have suggested removing the doctype from the SVG document. I did, and now I'm getting some different results. I'm not sure how much better they are, BUT I'm definitely seeing different things now. So that's a plus.

Update 4
I've tried explicitly setting the spacing between each character (instead of just the sentence), but still no dice. This seemed to offset everything even more. And looked uglier. Though the function works well and is useful for images (possibly analyzing CAPTCHAs?)

Here's the code, if anyone is interested:

/**
* Function: Returns the space between letters, and the width of letters.
*           The first index is set to 0, as that is where it starts
*           Since spaces don't register (no pixels), strlen may not work.
* $image to analyze vertical gaps (from imagecreatetruecolor)
*/
function get_letter_dimensions($image)
{
   $bg           = imagecolorallocatealpha($image, 0, 0, 0, 127);
   $height       = imagesy($image) - 1; // was causing issues without
   $width        = imagesx($image);
   $bottom       = 0;
   $right        = 0;
   $letter_space = array(0=>0); // This holds the pixels between the letters
   $letter_width = array(0=>0); // This holds the width of the letters
   $y            = 0;
   $spacing      = 0;
   $lettering    = 0;
   $data         = array();

   for ($x = 0 ; $x < $width ; $x++) 
   {
      while((imagecolorat($image, $x, $y) == $bg) && ($y < ($height))) $y++;

      if($y == $height) 
      {
         if($lettering) $letter_width[] = $lettering;
         $lettering = 0;
         $spacing++;
      }
      else
      {
         if($spacing) $letter_space[] = $spacing;
         $spacing = 0;
         $lettering++;
      }
      $y = 0;
   }

   foreach($letter_space as $k=>$val) $data[$k] = $val + $letter_width[$k];

   return $data;      
}

Update 5

The mighty attribute called "textLength" has been some help to me! I haven't gotten it to work COMPLETELY yet, BUT it's definitely a marked improvement. There are still some scaling issues that I'm having. I'm not sure if it's acceptable yet, however, it's pretty close.

Update 6

Here is what I've found the textLength element doing:

enter image description here

So, as you can see, the second line "Line up last L" look pretty much perfect, as far as lining up goes, WITHOUT textLength. However, the first line "Multiple words in a line" looks terrible. Everything is WAY off. WITH textLength, however, it seems to be a much closer fit.

Update 7

I think I solved it. Instead of just doing "textLength" and setting it to the length provided, I adjusted it by 2px, and it lines up pretty well. The tiny differences could simply be because of pixel width on the server vs the granularity of the SVG placement. I don't know if there's really anything more I can do to change it. I'm sure there is, but I'm going to answer my own question and mark it as solved.

Was it helpful?

Solution

OP, it's me again, OP. I just solved my problem, as closely as I'll be able to, that is.

I modified the text element generation of the SVG portion.

Previously

$svg .= "fill='rgb(0,0,0)'>{$line2['text']}</text>";

Modification

$line2['width'] = $line2['width'] + 2;
$svg .= "fill='#000' textLength='{$line2['width']}px'>$line2['text']}</text>";

This is going to have to be sufficient. Thank you, @feeela, for your help. It was much appreciated.

To any future people who read this question, I hope my plumb into insanity was beneficial to you too.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top