Shows how to automatically detect and redact people faces from documents.
1/* global faceapi, createProgress */
2/**
3 * Converts given Canvas to HTMLImageElement.
4 *
5 * @param {Canvas} canvas Canvas from which Image is created
6 * @returns {Promise} Resolving to HTMLImageElement
7 */
8function convertCanvasToImage(canvas) {
9 return new Promise(resolve => {
10 const base64ImageDataURL = canvas.toDataURL('image/jpeg');
11 const image = new Image();
12 image.onload = () => {
13 // resolve image once it is fully loaded
14 resolve(image);
15 };
16 image.src = base64ImageDataURL;
17 });
18}
19
20/**
21 * Creates RedactionAnnotation and adds them to document.
22 *
23 * Note: This doesn't apply faceDetections, if you want to apply faceDetections
24 * programmatically see AnnotationManager.applyRedaction()
25 *
26 * @param {WebViewerInstance} webViewerInstance current instance on WebViewer
27 * @param {Number} pageNumber Page number of the document where detections were found
28 * @param {FaceDetection[]} faceDetections Faces that were detected by face-api.js
29 */
30function createFaceRedactionAnnotation(webViewerInstance, pageNumber, faceDetections) {
31 if (faceDetections && faceDetections.length > 0) {
32 const { Annotations, annotationManager, Math } = webViewerInstance.Core;
33 // We create a quad per detected face to allow us use only one redaction annotation.
34 // You could create new RedactionAnnotation for each detected face, but in case where document contains
35 // tens or hundreds of face applying reduction comes slow.
36 const quads = faceDetections.map(detection => {
37 const x = detection.box.x;
38 const y = detection.box.y;
39 const width = detection.box.width;
40 const height = detection.box.height;
41
42 const topLeft = [x, y];
43 const topRight = [x + width, y];
44 const bottomLeft = [x, y + height];
45 const bottomRight = [x + width, y + height];
46 // Quad is defined as points going from bottom left -> bottom right -> top right -> top left
47 return new Math.Quad(...bottomLeft, ...bottomRight, ...topRight, ...topLeft);
48 });
49 const faceAnnotation = new Annotations.RedactionAnnotation({
50 Quads: quads,
51 });
52 faceAnnotation.Author = annotationManager.getCurrentUser();
53 faceAnnotation.PageNumber = pageNumber;
54 faceAnnotation.StrokeColor = new Annotations.Color(255, 0, 0, 1);
55 annotationManager.addAnnotation(faceAnnotation, false);
56 // Annotation needs to be redrawn so that it becomes visible immediately rather than on next time page is refreshed
57 annotationManager.redrawAnnotation(faceAnnotation);
58 }
59}
60
61/**
62 *
63 * @param {WebViewerInstance} webViewerInstance current instance on WebViewer
64 * @param {Number} pageNumber Page number of the document where detection is ran
65 * @returns {Promise} Resolves after faces are detected and RedactionAnnotations are added to document
66 */
67function detectAndRedactFacesFromPage(webViewerInstance, pageNumber) {
68 return new Promise(resolve => {
69 const doc = webViewerInstance.Core.documentViewer.getDocument();
70 const pageInfo = doc.getPageInfo(pageNumber);
71 const displaySize = { width: pageInfo.width, height: pageInfo.height };
72 // face-api.js is detecting faces from images, so we need to convert current page to a canvas which then can
73 // be converted to an image.
74 doc.loadCanvas({
75 pageNumber,
76 zoom: 0.5, // Scale page size down to allow faster image processing
77 drawComplete: function drawComplete(canvas) {
78 convertCanvasToImage(canvas).then(async image => {
79 const detections = await faceapi.detectAllFaces(
80 image,
81 new faceapi.SsdMobilenetv1Options({
82 minConfidence: 0.4,
83 maxResults: 300,
84 })
85 );
86 // As we scaled our image, we need to resize faces back to the original page size
87 const resizedDetections = faceapi.resizeResults(detections, displaySize);
88 createFaceRedactionAnnotation(webViewerInstance, pageNumber, resizedDetections);
89 resolve();
90 });
91 },
92 });
93 });
94}
95
96/**
97 * onClick handler factory for redact faces button. Creates new onClick handler that encloses
98 * webViewer instance inside closure.
99 *
100 * @param {WebViewerInstance} webViewerInstance current instance on WebViewer
101 * @returns {function} returns async click handler for redact faces button
102 */
103function onRedactFacesButtonClickFactory(webViewerInstance) {
104 return async function onRedactFacesButtonClick() {
105 webViewerInstance.UI.closeElements(['redact-faces-instructions-modal']);
106 const doc = webViewerInstance.Core.documentViewer.getDocument();
107 const numberOfPages = doc.getPageCount();
108 const { sendPageProcessing, showProgress, hideProgress } = createProgress(numberOfPages);
109 showProgress();
110 for (let pageNumber = 1; pageNumber <= numberOfPages; pageNumber++) {
111 sendPageProcessing();
112 await detectAndRedactFacesFromPage(webViewerInstance, pageNumber);
113 }
114 hideProgress();
115 };
116}
117
118// Load face-api.js model
119faceapi.nets.ssdMobilenetv1.loadFromUri('https://pdftron.s3.amazonaws.com/downloads/pl/face-redaction-models');
120
121function createInstructionsModal(buttonClickHandler) {
122 const instructionModal = {
123 dataElement: 'redact-faces-instructions-modal',
124 disableBackdropClick: true,
125 disableEscapeKeyDown: true,
126 render: function renderCustomModal() {
127 const container = document.createElement('div');
128 container.classList.add('redact-faces-instructions');
129 const textContainer = document.createElement('div');
130 textContainer.innerHTML = "Click 'Redact Faces' button below to start face redaction process.";
131 const button = document.createElement('button');
132 button.innerHTML = 'Redact Faces';
133 button.addEventListener('click', buttonClickHandler);
134 container.appendChild(textContainer);
135 container.appendChild(button);
136 return container;
137 },
138 };
139 return instructionModal;
140}
141
142WebViewer(
143 {
144 path: '../../../lib',
145 css: 'instruction-modal.css',
146 fullAPI: true,
147 enableRedaction: true,
148 enableFilePicker: true,
149 initialDoc: 'https://pdftron.s3.amazonaws.com/downloads/pl/pdftron-face-refaction-sample.pdf',
150 },
151 document.getElementById('viewer')
152).then(webViewerInstance => {
153 const FitMode = webViewerInstance.UI.FitMode;
154 webViewerInstance.UI.setFitMode(FitMode.FitWidth);
155 const onRedactFacesButtonClick = onRedactFacesButtonClickFactory(webViewerInstance);
156
157 const documentViewer = webViewerInstance.Core.documentViewer;
158 function onDocumentLoaded() {
159 const instructionModal = createInstructionsModal(onRedactFacesButtonClick);
160 webViewerInstance.UI.addCustomModal(instructionModal);
161 webViewerInstance.UI.openElements(['redact-faces-instructions-modal']);
162 }
163 documentViewer.addEventListener('documentLoaded', onDocumentLoaded);
164});
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales