Lazyload kirbytag with unknown image height

Hi
I would like to create a kirbytag for lazyloading images with unknow heights as mentionned here for example. It’s a cool way to maintain aspect ratio during the loading page.

This is my tag now, I have found it from Sonja response in this topic and added a src with base64 1x1 transparent png.

<?php
kirbytext::$tags['image'] = array(
  'attr' => array(
    'width',
    'height',
    'alt',
    'text',
    'title',
    'class',
    'imgclass',
    'linkclass',
    'caption',
    'link',
    'target',
    'popup',
    'rel'
  ),
  'html' => function($tag) {

    $url     = $tag->attr('image');
    $alt     = $tag->attr('alt');
    $title   = $tag->attr('title');
    $link    = $tag->attr('link');
    $caption = $tag->attr('caption');
    $file    = $tag->file($url);

    // use the file url if available and otherwise the given url
    $url = $file ? $file->url() : url($url);

    // alt is just an alternative for text
    if($text = $tag->attr('text')) $alt = $text;

    // try to get the title from the image object and use it as alt text
    if($file) {

      if(empty($alt) and $file->alt() != '') {
        $alt = $file->alt();
      }

      if(empty($title) and $file->title() != '') {
        $title = $file->title();
      }

    }

    // at least some accessibility for the image
    if(empty($alt)) $alt = ' ';

    // link builder
    $_link = function($image) use($tag, $url, $link, $file) {

      if(empty($link)) return $image;

      // build the href for the link
      if($link == 'self') {
        $href = $url;
      } else if($file and $link == $file->filename()) {
        $href = $file->url();
      } else if($tag->file($link)) {
        $href = $tag->file($link)->url();
      } else {
        $href = $link;
      }

      return html::a(url($href), $image, array(
        'rel'    => $tag->attr('rel'),
        'class'  => $tag->attr('linkclass'),
        'title'  => $tag->attr('title'),
        'target' => $tag->target()
      ));

    };

    // image builder
    $_image = function($class) use($tag, $url, $alt, $title) {
      return html::img('data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', array(
        'width'  => $tag->attr('width'),
        'height' => $tag->attr('height'),
        'class'  => $class,
        'title'  => $title,
        'alt'    => $alt,
        'data-original' => $url // add data-src attribute
      ));
    };

    if(kirby()->option('kirbytext.image.figure') or !empty($caption)) {
      $image  = $_link($_image($tag->attr('imgclass')));
      $figure = new Brick('figure');
      $figure->addClass($tag->attr('class'));
      $figure->append($image);
      if(!empty($caption)) {
        $figure->append('<figcaption>' . kirbytext($caption) . '</figcaption>');
      }
      return $figure;
    } else {
      $class = trim($tag->attr('class') . ' ' . $tag->attr('imgclass'));
      return $_link($_image($class));
    }

  }
);

The goal here is to wrap a div inside figure, adding padding from the image ratio. e.g. :

<figure>
<div class="container" style="width:100%; padding-bottom: calc(100% / <?php echo $vignette->ratio() ?>)"  >
     
          <img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" 
               data-original="<?php echo thumb($vignette, array('width' => 900))->url() ?>" 
      
      </div>
</figure>

With this style

.container {
 display: block;
 position: relative;
 height: 0;
}

.container img {
 position: absolute;
 top: 0;
 left: 0;
 width: 100%;
 height: 100%;
 padding-bottom: calc(100% / <?php echo $image->ratio() ?>)
}

So how can I make that with kirbytag ?

  • wrap a div inside the default figure from the tag
  • using aspect ratio from image
  • generate a thumb (for a better loading time)

Why do you need the additional div container? You might as well add the container class to the figure tag? Am I missing something?

Apart from that, you can use append() to append the div before you append the image, I think.

You right, no need for another div !
But I don’t understand this line : $figure = new Brick('figure'); and how to apply style (with the aspect ratio corresponding to the image) on the figure
I have to say I don’t know the append() method and how to use it.

