Telefono: 379 148 9430
Orari: dal lunedì al venerdì 9:00 - 18:00
11 Maggio 2025
Pubblicare fotografie online è diventato indispensabile per fotografi, e-commerce e blogger, ma secondo gli studi più recenti sul furto d’immagini, oltre il 70% dei contenuti visivi viene ricondiviso senza credito o autorizzazione. Una filigrana visibile protegge certamente l’autore, ma per renderla effettivamente valida ai fini del furto della proprietà intellettuale, spesso rovina l’estetica dello scatto con una “pecetta” tutt’altro che piacevole!
Al contrario, un watermark “invisibile” inscritto nei coefficienti della Trasformata Coseno Discreta (DCT), su cui si basa il formato JPEG, preserva la qualità percepita e può resistere a ridimensionamenti o compressioni leggere.
Codificando l’informazione nel segno di specifici coefficienti a bassa frequenza, è possibile garantire un compromesso efficace tra invisibilità e robustezza.
In questo progetto open-source, disponibile nella mia repository GitHub: https://github.com/thesimon82/php-dct-invisible-watermark, scritto in PHP 8.3 e basato su GD/Imagick per la manipolazione dei pixel, mostreremo passo passo come incorporare una firma testuale nei coefficienti a bassa frequenza (AC) di ciascun blocco 8×8, come recuperarne l’impronta e perché il segno del coefficiente rappresenta una strategia robusta contro le comuni compressioni JPEG, anche a qualità 80% o inferiore.
L’obiettivo non è solo didattico: il micro-servizio risultante può essere clonato dal repository e integrato, via CLI o API, in qualunque workflow di pubblicazione immagini.
Prima di scrivere una sola riga di codice conviene chiarire come l’algoritmo di compressione JPEG agisce sulle immagini che deve comprimere.
Il formato JPEG divide l’immagine in una griglia di blocchi 8×8 pixel; ciascun blocco viene trasformato tramite la Discrete Cosine Transform (DCT), che scompone l’informazione visiva in 64 coefficienti ordinati dalla frequenza più bassa (l’angolo in alto a sinistra, responsabile dell’intensità media) a quella più alta (i dettagli più fini).
I coefficienti a media-bassa frequenza sono il punto ideale per il nostro watermark: toccare il coefficiente a frequenza zero (DC) produrrebbe aloni visibili, mentre alterare solo le frequenze più alte porterebbe a una loro eliminazione già alla prima compressione JPEG.
Intervenendo invece su coefficienti intermedi possiamo codificare singoli bit modificando il segno del coefficiente (positivo = 0, negativo = 1), ottenendo così un marcatore invisibile all’occhio ma sufficientemente robusto da sopravvivere a ridimensionamenti e compressioni JPEG fino a qualità 80.
Questo principio matematico guiderà l’intera implementazione.
Leggeremo ogni blocco 8×8, calcoleremo la DCT tramite una funzione nativa in PHP, modificheremo il segno di uno o più coefficienti scelti e salveremo il nuovo JPEG tramite GD o Imagick.
In altre parole, il watermark sarà nascosto proprio nei coefficienti che l’algoritmo di compressione JPEG tende a preservare: sarà invisibile all’occhio ma difficile da rimuovere, anche in presenza di ricompressioni leggere.
Ogni blocco
di intensità
(dove
) viene convertito in 64 coefficienti
(con
) tramite:
[
C_{u,v}= frac{1}{4},alpha(u),alpha(v)!
sum_{x=0}^{7},sum_{y=0}^{7}
f_{x,y};
cos!Bigl[frac{(2x+1)upi}{16}Bigr],
cos!Bigl[frac{(2y+1)vpi}{16}Bigr]
tag{1}
]
dove
[
alpha(k)=
begin{cases}
dfrac1{sqrt{2}}, & k=0,\[6pt]
1, & kge 1.
end{cases}
]
(coefficiente DC) è la media luminosa del blocco. I coefficientiAC
descrivono contenuti sempre più fini man mano che
o
crescono.
Supponiamo un blocco che abbia tutto un valore di 128, un grigio medio. Allora:
, mentre
per
perché non ci sono variazioni di frequenza.
L’immagine compressa conserva i coefficienti più bassi, quelli più alti sono quantizzati in modo aggressivo, spesso fino a 0.
Prima di essere salvati in JPEG, i coefficienti DCT vengono divisi per una matrice di quantizzazione
, specifica per ogni frequenza:
[
tilde{C}{u,v}= operatorname{round}!Bigl(frac{C{u,v}}{Q_{u,v}}right)Big).
tag{2}
]
Più alto è il valore di
, maggiore sarà la perdita di precisione, con la possibilità che i coefficienti vengano ridotti a zero.
Per questo motivo, nel nostro algoritmo operiamo su un sottoinsieme a bassa frequenza dei coefficienti AC, in posizioni dove la quantizzazione è moderata e i valori sopravvivono meglio alla compressione.
Evitiamo inoltre i coefficienti troppo bassi (DC incluso), che potrebbero introdurre artefatti visibili se modificati.
Per garantire la sopravvivenza del bit dopo la compressione JPEG, invece di usare la parità, imponiamo il segno del coefficiente in base al bit da scrivere:
[
operatorname{embed}(c, b) =
begin{cases}
+text{minAmp} + text{boost}, & b = 0 \\
-text{minAmp} – text{boost}, & b = 1
end{cases}
tag{3’}
]
Dove:
•
garantisce un valore sufficientemente lontano da zero (tipicamente ≥ 80),
•
(es. 64) aumenta la distanza dal centro per evitare l’annullamento in fase di quantizzazione.
In questo modo, anche dopo trasformazioni lossy, il segno resta inalterato e il bit può essere recuperato in lettura.
Se successivamente desideriamo leggere il bit, ci basterà valutare il segno del coefficiente DCT quantizzato:
[
b =
begin{cases}
0, & tilde{C}{u,v} ge 0 \\
1, & tilde{C}{u,v} < 0
end{cases}
tag{4’}
]
In altre parole, un coefficiente positivo indica bit 0, uno negativo indica bit 1.
Questo metodo è più robusto, perché il segno di un coefficiente sopravvive a molte trasformazioni lossy, a differenza del singolo bit meno significativo.
Dopo aver modificato i coefficienti, invertiamo la trasformazione per ottenere il blocco d’immagine marcato:
[
f_{x,y}= frac{1}{4}!
sum_{u=0}^{7}sum_{v=0}^{7}
alpha(u),alpha(v),
tilde{C}_{u,v},
cos!Bigl[frac{(2x+1)upi}{16}Bigr],
cos!Bigl[frac{(2y+1)vpi}{16}Bigr].
tag{5}
]
L’errore massimo introdotto da una variazione di ±1 su un coefficiente di media frequenza è inferiore a 1 quantum in luminanza, sotto la soglia di percezione umano-visiva per fotografie reali.
Per fronteggiare compressioni o ritagli, codifichiamo il messaggio M duplicando ciascun bit k volte e applicando un voto di maggioranza in lettura. Con k=3:
[
M = b_1b_2dots b_n
;longrightarrow;
b_1b_1b_1,b_2b_2b_2dots b_nb_nb_n
tag{6}
]
Il recupero corretto è garantito finché almeno due copie su tre sopravvivono.
Ora che abbiamo formalizzato l’intero impianto matematico, possiamo tradurre l’equazione (1) e la sua inversa direttamente in PHP.
Per mantenere il progetto in puro PHP, senza estensioni native né dipendenze esterne, implementiamo la DCT manualmente su blocchi 8×8.
Questo approccio è sufficientemente veloce per immagini fino a qualche megapixel e, soprattutto, è trasparente e facile da comprendere e modificare.
Il progetto è organizzato con Composer e autoload PSR-4, ma la classe Dct è completamente autonoma e non richiede librerie esterne.
Se in futuro vorrai estendere il sistema con analisi matriciali (es. box counting o misure di distorsione), potrai integrare MathPHP, ma non è necessaria per la DCT.
{
"name": "renornad/php-dct-invisible-watermark",
"description": "Pure-PHP DCT watermarking library for JPEG images.",
"type": "project",
"license": "MIT",
"require": {
"php": ">=8.3"
},
"autoload": {
"psr-4": {
"Renor\Watermark\": "src/"
}
}
}
Dopo aver definito il nostro file composer.json procediamo all’installazione. Andiamo su terminale e digitiamo il comando:
composer install
A questo punto composer creerà il file composer.lock e la cartella vendor che però non installerà pacchetti da Packagist perché non stiamo includendo dipendenze.
Ora possiamo finalmente scrivere la classe Dct. Ricordo che è preferibile inserire i commenti al codice, come standard in inglese:
<?php
declare(strict_types=1);
namespace RenorWatermark;
/**
* 2-D Discrete Cosine Transform (DCT) and its inverse (IDCT) for 8x8 blocks.
*/
final class Dct
{
private const N = 8;
private const SQRT_2_INV = 0.7071067811865476; // 1 / sqrt(2)
/**
* Performs the forward DCT on an 8x8 block of pixel intensities (0-255).
*/
public static function forward(array $block): array
{
self::assertBlock($block);
$C = [];
for ($u = 0; $u < self::N; ++$u) {
$alphaU = ($u === 0) ? self::SQRT_2_INV : 1.0;
for ($v = 0; $v < self::N; ++$v) {
$alphaV = ($v === 0) ? self::SQRT_2_INV : 1.0;
$sum = 0.0;
for ($x = 0; $x < self::N; ++$x) {
for ($y = 0; $y < self::N; ++$y) {
$cosX = cos((2 * $x + 1) * $u * M_PI / (2 * self::N));
$cosY = cos((2 * $y + 1) * $v * M_PI / (2 * self::N));
$sum += $block[$x][$y] * $cosX * $cosY;
}
}
// The DCT formula includes a factor of 1/4
$C[$u][$v] = 0.25 * $alphaU * $alphaV * $sum;
}
}
return $C;
}
/**
* Performs the inverse DCT on an 8x8 block of coefficients,
* returning pixel values in the range [0..255].
*/
public static function inverse(array $coeff): array
{
self::assertBlock($coeff);
$f = [];
for ($x = 0; $x < self::N; ++$x) {
for ($y = 0; $y < self::N; ++$y) {
$sum = 0.0;
for ($u = 0; $u < self::N; ++$u) {
$alphaU = ($u === 0) ? self::SQRT_2_INV : 1.0;
for ($v = 0; $v < self::N; ++$v) {
$alphaV = ($v === 0) ? self::SQRT_2_INV : 1.0;
$cosX = cos((2 * $x + 1) * $u * M_PI / (2 * self::N));
$cosY = cos((2 * $y + 1) * $v * M_PI / (2 * self::N));
$sum += $alphaU * $alphaV * $coeff[$u][$v] * $cosX * $cosY;
}
}
$pixel = (int) round(0.25 * $sum);
$f[$x][$y] = max(0, min(255, $pixel));
}
}
return $f;
}
/**
* Checks that the array is exactly 8x8.
*/
private static function assertBlock(array $block): void
{
if (count($block) !== self::N) {
throw new InvalidArgumentException('Block must have 8 rows.');
}
foreach ($block as $row) {
if (count($row) !== self::N) {
throw new InvalidArgumentException('Each row must have 8 columns.');
}
}
}
/**
* Disallow instantiation.
*/
private function __construct()
{
}
}
Questa classe è il cuore del progetto. Racchiude l’algoritmo della Trasformata Coseno Discreta bidimensionale applicata a blocchi 8×8 pixel, lo stesso schema impiegato dallo standard JPEG. All’avvio del metodo forward, la classe controlla che l’array ricevuto contenga esattamente otto righe e otto colonne; in caso contrario, lancia un’eccezione per evitare che un errore di dimensionamento si propaghi silenziosamente.
Una volta verificata la forma, per ogni coppia di indici di frequenza
, calcola la doppia somma definita dalla formula canonica della DCT: prende ciascun valore di luminosità
, lo moltiplica per due coseni — uno in funzione di
e della coordinata
, l’altro in funzione di
e della coordinata
— e accumula il risultato. Terminata la scansione del blocco, applica il fattore di normalizzazione
, dove
e
per
, così che il coefficiente
conservi la media luminosa e tutti gli altri rappresentino, con la corretta scala, le componenti di dettaglio via via più fini. Il metodo restituisce quindi una matrice 8×8 di coefficienti in virgola mobile: è su questa rappresentazione in frequenza che più avanti incideremo i bit del watermark.
Il percorso inverso è affidato al metodo inverse. Anche qui, prima di qualsiasi elaborazione, la classe impone la verifica della forma del blocco. Per ogni coppia di coordinate di pixel
, ricostruisce la luminanza sommando le 64 frequenze, ciascuna ponderata dal prodotto dei coseni e dai fattori di normalizzazione
. Il valore finale viene moltiplicato per
, arrotondato all’intero più vicino e limitato all’intervallo 0–255, così da evitare che eventuali errori numerici facciano uscire il risultato dallo spazio colore.
L’implementazione segue fedelmente la definizione matematica, senza ricorrere ad accelerazioni FFT o SIMD, privilegiando la chiarezza didattica rispetto alle prestazioni. Il costruttore è privato, la classe non può essere istanziata, e tutti i metodi sono statici e privi di stato interno: in questo modo la DCT si comporta come una funzione pura, facilmente testabile e riutilizzabile. Quando, nei prossimi paragrafi, la useremo per incastonare un bit modificando il segno di un coefficiente a bassa frequenza, potremo essere certi che la trasformazione diretta e inversa siano numericamente coerenti e che l’alterazione resti confinata ai valori che intendiamo manipolare.
La classe Watermarker è la regista che orchestra la fase più delicata dell’intero progetto. Riceve in ingresso o in uscita una sequenza di blocchi 8×8 di coefficienti DCT già calcolati dalla classe Dct e, senza occuparsi della lettura fisica del file JPEG, si limita a manipolare quei numeri per nascondere o recuperare un testo ASCII.
<?php
declare(strict_types=1);
namespace RenorWatermark;
/**
* Watermarker class
*
* Embeds/extracts a short ASCII message (max 10 characters + terminator)
* into low-frequency DCT coefficients. Instead of relying on the single
* parity bit – too fragile after rounding and quantization – here
* we encode the information into the **sign** of the coefficient:
*
* bit 0 → positive coefficient
* bit 1 → negative coefficient
*
* The sign is never affected during:
* 1. IDCT → integer rounding,
* 2. division by JPEG quantization tables,
* 3. further rounding or transformations.
*/
final class Watermarker
{
/** Low-frequency coordinates (survive even at q≈90–100). */
private const EMBED_COORDS = [
[0, 1], [1, 0], [1, 1], [0, 2],
[2, 0], [1, 2], [2, 1], [2, 2]
];
/** Redundancy: each bit is repeated 25 times. */
private const REDUNDANCY = 25;
private const TERMINATOR = "