kid1296 / blind-watermark
PHP Library for DWT‑DCT‑SVD Blind Image Watermark.
Requires
- php: >=8.1
- ext-gd: *
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
PHP Library for Blind&Invisible Image Watermark.
- 中文 README_ZH.md
Features
| 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.
Requirements
- 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.
Install
composer require kid1296/blind-watermark
Usage
All 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).
01 Embed & Extract — String Watermark
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
02 Embed & Extract — Bit-Array Watermark
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 === $bits
Output:
original bits: 1011010011010110
embed done in 2.64s, wm_size = 16
extract done in 1.55s
extracted bits: 1011010011010110
RESULT: PASS
03 In-Memory Pipeline (No-Temp-Files)
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
04 Attack / Recover — Robustness Test
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.







