Photo Editing

WordPress Watermark: Which File Actually Gets Stamped

A watermark can go on the original, on the sub-sizes WordPress generates, or on top at display time. Only two of those put pixels in the file, and only one of them is reversible.

WordPress Watermark: Which File Actually Gets Stamped

There are two kinds of watermark. One is a signature: small, quiet, there so that a picture travelling around the internet keeps a thread back to whoever made it. The other is a fence: large, obtrusive, there to make the image useless until somebody pays. They are different jobs and they need different designs, and most of the bad watermarks in existence are one job done with the other’s tools.

On a WordPress site there is a second question, and it is the one the tutorials skip. A single upload does not produce a single file. It produces an original, usually a scaled version of that original, and one file per registered size. So a WordPress watermark is really a question about which of those files gets pixels written into it, and at what point during the upload it happens.

Get the design wrong and you have an ugly picture. Get the plumbing wrong and you have a library you cannot un-stamp. This article covers both, design first, because the design decisions are the ones you have to make before any code is worth writing.

Decide what you are actually protecting against

Be honest about the threat. A watermark does not stop a determined thief, and software has been able to reconstruct what sits behind a mark for years. What it does do is:

  • make casual reuse slightly awkward, which stops most of it
  • keep your name attached when an image is screenshotted, reposted and stripped of every other trace
  • mark a proof so it cannot be mistaken for the final file

Those are real and worth having. What a watermark cannot do is prevent theft, and designing as though it can is how you end up with a picture nobody wants to look at, including the people you hoped would hire you.

Where to put it

The corner is the default and it is defensible: it is out of the way, it is conventional, and readers ignore it the way they ignore a page number. Its weakness is equally obvious, because a corner crops off in two seconds.

If crop resistance matters, you have three options, in ascending order of aggression.

Off center, over the subject. A single mark placed where it overlaps part of the actual content cannot be removed by cropping without removing the reason the picture exists. Keep it small and low contrast and it is remarkably unobtrusive.

Tiled across the whole frame. A repeating pattern at low opacity, usually rotated so it does not line up with anything in the picture. This is the proof watermark, and it is deliberately annoying, which is its function. Rotation matters: a mark at a slight angle is much harder to remove cleanly than one aligned to the frame.

A bar across the image. Honest, ugly, effective. Reserve it for cases where the image genuinely must not be usable.

Table comparing four watermark placements, showing that a corner mark is the only one a crop removes and that the more crop resistant placements are also the more intrusive ones.

The design details that decide whether it is bearable

Opacity is not the only lever

Everyone reaches for opacity first, and a mark at 30 per cent still sits on top of the picture as a foreign object. Two things work better in combination. The first is blend mode: a white mark set to overlay or soft light interacts with the tones underneath instead of covering them, so it stays visible on both light and dark areas without ever looking pasted on. The second is a subtle shadow or outline, one or two pixels, which is what keeps a light mark readable when it crosses a bright area.

Size, relative to the frame

A signature mark should occupy somewhere between five and ten per cent of the image width. Smaller and it vanishes on a phone, larger and it becomes the subject. Because it is relative, it has to be recalculated per image, and that arithmetic is the reason hand-placed watermarks get abandoned after the first twenty pictures.

Contrast against unpredictable backgrounds

The problem with a fixed watermark is that it lands on a white sky in one photograph and a black jacket in the next. Pure white with a soft dark shadow survives both. Pure black almost never does. It is the same problem as keeping caption text readable over a photograph, and the solutions are the same.

Vector, not raster

If your mark is a logo, keep the vector version as the master and export the raster copies you need from it. A raster logo scaled up on a large image goes soft, and a soft watermark reads as sloppy rather than subtle. Export one PNG per size band rather than one PNG that gets stretched.

If the point is to stamp the file you hand out rather than the one WordPress generates, the simplest route is to stamp it before it goes anywhere near the library.