That code creates a new figure tag using the Brick class, you can then use the methods of that class to add classes (addClass(), see example above) or append etc. See the documentation or source code.

Instead of using Brick, you might as well create a snippet with your HTML and use that instead.

A snippet sounds more confortable to me. I tried but I can’t get the array of values from the image builder (from my kirbytag), wich seems to be part of $_image.

$figureimage = snippet('figure', $_image, true);

I have set a figure.php file in /snippet and this code for the begining :

<img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" 
     data-original="<?php echo $data-original ?>" />

And the image builder from the kirbytext is :

// image builder
    $_image = function($class) use($tag, $url, $alt, $title) {
      return html::img('data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', array(
        'width'  => $tag->attr('width'),
        'height' => $tag->attr('height'),
        'class'  => 'b-lazy',
        'title'  => $title,
        'alt'    => $alt,
        'data-original' => $url // add data-src attribute
      ));
    };

Tomorrow, too tired now.

That article you referenced is a few years old now. Lazy Loaders have improved since. You could try lozad, which is extremely fast. I’ve not noticed any content flashing, even with dozens of images on the page. It even works on the picture tag and iframes.

Thanks but I’m not sure Lozad does better. I didn’ test it on a normal page with images + text, but I tested it on my case with a masonry layout, and it doesn’t work.As you said, Interaction Oberserver is faster and smarter, but that’s the only points. Masonry have to know the height of each grid-item before loading the script, that’s why I’m doing that…:wink:

Your syntax is not correct, the snippet() function expects an array as second parameter:

$figureimage = snippet('figure', ['_image' => $_image], true);

@jimbobrjames Off topic, how does Lozad.js compare to Lazysizes.js (which I have used in the past), any insights?

I don’t believe I have used lazysizes before, so an “in practice” comparison is hard. However, comparing the demo pages with an empty cache, lozad seems to do its job faster. Its also 3x smaller than lazysizes. (just over 1kb versus 3.3kb for lazysizes after gzip). You can get the gzip transfer size on a file in the file in terminal by running:

gzip -c yourfile.min.js | wc -c

If you want to see lozad in action on a real website, i’m using it all over The Work of Others site, and on Hash&Salt.

1 Like

If think my limits are reached here…:confused: I have to deal with caption, I don’t know how to set the ratio image or if I have to use another array. This is my actual code, knowing it’s not complete or working.

if(kirby()->option('kirbytext.image.figure') or !empty($caption)) {
      $image  = $_link($_image($tag->attr('imgclass')));
      $figureimage = snippet('figure', ['_image' => $_image], true);
     
      if(!empty($caption)) {
        $figure->append('<figcaption>' . kirbytext($caption) . '</figcaption>');
      }
      return $figureimage;
    } else {
      $class = trim($tag->attr('class') . ' ' . $tag->attr('imgclass'));
      return $_link($_image($class));
    }

Well, I finally used the first method with Bricks.
I could set the style to the figure with :

$figure->attr('style', 'position:relative;height: 0;display:block');

And the style of the image with :

// image builder
    $_image = function($class) use($tag, $url, $alt, $title) {
      return html::img('data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', array(
        'width'  => $tag->attr('width'),
        'height' => $tag->attr('height'),
        'class'  => 'b-lazy',
        'style' => 'position: absolute;top: 0;left: 0;width: 100%;height: 100%; padding-bottom:calc(100% / ' . $image->ratio . ')', // need to put the ratio here,
        'title'  => $title,
        'alt'    => $alt,
        'data-original' => $url // add data-src attribute
      ));
    };

What I just need now is to set the ratio image with padding-bottom:calc(100% / ' . $image->ratio . ')'

Do you know how to retrieve the image ratio ?

Doesn’t

$ratio = $file->ratio();

work? I seem to missing the problem?

But note that ration returns something like 0.8 or 1.2.

Arg, sorry, firstly the padding-bottom has to be set on the figure (the container). And nope, the ratio doesn’t seem to work here :confused:

