Notes › Life Essays › A 5% White Watermark On A Light Base Adds Nothing: The PNG Exports At 4,782 Bytes, Identical To The Base Image

A 5% White Watermark On A Light Base Adds Nothing: The PNG Exports At 4,782 Bytes, Identical To The Base Image

The 5% step is not "too faint to see", it is touched=0

A white watermark over a light base is the most common pairing in design files. Push the opacity slider to 5% and the exported PNG is 4,782 bytes; the same image size exported without any watermark is also 4,782 bytes, not one byte different. The measuring script records touched=0: no pixel in the frame was changed at all. The 6% step is touched=0 as well.

Scan all 96 opacity steps and only 15 distinct integers actually land on a light base; the remaining steps collapse onto one another.

The template is not what does the blending

Hunting through the code for a blend function is a disappointment. The whole template holds just two assignments, ctx.globalAlpha = opacity on line 293 and on line 461, with no getImageData and no hand-written dst*(1-a)+src*a. The pixel-by-pixel arithmetic is carried out by the browser compositor; the template only hands the alpha over.

That sweep across all 96 steps is 1,152 measured points. Compared with an ideal round(), a deviation of 0 hits 828 times, or 71.875%; a deviation of -1 hits 240 times and +1 hits 80 times, and the largest absolute deviation is 1 level.

The same alpha routed through fillText and through drawImage matches on 96/96 platform values one by one, so the text path and the logo path do not run on a second set of rules. The three channels of a coloured logo such as #ffd700 land independently, and the B channel spans only 18.

Contrast is something you measure

A white watermark pressed onto a dark base (RGB 20) and onto a light base (RGB 240) does not differ by whether it is visible, but by how high the WCAG ratio can climb. To re-check these steps, upload a solid-colour base image on the Image Watermark page and compare the exported byte counts of the two lowest steps.

Measured itemDark side, RGB 20Light side, RGB 240
WCAG contrast ratio, first step scanned1.11771.0000 (5% step, touched=0)
WCAG contrast ratio at 80%11.94411.1108
Pixels changed at the 5% and 6% steps—0
Exported PNG size in bytes—4,782, the same as the base image
Distinct integers left after the 96 steps collapse—15

A single watermark that straddles the hard boundary between dark and light says the most: at 80% the dark side reaches 11.9441 while the light side only gets 1.1108. Same alpha, same call, and the two sides sit an order of magnitude apart.

Dark first step Dark at 80% Light first step Light at 80% 1.1177 11.9441 1.0000 1.1108 WCAG contrast ratio White, both ends of 96 steps

Fig. 1: the measured contrast ratio of one white watermark on the dark side and the light side.

Tiling spacing is not clearance

The tiling step is sx = measureText width + gapx and sy = fontSize + gapy. At font size 48 the box is 48 tall, while the ink of Watermark is only 35 tall, so a nominal vertical spacing of 120 px on the slider leaves 133 px of real clearance.

Pull gap up to 400, a legal slider value, and coverage is 0.0000%. The step arithmetic is not wrong: the loop origin is hard-coded as -wW and -wH, so origin and step grow together and the first cell lands outside the picture.

Rotation turns the drawn item only, never the step. Under font size 48 with gap 40, rot0 leaves 254 empty rows; rot15, 30, 45 and 135 at that same gap40 already have 0 empty rows and 0 empty columns. Coverage rises from 16.1331% to 17.9763% (45°) and 19.4256% (135°).

Can the original be solved back? No, but the distance is measurable

Consider only the 2,963 pixels the watermark covers completely. For one frame the error amplification factor is exactly 1/(1-a): an average error of 0.270 at 5%, 1.310 at 80%, and a maximum of 5.

Hoping to wash the watermark out in post-processing hurts on every count. Mean per-channel error, measured on that 80% frame:

  • raw frame 78.461
  • after contrast stretch 79.839
  • after inversion 109.599
  • after thresholding 111.953
  • joint least squares over 6 steps 0.467, with the maximum error falling from 153 to 1

Only the joint multi-step fit drives the error down, and that is a fit across 6 samples taken at different alphas, not a restoration of the original image.

One more result: taking the per-channel minimum over 6 frames as an envelope gives an average error of 4.480, exactly the 4.480 of the single 5% frame. The envelope has no extrapolation power; it equals nothing but the observation at the lowest step.

Raw Stretch Invert Threshold Joint 6 steps 78.461 79.839 109.599 111.953 0.467 Mean per-channel error That 80% frame

Fig. 2: all three post-processing steps are worse than the raw frame, and only the joint fit over 6 steps pulls the error down.

Nothing gates the export path

Export is a single line, toBlob(..., 'image/png'); the quality argument is undefined, the file name looks like watermarked_400x400.png, and the tool has no JPEG branch at all. file.size, img.width and any size clamp never appear in the template, so a 4,000 × 4,000 base image still runs all the way out to a 1,580,930 byte PNG.

Broken images are not stopped either. Lines 207 to 215 bind only onload and never bind onerror; a 72 byte corrupt.png clears the MIME gate and then fails silently, with baseImage still null 4,000 ms later.

The MIME gate is one unanchored substring search: lines 204 and 232 both run file.type.match(/image\//). The very same PNG bytes, declared as x-image/png and declared as image/png, yield exactly the same 400x400 / 4,659 pixels / 10,676 bytes; writing application/image+x is what gets rejected.

Batch, corner drag and the scale slider: none of the three holds

The file control has no multiple, and both onchange and drop take only files[0], so feeding in 3 files loads just the first image.

Dragging the lower-right corner in logo mode moves wmScale from 1 to 1.4951, yet the logo box that can be exported is still 60x60@170,170, still 3,600 pixels and 114,858 bytes. Text mode is blunter: the images exported at the 5, 30 and 100 readings of the watermark scale slider are byte-for-byte identical, all of them 4,659 pixels and 121,019 bytes, while the slider keeps printing 60% — wmScale is never written back anywhere in the template.

Beyond these numbers

This article reports measured pixel-level steps only. Where a watermark should sit and how to trade off its density belong to the older piece on deterring image theft, and no "right opacity" is offered here: on a light base 5% and 6% are both touched=0, while on a dark base the same step measures 1.1177. The difference is set by the base colour, not by some universal value.

Q: What is actually different between 5% and 6%?

On a light base RGB 240 both steps report touched=0 and export 4,782 bytes, matching the base image, so only the slider reading differs.

Q: Can a watermarked image be computed back into the original?

No. The single-frame amplification factor is 1/(1-a), and contrast stretch 79.839, inversion 109.599 and thresholding 111.953 are all worse than the raw 78.461; the least squares fit over 6 steps reaches 0.467, which is a fit across repeated samples.

Q: Why does export set no size limit and no broken-image guard?

There is a single encoding path, image/png, quality is undefined, and there is no file.size ceiling; <img> never binds onerror, so a broken image clears the MIME gate and fails silently.

📎 Related articles

Landscape Loses 25% And Portrait 43.75%: A Cell-By-Cell Geometry Audit Of 7 Collage Templates 2026-10-28 Add One Month to January 31: Only Days, Hours, Minutes, Seconds 2026-10-28 How many characters fit on a copybook page: the default A4 gives 20, not 100 ÷ 4 = 25 2026-10-27 A day on both the weekend list and the make-up list: which rule the workday tool obeys 2026-10-27 A Countdown Day Is 86400 Seconds, So DST Can Steal a Day 2026-10-27