Drop your images below, set the mark once, and they are all processed in your browser and handed back as a zip. Text or a logo, corner or tiled, with the opacity and the placement you actually want rather than the three the plugin offers.

Batch watermark

Stamp a whole batch of pictures with a text or an image watermark, then take them away as one ZIP. Every picture is drawn in this browser tab: nothing is uploaded, and no file leaves your computer.

Images to stamp
Drop images here
or press Enter to pick some. Up to 100 files, and every one of them stays on your disk.
FileSizePixelsRemove
Preview of the first image

Drawing a sample photo.

Mark
Placement
Corner or centre
Output
Download all as ZIP Nothing stamped yet

A watermark plugin in the media library only stamps the sizes that are generated after you switch it on, so older thumbnails keep the picture they already had, and the full size original is usually left alone and stays reachable at its own URL. Stamping the file before you upload it puts the mark on every size, the original included.

WunderPaint
The Dynamic Design and Automation Studio
WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

Three places a WordPress watermark can land

Design settled, the WordPress question is where the mark lands. There are exactly three points, and they are not equivalent.

Before upload. You stamp the file on your own machine and upload the stamped version. Every file WordPress derives from it inherits the mark, including the original. This is the most predictable option and the least reversible one, because nothing on the server ever held an unmarked copy.

During upload, on the generated sub-sizes. WordPress writes the main file, then writes the sub-sizes from it. Code that runs after that step can open any of those sub-size files and composite a mark into them. The original stays clean unless you go out of your way to touch it, which is both the appeal and the catch.

At display time. A CSS pseudo-element, a positioned overlay, a canvas trick, a right-click blocker. None of these put a single pixel into the file. The image is still sitting at its own URL, unmarked, and anyone can open it in a new tab, or read it out of the srcset attribute, or ask any crawler that has already fetched it. A display-time overlay is a hint that you would prefer people not to copy the picture. It is not protection, and treating it as protection is the most common mistake in this area.

Only the first two change the bytes on disk. If the requirement is that the file must carry the mark wherever it ends up, the display-time option is off the table before the conversation starts.

Table of the three points where a watermark can be applied to a WordPress upload, showing that a display time overlay writes no pixels into any file, alongside the signature of the wp_generate_attachment_metadata filter.

wp_generate_attachment_metadata, the hook that stamps files

wp_generate_attachment_metadata() lives in wp-admin/includes/image.php. It takes an attachment ID and a path to a file that is already on disk, hands that file to wp_create_image_subsizes(), which writes every registered size and reads the EXIF and IPTC blocks, and returns the metadata array that ends up in the _wp_attachment_metadata meta field. The last thing it does is run a filter of the same name.

That filter is the hook. It receives three arguments: the metadata array, the attachment ID, and a context string that is 'create' on a fresh upload and 'update' when the sizes are regenerated or the image is edited in the admin. Checking that third argument is not optional. Without it your code stamps the same file a second time every time somebody regenerates thumbnails, and two marks on top of each other is how you find out.

By the time the filter runs, the sub-size files exist. The metadata array tells you where. $metadata['file'] is the path of the main file relative to the uploads directory, something like 2026/08/harbour-scaled.jpg, and each entry under $metadata['sizes'] holds a bare filename plus width, height, mime type and, since WordPress 6.0, a filesize. There is more detail on how those sizes come to exist in the piece on what actually happens when you upload one image.

Here is a version that marks one size and leaves the rest alone. It belongs in a small plugin or a child theme’s functions.php, and the wider question of where custom code should live is worth settling before you paste anything. It rewrites real files and there is no undo, so run it on a copy of the site first.

<?php
/**
 * Stamp a watermark onto one registered sub-size at upload time.
 *
 * Fires after WordPress has written the sub-size files, so the file
 * this touches already exists. It overwrites that file in place.
 * The only way back is regenerating the size from the original.
 */
