Add a Watermark to an Excel Sheet with Python
A draft budget that gets forwarded as if it were final, a confidential salary report printed and left on a desk — a watermark across the page is the usual defence. Word has a Watermark button; Excel does not. What Excel users call a watermark is one of two workarounds: a picture placed in the page header, which Excel prints on every page and shows in Page Layout view, or a sheet background image, which shows on screen but never prints. Python can produce both, and when the report is shared as a PDF, it can stamp the PDF itself, which is the most reliable of all. This page is part of Inserting Images and Logos into Excel.
Prerequisites
pip install xlsxwriter pillow pypdf reportlab openpyxl
The examples build a small budget sheet from scratch with xlsxwriter, which supports header images and backgrounds directly. openpyxl can set the header text that references a picture, but it cannot add the picture itself, and — as Step 5 shows — it removes existing ones when it saves.
Step 1 — Make a transparent watermark image
The watermark is an ordinary PNG: large pale text, rotated, on a transparent background so the cells show through. Pillow draws it:
from PIL import Image, ImageDraw, ImageFont
def watermark_png(text, path, size=(1000, 360), rgba=(220, 38, 38, 60), angle=35):
img = Image.new("RGBA", size, (255, 255, 255, 0))
draw = ImageDraw.Draw(img)
try:
font = ImageFont.truetype("DejaVuSans-Bold.ttf", 200)
except OSError:
font = ImageFont.load_default(size=200)
draw.text((size[0] / 2, size[1] / 2), text, font=font, fill=rgba, anchor="mm")
img = img.rotate(angle, expand=True) if angle else img
img.save(path)
return img.size
print(watermark_png("DRAFT", "draft.png"))
The alpha value of 60 out of 255 makes the text faint enough to read the numbers through. rotate(..., expand=True) grows the canvas so the rotated word is not cropped at the corners — without it, the ends of the letters are cut off. Any TrueType font works; the fallback keeps the function running on machines without DejaVu installed.
Step 2 — Put the image in the page header
Excel's watermark technique is a picture in the centre section of the header, referenced by the &[Picture] code (stored as &G). xlsxwriter's set_header takes the header string and the image:
import xlsxwriter
wb = xlsxwriter.Workbook("budget_draft.xlsx")
ws = wb.add_worksheet("Budget")
bold = wb.add_format({"bold": True})
money = wb.add_format({"num_format": "#,##0"})
ws.write_row(0, 0, ["Cost centre", "Q1", "Q2", "Q3", "Q4"], bold)
rows = [("Marketing", 42000, 45000, 39000, 51000), ("Engineering", 180000, 184000, 190000, 195000),
("Sales", 96000, 99000, 101000, 108000), ("Support", 51000, 51000, 53000, 55000)]
for r, row in enumerate(rows, start=1):
ws.write(r, 0, row[0])
ws.write_row(r, 1, row[1:], money)
ws.set_column("A:A", 16)
ws.set_column("B:E", 11)
ws.set_header("&C&[Picture]", {"image_center": "draft.png"})
ws.set_page_view()
wb.close()
print("written")
set_page_view() makes the sheet open in Page Layout view, where header pictures are visible; in Excel's Normal view they are hidden, which is the most common reason people think the watermark did not work. Every printed page and every PDF made from Excel carries the picture, centred at the top — if the image is taller than the header area, Excel lets it extend down over the page, which is what makes it look like a watermark rather than a logo. Make the transparent image taller, or add blank lines before &[Picture] in the header string, to move it further down the page.
LibreOffice does not display pictures in .xlsx headers, so this step has to be checked in Excel. Add Headers, Footers and Page Numbers to an Excel PDF covers the other header codes.
Step 3 — A background for the screen
A sheet background tiles an image behind the cells in Normal view. Excel never prints it, so it suits a workbook that is read on screen — "CONFIDENTIAL" behind a salary sheet, say:
print(watermark_png("CONFIDENTIAL", "confidential.png", size=(1400, 300), rgba=(67, 56, 202, 40), angle=0))
wb = xlsxwriter.Workbook("salaries.xlsx")
ws = wb.add_worksheet("Salaries")
ws.write_row(0, 0, ["Employee", "Salary"])
for r, (name, pay) in enumerate([("A. Okafor", 72000), ("J. Lindqvist", 68000), ("C. Martin", 55000)], start=1):
ws.write_row(r, 0, [name, pay])
ws.set_background("confidential.png")
wb.close()
print("written")
Background images are repeated across and down the whole sheet, so a wide, short, unrotated image like this one tiles into horizontal bands of text — easier to read around than a diagonal pattern. Because the background is not printed by Excel, pair it with a header picture if the sheet may be printed too. One inconsistency to be aware of: LibreOffice's PDF export does draw sheet backgrounds, so the same file can print differently depending on the program.
Step 4 — Stamp the PDF instead
When the report is distributed as a PDF, stamp the PDF rather than the workbook: the mark is on every page regardless of how the workbook's print settings or viewer behave, and it cannot be removed by editing the spreadsheet. Convert the workbook to PDF, draw the watermark on a transparent page with reportlab, and merge it onto each page of a writer cloned from the export with pypdf:
soffice -env:UserInstallation=file://$PWD/lo_profile --headless --convert-to pdf budget_draft.xlsx > /dev/null 2>&1
ls budget_draft.pdf
from pypdf import PdfReader, PdfWriter
from reportlab.lib.colors import Color
from reportlab.pdfgen import canvas
def stamp_pdf(src, dst, text="DRAFT"):
writer = PdfWriter(clone_from=src)
box = writer.pages[0].mediabox
w, h = float(box.width), float(box.height)
c = canvas.Canvas("_stamp.pdf", pagesize=(w, h))
c.setFillColor(Color(0.86, 0.15, 0.15, alpha=0.18))
c.setFont("Helvetica-Bold", 110)
c.translate(w / 2, h / 2)
c.rotate(40)
c.drawCentredString(0, -35, text)
c.save()
stamp = PdfReader("_stamp.pdf").pages[0]
for page in writer.pages:
page.merge_page(stamp)
writer.write(dst)
return len(writer.pages)
print(stamp_pdf("budget_draft.pdf", "budget_draft_stamped.pdf"), "page(s) stamped")
The stamp is drawn at 18% opacity in the middle of the page, rotated 40 degrees, and merge_page lays it over each page's content — the numbers remain readable through it. Size the stamp from each page's own mediabox if a document mixes portrait and landscape pages. Convert Excel to PDF on Linux with LibreOffice covers the conversion step in detail.
Step 5 — Do not let openpyxl remove it
A common pipeline builds a workbook with xlsxwriter and then opens it with openpyxl to add a finishing touch. openpyxl keeps the header text but drops the header image itself, so the watermark disappears without an error:
import zipfile
from openpyxl import load_workbook
def media(path):
return [n for n in zipfile.ZipFile(path).namelist() if n.startswith("xl/media/")]
wb = load_workbook("budget_draft.xlsx")
print("header text:", repr(wb["Budget"].oddHeader.center.text))
wb.save("budget_draft_resaved.xlsx")
print("images before:", media("budget_draft.xlsx"), "after:", media("budget_draft_resaved.xlsx"))
The header still says &G — "a picture goes here" — but the picture is gone from the file. Add the watermark as the last step, with the library that writes the final file, or stamp the PDF. What openpyxl Loses When Saving an Existing Workbook lists the other things a re-save discards.
Performance notes
None of these steps is expensive. The header picture is stored once in the file however many pages it prints on, so it adds only the size of the PNG — keep it at a modest resolution, since it is displayed large but faint. Stamping PDFs with pypdf handles hundreds of pages per second; converting the workbook to PDF is the slow part, dominated by LibreOffice's start-up, so batch several workbooks into one conversion call when watermarking a whole set of reports.
Common pitfalls
- Looking in Normal view. Header pictures only appear in Page Layout view, print preview and printed output.
- An opaque image. A white background on the PNG hides the cells; save with transparency.
- Cropped rotated text. Rotate with
expand=True. - Re-saving with openpyxl. The header image is dropped; watermark last.
- Relying on a watermark for security. It marks the document; it does not protect it. Combine with Password Protect an Excel File with Python when the content matters.
Conclusion
Draw a pale, rotated, transparent PNG with Pillow. For printed workbooks, put it in the centre header with xlsxwriter's set_header("&C&[Picture]", {"image_center": ...}) and open the sheet in Page Layout view; for on-screen marking, use set_background. When the PDF is what people receive, stamp the PDF with reportlab and pypdf instead. Whichever you choose, add the watermark in the final step, because an openpyxl re-save removes header pictures.
Frequently asked questions
How do I add a watermark to an Excel file with Python?
Create a transparent PNG and add it as a header picture with xlsxwriter's set_header("&C&[Picture]", {"image_center": "draft.png"}). Excel shows it in Page Layout view and prints it on every page.
Why can't I see my watermark in Excel? Header pictures are hidden in Normal view. Switch to Page Layout view or print preview. If it is missing there too, check whether the file was re-saved with openpyxl, which drops header images.
Can openpyxl add a watermark? openpyxl can set the header text that references a picture, but it cannot embed the picture, and it removes existing header pictures when it saves. Use xlsxwriter for the watermark, or stamp the PDF.
How do I watermark the PDF version of a report? Draw the watermark on a transparent page with reportlab, then merge that page onto every page of the PDF with pypdf's merge_page and write the result to a new file.
Related
- Up one level: Inserting Images and Logos into Excel — pictures in and out of workbooks.
- Add a Logo Image to an Excel Report with openpyxl — placing images in cells rather than headers.
- Convert Excel File to PDF with Python — the conversion before stamping.
- Set the Print Area and Page Setup with openpyxl — the print settings a watermark depends on.
More on charts & images
- How-toBuild a Dashboard Sheet with Charts from Multiple Tabs
- How-toBuild a Rolling 12-Month Chart on an Excel Dashboard
- How-toKeep Charts and Images When Filling an Excel Template
- How-toAdd a Chart to an Excel File with xlsxwriter
- How-toAdd Sparklines to an Excel Report with xlsxwriter
- How-toBuild a Waterfall Chart in Excel with Python