Recently, I took on a challenge to add dynamic watermarks to images in a WordPress gallery system.
There was a custom post type called **gallery**. When a user adds a gallery, it creates a post and stores all image IDs from the media library in a post meta field (`gallery_images`).
The system already had functionality to load watermarks dynamically. When creating a gallery post, a user could add a gallery title, gallery images, and custom watermark text for each image in the gallery.
Previously, the system worked by loading the original image and using JavaScript to render it on a canvas with the watermark text.
This method worked fine if the watermark text was simple. However, in our case, we had to render a placeholder watermark image and then write the custom text onto it, like this:
The system would load all images, render them one by one, render the watermark image for each gallery image, and add the placeholder `Watermark Text`.
Although this seems simple and fast, it was not feasible for our case because each gallery contained 150–200 images.
Rendering on the client side took too long, so our first idea was to merge the watermark into each image when creating the gallery. However, the client also wanted the original images to be available for sale.
So we decided to store both the original version and the watermark-merged version for each image. At the same time, the client needed the ability to change the watermark text at any time. This meant that every time the watermark text was edited, the server would need to process all images again, save them, and remove the previous merged versions.
This approach was too complex, prone to bugs, and still allowed users with basic knowledge of dev tools to access the original images.
Our next approach was to create a REST route. On the frontend, when loading images, we request that route with the image URL and gallery post ID. The server processes the watermark with PHP and outputs it using `header(“image/png”)`, so it can be used in the `` tag.
Example:
<img src="site.com/wp-json/kmfoysal06/merge-watermark/?url=original_url&post_id=123" />
However, the original image URL was still visible, which was a problem. So we replaced it with the image ID:
<img src="site.com/wp-json/kmfoysal06/merge-watermark/?image_id=456&post_id=123" />
This made the URL safer, but loading 150–200 images still took about 30 seconds because all processing happened server-side before anything could display, resulting in a blank page for that time.
The simplest solution was to use **lazy loading**. We added the `loading=”lazy”` attribute to all gallery images:
<img src="site.com/wp-json/kmfoysal06/merge-watermark/?image_id=456&post_id=123" />
This improved initial page load times. However, the client also wanted single gallery images to be shareable on social media, and the URL structure still looked awkward.
To solve this, we created a page with the slug `gallery_image` and a **Page Template** in the theme. (Just a normal PHP file with a comment at the top: `/* Template Name: Single Gallery */`). We selected this template from the page editor. Initially, the page was blank.
We then added the REST route code to the page template to process and merge the watermark: it retrieves the `gallery_id` and custom credit text, gets the image from the `image_id`, merges them, and returns the image with `header(“image/png”)`.
Now the frontend code looks like this:
<img src="site.com/gallery_images?image_id=456&post_id=123" />
Finally, we made the URL shorter and cleaner using **query vars** and **rewrite rules**:
function kmf__img_proxy_rule() {
add_rewrite_rule(
'photo/([0-9]+)/([0-9]+)/?$',
'index.php?pagename=photo&album_id=$matches[1]&image_id=$matches[2]',
'top'
);
}
add_action('init', 'kmf__img_proxy_rule');
// Add query vars
add_filter("query_vars", function($vars){
$vars[] = 'album_id';
$vars[] = 'image_id';
return $vars;
});
Now the URL works like this:
site.com/gallery_images/album_id/image_id
Example:
<img src="site.com/gallery_images/123/456" loading="lazy" />
**Results:**
* Before: Initial loading of a single gallery page was 30–35 seconds.
* After: Using lazy loading and server-side watermark processing, initial loading is 5–6 seconds. The server only processes images as needed, optimizing resource usage