Jump to content

Second Episode: "How can I add a watermark to all pageimages of a site?"


horst
 Share

Recommended Posts

Second Episode: "How can I add a watermark to all pageimages of a site?"

  1. first episode "Tutorial how to add a site wide optional watermarking method to all pageimages"
  2. second episode "Second Episode: "How can I add a watermark to all pageimages of a site?""

In the first episode we created a function that adds a watermark overlay to our original images. This function was hooked via the name "wm" to the pageimage object. We positioned a big centralized overlay to the original images. This suites right if you or your customer will not use cropped portions of the images somewhere down in the site. So, given the setup of the first episode is fine for a sites images concept, one can add a simple security layer to protect the original (unwatermarked) images.
(Read on here how to secure the original images and how to keep them accessible / viewable in the admin for logged in users.)

So in the second episode we want to support sites that need a more individual handling of images, including different output sizes and different cropped parts from an original image. Also we want to show a watermark in the bottom right corner in a 200px width, regardless of the width of the image variation.

1) Setup our site with a new overlay image

1.1) Create a new PNG image of 400 px width and 140 px height. Give it a half transparent background-color (40 - 60 percent opacity) and add some none transparent text over it. The text should be more then in the double size as you want to see it in the output on your site.
For example like this one: 

new-watermark-400x140.png.4151b9ffd1fbd37551412b87fc850b0d.png

1.2) Upload it into the centralized image field (called setting_watermark in the previous episode)

 

2) Lets update our watermark function in site/ready.php

2.1) We now cannot any longer watermark the original images at first, because we don't know the exact final output dimensions and also not the final (maybe cropped) parts of the variations. Therefore we will work with the pageimage objects as usual and add our call to the watermark method as LAST action to each pageimage that should get a watermark overlay.

$image = $page->images->first();
$variation = $image->crop('some settings here')->width(800)->wm();
?>
    <img src='<?=$variation->url?>' alt='<?=$variation->description?>' />

So we will rewrite our function to suite our new needs. The first part can be taken mainly unchanged from the first episode:

/**
* Example Pageimage Hook for watermarking functionality,
* adds a new method to Pageimage objetcs named wm.
* Used in a small tutorial series in the ProcessWire forums
* https://processwire.com/talk/topic/25752-tutorial-how-to-add-a-site-wide-optional-watermarking-method-to-all-pageimages/
*
* @version 0.0.2
*
*/
$this->addHook('Pageimage::wm', function(HookEvent $event) {

    // get the image out of the event object
    $finalImage = $event->object;                   // this is no longer the $originalImage from episode 1 !!

    // access the field of the transparent watermark overlay image in a robust way,
    // regardless if it is set to 1 image or multiple images, and if it has changed
    // in this regard since we wrote this hook function
    $watermarkImages = wire()->pages->get('id=1')->getUnformatted('setting_watermark');

    // check if the imagefield exists and it at least contain one image, than fetch it
    if(!$watermarkImages || 0 == count($watermarkImages)) {

        // return the unmodified original image to do not break our front end site
        $event->return = $finalImage;

        // inform the admin about the missing watermark overlay, (or do something other useful)
        wireMail('admin@example.com', 'noreply@example.com', 'MISSING SITEWIDE WATERMARK OVERLAY IMAGE');
        wireLog('errors', 'MISSING SITEWIDE WATERMARK OVERLAY IMAGE');

        // stop further execution
        return;
    }

 

2.2) Now lets have a short stop and think over. What do we want to achieve?

We want create watermarked image variations and protect the unwatermarked images. Unwatermarked images are the original images and the intermediate variations that are created before we add an overlay to them. So in this situation, when we would following the regular image processing chain, we would create a variation with the resized and / or cropped version and after that, a watermarked variation, that we want to send to the browsers:

  •     original image: basename.jpg
  •     intermediate: basename.800x0.jpg
  •     watermarked: basename.800x0.-pim2-wm.jpg

This would let all intermediate images unsafe in public scope, for everyone who knows a bit about the PW image processing routines. So the best solution would be to create the intermediate variations, watermark them and restore with the intermediate filename, not a filename that reflects that different processing step. This sounds fine, and indeed it is, with only one thing that we have to solve later: How can we detect if the current version is a watermarked (final) variation, or a unwatermarked "real" intermediate variation?

 

2.3) Following we rewrite the function from the first episode.
First we have to check the incoming image, mainly its dimensions and if fits fine with the watermark sizes:


    // as we will overwrite this image with a watermarked version,
    // we have to make sure that this really is a variation and NOT an original image
    $isOriginal = null == $finalImage->getOriginal();
    if($isOriginal) {
        // throw an exception, as this must be a bug in our code
        throw new WireException('Called a watermark method for final variation images on a original image!');
        return;
    }


	// inspect the given image
    $ii = new ImageInspector();
    $info = (object) $ii->inspect($finalImage->filename)['info'];

    // our preferred watermark width is 200px, so lets check if it fits into the variation
    if(200 >= $info->width) {
        // does not fit, is it a portrait oriented image with more then 300 px height?
        if(200 < $info->height) {
            // ok, we have to rotate our watermark -90 degree to make it fit to the bottom right
            $pngAlphaImage = $watermarkImages->first()->width(200)->pim2Load('rot270')->rotate(270)->pimSave();
        } else {
            // the variation is to small for useful watermarking, we will leave and output it as is
            $event->return = $finalImage;
            return;
        }
    } else {
        // the width fits, so get a watermark image of 200px
        $pngAlphaImage = $watermarkImages->first()->width(200);
    }


    // now lets apply the watermark to our final variation,
    // this has to be done a bit different than we have done in the first episode.
    // The difference is, that we previously created another intermediate variation.
    // Now we want to avoid this, as it would make our watermarking processing nearly useless
    // in regard of protecting the unwatermarked images

    // we invoke the Pageimage Manipulator in a way that let us "overwrite" the loaded image,
    // what is not possible within its default processing
    $im = new \PageImageManipulator02();
    $im = $im->imLoad($finalImage->filename, ['targetFilename' => $finalImage->filename])->setTargetFilename($finalImage->filename);
    $filename = $im->watermarkLogo($pngAlphaImage, 'SE', 0)->save();
    $success = $finalImage->filename == $filename; // the imLoad()->save() process returns the filename of the processed image on success

    // $watermarkedImage = $finalImage;     // this just should visualize what has happen in the black box :-)
    // With the previous episode, we had to return the watermarked variation and therefore had to replace the event object with it:
    // $event->return = $watermarkedImage;

    // But here, in fact we have overwritten the discfile that belongs to our incoming image object,
    // so there is no NEW image object that we have to change with the initial one!

    $event->return = $finalImage;

});

 

