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.