NAME

   Image::JpegMinimal - create JPEG previews without headers

SYNOPSIS

       my $compressor = Image::JpegMinimal->new(
           xmax => 42,
           ymax => 42,
           jpegquality => 20,
       );

       sub gen_img {
           my @tags;
           for my $file (@_) {
               my $imager = Imager->new( file => $file );
               my $preview = $compressor->data_preview( $file );
               my ($w, $h ) = ($imager->getwidth,$imager->getheight);

               my $html = <<HTML;
       <img width="${w}px" height="${h}px"
          data-preview="$preview"
          src="$file"
          />
       HTML
               push @tags, $html;
           };

           return @tags
       }

       # This goes into your HTML
       print join "\n", gen_img(@ARGV);

       # The headers accumulate in $compressor
       my %headers = $compressor->headers;

       # This goes into your Javascript
       print $headers{l};
       print $headers{p};

DESCRIPTION

   This module implements the ideas from
   https://code.facebook.com/posts/991252547593574 to create the data
   needed for inline previews of images that can be served within the HTML
   page while keeping a low overhead of around 250 bytes per image
   preview. This is achieved by splitting up the preview image into a JPEG
   header which is common to all images and the JPEG image data. With a
   Javascript-enabled browser, these previews will be shown until the
   request for the real image has finished loading the data. This reduces
   the latency and bandwidth needed until the user sees an image.

   It turns the following image

   into 250 bytes of image data representing this image:

   The Javascript on the client side then scales and blurs that preview
   image to create a very blurry placeholder until the real image data
   arrives from the server.

   See below for the Javascript needed to reassemble the image data from
   the split header and scan data.

METHODS

Image::JpegMinimal->new( %OPTIONS )

     my $compressor = Image::JpegMinimal->new(
         xmax => 42,
         ymax => 42,
         jpegquality => 20,
     );

   Creates a new compressor object. The xmax and ymax values give the
   maximum dimensions for the size of the preview image. It is suggested
   that the preview image is heavily blurred when presenting the preview
   image to the user to hide the JPEG artifacts.

$compressor->data_preview

     my $data_preview = $compressor->data_preview( $file );

   Reads the JPEG data from a file and returns a base64 encoded string of
   the reduced image data. You stuff this into the data-preview attribute
   of the img tag in your HTML.

$compressor->headers

     my %headers = $compressor->headers;

   After processing all files, this method returns the headers that are
   common to the images. You need to pass this to your Javascript.

HTML

   Each image that has a pre-preview placeholder will need to store the
   placeholder data in the data-preview attribute. That is all the
   modification you need. You should also set the width and height
   attributes of the image so that no ugly image-popping occurs when the
   real data arrives. The $payload64 is the data that is returned from the
   ->data_preview call.

       <img width="${final_width}px" height="${final_height}px"
          data-preview="$payload64"
          src="$file"
          />

JAVASCRIPT

   You will need to include some Javascript like the following in your
   page, preferrably near the end so the code runs right after the HTML
   has loaded completely but image loading has not yet fired.

   The hash %headers should be set to the base64 encoded fixed headers as
   returned by the ->headers( $file ) call. The image HTML should have
   been constructed as outlined above.

       "use strict";

       var header = {
           l : atob("$headers{l}"),
           h : atob("$headers{p}"),
       };
       function reconstruct(data) {
           // Reconstruct a JPEG header from our special data structure
           var raw = atob(data);
           // Keep as "char" so we don't have to bother with Unicode vs. ASCII
           var width  = raw.charAt(0);
           var height = raw.charAt(1);
           var payload = raw.substring(2,raw.length);
           var head;
           if( width < height ) {
               head = header["p"]
           } else {
               head = header["l"]
           };
           var dimension_patch = width+height;
           var patched_header = head.substring(0,head.length-13)
                              + width
                              + head.substring(head.length-12,head.length-11)
                              + height
                              + head.substring(head.length-10,head.length);
           var reconstructed = patched_header+payload;
           var encoded = "data:image/jpeg;base64,"+btoa(reconstructed);
           return encoded;
       }

       var image_it = document.evaluate("//img[\@data-preview]",document, null, XPathResult.ANY_TYPE, null);
       var images = [];
       var el = image_it.iterateNext();
       while( el ) {
           images.push(el);
           el = image_it.iterateNext();
       };

       for( var i = 0; i < images.length; i++ ) {
           var el = images[ i ];
           if( !el.complete || el.naturalWidth == 0 || el.naturalHeight == 0) {

               var fullsrc = el.src;
               var loadsrc = reconstruct( el.getAttribute("data-preview"));
               var container = document.createElement('div');
               container.style.overflow = "hidden";
               container.style.display = "inline";
               container.style.position = "relative";

               var parent = el.parentNode;
               parent.insertBefore(container, el);
               container.appendChild(el);

               // Set up the placeholder data
               el.src = loadsrc;
               el.style.filter = "blur(8px)";
               var img = document.createElement('img');
               img.width = el.width;
               img.height = el.height;
               // Shouldn't we also copy the style and maybe even some events?!
               // img = el.cloneNode(true); // except this doesn't copy the eventListeners etc. Duh.
               (function(img,container,src) {
                   img.onload = function() {
                       // Put the loaded child in the place of the preloaded data
                       parent.replaceChild(img,container);
                   };
                   var timeout = 1000+Math.random()*3000;
                   // Kick off the loading
                   // The timeout is just for demonstration purposes
                   // window.setTimeout(function() {
                       img.src = src;
                   //}, timeout);
               }(img,container,fullsrc));
           } else {
               // Image has already been loaded (from cache), nothing to do here
           };
       };

REPOSITORY

   The public repository of this module is
   http://github.com/Corion/image-jpegminimal.

SUPPORT

   The public support forum of this module is https://perlmonks.org/.

BUG TRACKER

   Please report bugs in this module via the RT CPAN bug queue at
   https://rt.cpan.org/Public/Dist/Display.html?Name=Image-JpegMinimal or
   via mail to [email protected].

AUTHOR

   Max Maischein [email protected]

COPYRIGHT (c)

   Copyright 2015 by Max Maischein [email protected].

LICENSE

   This module is released under the same terms as Perl itself.