2.4) So, now we nearly have all together, but not how we can detect if a intermediate variation already has become a watermarked (final) version. Therefore the watermarking process is constantly executed again, with every view call now! So, what options are available to store the variations state for read / write in the processing chain?

  • Into the DB?
    (has to be implemented first, and has to be cleared whenever an image and or its variations gets removed from the system)
  • Into a sidecar file?
    (has to be implemented first, and also has to be cleared whenever an image and or its variations gets removed from the system)
  • Into the variation file itself?
    (can be done via IPTC-data, but only for JPEGs)

We also have since version 3.0.164 the possibility to store additional information for images & files into the DB. But this is only useful for the original image (or the complete group of original image and all its variation files), and not for individual single variation files.

For this example we will go with the IPTC-data and take a simple helper class, that encapsulates this processes for us. For simplicity in this example, we also copy its code into the ready.php file.

 

2.5) Now we add the tracking into our function from above:

...
	// as we will overwrite this image with a watermarked version,
    // we have to make sure that this really is a variation and NOT an original image
    $isOriginal = null == $finalImage->getOriginal();
    if($isOriginal) {
        // throw an exception, as this must be a bug in our code
        throw new WireException('Called a watermark method for final variation images on a original image!');
        return;
    }

    // also we have to check if it already got watermarked, what we cannot get from the pageimage objects hirarchy
    // (that also is reflected in the filenames), as we have or will obfuscate(d) it:
    // read from IPTC tag if it is already processed
    $iptc = new hnReadWriteIptc($finalImage->filename);
    $isWatermarked = (bool)$iptc->readTag('isWatermarked');

    if(!$isWatermarked) {
...

So, if it is NOT watermarked, we will do it now with the code from our already built function. The only addition is to store the successful processing into the IPTC data, so that it is known with the next view call:

...
        // write our successful processing into our dedicated IPTC field to avoid repetitive processing
        $iptc->writeTag('isWatermarked', '1');
        $iptc->writeIptcIntoFile();
    }

    $event->return = $finalImage;

});


3) Our final code in the site/ready.php looks like this now: 

<?php namespace ProcessWire;
Spoiler


/**
* Class that helps with reading / writing IPTC data from / into JPEG image files
* https://github.com/horst-n/hnReadWriteIptc
*
* @date 2021-06-19
* @version  0.1.1
*/
class hnReadWriteIptc {

