Skip to content

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 background image" or a "header image."
  • When generating watermark images, you must adjust the image dimensions based on the Excel PaperSize setting, and note that width and height must be swapped for landscape printing.
  • EPPlus can set the background via sheet.BackgroundImage.Image, but be aware of .NET 6+ limitations regarding System.Drawing.Common support.
  • NPOI lacks a direct API; you must customize VmlDrawing via the underlying POIXMLDocumentPart and manually manipulate the XML structure to attach the image.
  • It is recommended to use SkiaSharp instead of System.Drawing.Common to resolve cross-platform compatibility issues.

Principles of Generating Full-Page Watermark Images

The watermark effect in Excel depends on the view mode:

  • Normal and Page Break Preview: Can be displayed by setting a Background Image.
  • Page Layout and Printing: Can be displayed by setting a Header Image.

Before generating the image, you must obtain the target paper size (PaperSize). Below are pixel references for common paper sizes (based on default resolution):

PaperNameWidthHeight
A48271169
A311691654
Letter8501100

Adjusting Image Dimensions

If the image dimensions do not match the paper, you must perform scaling and centering. If the PaperSize is landscape, be sure to swap the width and height parameters.

csharp
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);
}

Generating Watermarks with EPPlus

When you might encounter this issue: When the development environment uses EPPlus and the project targets .NET Framework or older versions of .NET.

In EPPlus, you can set it directly via the HeaderFooter or BackgroundImage properties:

csharp
sheet.HeaderFooter.OddHeader.InsertPicture(watermark, PictureAlignment.Centered);
sheet.BackgroundImage.Image = watermark;

WARNING

Versions of EPPlus 6 and later have removed the dependency on System.Drawing.Common. If you upgrade to a newer version, you must use a different graphics library to handle image objects.

Generating Watermarks with NPOI (XLSX)

When you might encounter this issue: When the project cannot use EPPlus or requires lower-level control.

NPOI does not have an API for setting background images directly; you must manually create a VmlDrawing and handle XML relationships.

1. Define the VmlDrawing Class

You must inherit from POIXMLDocumentPart and implement the Commit method to write image information into the VML structure:

csharp
private class VmlDrawing : POIXMLDocumentPart {
    public string PictureRelId { get; set; }
    public Image Image { get; set; }

    protected override void Commit() {
        PackagePart part = GetPackagePart();
        using Stream @out = part.GetOutputStream();
        // Convert Pixels to Points
        float width = Image.Width * 72 / Image.HorizontalResolution;
        float height = Image.Height * 72 / Image.VerticalResolution;

        // Write VML XML structure
        // Note: You need to fill in the correct XML nodes and attributes based on actual requirements
    }
}

2. Set Watermark Relationship

Use AddRelation to associate the image with the Sheet, and point legacyDrawingHF to the custom VmlDrawing:

csharp
// Add image to Workbook
int pictureIdx = workbook.AddPicture(imageMs.ToArray(), PictureType.PNG);
POIXMLDocumentPart docPart = workbook.GetAllPictures()[pictureIdx] as POIXMLDocumentPart;

// Create VML relationship
VmlDrawing drawing = (VmlDrawing)sheet.CreateRelationship(VmlRelation.Instance, XSSFFactory.GetInstance(), drawingNumber);
drawing.Image = watermark;
drawing.PictureRelId = headerRelPart.Relationship.Id;

// Set header image
sheet.Header.Center = HeaderFooter.PICTURE_FIELD.sequence;
sheet.GetCTWorksheet().legacyDrawingHF = new CT_LegacyDrawing {
    id = sheet.GetRelationId(drawing)
};

Example Project

For more implementation details and adjustments for the .NET 10 environment (switching to SkiaSharp), please refer to: CloudyWing/ExcelWatermarkSample.


Changelog

    • Initial document creation.
    • Added link to GitHub example project.