PHP Library for Blind&Invisible Image Watermark.
- 中文 README_ZH.md
| Item | Before | Now |
|---|---|---|
| Speed (940×940 embed) | ~4.9 s | ~2.5 s (2× faster) |
| Speed (extract) | ~3.8 s | ~1.5 s (2.5× faster) |
| Peak memory (embed+extract, 940×940) | required memory_limit=2G |
~61 MB (fits in default 128 MB) |
| Peak memory (1358×780) | OOM at 128 MB | ~81 MB |
| Image quality (PSNR, 940×940) | ~36.9 dB | 35.6 dB |
| Robustness (salt-pepper / bright / shelter / crop) | PASS | PASS |
| Robustness (resize, natural photo) | PASS | PASS |
PSNR 35–40 dB is generally considered "visually lossless" for photographs. The
d1/d2default was reduced from 36/20 to 27/15 for higher PSNR, while still tolerating salt-pepper / brightness / shelter / crop attacks on typical host images.
- PHP >= 8.1
- GD extension (enabled by default in most PHP distributions)
The GD extension is required for all image I/O (reading JPEG/PNG, writing PNG, and applying attack simulations such as resize / salt-pepper / brightness). Verify with
php -m | grep gd.
composer require kid1296/blind-watermarkAll examples run with the default PHP memory limit — no
-d memory_limit=2Gflag is needed anymore (peak memory stays well under 128 MB even for 1.3 megapixel images).
Original image + watermark text = invisible watermark image.
+ 'This is a test sentence.' → 
Full example: 01_embed_extract_str.php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Kid1296\BlindWatermark\WaterMark;
// ========== embed ==========
$bwm1 = new WaterMark(passwordWm: 1, passwordImg: 1);
$bwm1->read_img('examples/pics/origin.jpg');
$wm = 'This is a test sentence.';
$bwm1->read_wm($wm, 'str');
$bwm1->embed('examples/output/embedded_str.png');
$lenWm = $bwm1->wmSize; // remember this length
// ========== extract ==========
$bwm2 = new WaterMark(passwordWm: 1, passwordImg: 1);
$extracted = $bwm2->extract(
filename: 'examples/output/embedded_str.png',
wmShape: $lenWm,
mode: 'str'
);
// $extracted === 'This is a test sentence.'Output:
embed done in 2.51s
Put down the length of wm_bit 191
extract done in 1.49s
extracted: This is a test sentence.
RESULT: PASS
Embed an arbitrary array of 0/1 bits (useful for custom protocols / binary payloads).
Full example: 02_embed_extract_bit.php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Kid1296\BlindWatermark\WaterMark;
$bits = [1,0,1,1,0,1,0,0,1,1,0,1,0,1,1,0];
// embed
$bwm1 = new WaterMark(1, 1);
$bwm1->read_img('examples/pics/origin.jpg');
$bwm1->read_wm($bits, 'bit');
$bwm1->embed('examples/output/embedded_bit.png');
$lenWm = $bwm1->wmSize;
// extract
$bwm2 = new WaterMark(1, 1);
$extracted = $bwm2->extract(
filename: 'examples/output/embedded_bit.png',
wmShape: $lenWm,
mode: 'bit' // returns int[] of 0/1
);
// $extracted === $bitsOutput:
original bits: 1011010011010110
embed done in 2.64s, wm_size = 16
extract done in 1.55s
extracted bits: 1011010011010110
RESULT: PASS
Suited for web services: read host image from request / database, embed, return BGR data directly without touching disk.
Full example: 03_in_memory.php
<?php
require __DIR__ . '/../vendor/autoload.php';
use Kid1296\BlindWatermark\WaterMark;
use Kid1296\BlindWatermark\Image\ImageIo;
$img = ImageIo::read('examples/pics/origin.jpg');
// $img = ['data'=>string, 'h'=>int, 'w'=>int, 'channels'=>int]
// embed — omit $filename, returns in-memory result array
$bwm1 = new WaterMark(1, 1);
$bwm1->read_img($img); // pass array directly
$bwm1->read_wm('This is a test sentence.', 'str');
$embedded = $bwm1->embed(); // no filename → returns array
$lenWm = $bwm1->wmSize;
// $embedded is ['data'=>string, 'h'=>int, 'w'=>int, 'channels'=>int]
// You can save it with ImageIo::write(...) later, or stream to client.
// extract — pass embedImg array (instead of filename)
$bwm2 = new WaterMark(1, 1);
$extracted = $bwm2->extract(embedImg: $embedded, wmShape: $lenWm, mode: 'str');Output:
host image: 940x940, channels=3
embed done in 2.16s
embedded: 940x940, channels=3, data size = 21206400 bytes
extract done in 1.18s
extracted: This is a test sentence.
RESULT: PASS
Demonstrates the watermark surviving common post-processing attacks, plus crop-and-recover.
Embedded image (host = origin.jpg, wm = 'This is a attack test'):
Attack results (watermark extracted successfully from every image below):
| salt-pepper 2% | brightness ×0.9 | resize 940²→800×600 |
|---|---|---|
![]() |
![]() |
![]() |
| shelter 5 blocks @5% | crop right 20% | crop recovered |
|---|---|---|
![]() |
![]() |
![]() |
Full example: 04_with_attacks.php
$ php examples/04_with_attacks.php
embedded wm_size = 167
-- attacks --
salt-pepper 2% -> 'This is a attack test' [PASS]
brightness 0.9 -> 'This is a attack test' [PASS]
resize 940x940 -> 800x600 -> back -> 'This is a attack test' [PASS]
shelter 5 blocks @5% -> 'This is a attack test' [PASS]
-- crop + recover --
crop right 20% + recover -> 'This is a attack test' [PASS]
estimate_crop_parameters -> [0,0,752,940] (expected same) [PASS]Note on resize robustness: The resize attack uses GD's bilinear re-sampling. It works reliably on typical photographs with good tonal range (like
origin.jpg, mean ≈ 120, stdev ≈ 40). It may fail on extremely uniform / near-solid-colour host images (e.g. mean > 250/255) because the relative watermark strength is too low to survive the low-pass filter. This is a fundamental limitation of the DWT-DCT-SVD blind-watermark paradigm, not a library bug. To verify resize robustness, run the example withorigin.jpgas the host.







