If you sell digital books, sheet music, sewing patterns, design templates, or any kind of PDF‑based product, you’ve probably been told or have come to understand that “piracy is inevitable.”
It’s true that once something is on the internet it can be copied, but you can still tilt the odds in your favor. You can watermark and password your files, customizing them on-the-fly using plugins like PDF Ink. You can also easily add metadata and digital signatures to a PDF. But what else can you do?
But one surprisingly effective—and often overlooked—technique is embedding an invisible watermark on a handful of unpredictable pages. That’s easy to do using PDF Ink out-of-the-box, with settings included in the settings panels. Below we’ll explore why this works, how it keeps pirates guessing, and how it can become a powerful weapon in a DMCA takedown fight. But we’ll also get into ways to make these invisible watermarks even harder to find, using steganography (More about steganography on WikiPedia). 😳
Quarter-way through this blog post, more complex code examples are shared. These won’t be suited to the average WordPress “weekend warrior.” If you’re very serious about amping up your PDF security, please pass this information on to your developer!
The Psychology of Uncertainty
Pirates love certainty. When they crack a PDF, they typically look for a single, obvious watermark (a logo in the corner, a faint background pattern, or “©2025 Acme Corp. All Rights Reserved.” text). Once they locate it, they can either remove it with a PDF editor or simply ignore it because it’s everywhere and therefore expected.
An invisible, randomly‑placed watermark flips that script. Because the marker isn’t on every page and it isn’t visible to the naked eye, a pirate can’t be sure whether a given copy even contains a watermark at all. And worse for them, what if the watermark contains their name and email? That uncertainty forces them to spend extra time:
- Scanning every page for hidden data (metadata, steganographic bits, or transparent content).
- Testing removal tools on a sample of pages, hoping they won’t break the file.
- Deciding whether the effort is worth it—many will simply abandon a file that seems “more protected than meets the eye.”
The result? Higher friction for anyone who tries to redistribute your work without permission. And higher friction translates directly into fewer illicit copies floating around and less DMCA fight.
Add an Invisible Text Watermark to Random Pages – Example:
Using PDF Ink out-of-the-box, without any coding, start by setting random pages to mark.

Then write out your text (which could include the buyer’s name and email), set the font transparency either with the easy settings input

or by using the {OPAC} tag set at zero:

