Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Download the watermark with aiohttp, open the PDF with PyMuPDF, and insert the image on every page with Page.insert_image(). Set overlay=False to put it behind existing text, reuse the returned image cross-reference (xref) for repeated pages, and save to a new file. Use await response.read() for small images; stream chunks to a temporary file when the image could be large.
Install the libraries
Install the two runtime dependencies in the environment that will run the script:
python -m pip install aiohttp pymupdf
PyMuPDF is imported as pymupdf. The examples assume a local input PDF and a writable output directory.
Free tools Windows power users keep installed
One-click scans. No signup required.
Complete example: download and watermark every page
This version keeps the HTTP operation asynchronous, checks the response status before reading it, places the image below existing PDF content, and reuses the embedded image on subsequent pages.
#1 Best Overall
import asyncio
from pathlib import Path
import aiohttp
import pymupdf
async def download_bytes(session: aiohttp.ClientSession, url: str) -> bytes:
async with session.get(url) as response:
response.raise_for_status()
return await response.read()
def watermark_pdf(
input_path: str,
output_path: str,
image_bytes: bytes,
*,
overlay: bool = False,
) -> None:
document = pymupdf.open(input_path)
try:
image_xref = 0
for page in document:
image_xref = page.insert_image(
page.rect,
stream=image_bytes,
xref=image_xref,
overlay=overlay,
keep_proportion=True,
)
document.save(output_path)
finally:
document.close()
async def main() -> None:
image_url = "https://example.com/watermark.png"
input_pdf = "input.pdf"
output_pdf = "watermarked.pdf"
timeout = aiohttp.ClientTimeout(total=90)
async with aiohttp.ClientSession(timeout=timeout) as session:
image = await download_bytes(session, image_url)
watermark_pdf(input_pdf, output_pdf, image)
if __name__ == "__main__":
asyncio.run(main())
The call to page.insert_image() uses the page rectangle, so the image is placed across the page area. keep_proportion=True preserves its aspect ratio; the resulting image may be letterboxed or centered depending on the source and page shape. The output is a separate PDF, leaving the input unchanged.
Choose foreground or background layering
| Goal | Setting | Result |
|---|---|---|
| Keep text and drawings readable | overlay=False |
Image is inserted behind existing page content. |
| Make the mark sit above the page | overlay=True (the default) |
Image is inserted in the foreground. |
A foreground watermark should normally be a PNG (or another source) that already contains transparency. PyMuPDF uses the image’s own transparency; the API does not turn an opaque bitmap into a translucent one for you. An opaque full-page foreground image can hide text.
Place a logo or stamp in a custom rectangle
Replace page.rect with a pymupdf.Rect when a full-page mark is not appropriate. Coordinates are PDF points, with the page’s coordinate system used by PyMuPDF.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutedef watermark_with_logo(input_path: str, output_path: str, image_bytes: bytes) -> None:
document = pymupdf.open(input_path)
try:
image_xref = 0
for page in document:
# Example: a 180 x 60 point logo near the lower-right corner.
box = pymupdf.Rect(
page.rect.x1 - 200,
page.rect.y1 - 80,
page.rect.x1 - 20,
page.rect.y1 - 20,
)
image_xref = page.insert_image(
box,
stream=image_bytes,
xref=image_xref,
overlay=False,
keep_proportion=True,
)
document.save(output_path)
finally:
document.close()
Adjust the rectangle for your page size and desired margins. A source image with a transparent background works well for logos and stamps.
Rank #2
Stream a large watermark image instead of holding it in memory
response.read() loads the complete response body. For a large asset, stream 64 KiB chunks to a temporary file and pass that filename to PyMuPDF. This limits the download buffer rather than creating one large bytes object.
import asyncio
import tempfile
from pathlib import Path
import aiohttp
import pymupdf
async def download_file(
session: aiohttp.ClientSession,
url: str,
filename: Path,
) -> None:
async with session.get(url) as response:
response.raise_for_status()
with filename.open("wb") as output:
async for chunk in response.content.iter_chunked(64 * 1024):
output.write(chunk)
def watermark_from_file(
input_path: str,
output_path: str,
image_filename: str,
) -> None:
document = pymupdf.open(input_path)
try:
image_xref = 0
for page in document:
image_xref = page.insert_image(
page.rect,
filename=image_filename,
xref=image_xref,
overlay=False,
keep_proportion=True,
)
# Compression can be considered when writing the result.
document.save(output_path, deflate=True)
finally:
document.close()
async def large_image_workflow() -> None:
timeout = aiohttp.ClientTimeout(total=90)
async with aiohttp.ClientSession(timeout=timeout) as session:
with tempfile.TemporaryDirectory() as directory:
image_path = Path(directory) / "watermark-image"
await download_file(session, "https://example.com/watermark.png", image_path)
watermark_from_file("input.pdf", "watermarked.pdf", str(image_path))
if __name__ == "__main__":
asyncio.run(large_image_workflow())
The temporary directory is removed after the PDF has been written. Keep the file until document.save() completes because PyMuPDF still needs to read it during insertion.
Reuse one aiohttp session correctly
Create one ClientSession for a workflow and close it with async with. This reuses the session’s connection resources and avoids creating a session for every page. If several watermark URLs must be downloaded, pass the same session to each download function. Do not perform blocking PDF work inside a long-running asynchronous download loop if your application has strict event-loop latency requirements; run CPU-heavy jobs in a worker process when necessary.
Control memory, output size and repeated-page work
- Use in-memory bytes for a small image and a streamed temporary file for a large one.
- Pass the first
insert_image()return value asxrefon later pages. The same embedded image data can then be reused instead of repeatedly embedding it. - Keep the source image at a sensible pixel size. Inserted images retain their original quality, so an unnecessarily huge source can inflate the PDF.
- Consider
deflate=Truewhen saving, then check the resulting file size and visual quality with the viewers your users actually use. - Always write to a different output path. Saving over the input complicates recovery if the process is interrupted.
HTTP and PDF validation checks
raise_for_status() catches HTTP failures before an HTML error page or access-denied response is treated as an image. You should also verify that the URL is permitted to serve the asset to your server; a browser-visible image is not automatically available for server-side downloads.
After saving, open the result in the target PDF viewer and inspect pages containing text, transparency, rotations and unusual dimensions. A successful HTTP response and a successful save do not guarantee that the source bytes are a suitable image or that every viewer renders its transparency identically.
Troubleshooting
401, 403 or another HTTP error
The image server rejected the request. Check the URL, required authentication and hotlink or referrer policy. Because raise_for_status() runs before reading, the failure is explicit instead of producing a misleading PDF error.
The downloaded bytes are not an image
Some servers return an HTML login page, redirect target or bot challenge with a success status. Save the streamed response temporarily and inspect its content type and first bytes, or download a known-good image URL. Do not assume an extension proves the format.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
The watermark hides text
Use overlay=False for a background mark, or supply a source image containing transparency when using the default foreground layer. A large opaque image can cover selectable text even when the PDF itself is otherwise valid.
The logo is stretched or clipped
Use keep_proportion=True and design a rectangle with a similar aspect ratio to the source. A full-page rectangle is rarely the right geometry for a small logo; use a custom pymupdf.Rect.
The output PDF is unexpectedly large
Reduce the source image dimensions or encoding before insertion, reuse the xref, and try deflate=True when saving. Measure your own files; the available evidence does not establish a universal size reduction or processing-time percentage.
Later pages are slow or contain repeated image data
Initialize image_xref = 0 once, assign the return value from the first insertion, and pass that xref to subsequent calls. Ensure the same image bytes or filename are used for every page.
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 →The source PDF is locked or malformed
PyMuPDF may refuse to open, edit or save a protected document. Confirm that the file opens normally, handle any required password through your application’s document-access flow, and test with a repaired or unlocked copy where permitted.
Best Value
Or skip the browser setup
If what you actually need is a clean screenshot of a web page to use as an image asset, ScreenshotNeo returns PNG, JPEG or WebP from one request. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; each step can be disabled. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets Claude, Cursor and other MCP clients call take_screenshot, get_page_info and capture_pdf.
For a one-call image, see the ScreenshotNeo API documentation:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python and Node.js clients can use the same endpoint:
Recommended Free Tools
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
The Free plan includes 1,000 screenshots each month with no card. Paid plans start at $5 for 3,000 shots; every feature is included on every plan. Create a free ScreenshotNeo account.
Frequently Asked Questions
Can I watermark only selected pages?
Yes. Replace the loop over the whole document with a loop over the page indexes you choose, and call document.load_page(index) for each selected page.
Will the watermark become permanent page content?
The inserted image is stored in the output PDF page content. It is not a removable viewer annotation, so remove or replace it by editing the PDF again.
Can aiohttp download a local file path?
No. aiohttp is for HTTP-based transfers. For a local watermark, skip the download and pass its path with PyMuPDF’s filename= parameter.
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.

