Secure your PDFs with custom watermarks on Android
Adding a non-removable watermark to documents can discourage viewers from sharing your content or taking screenshots. For an additional layer of security, you can use a custom watermark for each individual user that contains identifying information such as their name, timestamp, and ID. This makes it easier to trace any leaks backs to the source.
Nutrient Android SDK allows you to draw content above a displayed document using PdfDrawable
, which is a subtype of Android’s Drawable
class and thus similar in use. This article covers using the PdfDrawable
API to add a watermark into a document.
Our Android Catalog application contains a working example, WatermarkExample
, with the code detailed here. For more information on our drawable API itself, refer to its specific guide.
Creating the DrawableProvider
To draw a watermark onto a page, you’ll need to use the PdfDrawable
API. To do so, you must provide your implementation of PdfDrawable
with a PdfFragment
using a PdfDrawableProvider
. In addition, you can provide it to PdfThumbnailBar
and PdfThumbnailGrid
to also add the watermark to thumbnails.
Inside your PdfActivity
, you must add this provider, like so:
class WatermarkExampleActivity : PdfActivity() { /** * Drawable provider that provides example watermarks. */ private val customTestDrawableProvider: PdfDrawableProvider = object : PdfDrawableProvider() { override fun getDrawablesForPage(context: Context, document: PdfDocument, @IntRange(from = 0) pageIndex: Int): List<PdfDrawable> { return listOf( // You can pass in multiple drawables if needed. Here is a single text watermark, // tilted by 45 degrees with the bottom-left corner at (350, 350) in PDF coordinates. WatermarkDrawable("Watermark", PointF(350f, 350f)) ) } } }
public class WatermarkExampleActivity extends PdfActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final PdfDrawableProvider customTestDrawableProvider = new PdfDrawableProvider() { @Nullable @Override public List<? extends PdfDrawable> getDrawablesForPage( @NonNull Context context, @NonNull PdfDocument document, @IntRange(from = 0) int pageIndex) { return Arrays.asList( // You can pass in multiple drawables if needed. Here is a single text watermark, // tilted by 45 degrees with the bottom-left corner at (350, 350) in PDF coordinates. new WatermarkDrawable("Watermark", new Point(350, 350))); } }; } }
Notice the parameters being passed in. You can use information contained in the PdfDocument
or the pageIndex
to, for example, switch up the watermark drawables returned depending on different factors as desired.
Then, on the onCreate
handler, register the provider on all of the applicable views:
class WatermarkExampleActivity : PdfActivity() { /** * Drawable provider that provides example watermarks. */ private val customTestDrawableProvider: PdfDrawableProvider = ... override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Register the drawable provider on `PdfFragment` to provide drawables to document pages. requirePdfFragment().addDrawableProvider(customTestDrawableProvider) // Also register the drawable provider on the thumbnail bar and thumbnail grid. pspdfKitViews.thumbnailBarView?.addDrawableProvider(customTestDrawableProvider) pspdfKitViews.thumbnailGridView?.addDrawableProvider(customTestDrawableProvider) // Outline displays page previews in the bookmarks list. Bookmarks are enabled in this example // so you need to register the drawable provider on the outline view too. pspdfKitViews.outlineView?.addDrawableProvider(customTestDrawableProvider) } }
public class WatermarkExampleActivity extends PdfActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final PdfDrawableProvider customTestDrawableProvider = ...; // Register the drawable provider on `PdfFragment` to provide drawables to document pages. getPdfFragment().addDrawableProvider(customTestDrawableProvider); // Also register the drawable provider on the thumbnail bar and thumbnail grid. PdfThumbnailBar thumbnailBarView = getPSPDFKitViews().getThumbnailBarView(); if (thumbnailBarView != null) { thumbnailBarView.addDrawableProvider(customTestDrawableProvider); } PdfThumbnailGrid thumbnailGridView = getPSPDFKitViews().getThumbnailGridView(); if (thumbnailGridView != null) { thumbnailGridView.addDrawableProvider(customTestDrawableProvider); } // Outline displays page previews in the bookmarks list. Bookmarks are enabled in this example // so you need to register the drawable provider on the outline view too. PdfOutlineView outlineView = getPSPDFKitViews().getOutlineView(); if (outlineView != null) { outlineView.addDrawableProvider(customTestDrawableProvider); } } }
Creating the PdfDrawable
With the provider in place, you need to now actually create the PdfDrawable
. You’ll use the same example as the section above, called WatermarkDrawable
.
The implemented class accepts a string
and a PointF
as parameters, representing the text to write as the watermark and its starting coordinates.
There’s also some simple math involved with page coordinates and how Nutrient does redrawing whenever the view changes. However, the main method is draw
, where the content is put into the screen. As it uses the UI thread, you must ensure this method is as performant as possible.
The full snippet is presented below:
private class WatermarkDrawable(private val text: String, startingPoint: PointF) : PdfDrawable() { private val redPaint = Paint().apply { color = Color.RED style = Paint.Style.FILL alpha = 50 textSize = 100f } private val pageCoordinates = RectF() private val screenCoordinates = RectF() init { calculatePageCoordinates(text, startingPoint) } private fun calculatePageCoordinates(text: String, point: PointF) { val textBounds = Rect() redPaint.getTextBounds(text, 0, text.length, textBounds) pageCoordinates.set( point.x, point.y + textBounds.height().toFloat(), point.x + textBounds.width().toFloat(), point.y ) } private fun updateScreenCoordinates() { pdfToPageTransformation.mapRect(screenCoordinates, pageCoordinates) // Rounding out ensures no clipping of content. val bounds = bounds screenCoordinates.roundOut(bounds) this.bounds = bounds } /** * This method performs all the drawing required by this drawable. * Keep this method fast to maintain a performant UI. */ override fun draw(canvas: Canvas) { val bounds = bounds.toRectF() canvas.save() // Rotate canvas by 45 degrees. canvas.rotate(-45f, bounds.left, bounds.bottom) // Recalculate text size to much new bounds. setTextSizeForWidth(redPaint, bounds.width(), text) // Draw the text on the rotated canvas. canvas.drawText(text, bounds.left, bounds.bottom, redPaint) canvas.restore() } private fun setTextSizeForWidth( paint: Paint, desiredWidth: Float, text: String ) { // Pick a reasonably large value for the test. val testTextSize = 60f // Get the bounds of the text using `testTextSize`. paint.textSize = testTextSize val bounds = Rect() paint.getTextBounds(text, 0, text.length, bounds) // Calculate the desired size as a proportion of `testTextSize`. val desiredTextSize = testTextSize * desiredWidth / bounds.width() // Set the paint for that size. paint.textSize = desiredTextSize } /** * PSPDFKit calls this method every time the page was moved or resized on the screen. * It will provide a fresh transformation for calculating screen coordinates from * PDF coordinates. */ override fun updatePdfToViewTransformation(matrix: Matrix) { super.updatePdfToViewTransformation(matrix) updateScreenCoordinates() } @UiThread override fun setAlpha(alpha: Int) { redPaint.alpha = alpha invalidateSelf() } @UiThread override fun setColorFilter(colorFilter: ColorFilter?) { redPaint.colorFilter = colorFilter invalidateSelf() } override fun getOpacity(): Int { return PixelFormat.TRANSLUCENT } }
class WatermarkDrawable extends PdfDrawable { private final Paint redPaint = new Paint(); @NonNull private final RectF pageCoordinates = new RectF(); @NonNull private final RectF screenCoordinates = new RectF(); private String text; public WatermarkDrawable(String text, Point startingPoint) { this.text = text; redPaint.setColor(Color.RED); redPaint.setStyle(Paint.Style.FILL); redPaint.setAlpha(50); redPaint.setTextSize(100); calculatePageCoordinates(text, startingPoint); } private void calculatePageCoordinates(String text, Point point) { Rect textBounds = new Rect(); redPaint.getTextBounds(text, 0, text.length(), textBounds); pageCoordinates.set( point.x, point.y + textBounds.height(), point.x + textBounds.width(), point.y); } private void updateScreenCoordinates() { getPDFToPageTransformation().mapRect(screenCoordinates, pageCoordinates); // Rounding out ensures no clipping of content. final Rect bounds = getBounds(); screenCoordinates.roundOut(bounds); setBounds(bounds); } /** Here all the drawing is performed. Keep this method fast to maintain 60 fps. */ @Override public void draw(@NonNull Canvas canvas) { Rect bounds = getBounds(); canvas.save(); // Rotate text by 45 degrees. canvas.rotate(-45, bounds.left, bounds.bottom); // Recalculate text size to much new bounds. setTextSizeForWidth(redPaint, bounds.width(), text); canvas.drawText(text, bounds.left, bounds.bottom, redPaint); canvas.restore(); } private void setTextSizeForWidth(Paint paint, float desiredWidth, String text) { // Pick a reasonably large value for the test. final float testTextSize = 60f; // Get the bounds of the text using `testTextSize`. paint.setTextSize(testTextSize); Rect bounds = new Rect(); paint.getTextBounds(text, 0, text.length(), bounds); // Calculate the desired size as a proportion of `testTextSize`. float desiredTextSize = testTextSize * desiredWidth / bounds.width(); // Set the paint for that size. paint.setTextSize(desiredTextSize); } /** * PSPDFKit calls this method every time the page was moved or resized on the screen. It will * provide a fresh transformation for calculating screen coordinates from PDF coordinates. */ @Override public void updatePDFToViewTransformation(@NonNull Matrix matrix) { super.updatePDFToViewTransformation(matrix); updateScreenCoordinates(); } @UiThread @Override public void setAlpha(int alpha) { redPaint.setAlpha(alpha); // Drawable invalidation is only allowed from a UI thread. invalidateSelf(); } @UiThread @Override public void setColorFilter(ColorFilter colorFilter) { redPaint.setColorFilter(colorFilter); // Drawable invalidation is only allowed from a UI thread. invalidateSelf(); } @Override public int getOpacity() { return PixelFormat.TRANSLUCENT; } }
Note that this guide is meant as a focused example. For a deeper dive into the PdfDrawable
API itself, see our drawable API guide.