We’ve set the font to be tiny (2pt) and white, too–extra sneaky. You’re off to the races! Every downloaded PDF will now have an invisible watermark on pages 6, 11, and 36 with the buyer’s name and email.
If you wanted to mark pages based on the purchaser’s identifying information, you could do something like encrypt an identifier (like an email) using your own stored key, derive a set of random numbers from the encrypted identifier, then use the key and a reversing function if at a later time you need convincing proof that the PDF at one point was sold to that purchaser.
Types of Obvious and Not-So-Obvious PDF Watermarks & How They Work
Below we detail four ways to embed something in a PDF file. The first two are visible using a reader (or by viewing the PDF source code), and the last two the human eye can’t see but a computer can read.
1. Metadata tags (XMP, PDF‑Info)
Metadata tags store arbitrary key/value pairs in the document’s Info dictionary (legacy) or in an XMP packet (XML) that lives in the PDF catalog. The typical file size impact is usually < 1 KB (the XML block itself is often a few hundred bytes; any extra data you add is just appended).
Metadata tags are easily read/written with any PDF library. They are not hidden from casual inspection – anyone opening the file’s properties can see the data.
Add XMP – Example:
Below is an example of how to add XMP data to your PDF, when using TCPDF:
function add_xmp_order_id( $file, $handler, $settings ) {
$order_number = $settings['order_id'] ?? '';
if ( empty( $order_number ) ) {
return;
}
// Define a custom namespace 'order' pointing to a dummy URI (standard practice)
// and add the property 'OrderID'.
$xmp_snippet = '<rdf:Description rdf:about=""
xmlns:order="http://www.yourdomain.com/order#"
order:OrderID="' . htmlspecialchars($order_number, ENT_XML1, 'UTF-8') . '"
order:GeneratedDate="' . date('Y-m-d\TH:i:sP') . '" />
';
// Inject the snippet inside the <rdf:RDF> block
$handler->setExtraXMPRDF( $xmp_snippet );
}
add_action( 'pdfink_before_start_page', 'add_xmp_order_id', 10, 3 );
Here’s another example, in case you have more complex XMP you wish to add to your PDF files:
function add_my_xmp_rdf( $file, $handler, $settings ) {
$handler->setExtraXMPRDF( '<rdf:Description rdf:about="">
<dc:creator>Your Name</dc:creator>
<dc:title>Your Document Title</dc:title>
<dc:description>A detailed description of the content, outlining its purpose, context, and any relevant information that reinforces ownership.</dc:description>
<dc:rights>Copyright © 2023 Your Name. All rights reserved.</dc:rights>
<copyright:owner>Your Name</copyright:owner>
<dc:language>en-US</dc:language> <!-- Language of the content -->
<dc:date>2025-11-15</dc:date> <!-- Creation date -->
<dc:relation>Related Work (if any or link to a project)</dc:relation> <!-- Link to related content -->
<dc:identifier>Unique-Identifier-12345</dc:identifier> <!-- Unique ID for the work -->
<dc:type>TextDocument</dc:type> <!-- Type of the work -->
<dc:format>application/pdf</dc:format> <!-- Format of the file -->
<dc:coverage>Global</dc:coverage> <!-- Geographic coverage -->
<dc:rightsHolder>Your Name</dc:rightsHolder> <!-- Rights holder information -->
<dc:copyright>Licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License</dc:copyright>
</rdf:Description>' );
}
add_action( 'pdfink_before_start_page', 'add_my_xmp_rdf', 10, 3 );
2. Digital Signatures
A PDF signature computes a cryptographic hash of the entire PDF (or selected byte ranges) and signs it with a private key. The signature is stored as a Signature Dictionary (PDF‑Sig) inside the file. This adds a signature object (typically 300-800 bytes for an RSA‑2048 PKCS#7/CMS container) plus the incremental update that stores the signature (a few hundred bytes). No separate .sig file is required; the signature lives inside the PDF.
Signatures is not implemented yet between PDF Ink and SetaPDF-Signer. This would provide integrity/authenticity, but not secrecy. A signature is visible to anyone who inspects the PDF’s AcroForm or Signature fields. It does not hide data, but it can be used to prove that hidden data (e.g., metadata) has not been altered. In other words, signatures protect the integrity of whatever you’ve already embedded.
3. Least‑Significant‑Bit (LSB) steganography
If your PDF will have graphics, this could be an option for you. LSB alters the lowest‑order bits of pixel data in raster images (e.g., JPEG, PNG) that are embedded in the PDF. The altered bits encode the hidden payload. The visual change is invisible, but the file size grows by the size of the payload (often < 2 % for a few dozen bytes). If you embed a few hundred bytes, the increase is roughly the same number of bytes.
LSB works only when the PDF contains uncompressed or losslessly compressed bitmap streams (e.g., PNG, BMP, TIFF, or JPEG‑2000). Regular JPEG streams are already compressed so are not candidates for LSB. Flipping LSBs can slightly increase entropy but may cause a modest size bump after re-compression.
Adding Identifying LSB to an Image In Your PDF – Example:
You could embed a 16‑byte user ID or order ID in the LSB of the first image on page 7, another on page 22, and a third on page 41. The randomness means that even if a pirate discovers a watermark on page 7, they have no clue that page 22 carries a different marker or any at all.
/**
* PNG LSB Steganography Encoder for PHP
*
*
* Note: This script converts the output to PNG to preserve the hidden data.
* Use a flattened PNG or BMP. JPG compression destroys LSB data
*/
function pdf_image_steganography( $page_no, $handler, $size, $settings ) {
/**
* Here you *could* use $page_no to determine which PDF pages we add an LSB'd
* image to, but for the sake of brevity, we will skip that logic.
*/
/**
* @todo: DEFINE the IMAGE SOURCE FILE
* `PDFINK_UPLOADS_PATH` is a constant leading to .../wp-content/uploads/pdf-ink/
*/
$input_file = PDFINK_UPLOADS_PATH . 'steganography/images/logo.png';
if ( ! file_exists( $input_file ) ) {
error_log( "Steganography error: Input file $input_file not found." );
return;
}
/**
* @todo: DEFINE where to store the altered image
* We'll leave you to sort out image storage/cleanup
*/
$output_file = PDFINK_UPLOADS_PATH . 'steganography/images/tmp/logo.png';
/**
* @todo: DEFINE our SECRET MESSAGE.
* Luckily if using WooCommerce and PDF Ink v2 we have some values
* available in the $settings array
*/
$secret_message = $settings['order_id'] ?? '';
if ( empty( $secret_message ) ) {
$secret_message = $settings['email'] ?? '';
}
if ( empty( $secret_message ) ) {
error_log( "Steganography error: No secret message to encode." );
return;
}
// Get image size & MIME
$image_info = getimagesize( $input_file );
if ( ! $image_info ) {
error_log( "Steganography error: Could not read image file." );
return;
}
$width = $image_info[0];
$height = $image_info[1];
$mime_type = $image_info['mime'];
// Create a GdImage based on mime type
switch ( $mime_type ) {
case 'image/jpeg':
$image = imagecreatefromjpeg( $input_file );
break;
case 'image/png':
$image = imagecreatefrompng( $input_file );
break;
case 'image/gif':
$image = imagecreatefromgif( $input_file );
break;
default:
error_log( "Steganography error: Unsupported image format: $mime_type ");
return;
}
if ( ! $image ) {
error_log( "Steganography error: Failed to load image." );
return;
}
/**
* We prepend the length of the message (4 bytes) so the decoder knows when to stop.
* Then we convert the message to binary
*/
$length = strlen( $secret_message );
if ( $length > 255 ) {
// Simple limitation: 1 byte for length (0-255 chars)
// For longer messages, you'd need 4 bytes for length (uint32)
error_log( "Steganography error: Message too long. Maximum 255 characters for this simple script." );
return;
}
/**
* Create a binary string: 4 bytes for length (padded) + message bytes
* We use a simple 1-byte length prefix for this demo (max 255 chars)
*/
$binary_data = str_pad( decbin( $length ), 8, '0', STR_PAD_LEFT ); // Length (8 bits)
$binary_data .= implode( '', array_map( function( $char ) {
return str_pad( decbin( ord( $char ) ), 8, '0', STR_PAD_LEFT );
}, str_split( $secret_message ) ) );
$total_bits = strlen( $binary_data );
$total_pixels_needed = ceil( $total_bits / 3 ); // 3 bits per pixel (R, G, B)
if ( $total_pixels_needed > ( $width * $height ) ) {
error_log( "Steganography error: Image too small to hold this message. Need {$total_pixels_needed} pixels, have " . ($width * $height) );
return;
}
$bit_index = 0;
$pixel_count = 0;
// Encode the Data
for ( $y = 0; $y < $height; $y++ ) {
for ( $x = 0; $x < $width; $x++ ) {
if ( $bit_index >= $total_bits ) {
break 2; // Done encoding
}
$rgb = imagecolorat( $image, $x, $y );
$r = ( $rgb >> 16 ) & 0xFF;
$g = ( $rgb >> 8 ) & 0xFF;
$b = $rgb & 0xFF;
if ( $bit_index < $total_bits ) {
$r = ( $r & 0xFE ) | intval( $binary_data[$bit_index] ); // red
$bit_index++;
}
if ( $bit_index < $total_bits ) {
$g = ( $g & 0xFE ) | intval( $binary_data[$bit_index] ); // green
$bit_index++;
}
if ( $bit_index < $total_bits ) {
$b = ( $b & 0xFE ) | intval( $binary_data[$bit_index] ); // blue
$bit_index++;
}
$new_color = imagecolorallocate( $image, $r, $g, $b );
imagesetpixel( $image, $x, $y, $new_color );
}
}
if ( pathinfo( $output_file, PATHINFO_EXTENSION ) === 'jpg' || pathinfo( $output_file, PATHINFO_EXTENSION) === 'jpeg' ) {
error_log( "Steganography warning: Saving as JPG will likely destroy the hidden data due to compression. Converting to PNG." );
$output_file = pathinfo( $output_file, PATHINFO_FILENAME ) . ".png";
}
// Save the Image; default to PNG
if ( false !== strpos( $output_file, '.bmp' ) ) {
if ( ! imagebmp( $image, $output_file ) ) {
error_log( "Steganography error: Failed to save image" );
}
} else {
if ( ! imagepng( $image, $output_file ) ) {
error_log( "Steganography error: Failed to save image as PNG" );
}
}
imagedestroy( $image ); // Free up memory
/**
* Use TCPDF (bundled in PDF Ink) to add our image 10mm from top of left corner
* Other acrobatics could be done with TCPDF first, to move the cursor elsewhere, but
* we'll leave that up to you
*/
$handler->Image( $output_file, 10, 10, $width, $height );
}
add_action( 'pdfink_after_setup_page', 'pdf_image_steganography', 10, 4 );
Images with alpha channel (transparent images) will dramatically complicate the LSB encode/decode computation, so our example handles a non-transparent PNG.
What this does:
- Fires while looping through pages (as set in settings; we’ll presume “all” pages are set to mark), before any (if any) placements are added
- “Engraves” some customer data on a PNG file (your logo, maybe?)
- Places the PNG image (visually-identical to the original) on the page
- Provides debugging in the WP/PHP error logs in case something has gone wrong
You can replace this with a more sophisticated LSB steganography routine if you need alpha channel handling or extra robustness against aggressive re‑compression. The core idea stays the same: only a few unpredictable pages carry the secret marker.
4. Frequency‑domain embedding
Frequency-domain embedding can embed data by first transforming an image file into its frequency domain representation using transforms such as the Discrete Cosine Transform (DCT) or Fast Fourier Transform (FFT). It then reverses the transform, leaving behind nearly imperceptible changes to the image. The changes survive JPEG recompression because they are made in the transform domain rather than the raw pixel domain. There is minimal file size change because you’re tweaking existing coefficient values, not adding new data. The payload is usually a few hundred bytes spread across many coefficients.
Frequency-domain embedding is more robust form of steganography, but you need control over the JPEG encoding parameters. If a downstream tool re‑encodes the JPEG at a different quality factor, the hidden bits can be lost.
“Nice” that we discussed frequency-domain embedding, because currently there is no commercial software available to do that for you. 🥴
From Watermark to Evidence: The DMCA Angle
When you discover an infringing copy of your PDF online, the first step is usually a DMCA takedown notice. The notice must include “proof of ownership”—something that convinces the host that you are indeed the rights holder. An invisible watermark gives you exactly that:
- Identify the source – Extract the hidden data from the suspect PDF. If you used buyer‑specific identifiers (e.g., an email address), you can point to the original transaction record in your database.
- Show uniqueness – Because the watermark appears only on a random subset of pages, you can demonstrate that the infringing file matches your version down to those exact pages. This is far stronger than a generic claim of ownership.
- Speed up the process – Hosts often accept a DMCA notice with a clear, verifiable marker much faster than a vague claim. The quicker the takedown, the less chance the file spreads further.
In practice, you’d run a small script (or use a service) that reads the hidden markers from the PDF, compares them against your records, and spits out a concise “ownership proof” paragraph you paste into the DMCA form. The result is a professional, evidence‑backed request that stands out among the sea of generic takedowns.
Best Practices to Keep Your Watermark Effective
The following advice is beyond the means of most WordPress administrators, but that isn’t to say it can’t be done!
| Practice | Why It Matters |
|---|---|
| Rotate the selection – Change which pages receive a tag for each new sale | Prevents pattern learning; pirates can’t build a “cheat sheet.” |
| Vary the technique – Alternate between watermarks, metadata, and LSB | Increases resilience; a single removal tool won’t wipe everything. |
| Log the exact pages in your order database | Makes it easier to pull the right evidence when a DMCA notice is needed. |
| Keep the hidden payload small (under 32 bytes) | Minimizes file‑size bloat and reduces the chance of accidental corruption. |
| Test with common editors (Adobe Acrobat, PDFtk [toolkit], Preview) | Ensures the watermark survives typical user actions like “Save As.” |
| Document the process for your legal team | A clear chain‑of‑custody strengthens your DMCA claim. |
The Bottom Line
Invisible watermarks placed on random, unpredictable pages turn a static PDF into a living piece of evidence. They raise the cost of piracy, keep would‑be thieves guessing, and give you a concrete, verifiable hook for DMCA takedown notices. While no method can guarantee 100% protection, combining a subtle technical shield with a solid legal process dramatically improves your odds of staying in control of your own work.
If you’re ready to start protecting your PDFs, try the snippet above and experiment with a few different hidden‑payload strategies. Once you see how easy it is to embed a buyer‑specific tag that no one else can spot, you’ll wonder how you ever released PDFs without it.
Happy watermarking—and may your PDFs stay both beautiful and un‑piratable!