add_filter( 'wp_generate_attachment_metadata', 'wpie_watermark_one_size', 10, 3 );

function wpie_watermark_one_size( $metadata, $attachment_id, $context ) {

    // Fresh uploads only. 'update' fires on regeneration and on admin
    // image edits, and stamping again would double the mark.
    if ( 'create' !== $context ) {
        return $metadata;
    }

    // One size, deliberately. 'large' is 1024px wide by default.
    $size = 'large';

    if ( empty( $metadata['file'] ) || empty( $metadata['sizes'][ $size ]['file'] ) ) {
        return $metadata;
    }

    // Sub-sizes live next to the main file, so build the path from its folder.
    $uploads = wp_get_upload_dir();
    $folder  = trailingslashit( $uploads['basedir'] );
    $subdir  = dirname( $metadata['file'] );

    if ( '.' !== $subdir ) {
        $folder .= trailingslashit( $subdir );
    }

    $target = $folder . $metadata['sizes'][ $size ]['file'];

    // A transparent PNG kept outside the media library.
    $mark_file = WP_CONTENT_DIR . 'https://cdn.wp-image-editor.com/watermark/mark.png';

    if ( ! file_exists( $target ) || ! file_exists( $mark_file ) ) {
        return $metadata;
    }

    // Scale the mark to 18 per cent of the target width, using the same
    // image editor WordPress uses for its own sizes. Note that resize()
    // never scales up: keep the master mark wider than the widest size
    // you intend to stamp.
    $mark = wp_get_image_editor( $mark_file );

    if ( is_wp_error( $mark ) ) {
        return $metadata;
    }

    $mark_width = (int) round( $metadata['sizes'][ $size ]['width'] * 0.18 );
    $resized    = $mark->resize( $mark_width, null );

    if ( is_wp_error( $resized ) ) {
        return $metadata;
    }

    $tmp   = trailingslashit( get_temp_dir() ) . uniqid( 'wpie-mark-' ) . '.png';
    $saved = $mark->save( $tmp, 'image/png' );

    if ( is_wp_error( $saved ) ) {
        return $metadata;
    }

    $stamped = wpie_composite_bottom_right( $target, $saved['path'], 24 );

    wp_delete_file( $saved['path'] );

    // The file on disk just changed size, so the recorded number is stale.
    if ( $stamped && isset( $metadata['sizes'][ $size ]['filesize'] ) ) {
        $metadata['sizes'][ $size ]['filesize'] = wp_filesize( $target );
    }

    return $metadata;
}

The one thing wp_get_image_editor() will not do for you is the overlay itself. Open WP_Image_Editor in wp-includes/class-wp-image-editor.php and the abstract methods are load(), save(), resize(), multi_resize(), crop(), rotate(), flip() and stream(). There is no composite. That is why every watermark plugin eventually drops to Imagick or GD, and why the helper below has two branches. Use the editor API for loading, scaling and saving, where it handles engine differences and the image_editor_output_format filter for you, and reach for the extension only for the compositing step.

<?php
/**
 * Load an image file with GD, using the type the file actually has.
 * Returns array( 'image' => resource, 'type' => IMAGETYPE_* ) or false.
 */
function wpie_gd_load( $path ) {

    $info = wp_getimagesize( $path );
    $type = $info ? $info[2] : 0;

    switch ( $type ) {
        case IMAGETYPE_JPEG:
            $loader = 'imagecreatefromjpeg';
            break;
        case IMAGETYPE_PNG:
            $loader = 'imagecreatefrompng';
            break;
        case IMAGETYPE_WEBP:
            $loader = 'imagecreatefromwebp';
            break;
        default:
            return false;
    }

    if ( ! function_exists( $loader ) ) {
        return false;
    }

    $image = $loader( $path );

    return $image ? array( 'image' => $image, 'type' => $type ) : false;
}

/**
 * Composite $mark over $target, inset from the bottom right corner.
 * Overwrites $target. Returns true when the file was rewritten.
 */
