This sample shows how to automatically detect faces in a PDF document using the open-source face-api.js
and permanently redact them using the Apryse WebViewer JavaScript PDF library.
WebViewer provides a slick out-of-the-box responsive UI that enables you to view, annotate and manipulate PDFs and other document types inside any web project.
Click the button below to view the full project in GitHub.
1/**
2 * Converts given Canvas to HTMLImageElement.
3 *
4 * @param {Canvas} canvas Canvas from which Image is created
5 * @returns {Promise} Resolving to HTMLImageElement
6 */
7function convertCanvasToImage(canvas) {
8 return new Promise(function(resolve) {
9 const base64ImageDataURL = canvas.toDataURL('image/jpeg');
10 const image = new Image()
11 image.onload = () => {
12 // resolve image once it is fully loaded
13 resolve(image)
14 }
15 image.src = base64ImageDataURL;
16 });
17}
18
19/**
20 * Creates RedactionAnnotation and adds them to document.
21 *
22 * Note: This doesn't apply faceDetections, if you want to apply faceDetections
23 * programmatically see AnnotationManager.applyRedaction()
24 *
25 * @param {WebViewerInstance} webViewerInstance current instance on WebViewer
26 * @param {Number} pageNumber Page number of the document where detections were found
27 * @param {FaceDetection[]} faceDetections Faces that were detected by face-api.js
28 */
29function createFaceRedactionAnnotation(webViewerInstance, pageNumber, faceDetections) {
30 if (faceDetections && faceDetections.length > 0) {
31 const { Annotations, annotationManager } = webViewerInstance.Core;
32 // We create a quad per detected face to allow us use only one redaction annotation.
33 // You could create new RedactionAnnotation for each detected face, but in case where document contains
34 // tens or hundreds of face applying reduction comes slow.
35 const quads = faceDetections.map((detection) => {
36 const x = detection.box.x;
37 const y = detection.box.y;
38 const width = detection.box.width;
39 const height = detection.box.height;
40
41 const topLeft = [x, y];
42 const topRight = [x + width, y];
43 const bottomLeft = [x, y + height];
44 const bottomRight = [x + width, y + height];
45 // Quad is defined as points going from bottom left -> bottom right -> top right -> top left
46 return new Annotations.Quad(...bottomLeft, ...bottomRight, ...topRight, ...topLeft);
47 });
48 const faceAnnotation = new Annotations.RedactionAnnotation({
49 Quads: quads,
50 });
51 faceAnnotation.Author = annotationManager.getCurrentUser();
52 faceAnnotation.PageNumber = pageNumber;
53 faceAnnotation.StrokeColor = new Annotations.Color(255, 0, 0, 1);
54 annotationManager.addAnnotation(faceAnnotation, false);
55 // Annotation needs to be redrawn so that it becomes visible immediately rather than on next time page is refreshed
56 annotationManager.redrawAnnotation(faceAnnotation);
57 }
58}
59
60/**
61 *
62 * @param {WebViewerInstance} webViewerInstance current instance on WebViewer
63 * @param {Number} pageNumber Page number of the document where detection is ran
64 * @returns {Promise} Resolves after faces are detected and RedactionAnnotations are added to document
65 */
66function detectAndRedactFacesFromPage(webViewerInstance, pageNumber) {
67 return new Promise(function(resolve, reject) {
68 const doc = webViewerInstance.Core.documentViewer.getDocument();
69 const pageInfo = doc.getPageInfo(pageNumber);
70 const displaySize = { width: pageInfo.width, height: pageInfo.height }
71 // face-api.js is detecting faces from images, so we need to convert current page to a canvas which then can
72 // be converted to an image.
73 doc.loadCanvas({
74 pageNumber,
75 zoom: 0.5, // Scale page size down to allow faster image processing
76 drawComplete: function drawComplete(canvas) {
77 convertCanvasToImage(canvas).then(async (image) => {
78 const detections = await faceapi.detectAllFaces(image, new faceapi.SsdMobilenetv1Options({
79 minConfidence: 0.40,
80 maxResults: 300
81 }));
82 // As we scaled our image, we need to resize faces back to the original page size
83 const resizedDetections = faceapi.resizeResults(detections, displaySize);
84 createFaceRedactionAnnotation(webViewerInstance, pageNumber, resizedDetections)
85 resolve();
86 });
87 }
88 });
89 });
90}
91
92/**
93 * onClick handler factory for redact faces button. Creates new onClick handler that encloses
94 * webViewer instance inside closure.
95 *
96 * @param {WebViewerInstance} webViewerInstance current instance on WebViewer
97 * @returns {function} returns async click handler for redact faces button
98 */
99function onRedactFacesButtonClickFactory(webViewerInstance) {
100 return async function onRedactFacesButtonClick() {
101 const doc = webViewerInstance.Core.documentViewer.getDocument();
102 const numberOfPages = doc.getPageCount();
103 const { sendPageProcessing, showProgress, hideProgress } = createProgress(numberOfPages)
104 showProgress();
105 for (let pageNumber = 1; pageNumber <= numberOfPages; pageNumber++) {
106 sendPageProcessing();
107 await detectAndRedactFacesFromPage(webViewerInstance, pageNumber);
108 }
109 hideProgress()
110 }
111}
112
113/**
114 * Add custom redact faces button to the top menu
115 *
116 * @param {WebViewerInstance} webViewerInstance current instance on WebViewer
117 * @param {function} onRedactFacesButtonClick Click handler executed when custom redact faces button is clicked
118 */
119function addRedactFacesButtonToHeader(webViewerInstance, onRedactFacesButtonClick) {
120 const image = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-6-3a2 2 0 11-4 0 2 2 0 014 0zm-2 4a5 5 0 00-4.546 2.916A5.986 5.986 0 0010 16a5.986 5.986 0 004.546-2.084A5 5 0 0010 11z" clip-rule="evenodd"></path></svg>';
121
122 /** Legacy UI: Uncomment this to add the button to the header */
123 // webViewerInstance.UI.setHeaderItems(function setHeaderItemsCallback(header) {
124 // const items = header.getItems();
125 // const redactButton = {
126 // type: 'actionButton',
127 // img: image,
128 // title: 'Redact faces',
129 // onClick: onRedactFacesButtonClick,
130 // };
131 // items.splice(10, 0, redactButton);
132 // header.update(items);
133 // });
134 /** End of Legacy UI */
135
136
137 /** Modular UI: Add the button to the header */
138 // Comment this out on legacy UI
139 const redactFacesButton = new webViewerInstance.UI.Components.CustomButton({
140 dataElement: 'customButton',
141 title: 'Redact faces',
142 img: image,
143 onClick: onRedactFacesButtonClick,
144 });
145 const defaultHeader = webViewerInstance.UI.getModularHeader('default-top-header');
146 const groupedItems = defaultHeader.getItems('groupedItems')[0];
147 groupedItems.setItems([...groupedItems.items, redactFacesButton]);
148 /** End of Modular UI */
149}
150
151// Load face-api.js model
152faceapi.nets.ssdMobilenetv1.loadFromUri('/models');
153
154WebViewer(
155 {
156 licenseKey: 'Insert your license key here',
157 path: '/lib',
158 fullAPI: true,
159 enableRedaction: true,
160 enableFilePicker: true,
161 // ui: 'legacy',
162 initialDoc: '/pdftron-people.pdf'
163 },
164 document.getElementById('viewer')
165).then(function(webViewerInstance) {
166 const FitMode = webViewerInstance.UI.FitMode;
167 webViewerInstance.UI.setFitMode(FitMode.FitWidth);
168 const onRedactFacesButtonClick = onRedactFacesButtonClickFactory(webViewerInstance);
169 addRedactFacesButtonToHeader(webViewerInstance, onRedactFacesButtonClick)
170});
171
Did you find this helpful?
Trial setup questions?
Ask experts on DiscordNeed other help?
Contact SupportPricing or product questions?
Contact Sales