Generating Excel with Watermarks using .NET
TLDR
- Excel does not have a built-in watermark feature; it can be simulated by setting a "full-page transparent background image."
- Watermark display positions:
Backgroundis suitable for Normal and Page Layout views;Headeris suitable for Page Layout view and printing. - When generating a watermark image, you must set the pixel dimensions according to the Excel
PaperSizeconfiguration. - When using NPOI, due to the lack of a direct API, you must implement a custom
VmlDrawingclass and manipulate the underlying XML structure. - It is recommended to use SkiaSharp instead of
System.Drawing.Commonin modern .NET environments to resolve cross-platform support issues.
Principles and Limitations of Simulating Watermarks
Excel does not provide a native watermark feature; developers must achieve this by setting a full-page transparent background image. Depending on the requirements, there are two ways to configure this:
- Background Setting: Suitable for "Normal View" and "Page Layout View."
- Header Setting: Suitable for "Page Layout View" and "Printing."
Currently, there is no effective method for setting watermarks in "Page Break Preview."
Generating Full-Page Watermark Images
Before generating the image, you must obtain the paper size (PaperSize) from the Excel page setup. If the paper orientation is landscape, you need to swap the width and height values.
Obtaining Paper Dimensions
You can obtain the system's default paper specifications via PrinterSettings:
PrinterSettings settings = new PrinterSettings() {
PrinterName = "Microsoft XPS Document Writer"
};
foreach (System.Drawing.Printing.PaperSize printerPaperSize in settings.PaperSizes) {
// Get RawKind, PaperName, Width, Height
}Image Scaling and Drawing
If the image dimensions do not match the page, you need to scale and center it:
public Image ResizeImageBackgroundToFullPage(Image watermark, int width, int height){
if (watermark.Width > width || watermark.Height > height) {
using (Image image = ZoomOutImage(width, height)) {
return ResizeImageBackgroundToFullPageInternal(width, height, image);
}
}
return ResizeImageBackgroundToFullPageInternal(width, height, watermark);
}
private Image ResizeImageBackgroundToFullPageInternal(int pageWidth, int pageHeight, Image image) {
Image bitmap = new Bitmap(pageWidth, pageHeight);
using Graphics graphics = Graphics.FromImage(bitmap);
graphics.Clear(Color.White);
graphics.DrawImage(image, (pageWidth - image.Width) / 2, (pageHeight - image.Height) / 2);
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.Save();
return bitmap;
}Generating Text Watermarks
If you need to convert text into an image, you can use Graphics to rotate and draw it:
public Image DrawText(string text, Font font, Color textColor, Color backColor, int width, int height) {
Image img = new Bitmap(width, height);
using (Graphics drawing = Graphics.FromImage(img)) {
SizeF textSize = drawing.MeasureString(text, font, 0, StringFormat.GenericTypographic);
float x = (width - textSize.Width) / 2;
float y = (height - textSize.Height) / 2;
drawing.TranslateTransform(x + (textSize.Width / 2), y + (textSize.Height / 2));
drawing.RotateTransform(-45);
drawing.TranslateTransform(-(x + (textSize.Width / 2)), -(y + (textSize.Height / 2)));
drawing.Clear(backColor);
Brush textBrush = new SolidBrush(textColor);
drawing.DrawString(text, font, textBrush, x, y);
drawing.Save();
return img;
}
}Generating Watermarks Using EPPlus
In older versions of EPPlus, you could set it directly via HeaderFooter and BackgroundImage:
sheet.HeaderFooter.OddHeader.InsertPicture(watermark, PictureAlignment.Centered);
sheet.BackgroundImage.Image = watermark;WARNING
EPPlus 6 and later versions have removed the dependency on System.Drawing.Common. The code above may no longer work; it is recommended to consult the latest API documentation.
Generating Watermarks Using NPOI (XLSX)
NPOI does not have an API for setting background images directly; you must implement a custom VmlDrawing class and manipulate the underlying XML structure.
Custom VmlDrawing Class
You need to inherit from POIXMLDocumentPart and implement the XML writing logic:
private class VmlDrawing : POIXMLDocumentPart {
public string PictureRelId { get; set; }
public Image Image { get; set; }
protected override void Commit() {
PackagePart part = GetPackagePart();
Stream @out = part.GetOutputStream();
Write(@out);
@out.Close();
}
private void Write(Stream stream) {
float width = Image.Width * 72 / Image.HorizontalResolution;
float height = Image.Height * 72 / Image.VerticalResolution;
using StreamWriter sw = new(stream);
XmlDocument doc = new();
doc.LoadXml($@"
<xml xmlns:v=""urn:schemas-microsoft-com:vml"" xmlns:o=""urn:schemas-microsoft-com:office:office"" xmlns:x=""urn:schemas-microsoft-com:office:excel"">
<v:shape id=""CH"" type=""#_x0000_t75"" style=""position:absolute;margin-left:0;margin-top:0;width:{width}pt;height:{height}pt;z-index:1"">
<v:imagedata o:relid=""{PictureRelId}"" o:title="""" />
<o:lock v:ext=""edit"" rotation=""t"" />
</v:shape>
</xml>");
doc.Save(stream);
}
}Setting the Watermark
Add the image to the Workbook and associate it with the Sheet:
MemoryStream imageMs = new MemoryStream();
watermark.Save(imageMs, System.Drawing.Imaging.ImageFormat.Png);
int pictureIdx = workbook.AddPicture(imageMs.ToArray(), PictureType.PNG);
POIXMLDocumentPart docPart = workbook.GetAllPictures()[pictureIdx] as POIXMLDocumentPart;
POIXMLDocumentPart.RelationPart backgroundRelPart = sheet.AddRelation(null, XSSFRelation.IMAGES, docPart);
sheet.GetCTWorksheet().picture = new CT_SheetBackgroundPicture {
id = backgroundRelPart.Relationship.Id
};
// Create VmlDrawing association
VmlDrawing drawing = (VmlDrawing)sheet.CreateRelationship(VmlRelation.Instance, XSSFFactory.GetInstance(), drawingNumber);
POIXMLDocumentPart.RelationPart headerRelPart = drawing.AddRelation(null, XSSFRelation.IMAGES, docPart);
drawing.Image = watermark;
drawing.PictureRelId = headerRelPart.Relationship.Id;
sheet.Header.Center = HeaderFooter.PICTURE_FIELD.sequence;
sheet.GetCTWorksheet().legacyDrawingHF = new CT_LegacyDrawing {
id = sheet.GetRelationId(drawing)
};Changelog
- 2023-02-24 Initial document created.
- 2026-05-17 Added link to GitHub sample project.