function wpie_composite_bottom_right( $target, $mark, $inset = 24 ) {

    if ( class_exists( 'Imagick' ) ) {
        try {
            $base  = new Imagick( $target );
            $layer = new Imagick( $mark );

            $x = $base->getImageWidth()  - $layer->getImageWidth()  - $inset;
            $y = $base->getImageHeight() - $layer->getImageHeight() - $inset;

            $base->compositeImage(
                $layer,
                Imagick::COMPOSITE_OVER,
                max( 0, $x ),
                max( 0, $y )
            );
            $base->writeImage( $target );

            $layer->clear();
            $base->clear();

            return true;
        } catch ( Exception $e ) {
            return false;
        }
    }

    // GD fallback. Both files are read as whatever type they really are,
    // because image_editor_output_format can turn a saved PNG into WebP.
    $base  = wpie_gd_load( $target );
    $layer = wpie_gd_load( $mark );

    if ( ! $base || ! $layer ) {
        if ( $base ) {
            imagedestroy( $base['image'] );
        }
        if ( $layer ) {
            imagedestroy( $layer['image'] );
        }
        return false;
    }

    // Alpha blending is on by default, so the transparency in the mark
    // is honoured over an opaque photograph.
    $x = imagesx( $base['image'] ) - imagesx( $layer['image'] ) - $inset;
    $y = imagesy( $base['image'] ) - imagesy( $layer['image'] ) - $inset;

    imagecopy(
        $base['image'], $layer['image'],
        max( 0, $x ), max( 0, $y ),
        0, 0,
        imagesx( $layer['image'] ), imagesy( $layer['image'] )
    );

    switch ( $base['type'] ) {
        case IMAGETYPE_JPEG:
            $written = imagejpeg( $base['image'], $target, 82 );
            break;
        case IMAGETYPE_PNG:
            imagesavealpha( $base['image'], true );
            $written = imagepng( $base['image'], $target );
            break;
        case IMAGETYPE_WEBP:
            $written = imagewebp( $base['image'], $target, 82 );
            break;
        default:
            $written = false;
    }

    imagedestroy( $base['image'] );
    imagedestroy( $layer['image'] );

    return (bool) $written;
}

Three details in there are worth stating plainly. Re-encoding a JPEG to composite something onto it is a second lossy pass, so the stamped file is slightly worse than the one WordPress produced, which is an argument for stamping the size you actually publish and nothing else. The quality number in the fallback is a literal 82 rather than whatever your site is configured to use, because GD is being driven directly at that point instead of through the editor that would have consulted wp_editor_set_quality. And a sub-size that carries its own transparency is the awkward case for the GD branch, where Imagick composites alpha properly and GD needs more care than a single imagecopy().

Does the thumbnail get the mark

This is the decision the hook forces on you, and it is the one that separates a watermark setup that looks deliberate from one that looks like a bug. A mark designed at eight per cent of a 1200 pixel image is roughly 96 pixels wide. The same eight per cent of a 150 pixel thumbnail is 12 pixels. A logo at 12 pixels is a smudge, and a wordmark at that size is not readable by anyone.

The usual answer is that the small sizes go unmarked. Nobody steals a 150 pixel thumbnail, and the grid in the media library becomes unreadable when every tile carries a grey blob. Mark the sizes people can actually use: large, the main file if you have decided to touch it, and any custom size your theme uses for a lightbox or a portfolio.

If you do want a mark on the small sizes, design a second one for them. A single glyph, no wordmark, sized in absolute pixels rather than as a percentage. That is two assets and two rules in your code, which is more work, and it is why the honest default is to leave thumbnails alone.

The original is not covered

Hook code that walks $metadata['sizes'] touches sub-sizes only. The file at $metadata['file'] is not in that array, and neither is the untouched original that WordPress keeps when an upload is wider than the big_image_size_threshold, 2560 pixels by default. Above that threshold WordPress saves a scaled copy as the main file, with -scaled in the name, and files the true original alongside it, retrievable with wp_get_original_image_path().

