Image-JpegMinimal

 view release on metacpan or  search on metacpan

lib/Image/JpegMinimal.pm  view on Meta::CPAN

L<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

=for html
  <img width="285" height="427" src="t/data/IMG_7468.JPG" />
  <img width="285" height="427" src="../../t/data/IMG_7468.JPG" />

into 250 bytes of image data representing this image:

=for html
  <img width="28" height="42" src="t/data/IMG_7468_preview.JPG" />
  <img width="28" height="42" src="../../t/data/IMG_7468_preview.JPG" />

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.

=for html
  <img width="285" height="427" src="t/data/IMG_7468_blurred.JPG" />
  <img width="285" height="427" src="../../t/data/IMG_7468_blurred.JPG" />

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

=head1 METHODS

=head2 C<< Image::JpegMinimal->new( %OPTIONS ) >>

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

Creates a new compressor object. The C<xmax> and C<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.

=cut

sub new {
    my( $class, %options ) = @_;

    # We really need Jpeg-support
    croak "We really need jpeg support but your version of Imager doesn't support it"
        unless $Imager::formats{'jpeg'};

    $options{ jpegquality } ||= 20;
    $options{ xmax } ||= 42;
    $options{ ymax } ||= 42;

    bless \%options => $class
}

sub get_imager {
    my( $self, $file ) = @_;
    # We should check that Imager can write jpeg images
    Imager->new( file => $file )
        or croak "Couldn't read $file: " . Imager->errstr();
}

sub compress_image {
    my( $self, $file, $xmax, $ymax, $jpegquality ) = @_;
    $xmax ||= $self->{xmax};
    $ymax ||= $self->{ymax};
    $jpegquality ||= $self->{jpegquality};
    my $imager = $self->get_imager( $file );
    
    # Rotate if EXIF data indicates portrait, this wrecks our headers,
    # so disabled :-((
    # We need two headers, one for portrait and one for landscape
    if( my $orientation = $imager->tags(name => 'exif_orientation')) {
        my %rotate = (
            1 => 0,
            #2 => 180,
            3 => 180,
            #4 => 0,
            #5 => 90,
            6 => 270,
            #7 => 0,
            8 => 90,
        );
        my $deg = $rotate{ $orientation };
        $imager = $imager->rotate( right => $deg );
    };
    
    # Resize
    $imager = $imager->scale(xpixels=> $xmax, ypixels=> $ymax, type=>'min')
        or die Imager->errstr;
    # Write with Q20
    $imager->write(type => 'jpeg', data => \my $data, jpegquality => $jpegquality);

    # Debug output for checking the original and reconstruction
    # of the image data in base64
    #(my $data64 = encode_base64($data)) =~ s!\s+!!g;
    #print $data64,"\n";
    
    my( $width,$height ) = ($imager->getheight, $imager->getwidth);
    return ($width,$height,$data);
}

sub strip_header {
    my( $self,$width,$height,$jpeg ) = @_;
    
    # Deparse the JPEG file into its sections
    # Maybe some other module already provides a JPEG header parser?
    my @sections;
    while($jpeg =~ /\G(((\x{ff}[^\0\x{d8}\x{d9}])(..))|\x{ff}\x{d8}|\x{ff}\x{d9})/csg) {
        my $header = $3 || $1;
        my $payload;
        if( $header eq "\x{ff}\x{da}" ) {
            # Start of scan

 view all matches for this distribution
 view release on metacpan -  search on metacpan

( run in 1.784 second using v1.00-cache-2.02-grep-82fe00e-cpan-1925d2aa809 )