Auto-Include Width and Height on Img Elements With PHP

How to optimize image-loading and automatically include width and height attributes on img elements with PHP.

1948 views
d

By. Jacob

Edited: 2021-02-11 15:49

auto-add width and height img

One of the things that tends to cause layout shifts doing page-load is missing width and height attributes on img elements. The reason this happens is because a browser will not know the width and height without first downloading an image, and this causes the page-load to feel jerky.

When the HTML first loads and a browser encounters an img in the source without dimensions specified, the browser will collapse the image until it has been loaded and the dimensions are known; once the dimensions are known, a re-paint will take place, and the page content will appear to "jump" down on the page. This often happens so fast that users might not notice, but on some pages, this can be a cause for frustration and accidental clicks on elements that users did not intend to click on.

For this reason, images should generally always have a set width and height. Then you might ask:

Will this not break my responsive images that I control with relative units?

It actually will not have to — you can prevent breaking the aspect ratio on your responsive images by including the following somewhere in your CSS:

img {aspect-ratio: attr(width) / attr(height);height:auto;}

The height:auto; deceleration is meant to override the height on images in the HTML, and will make possible for the browser to automatically resize your images based on the aspect-ratio; note that the aspect ratio is based on the relationship of the width and height attributes.

The value of the attributes is in pixels by default, and should be specified as an integer, without px at the end. For example:

<img src="/images/some-img.webp" width="50" height="100" alt="example image">

In the following sections, I will show how you can easily add the width and height attributes from PHP while also preserving existing attributes.

Finding images in the HTML using PHP

Before you can add missing width and height attributes to images in the HTML on existing pages automatically, you will need to scan the HTML to find the images that needs to be updated. To do that, you can use the preg_match_all function.

It is easier if you create a function that can be called and reused, but for simplicity I will first give some examples with plain procedural PHP.

The following piece of code can return all of the img elements in the HTML:

$html = get_html_code_from_somewhere();
if (!preg_match_all('/(<img([^>]*)>)/iu', $html, $matches, PREG_PATTERN_ORDER)) {
  exit();
}

The elements themselves are stored in the $matches[1] array, and can be traversed using a simple foreach loop:

foreach ($matches[1] as $element) {
  echo $element . "\n";
}
exit();

This example is just to show you how the regular expression works; but from this point, you just need to extract the src value of the images, and obtain the dimensions using getimagesize — you can even replace the images on-the-fly from the same loop.

You also need to be careful, and make sure you account for failures. If the getimagesize fails, for any reason, the image should just be left untouched in the source.

Extracting attributes from img elements

To obtain the path of images, and avoid loosing existing attributes, you should extract them and re-insert them together with the width and height attributes before replacing the original images in the source.

The most challenging part at this point, is how to extract existing attributes from the img element — for this you should probably create a function, since it will make your code easier to read. As a bonus, you will also be able to call this code in other places where you might need to obtain the attributes of an HTML element. I created the following:

function get_element_attributes(string $element): array {
  if (false !== preg_match_all('/([a-z0-9]+)=[\"\']{1}(.*?)[\"\']{1}/sui', $element, $found_attrs, PREG_PATTERN_ORDER)) {
    $i = 0;
    $attributes = [];
    foreach ($found_attrs[1] as $name) {
      $attributes["$name"] = $found_attrs[2]["$i"];
      ++$i;
    }
    return $attributes;
  }
  return [];
}

The challenging part about making this function, was that the matched attribute names are stored in the $found_attrs[1] array, while the attribute values are stored in $found_attrs[2] — to beat this challenge, I included a counter, $i, and used it to obtain the attribute value from the other array.

If an image does not have any attributes, an empty array is returned instead, and the image is left untouched in the source. This is of course unlikely, but it can happen in cases where people manually edit their HTML. An img element should at least include a src attribute for you to be able to obtain the dimensions of the image. If the image source is updated with JavaScript, this function will not work.

Create the new img element, if needed

If an img already has dimensions applied, or if the src attribute is empty, we leave the image in the source without touching it.

The below code should be placed inside the first loop we created — note that the full code is included last in the article:

$attributes = get_element_attributes($element);
if (
  (!empty($attributes['src'])) &&
  (isset($attributes['width'])) &&
  (isset($attributes['height']))
) {
  // Skip the current iteration of the loop
  continue;
}
// If everything was as desired, subsequent code will get executed

Assuming that everything is as we want, we can now check if the file exist in the local file system; we should perform this check before attempting to use getimagesize on the file.

Note. This assumes that the path in the src attribute is defined as root relative. You need to provide a $site_directory to be able to locate images in the file system; an example would be /var/www/my_site/, but it has to reflect the URL in the src to work.

It is also possible to convert an absolute URL to a root relative URL, but how to do that will not be covered in this tutorial.

You can check that the file exists with file_exists:

if (!file_exists($path)) {
  continue;
}

Finally, you should try to obtain the image dimensions:

if (false === ($img_details = getimagesize($path))) {
  continue;
}

This will also elegantly define the $img_details variable while testing if getimagesize was successful.

Now we can add new attributes, and create a new element to replace the old element in the source:

$attributes['width'] = $img_details[0];
$attributes['height'] = $img_details[1];
$attributes['loading'] = 'lazy';

To create the replacement element:

$new_element = '';
foreach($attributes as $key => $value) {
  $new_element .= ' '. $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
}
$new_element = '<img' . $new_element .'>';

And finally, the original element in the HTML is replaced with the new element:

$html = str_replace($element, $new_element, $html);

The full code:

function img_add_dimensions(string $html, string $site_directory) : string
{
  if (!preg_match_all('/(<img([^>]*)>)/iu', $html, $matches, PREG_PATTERN_ORDER)) {
    return $html;
  }
   
  foreach ($matches[1] as $element) {
    $attributes = get_element_attributes($element);
    if (
      (!empty($attributes['src'])) &&
      (isset($attributes['width'])) &&
      (isset($attributes['height']))
     ) {
      continue;
    }
    $path = $site_directory . ltrim($attributes['src'], '\\');
    if (!file_exists($path)) {
      continue;
    }
    if (false === ($img_details = getimagesize($path))) {
      continue;
    }
    $attributes['width'] = $img_details[0];
    $attributes['height'] = $img_details[1];
    $attributes['loading'] = 'lazy';
            
    $new_element = '';
    foreach($attributes as $key => $value) {
      $new_element .= ' '. $key . '="' . htmlspecialchars($value, ENT_QUOTES) . '"';
    }
    $new_element = '<img' . $new_element .'>';

    $html = str_replace($element, $new_element, $html);
  }
  return $html;
}

function get_element_attributes(string $element): array
{
  if (false !== preg_match_all('/([a-z0-9]+)=[\"\']{1}(.*?)[\"\']{1}/sui', $element, $found_attrs, PREG_PATTERN_ORDER)) {
    $i = 0;
    $attributes = [];
    foreach ($found_attrs[1] as $name) {
      $attributes["$name"] = $found_attrs[2]["$i"];
      ++$i;
    }
    return $attributes;
  }
  return [];
}

Tell us what you think:

  1. In this Tutorial, it is shown how to redirect all HTTP requests to a index.php file using htaccess or Apache configuration files.
  2. How to create a router in PHP to handle different request types, paths, and request parameters.
  3. Tutorial on how to use proxy servers with cURL and PHP
  4. When using file_get_contents to perform HTTP requests, the server response headers is stored in a reserved variable after each successful request; we can iterate over this when we need to access individual response headers.
  5. How to effectively use variables within strings to insert bits of data where needed.

More in: PHP Tutorials