Hello folks. In this article I want to show how to apply a watermark to your PDF document. I'll show the approach that it'll work not only on Windows but and on Mac OS or Linux. This project you can download and run on your PC and it'll be work.
Preconditions
You need to create a simple Web API application. And also you need to install one NuGet package - PDFtoImage.
<PackageReference Include="PDFtoImage" Version="2.3.0" />
Implementation
Please, create WatermarkController
in the Controller
folder. Next, you should add a base route:
[ApiController]
[Route("api/v1/pdf")]
public class WatermarkController : ControllerBase
And sure, add one endpoint:
[HttpPost]
[Route("watermark")]
public IActionResult AddWatermark(IFormFile? watermark, IFormFile? document)
{
}
If you are familiar with Windows realization, there exists multipart data and no need for any parameters. Here I used IFormFile
. Add this code:
[HttpPost]
[Route("watermark")]
public IActionResult AddWatermark(IFormFile? watermark, IFormFile? document)
{
try
{
if (watermark is null || document is null)
{
return BadRequest("Both PNG and PDF images are required.");
}
byte[]? pngBytes = null;
byte[]? pdfBytes = null;
if (watermark.ContentType == "image/png")
{
using var memoryStream = new MemoryStream();
watermark.CopyTo(memoryStream);
pngBytes = memoryStream.ToArray();
}
else
{
return BadRequest("The watermark should be a .PNG format");
}
if (document.ContentType == "application/pdf")
{
using var memoryStream = new MemoryStream();
document.CopyTo(memoryStream);
pdfBytes = memoryStream.ToArray();
}
else
{
return BadRequest("The document should be a .PDF format");
}
var mergedImages = MergeImages(pngBytes, pdfBytes);
var pdfDocument = ConvertToPdf(mergedImages);
return File(pdfDocument, "application/pdf", "watermarked.pdf");
}
catch (Exception ex)
{
return BadRequest(ex);
}
}
Let's find out what's going on in this code. From entry params, we check out file types and copy bytes. For adding the watermark, it needs to merge files. For this, you need to add this method:
private IEnumerable<SKBitmap> MergeImages(byte[] pngBytes, byte[] pdfBytes)
{
var pngImage = MakeBackgroundTransparent(pngBytes);
foreach (var pdfImage in PDFtoImage.Conversion.ToImages(pdfBytes))
{
var mergedImage = new SKBitmap(pdfImage.Width, pdfImage.Height);
using (var canvas = new SKCanvas(mergedImage))
{
canvas.DrawBitmap(pdfImage, 0, 0);
int x = pdfImage.Width - pngImage.Width - 5;
int y = pdfImage.Height - pngImage.Height - 5;
canvas.DrawBitmap(pngImage, x, y);
}
yield return mergedImage;
}
}
The first row refers to another method. Why does it need? The watermark is an ordinary monochrome PNG picture on a white background. If we'll skip this method then the picture will be merged with a white background and it'll look not great. For this reason let's add another method:
private SKBitmap MakeBackgroundTransparent(byte[] pngBytes)
{
using var pngStream = new MemoryStream(pngBytes);
var image = SKBitmap.Decode(pngStream);
var transparentImage = new SKBitmap(image.Width, image.Height, SKColorType.Bgra8888, SKAlphaType.Premul);
for (var x = 0; x < image.Width; x++)
{
for (var y = 0; y < image.Height; y++)
{
var pixelColor = image.GetPixel(x, y);
if (pixelColor.Red == 255 && pixelColor.Green == 255 && pixelColor.Blue == 255)
{
pixelColor = new SKColor(255, 255, 255, 0);
}
else
{
pixelColor = new SKColor(0, 0, 0, 64);
}
transparentImage.SetPixel(x, y, pixelColor);
}
}
return transparentImage;
}
We decode pictures to the image and check each pixel, and if they are white, then we set the opacity to 0, else 64. And now, let's return to the MergeImages
method. Next, we need split PDF documents into multiple separated images. For this, we used the static class PDFtoImage from the installed package. It contains under the hood SkiaSharp and Pdfium. Sure, you can make convert it by yourself with these packages, but it's not a trivial task. This package is the simplest way to resolve this problem. This method returns image collection. We iterated each PDF image and merge our watermark with a padding of 5 px. We also return images collection. All received images we need to convert back to PDF. For this, add the following method:
private byte[] ConvertToPdf(IEnumerable<SKBitmap> images)
{
using var stream = new MemoryStream();
using (var document = SKDocument.CreatePdf(stream))
{
foreach (var image in images)
{
using var canvas = document.BeginPage(image.Width, image.Height);
canvas.DrawBitmap(image, 0, 0);
document.EndPage();
}
}
return stream.ToArray();
}
Received bytes convert to the PDF file on the fly.
Let's check this out. I'll be using Postman, but you can use any other REST client. I also downloaded a PDF document and created a facsimile. If you'll post a request by this URI:
http://localhost:5273/api/v1/pdf/watermark
, then you should get something like this.
Or this.
As you can see, the watermark doesn't have a background and also has opacity which you can set for your needs.
Conclusions
This approach can make your application really cross-platform for different OSs with minimal complexity.
See you in the next article, set likes, make subscribe, and happy coding.
The source code you can find by this link.