    /**
     * List of valid IPTC tags (@horst)
     *
     * @var array
     *
     */
    protected $validIptcTags = array(
        '005', '007', '010', '012', '015', '020', '022', '025', '030', '035', '037', '038', '040', '045', '047', '050', '055', '060',
        '062', '063', '065', '070', '075', '080', '085', '090', '092', '095', '100', '101', '103', '105', '110', '115', '116', '118',
        '120', '121', '122', '130', '131', '135', '150', '199', '209', '210', '211', '212', '213', '214', '215', '216', '217');

    /**
    * The IPTC tag where we store our custom settings as key / value pairs
    * (serialized)
    *
    * @var string
    */
    protected $tagname = '2#215';

    /**
    * Filename of the image to read from or write to
    *
    * @var filename
    */
    protected $filename = null;

    /**
     * Result of iptcparse(), if available
     *
     * @var mixed
     *
     */
    protected $iptcRaw = null;


    public function __construct($filename) {
        $this->readImagefile($filename);
    }


    public function readTag($key) {
        if(!isset($this->iptcRaw[$this->tagname])) return null;
        $data = unserialize($this->iptcRaw[$this->tagname][0]);
        if(!is_array($data) || !isset($data[$key])) return null;
        return $data[$key];
    }


    public function writeTag($key, $value) {
        $newData = [$key => $value];
        $oldData = isset($this->iptcRaw[$this->tagname]) && is_array($this->iptcRaw[$this->tagname]) && isset($this->iptcRaw[$this->tagname][0]) && is_string($this->iptcRaw[$this->tagname][0]) && 0 < strlen($this->iptcRaw[$this->tagname][0]) ? unserialize($this->iptcRaw[$this->tagname][0]) : [];
        $data = serialize(array_merge($oldData, $newData));
        $this->iptcRaw[$this->tagname][0] = $data;
    }


    public function readImagefile($filename) {
        if(!is_readable($filename)) {
            return false;
        }
        $this->filename = $filename;
        $imageInspector = new ImageInspector($this->filename);
        $inspectionResult = $imageInspector->inspect($this->filename, true);
        $this->iptcRaw = is_array($inspectionResult['iptcRaw']) ? $inspectionResult['iptcRaw'] : [];
    }


    public function writeIptcIntoFile($filename = null, $iptcRaw = null) {
        if(null === $iptcRaw) $iptcRaw = $this->iptcRaw;
        if(null === $filename) $filename = $this->filename;
        if(!is_writeable($filename)) return false;

        $content = iptcembed($this->iptcPrepareData($iptcRaw, true), $filename);
        if($content !== false) {
            $dest = $filename . '.tmp';
            if(strlen($content) == @file_put_contents($dest, $content, LOCK_EX)) {
                // on success we replace the file
                unlink($filename);
                rename($dest, $filename);
            } else {
                // it was created a temp diskfile but not with all data in it
                if(file_exists($dest)) {
                    @unlink($dest);
                    return false;
                }
            }
        }
        return true;
    }


    protected function iptcPrepareData($iptcRaw, $includeCustomTags = false) {
        $customTags = array('213', '214', '215', '216', '217');
        $iptcNew = '';
        foreach(array_keys($iptcRaw) as $s) {
            $tag = substr($s, 2);
            if(!$includeCustomTags && in_array($tag, $customTags)) continue;
            if(substr($s, 0, 1) == '2' && in_array($tag, $this->validIptcTags) && is_array($this->iptcRaw[$s])) {
                foreach($iptcRaw[$s] as $row) {
                    $iptcNew .= $this->iptcMakeTag(2, $tag, $row);
                }
            }
        }
        return $iptcNew;
    }

    protected function iptcMakeTag($rec, $dat, $val) {
        $len = strlen($val);
        if($len < 0x8000) {
            return @chr(0x1c) . @chr($rec) . @chr($dat) .
            chr($len >> 8) .
            chr($len & 0xff) .
            $val;
        } else {
            return chr(0x1c) . chr($rec) . chr($dat) .
            chr(0x80) . chr(0x04) .
            chr(($len >> 24) & 0xff) .
            chr(($len >> 16) & 0xff) .
            chr(($len >> 8) & 0xff) .
            chr(($len) & 0xff) .
            $val;
        }
    }

}

 

