Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchSome links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Generate the PDF as bytes, store those bytes in durable storage, and return either a public object URL or a time-limited signed URL. Keep the object key (not a temporary signed link) as your durable database reference. The PHP example below uses mPDF for rendering and Amazon S3 through the AWS SDK for PHP for storage and private delivery.
The complete workflow
- Render: Build the PDF with a PHP library such as mPDF and capture its binary output.
- Store: Write the bytes to a private local directory or upload them to object storage such as Amazon S3.
- Authorize: Decide whether anybody with the URL may read the file (public URL) or whether each link should expire (presigned URL).
- Return: Send the URL in JSON, an HTTP redirect, or an HTML response. Save the object key or file ID for future requests.
For documents containing personal, financial, or business data, use private storage and issue a fresh signed URL when a user is authorized to download. A signed URL is a bearer credential: anyone who obtains it can use it until it expires or the underlying credentials become invalid.
Generate PDF bytes safely in PHP
Install a renderer
Install mPDF and the AWS SDK with Composer:
composer require mpdf/mpdf aws/aws-sdk-php
mPDF can convert trusted HTML and CSS into a PDF. Its manual explicitly warns, “mPDF is not meant to receive HMTL/CSS from an outside user.” If users can edit templates or content, sanitize and validate that input on the server; browser-style sanitization alone is not sufficient.
Render an invoice into a string
<?php
require __DIR__ . '/vendor/autoload.php';
use MpdfMpdf;
$customerName = 'Ada Lovelace';
$invoiceNumber = 'INV-1007';
$amount = '$125.00';
// Escape values before inserting them into HTML.
$html = '<!doctype html>
<html><body>
<h1>Invoice ' . htmlspecialchars($invoiceNumber, ENT_QUOTES, 'UTF-8') . '</h1>
<p>Customer: ' . htmlspecialchars($customerName, ENT_QUOTES, 'UTF-8') . '</p>
<p>Amount due: ' . htmlspecialchars($amount, ENT_QUOTES, 'UTF-8') . '</p>
</body></html>';
$mpdf = new Mpdf(['tempDir' => __DIR__ . '/var/mpdf']);
$mpdf->WriteHTML($html);
$pdfBytes = $mpdf->Output('', 'S'); // Return PDF bytes, do not send them yet.
if ($pdfBytes === '') {
throw new RuntimeException('The PDF renderer returned no data.');
}
Give the process a writable temporary directory, keep generated files outside the web root when possible, and apply authorization before generating a document for a requested account or order.
#1 Best Overall
Option 1: save locally and serve through PHP
Local storage is suitable for one server or a mounted durable volume. It is not durable by itself when you deploy multiple instances, replace a container, or lose a machine.
<?php
// Continue after $pdfBytes has been created.
$storageDir = __DIR__ . '/var/private-pdfs';
if (!is_dir($storageDir) && !mkdir($storageDir, 0700, true)) {
throw new RuntimeException('Cannot create private PDF directory.');
}
$objectKey = 'invoice-' . bin2hex(random_bytes(16)) . '.pdf';
$path = $storageDir . '/' . $objectKey;
if (file_put_contents($path, $pdfBytes, LOCK_EX) === false) {
throw new RuntimeException('Cannot write generated PDF.');
}
// Store $objectKey in your database. Do not expose the filesystem path.
$url = '/download.php?id=' . rawurlencode($objectKey);
header('Content-Type: application/json');
echo json_encode(['url' => $url], JSON_THROW_ON_ERROR);
Your download.php endpoint must authenticate the caller, look up the key in a database, verify ownership, and then stream the file with Content-Type: application/pdf. Do not concatenate an unchecked request parameter into a filesystem path; map an opaque database ID to a known path. Set a download disposition and a conservative cache policy for sensitive files.
Option 2: upload to Amazon S3
Upload the generated bytes
<?php
require __DIR__ . '/vendor/autoload.php';
use AwsS3S3Client;
$s3 = new S3Client([
'version' => 'latest',
'region' => getenv('AWS_REGION'),
]);
$bucket = getenv('S3_BUCKET');
$objectKey = 'generated/invoices/' . date('Y/m/') . bin2hex(random_bytes(16)) . '.pdf';
$s3->putObject([
'Bucket' => $bucket,
'Key' => $objectKey,
'Body' => $pdfBytes,
'ContentType' => 'application/pdf',
'Metadata' => ['document-type' => 'invoice'],
]);
// Persist $objectKey and document ownership in your database.
Use the SDK’s normal AWS credential chain (for example, an instance or task role) rather than embedding access keys in source code. Generate unique keys, set the content type, and keep the bucket private unless public delivery is an explicit requirement.
Return a public object URL only when intended
A public URL is easy to consume but allows anyone who knows or guesses the address to retrieve the PDF. Public access normally requires a deliberate bucket/object policy and conflicts with the safer default of keeping S3 Block Public Access enabled. Never grant public write permission just to simplify downloads.
If a CDN is required, CloudFront can keep the S3 origin private with origin access control. Route readers through the CDN URL instead of exposing the origin when origin restrictions matter.
Rank #2
Generate a private presigned URL
A presigned URL authorizes a specific S3 operation for a limited period without changing the bucket policy. The signer must have permission for the requested GetObject operation.
<?php
// Continue with the configured $s3, $bucket and $objectKey.
$command = $s3->getCommand('GetObject', [
'Bucket' => $bucket,
'Key' => $objectKey,
]);
$request = $s3->createPresignedRequest($command, '+15 minutes');
$signedUrl = (string) $request->getUri();
header('Content-Type: application/json');
echo json_encode([
'url' => $signedUrl,
'expires_in' => 900,
], JSON_THROW_ON_ERROR);
The expiry is a maximum, not a guarantee that the link will outlive the credentials used to sign it. Temporary credentials can expire sooner. Anyone you send the link to can reuse it during its valid window, so avoid logging full URLs and do not place them in publicly visible pages.
CloudFront signed delivery
For private CDN delivery, CloudFront signed URLs or signed cookies can enforce an end time and, where configured, a start time or IP address/range restriction. This adds key-management and distribution configuration, but lets the S3 bucket remain private.
Public URL versus signed URL
| Decision | Public object URL | Presigned URL |
|---|---|---|
| Who can retrieve it? | Anyone allowed by the public policy who has the address | Anyone holding the signed link until it expires |
| Expiry | Normally none; access ends when policy or object changes | Configured duration, limited by signer credential lifetime |
| Bucket privacy | Requires intentional public delivery | Bucket can remain private |
| Forwarding risk | URL can be forwarded indefinitely while public | Forwarded link works only during its validity window |
| CDN choice | Optional; CloudFront can hide the origin | S3 signing or CloudFront signing, depending on the delivery path |
Store the object key, document ID, owner, content type, and creation time. When a user asks to download, authorize the request and create a new signed URL. Treat the URL itself as a secret rather than as the permanent record.
Return the URL from an HTTP endpoint
A typical endpoint should authenticate the caller, load the document record, check ownership or a sharing permission, generate the URL, and return JSON:
header('Content-Type: application/json');
http_response_code(200);
echo json_encode(['url' => $signedUrl], JSON_THROW_ON_ERROR);
For a browser download, issue a server-side redirect only after the same authorization check. Do not let clients choose an arbitrary S3 bucket, key, or expiration value.
Free tools Windows power users keep installed
One-click scans. No signup required.
Reliability, performance, and cleanup
- Stream large inputs: Avoid loading user-uploaded source material into memory unnecessarily. For very large PDFs, use a temporary file and upload it as a stream where your SDK configuration supports that pattern.
- Make retries safe: Generate the database document ID first and use a deterministic or recorded object key so a retry does not create untracked duplicates. Confirm the upload succeeded before marking the record ready.
- Use lifecycle rules: Delete abandoned temporary files and define object-retention rules for documents that have a known legal or business lifetime.
- Control concurrency: Queue expensive rendering jobs when requests can generate many PDFs at once; return a job ID and provide a status endpoint rather than holding an HTTP connection indefinitely.
- Cache deliberately: A signed URL is cacheable by intermediaries unless response headers prevent it. For confidential PDFs, use private/no-store headers on your application response and avoid leaking links into analytics or referrer headers.
Troubleshooting common failures
“Class MpdfMpdf not found”
Composer dependencies are missing or the autoloader was not included. Run Composer in the deployment build and require vendor/autoload.php from the correct path.
mPDF cannot write temporary files
Set an explicit writable tempDir outside the public directory and verify the PHP process user has permission. Do not make the entire application directory world-writable.
The PDF is blank or malformed
Log the rendered HTML length and inspect the template for unescaped markup, unsupported CSS, broken image URLs, or output accidentally sent before the PDF bytes. Ensure the response is not mixing HTML warnings with binary PDF data.
S3 returns AccessDenied
Check the runtime role’s permission for s3:PutObject and s3:GetObject, the bucket and region values, and any explicit deny or encryption requirement. A signer cannot create a usable URL for an operation it is not allowed to perform.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Rank #4
The signed link expires early
Compare the requested expiry with the lifetime of the credentials that signed it. Refresh credentials and generate a new URL; do not assume a previously issued link can be extended.
Users see an XML “NoSuchKey” response
The database key does not match the uploaded key, the object was deleted, or the request points at the wrong bucket. Record the exact key returned by your upload operation and verify it before issuing a link.
A public URL returns 403
The bucket is private, Block Public Access is enabled, or a policy denies the request. Either keep the object private and use a presigned URL, or deliberately configure public/CDN delivery after reviewing the exposure.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
If your next step is turning a web page into an image or PDF for a generated report, ScreenshotNeo provides a single HTTP request rather than requiring you to operate a headless browser. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing result.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsAfter creating your PDF or report URL, you can capture a page with cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
PHP:
<?php
import requests; r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90); open("shot.webp", "wb").write(r.content)
The PHP snippet above is intentionally shown as the supplied Python-style request; use the equivalent PHP HTTP client in your application. For Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. Every plan includes its features; 1,000 screenshots per month are free with no card, and paid plans start at $5 for 3,000 shots. See the ScreenshotNeo API documentation and create a free account.
Security checklist
- Escape template values and sanitize any user-controlled HTML before passing it to mPDF.
- Keep S3 Block Public Access enabled unless public retrieval is a documented requirement.
- Use least-privilege roles and never put cloud credentials in source control.
- Authorize every download against a document owner or sharing rule.
- Use random, non-guessable object keys and store them server-side.
- Protect signed URLs as credentials and generate them only when needed.
- Set retention and deletion rules for temporary and final files.
Frequently Asked Questions
Should I store the signed URL in my database?
No. Store the object key or document ID and generate a fresh signed URL after authorization; the signed URL can expire or become invalid when its signing credentials expire.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Can a presigned URL be revoked immediately?
Not generally by changing the URL itself. Delete or replace the object, invalidate the signing credentials, or use a server-controlled download endpoint when immediate revocation is required.
When is local storage preferable to S3?
Local storage can be adequate for a single server with a durable volume and modest traffic. Use shared or object storage when deployments are distributed, files must survive machine replacement, or multiple workers need access.
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.