$ratio = $file->ratio();
    if(kirby()->option('kirbytext.image.figure') or !empty($caption)) {
      $image  = $_link($_image($tag->attr('imgclass')));
      $figure = new Brick('figure');
      $figure->addClass($tag->attr('class'));
      $figure->attr('style', 'position:relative;height: 0;display:block;padding-bottom:calc(100% / '. $ratio . ')');
      $figure->append($image);
      if(!empty($caption)) {
        $figure->append('<figcaption>' . kirbytext($caption) . '</figcaption>');
      }
      return $figure;
    }

Method Kirbytext::__toString() must not throw an exception, caught Error: Call to a member function ratio() on null

I have a bit lost track where you are using this code, if that’s within a callback, you have to add $file in the list of variables passed to the use() keyword.

I think I have done ! Tell me what do you think about the code, in my case, it seems ok.

I have set the $ratio inside the if statement, and I can use it inside the figure style after the brick method.

<?php
kirbytext::$tags['image'] = array(
  'attr' => array(
    'width',
    'height',
    'alt',
    'text',
    'title',
    'class',
    'imgclass',
    'linkclass',
    'caption',
    'link',
    'target',
    'popup',
    'rel'
  ),
  'html' => function($tag) {

    $url     = $tag->attr('image');
    $alt     = $tag->attr('alt');
    $title   = $tag->attr('title');
    $link    = $tag->attr('link');
    $caption = $tag->attr('caption');
    $file    = $tag->file($url);

    // use the file url if available and otherwise the given url
    $url = $file ? $file->url() : url($url);

     // thumbs
    $url = thumb($file, array('width' => '1920', 'quality' => 100))->url();
    
    
    
    // alt is just an alternative for text
    if($text = $tag->attr('text')) $alt = $text;

    // try to get the title from the image object and use it as alt text
    if($file) {

      $ratio = $file->ratio(); // HERE THE RATIO
      
      if(empty($alt) and $file->alt() != '') {
        $alt = $file->alt();
      }

      if(empty($title) and $file->title() != '') {
        $title = $file->title();
      }

    }

    // at least some accessibility for the image
    if(empty($alt)) $alt = ' ';

    // link builder
    $_link = function($image) use($tag, $url, $link, $file) {

      if(empty($link)) return $image;

      // build the href for the link
      if($link == 'self') {
        $href = $url;
      } else if($file and $link == $file->filename()) {
        $href = $file->url();
      } else if($tag->file($link)) {
        $href = $tag->file($link)->url();
      } else {
        $href = $link;
      }

      return html::a(url($href), $image, array(
        'rel'    => $tag->attr('rel'),
        'class'  => $tag->attr('linkclass'),
        'title'  => $tag->attr('title'),
        'target' => $tag->target()
      ));

    };

    
    // image builder
    $_image = function($class) use($tag, $url, $alt, $title) {
      return html::img('data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==', array(
        'width'  => $tag->attr('width'),
        'height' => $tag->attr('height'),
        'class'  => 'b-lazy',
        'style' => 'position: absolute;top: 0;left: 0;width: 100%;height: 100%;',
        'title'  => $title,
        'alt'    => $alt,
        'data-original' => $url // add data-src attribute
      ));
    };

    if(kirby()->option('kirbytext.image.figure') or !empty($caption)) {
      $image  = $_link($_image($tag->attr('imgclass')));
      $figure = new Brick('figure');
      $figure->addClass($tag->attr('class'));
      $figure->attr('style', 'position:relative;height: 0;display:block;padding-bottom:calc(100% / '. $ratio .')');
      $figure->append($image);
      if(!empty($caption)) {
        $figure->append('<figcaption>' . kirbytext($caption) . '</figcaption>');
      }
      return $figure;
    } else {
      $class = trim($tag->attr('class') . ' ' . $tag->attr('imgclass'));
      return $_link($_image($class));
    }

  }
);

Looks good to me; only you should define $ratio = null so the variable is defined also if $file is false.

1 Like

I amost forget : Thank you very much Sonja for your help ! This kirbytag will be very usefull for my futur projects and I will use it couple of times. Bye :wave:

1 Like