/**
* Example Pageimage Hook for watermarking functionality,
* adds a new method to Pageimage objetcs named wm.
* Used in a small tutorial series in the ProcessWire forums
* https://processwire.com/talk/topic/25752-tutorial-how-to-add-a-site-wide-optional-watermarking-method-to-all-pageimages/
*
* @version 0.0.2
*
*/
$this->addHook('Pageimage::wm', function(HookEvent $event) {

    // get the image out of the event object
    $finalImage = $event->object;                   // this is no longer the $originalImage from episode 1 !!

    // access the field of the transparent watermark overlay image in a robust way,
    // regardless if it is set to 1 image or multiple images, and if it has changed
    // in this regard since we wrote this hook function
    $watermarkImages = wire()->pages->get('id=1')->getUnformatted('setting_watermark');

    // check if the imagefield exists and it at least contain one image, than fetch it
    if(!$watermarkImages || 0 == count($watermarkImages)) {

        // return the unmodified original image to do not break our front end site
        $event->return = $finalImage;

        // inform the admin about the missing watermark overlay, (or do something other useful)
        wireMail('admin@example.com', 'noreply@example.com', 'MISSING SITEWIDE WATERMARK OVERLAY IMAGE');
        wireLog('errors', 'MISSING SITEWIDE WATERMARK OVERLAY IMAGE');

        // stop further execution
        return;
    }

    // as we will overwrite this image with a watermarked version,
    // we have to make sure that this really is a variation and NOT an original image
    $isOriginal = null == $finalImage->getOriginal();
    if($isOriginal) {
        // throw an exception, as this must be a bug in our code
        throw new WireException('Called a watermark method for final variation images on a original image!');
        return;
    }

    // also we have to check if it already got watermarked, what we cannot get from the pageimage objects hirarchy
    // (that also reflects in the filenames), as we have or will obfuscate(d) them:
    // read from IPTC tag if it is already processed
    $iptc = new hnReadWriteIptc($finalImage->filename);
    $isWatermarked = (bool)$iptc->readTag('isWatermarked');

    if(!$isWatermarked) {
        // inspect the given image
        $ii = new ImageInspector();
        $info = (object) $ii->inspect($finalImage->filename)['info'];

        // our preferred watermark width is 200px, so lets check if it fits into the variation
        if(200 >= $info->width) {
            // does not fit, is it a portrait oriented image with more then 300 px height?
            if(200 < $info->height) {
                // ok, we have to rotate our watermark -90 degree to make it fit to the bottom right
                $pngAlphaImage = $watermarkImages->first()->width(200)->pim2Load('rot270')->rotate(270)->pimSave();
            } else {
                // the variation is to small for useful watermarking, we will leave and output
                // it as is
                $event->return = $finalImage;
                return;
            }
        } else {
            // the width fits, get a watermark image of 200px
            $pngAlphaImage = $watermarkImages->first()->width(200);
        }

        // now lets apply the watermark to our final variation,
        // this has to be done a bit different than we have done in the first episode.
        // The difference is, that we previously created a intermediate variation.
        // Now we want to avoid this, as it would make our watermarking processing nearly useless
        // in regard of protecting the unwatermarked images

        // we invoke the Pageimage Manipulator in a way that lets us overwrite the loaded image,
        // what is not possible within its default behave
        $im = new \PageImageManipulator02();
        $im = $im->imLoad($finalImage->filename, ['targetFilename' => $finalImage->filename])->setTargetFilename($finalImage->filename);
        $filename = $im->watermarkLogo($pngAlphaImage, 'SE', 0)->save();
        $success = $finalImage->filename == $filename; // the imLoad()->save() process returns the filename of the processed image on success

        // $watermarkedImage = $finalImage;  // this just should visualize what has happen in the black box :-)
        // to return the watermarked variation,
        // we have to replace the event object with our watermark variation
        // $event->return = $watermarkedImage;

        // But infact we have overwritten the discfile that belongs to our image object,
        // so there is no NEW imageobject that we have to change with the initial one

        // write our successful processing into our dedicated IPTC field to avoid repetitive processing
        $iptc->writeTag('isWatermarked', '1');
        $iptc->writeIptcIntoFile();
    }

    $event->return = $finalImage;

});

 

Now we can call our watermarked images like this: 

    $image = $page->images->first();
    $variation = $image->width(800)->wm();
?>
    <img src='<?=$variation->url?>' alt='<?=$variation->description?>' />

 

Thanks for reading! ?

 

Spoiler

20210609_204240.800x0.jpg.c548ddb5c7ae3ec6484be76abe456798.jpg

 

  • Like 5
  • Thanks 3
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...