So after a run of upload-time watermarking, a 4000 pixel photograph on your server exists as a marked large, an unmarked -scaled version at 2560 pixels, and an unmarked 4000 pixel original. Two of those three are guessable URLs sitting in the same uploads folder, and the -scaled one is what an image block links to when you set it to link to the media file.

That is not automatically a problem. Plenty of setups want a clean master on the server precisely so the marking stays reversible. It is only a problem when you believed the marking was complete. If the requirement is that no unmarked pixel exists anywhere, mark the file before upload and accept that you have no clean copy on the server at all.

Table of the six files a single 4000 pixel upload leaves on disk, with only the 1024 pixel large size marked while the original and the scaled main file stay unmarked.

Reversibility is the whole question

Everything above collapses into one property: can you get back. Stamping is destructive by definition, because the mark and the photograph become the same pixels.

If only sub-sizes were stamped and the original is clean, the way back is regeneration. Delete the sub-size files, run the sizes again from the original, and the library is as it was. That is a real, tested path, and it is the reason the sub-size approach is the one worth defaulting to.

If the original was stamped too, there is no way back. Not with a plugin, not with a database restore, not with any amount of effort, because the information is gone from the file. The only recovery is re-uploading from wherever you keep your masters, which means your masters had better be somewhere other than the media library.

The question to ask of any batch operation is not “will this work” but “what happens if it works on the wrong folder”.

Practically: take a real backup of the uploads directory before the first run, not a database backup, which contains none of the pixels. Let the code run on ten uploads and look at all ten at full size. WunderPaint’s image processor can watermark in the same pass as a batch resize and write versioned overwrites, which keep the previous file rather than discarding it when you decide the mark was two sizes too big.

Metadata is the invisible watermark

Every image file can carry embedded information: author, copyright, contact details, license. It costs nothing, it is invisible, and it survives resizing and re-encoding better than most people expect. WordPress already reads some of it on upload, which is why an attachment can arrive with a caption you did not type.

It does not survive a screenshot, and many platforms strip it on upload, so it is not a replacement for a visible mark. But it is the thing that lets someone who wants to license your work find you, and it is written once in the export settings of whatever produced the file. The same fields that carry your copyright line also carry the camera details and sometimes the GPS coordinates, which is worth a look before you decide to keep all of it.

Symptom and cause

The watermark appears twice on some images. The hook is running without a check on the context argument. Regenerating thumbnails, or editing an image in the admin, fires wp_generate_attachment_metadata again with a context of 'update' and stamps the already stamped file.

The full-size image in the lightbox has no mark. The lightbox is linking to the main file or the original, neither of which is in the sizes array your code walks. Check what the image block links to before assuming the code failed.

The mark is a grey blur on thumbnails. One mark is being scaled proportionally down to a size where its detail cannot survive. Skip the small sizes, or use a separate simplified mark below a width threshold.

Uploads fail or time out after adding the hook. Compositing loads whole images into memory on top of what the sub-size generation already allocated. A large upload plus Imagick can exceed a limit that had been sufficient for years, and the request dies without a useful message, which is a good moment to read up on the three memory ceilings a WordPress site sits under.

Older images have no watermark. The hook only fires when metadata is generated. Files uploaded before the code existed stay untouched until something regenerates their sizes, which is a batch job, not a hook.

The stamped file looks softer than the unstamped one. Compositing onto a JPEG means decoding and re-encoding it, a second lossy generation on top of the one WordPress already performed when it made the sub-size.

What to do with all this

For most sites the right answer is a maintained plugin, and that is not a cop-out. Watermarking touches the file system on every upload, has to cope with both GD and Imagick, has to handle regeneration and admin image edits, and breaks in ways that are only visible weeks later. That is a maintenance burden, and somebody else carrying it is worth the plugin.

