Guzzle can download an existing PDF, but it cannot draw on PDF pages. Use Guzzle for HTTP, then use FPDI to import each page and TCPDF to add the text watermark. This guide shows that flow in PHP, including validating a remote response, preserving page dimensions, returning or uploading the result, and handling common failures.
What Guzzle does—and what adds the watermark
Guzzle is an HTTP client: it can request a PDF from a URL and send a finished PDF elsewhere. It does not provide PDF page-import or drawing APIs. The PDF work belongs to another library. For direct control, combine FPDI, which imports pages from an existing PDF, with TCPDF, which creates output pages and draws text and graphics. Guzzle’s overview documents its Composer installation and HTTP transport options.
The processing flow is: download and validate the source, save it privately, import every intended page, add a page of the same dimensions, place the imported page as the background, draw the watermark, write the output, and then return or upload it. Temporary files should be removed on both success and failure.
Install the PHP packages
Install Guzzle and the FPDI/TCPDF integration with Composer:
Free tools Windows power users keep installed
One-click scans. No signup required.
#1 Best Overall
composer require guzzlehttp/guzzle setasign/fpdi-tcpdf
Pin dependency versions through your application’s normal Composer workflow and confirm that the versions you select work with your PHP version. FPDI and TCPDF APIs can differ between major versions; check the documentation for the versions in your lockfile before relying on method signatures in a production deployment.
Download, watermark every page, and return the PDF
The controller example below downloads a remote PDF to a private temporary file, verifies the status and basic PDF signature, applies a centered diagonal watermark to every imported page, and returns the result as a PDF response. It uses TCPDF methods shown in the implementation pattern; verify their signatures against your pinned package versions.
<?php
use GuzzleHttpClient;
use setasignFpdiTcpdfFpdi;
$sourceUrl = 'https://example.com/source.pdf';
$inputPath = tempnam(sys_get_temp_dir(), 'pdf-in-');
$outputPath = tempnam(sys_get_temp_dir(), 'pdf-out-');
$maxBytes = 25 * 1024 * 1024; // Application policy: 25 MiB.
if ($inputPath === false || $outputPath === false) {
throw new RuntimeException('Could not create temporary files.');
}
try {
$http = new Client([
'timeout' => 30,
'connect_timeout' => 10,
'allow_redirects' => ['max' => 5],
]);
$response = $http->request('GET', $sourceUrl, [
'sink' => $inputPath,
'http_errors' => false,
]);
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
throw new RuntimeException('Source server returned HTTP ' . $response->getStatusCode());
}
clearstatcache(true, $inputPath);
$size = filesize($inputPath);
if ($size === false || $size === 0 || $size > $maxBytes) {
throw new RuntimeException('Downloaded file is empty or exceeds the size limit.');
}
$handle = fopen($inputPath, 'rb');
$signature = $handle ? fread($handle, 5) : false;
if ($handle) {
fclose($handle);
}
if ($signature !== '%PDF-') {
throw new RuntimeException('The response is not a PDF file.');
}
$pdf = new Fpdi();
$pageCount = $pdf->setSourceFile($inputPath);
for ($pageNo = 1; $pageNo <= $pageCount; $pageNo++) {
$templateId = $pdf->importPage($pageNo);
$size = $pdf->getTemplateSize($templateId);
$orientation = $size['width'] > $size['height'] ? 'L' : 'P';
$pdf->AddPage($orientation, [$size['width'], $size['height']]);
$pdf->useTemplate($templateId);
// Center the text on the page and draw it at a diagonal.
$pdf->SetAlpha(0.20);
$pdf->SetFont('helvetica', 'B', 28);
$pdf->SetTextColor(120, 120, 120);
$pdf->StartTransform();
$pdf->Rotate(45, $size['width'] / 2, $size['height'] / 2);
$pdf->Text(35, $size['height'] / 2, 'CONFIDENTIAL');
$pdf->StopTransform();
$pdf->SetAlpha(1);
}
$pdf->Output($outputPath, 'F');
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="watermarked.pdf"');
header('Content-Length: ' . filesize($outputPath));
readfile($outputPath);
} finally {
if (is_file($inputPath)) {
unlink($inputPath);
}
if (is_file($outputPath)) {
unlink($outputPath);
}
}
The maximum size and timeout in this example are application choices, not library requirements. Adjust them to the expected document size and your server’s resource limits. In a framework controller, return a streamed or file response using that framework’s response API, and register cleanup so the output is not deleted before it has been sent.
Rank #2
Preserving page size and orientation
Each imported page can have different dimensions. The loop obtains the imported page’s width and height and creates a matching output page before placing the original as its background. This avoids forcing a mixed-size or landscape document into one fixed page size. The sample selects landscape when width exceeds height; unusual dimensions should still be checked in the output.
Adjusting the watermark
Change the text, font family, style, size, RGB text color, alpha, rotation, and position to fit the document. TCPDF’s text APIs include page-level text and artifact-content methods; its behavior and exact signatures should be checked for the installed version. The example applies the text once per imported page. If only selected pages should receive it, add a page-number condition inside the loop and leave the other imported pages unmarked.
Upload the finished PDF with Guzzle
If the next step is sending the result to a storage service or another HTTP endpoint, pass a readable stream as the request body. Set the content type explicitly and use the destination’s required authentication and response handling:
$result = $http->request('PUT', $destinationUrl, [
'headers' => ['Content-Type' => 'application/pdf'],
'body' => fopen($outputPath, 'rb'),
]);
For a large output, a stream avoids reading the whole file into a PHP string before upload. Check the destination’s status code and close any explicitly opened stream when your application is done with it.
Alternative: use a watermark wrapper
The tomedio/pdf-watermark project README describes a configuration-oriented wrapper for text and image watermarks built on FPDI. Its documented text controls include font size, color, opacity, style, background, rotation, position, page ranges, and page-number placeholders. The package says it modifies existing pages without adding new ones and recognizes page sizes and orientations. Consult its current README for the required setup, namespaces, and exact method signatures rather than copying an example written for a different release.
A conceptual configuration looks like this:
$textConfig = $factory->createTextWatermarkConfig('CONFIDENTIAL');
$textConfig
->setPosition(AbstractWatermark::POSITION_CENTER)
->setOpacity(0.20)
->setFontSize(28)
->setTextColor(120, 120, 120);
$watermarker = $factory->createWithTextWatermark($textConfig);
$watermarker->apply($inputPath, $outputPath);
This is a sketch of the documented configuration pattern, not drop-in code: use the current package’s README and release constraints to supply the correct factory construction and imports. Direct FPDI/TCPDF code is preferable when you need explicit per-page geometry or application-specific control of temporary files. A wrapper may reduce repetitive configuration code, but it does not eliminate the need to test compatibility with the PDFs you process.
Rank #4
PDF compatibility and production safeguards
Compressed and higher-version PDFs
The watermark project’s compatibility notes warn that compressed PDFs with versions higher than 1.4 may not be directly processable by FPDI. Its documented workaround is to uncompress the input with pdftk, apply the FPDI operation, and recompress the output. This introduces an external executable into your processing path: install and pin it deliberately, handle subprocess errors, and isolate it when processing untrusted files. Do not assume every PDF needs this step.
Validate remote input before parsing
- Require an expected successful HTTP status; a reachable URL can still return an error page.
- Check the file signature and enforce an application-specific maximum download size. A content type header alone does not prove the response is a PDF.
- Use private, unpredictable temporary paths and ensure cleanup runs after exceptions as well as successful processing.
- Restrict which URLs your service is allowed to fetch if users supply the source URL. This helps prevent requests to internal services or local network addresses.
Test documents with security or unusual structure
The available package guidance does not establish a complete guarantee for encrypted, malformed, permission-restricted, or digitally signed PDFs. Test representative documents from your actual workload and decide how to reject or route unsupported files. Rewriting a PDF to add a watermark can invalidate a digital signature or change security settings; do not present the rewritten file as signature-preserving.
Check visual results, not only successful output
Review portrait and landscape pages, mixed page sizes, and pages with important content near the margins. A watermark that is too opaque, large, or centrally placed can obscure content; one that is too faint or small may be ineffective. Tune opacity, placement, rotation, and font for the document’s purpose, then inspect the generated PDF in a viewer. A successfully written file is not proof that the watermark is legible or unobtrusive.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Scan for outdated or missing drivers - takes under a minute3Clear out junk files and repair common Windows errorsTroubleshooting common failures
| Symptom | Likely cause | What to check or change |
|---|---|---|
| Guzzle throws for a request or the source returns an error status | The URL is unavailable, redirects unexpectedly, or requires authentication. | Inspect the final status and response headers; confirm the URL and any required headers or credentials. Handle non-2xx responses before passing a file to FPDI. |
| FPDI reports that the file cannot be opened or parsed | The response may be HTML, truncated, malformed, encrypted, or outside the parser’s supported PDF features. | Check the downloaded signature, size, and source response. Test the same file independently, and assess whether the documented pdftk uncompressing workaround applies. |
| Output pages have the wrong orientation or are clipped | The output page geometry does not match the imported page, or the source has unusual dimensions. | Use the imported template’s dimensions for each output page and inspect mixed-size pages. Avoid assuming all pages are standard portrait pages. |
| Watermark is missing, too dark, or misplaced | Alpha, text color, transform coordinates, font size, or drawing order is unsuitable. | Confirm the watermark is drawn after useTemplate, test alpha and coordinates on both portrait and landscape pages, and inspect the actual PDF output. |
| Memory or execution limits are exceeded | Large or complex documents consume more time and resources than the request budget allows. | Stream the download to disk, set a defensible size limit, process in a worker for long jobs, and size PHP limits to the documents you support. Do not raise limits without bounding input size. |
| A signature no longer validates | The PDF was rewritten after it was signed. | Watermarking may invalidate signatures. Keep the original signed document or apply the watermark before signing when the workflow requires a valid signature. |
Or skip the browser setup
For a website screenshot rather than watermarking an existing PDF, ScreenshotNeo is a screenshot API and MCP server: one GET request with a URL returns a PNG, JPEG, WebP, or PDF. It is a different job from adding text to a PDF—you cannot use it to watermark an existing document.
Here is the one-call cURL example, with the target URL adapted for a PDF capture:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo documentation for request options. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots, and the free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for free and try ScreenshotNeo.
Frequently Asked Questions
Can Guzzle itself add text to a PDF?
No. Guzzle handles HTTP requests; a PDF library such as FPDI with TCPDF must import pages and draw the watermark.
Recommended Free Tools
Will watermarking preserve a PDF’s digital signature?
Do not assume so. Rewriting a signed PDF can invalidate its signature; test your workflow and retain the original when signature validity matters.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




