Sometimes our PDF files either have outdated links or links needing adjustment for other reasons. The ability to modify outbound links (and other annotations) in PDF files is possible using PDF Ink + the SetaPDF-Stamper library as an add-on. We’re not talking about manually editing PDF files here using Adobe or another application. This article is about changing PDFs programmatically, instantly, and by the thousands, over years… using some code applied once. Magical! 🧚🏽
Why modify links inside a PDF?
Maybe we want to add affiliate tracking, or campaign analytics with a custom query string. Here are three examples:
1. Attribute clicks that originate from a PDF that was delivered after a purchase:
http://your-url.com?customer=12345
2. Capture traffic coming from the PDF using any analytics platform like Google Analytics, letting you measure the effectiveness of downloadable assets:
http://your-url.com?utm_source=pdf&utm_medium=download&utm_campaign=spring‑sale
3. Replace a generic vendor URL with a branded landing page, improving brand consistency and giving control over the final destination.
https://your-url.com/redirect/welcome?source=pdf&customer=Alice
All of these scenarios boil down to adding or replacing query parameters in the URI stored in a PDF’s link annotations. With just a handful of lines of PHP, you turn every PDF generated by PDF Ink into a trackable, personalized asset that feeds data back into your marketing, analytics, or compliance pipelines.
How to do it using PDF Ink + SetaPDF Stamper
The good news is that SetaPDF Core already provides a clean API for walking through a PDF document’s pages and updating each link. All we need are some lines of code–a snippet–that ties the API into PDF Ink’s existing hook system.
PDF Ink’s SetaPDF-Stamper strategy already provides a $document object, a $writer object, a $settings array that includes some WooCommerce/Easy Digital Downloads order information, and a $pages collection. The plugin also fires an action hook called pdfink_before_setapdf_stamper_callback1. That hook is the perfect insertion point for our link‑rewriting logic.
The Snippet
Below is a complete, ready‑to‑paste snippet that you can drop into child theme’s functions.php or small plugin.
use setasign\SetaPDF2\Core\Document\Action\UriAction;
use setasign\SetaPDF2\Core\Document\Page\Annotation\Annotation;
function custom_add_affiliate_parameter( object $document, object $writer, array $settings, object $pages, object $stamp ) {
// Collect the identifiers
$order_id = isset( $settings['order_id'] ) ? intval( $settings['order_id'] ) : 0;
$product_id = isset( $settings['product_id'] ) ? intval( $settings['product_id'] ) : 0;
// Bail early if we have nothing to attach
if ( $order_id === 0 && $product_id === 0 ) {
return;
}
// Build a query string
$query_fragment = http_build_query([
'affiliate' => $order_id,
'product' => $product_id,
]);
$page_count = $pages->count();
$changed_count = 0;
// Iterate over every page
for ( $page_number = 1; $page_number <= $page_count; $page_number++ ) {
$page = $pages->getPage( $page_number );
$annotations_helper = $page->getAnnotations();
// Only link annotations (type = Annotation::TYPE_LINK)
$annotations = $annotations_helper->getAll( Annotation::TYPE_LINK );
// Go through each link annotation on page
foreach ( $annotations as $annotation ) {
$action = $annotation->getAction();
// Only touch URI actions (external HTTP/HTTPS links)
if ( $action && $action instanceof UriAction ) {
// Parse the existing URI
$original_uri = $action->getUri(); // e.g. https://example.com/item/42
$parsed = parse_url( $original_uri );
// Re‑assemble the base part (scheme + host + path)
$base = ( $parsed['scheme'] ?? 'https' ) . '://' . ( $parsed['host'] ?? '' ) . ( $parsed['path'] ?? '' );
// Existing query string (if any)
$existing_query = $parsed['query'] ?? '';
// Merge new query parameters with existing
$new_query = $existing_query
? $existing_query . '&' . $query_fragment
: $query_fragment;
// Preserve fragment (#anchor) if present
$anchor = isset( $parsed['fragment'] ) ? '#' . $parsed['fragment'] : '';
// Write the updated annotation
$action->setUri( $base . '?' . $new_query . $anchor);
$changed_count++;
}
}
}
// Log the operation
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log(
sprintf(
'[PDF Ink] Order %d / Product %d – rewrote %d link(s) in %d page(s).',
$order_id,
$product_id,
$changed_count,
$page_count
)
);
}
}
add_action( 'pdfink_before_setapdf_stamper_callback', 'custom_add_affiliate_parameter', 10, 5 );
What the code snippet does:
- Hooks into
pdfink_before_setapdf_stamper_callbackwith ourcustom_add_affiliate_parameterfunction - Collects the identifiers (WooCommerce
order_idandproduct_id) that PDF Ink already passed in the$settingsarray - Builds a query string (example
?affiliate=12345&product=678). You can swap this for your special parameters (utm_*,token=…, etc.) - Iterates over every page in the PDF
- Goes through each link annotation (
UriAction/external URL) on that page - Parses the existing URL
- Merges the new query parameters with any that already exist, and writes the updated URL back into the annotation.
- Logs the operation when WordPress debugging is active – handy for confirming that the hook fired correctly. It logs how many links were rewritten (useful for debugging, but will bloat if used on a production site)
Because the code runs before the document is saved, the final PDF that the customer downloads already contains the rewritten links. No post‑processing step is required.
Integrating the snippet into a real‑world workflow
- Create a small WP plugin (or if you’re not a developer, add the snippet to your WP child theme, or by using the WP Code Snippets plugin).
- Ensure PDF Ink passes the needed settings – the PDF Ink WooCommerce integration already exposes the Woo order ID and product ID in the
$settingsarray. If you need extra data (e.g., a campaign ID), extend the array using a filter hook likepdfink_filter_settings. - Test locally:
- Generate a PDF and a test order (so as to have order and product ID available)
- Open the PDF in Adobe Acrobat Reader (or any viewer that shows link destinations)
- Hover over a link – the tooltip should display the URL with the appended query string
- Deploy to production once you’ve verified that the URLs are correct and that no existing links are broken.
Common pitfalls & how to avoid them
Note: You MUST be using PDF Ink, WooCommerce, and SetaPDF-Stamper for this particular snippet to work!
Links lose their original query parameters after processing
Likely cause: The code overwrites the query string instead of merging
Fix: Maybe use the parse_url() + $existing_query logic shown above, or call something like http_build_query( array_merge( parse_str( $existing_query ), $new_params ) )
Some links remain unchanged
Likely cause: The annotation is not a UriAction (maybe it’s instead a GoTo‑E action or a JavaScript action)
Fix: The snippet intentionally skips non‑URI actions – that’s safe. If you need to handle them, add additional instanceof checks.
PDF size balloons dramatically
Likely cause: Your snippet involves extra loops or the callback is called multiple times.
Fix: Verify that the hook fires only once per document generation. You can add a static flag inside the function (static $already_run = false;)
Users report broken redirects after clicking a link
Likely cause: A typo, or the target site does not accept the extra query string.
Fix: Make the query addition optional (e.g., only for affiliate partners) or use a server‑side redirect that strips unknown parameters.
Other ways to enrich PDFs with PDF Ink
Replace a placeholder URL
Why not replace every instance of a specific url in your PDF (https://example.com/PLACEHOLDER) with a different and/or dynamic one? In the loop, test strpos( $original_uri, 'PLACEHOLDER' ) !== false and substitute the portion you need with data you have available.
Add a QR code with a customized URL
It’s easy to add any type of customized-on-the-fly barcode to your PDF files, including QR codes. Why not add personalized URL queries, or a tracking query to your QR code? Just set your barcode settings in the PDF backend (or pass them via $settings array), then filter the barcode content once you have WooCommerce (or other e-commerce) order/customer data available. This involves custom PHP development, but is not necessarily a complex project, nor does it require additional purchases aside from PDF Ink. Barcoding can be done with the free libraries bundled with PDF Ink, and with the SetaPDF-Stamper upgrade.
Insert a custom PDF metadata field
This is another relatively easy one for your development team, or a scrappy vibe coder. Let’s say we want to add a WooCommerce Order ID to the PDF Metadata (how helpful might that be later on, maybe even in a DMCA takedown or other IP dispute). Other information can be culled from an order ID via order object, such as name, email, address, etc., but for the sake of simplicity we’ll just use the order ID below in an example:
function custom_setapdf_core_document_info( $info, $document, $settings ) {
$order_id = isset( $settings['order_id'] ) ? intval( $settings['order_id'] ) : 0;
if (empty( $order_id ) ) {
return;
}
$info->setSyncMetadata( true );
$info->setCustomMetadata( 'OrderID', $order_id );
$info->syncMetadata();
}
add_action( 'pdfink_filter_setapdf_core_document_info', 'custom_setapdf_core_document_info', 10, 3 );
Ideas like these all rely on the same hook‑based architecture that PDF Ink exposes, so you can stack them together without touching the core plugin. PDF Ink is structured in a way where the entire Seta API can be taken advantage of, which allows a plethora of *fun* PDF manipulations.
- If you are not running WordPress, the hook can be triggered with your own function named
pdfink_before_setapdf_stamper_callback(), with the arguments:$document, $writer, $settings, $pages, $stamp↩︎