The value of knowing the hook is knowing what such a plugin is doing to your library. Ask which sizes it writes to, whether it touches the main file and the original, whether it re-encodes or rewrites, and what happens on regeneration. Those questions have concrete answers, and a plugin that cannot answer them is telling you something. A plugin that marks the full-size file by default has quietly made a decision you cannot undo.

And keep the design honest while you are in there. Watermark the proofs. Watermark the work that travels without you. For photographs whose entire job is to be shared, a mark makes them slightly worse at their only task in exchange for protection against something that mostly does not happen, and the effort is better spent on embedded copyright, a filename that says what the picture is, and being easy to find.

WordPress Watermark: Which File Actually Gets Stamped

Table of Contents

Learn it by building something

Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

One mail a week, and then it ends.
Unsubscribe in one click.

WordPress Images

How to Convert Images to WebP in WordPress

Core converts on upload through a single filter and ignores everything already in the library. Here is what that filter really covers, what regeneration adds, and why the uploads folder grows before it shrinks.

WordPress Images

Clean Up WordPress Media Library Without Breaking Pages

An attachment is a row in wp_posts; its files sit somewhere else, and deleting one does not delete the other. What to measure before you start, why "unused" is so hard to prove, and the order of operations that keeps the site up.

Speed & Performance

Bulk Resize Images in WordPress Without Breaking the Library

Regenerating thumbnails and resizing the original are different jobs, and only one of them is irreversible. What the scaled pair actually costs you, how to measure it with read only commands, what a safe bulk resize has to do step by step, and why most sites should fix the upload threshold instead.

Troubleshooting

Why Your WordPress Images Look Blurry, and Which Cause It Actually Is

Blurry WordPress images are not one problem but six, and each leaves a different fingerprint. Work out which symptom you have before you change anything, because most of the fixes do nothing for most of the causes.

Troubleshooting

The WordPress White Screen of Death Explained

A white screen with no error message isn't a WordPress bug. It's PHP dying silently mid-request. Here's how to see the real error in minutes and the exact order to check plugins, theme, and memory limits.

WordPress Development

WordPress Child Themes: When You Need One and When You Don’t

A child theme is the right answer to one problem: overriding template files in a theme that still receives updates. For CSS tweaks and a few PHP snippets, it is overhead you maintain forever for no benefit.

Download the free WunderPaint Plugin for WordPress

The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

The Image Editor & Design Studio

Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

Free

Chaos Art

Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

Pro

Particle Strokes

Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

Pro

City Diorama

Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

Free

Papercut Art

Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.

Pro

3D Earth Studio

A hyperrealistic globe - day and night with city lights, live clouds, atmosphere halo, country borders and highlights, click-to-place markers with flight-route arcs, satellite orbits, seamless rotation video and a live website embed.

Free

Mystic Studio

Turn a birth date into wall art - a real natal chart with houses and aspects, the moon of that night, zodiac and Chinese zodiac posters, numerology cards and a synastry wheel for two, in eight artful themes.

Free

Marble Bath

Marble paper on a virtual water bath - drop, rake and comb real Ebru patterns with gestures, flowers and classic recipes, razor-sharp at any size and re-editable as a layer.

Free

Day Ring

Turn a day into a beautiful circular schedule - colour-coded time blocks as arcs around a 24-hour clock, with concentric rings for overlaps, emoji, templates and a legend.

Free

Code Shot

Turn code into a gorgeous, share-ready image - syntax highlighting, editor themes, window frames and diff highlighting - then drop it into your design as a re-editable layer.

Pro

3D Solar System Studio

Build a date-accurate 3D solar system - real planet positions for any date, photoreal textures and one slider from artistic to true scale - then drop it into your design as an editable layer.

Pro

3D Molecule Studio

Build a real 3D molecule - from a curated library, the periodic table or a SMILES string - then style it, measure it and drop it into your design as an editable layer.