import {PDFViewerApplication} from '../pdfjs-legacy/js/viewer.mjs'; import UUID from './uuid.js'; let zoomScaleValue = 1.0; let activeOverlay; let drawingLayer = null; const doNothing = () => {}; function addRedactedPagePreview(pagesSelector) { document.querySelectorAll(pagesSelector).forEach((page) => { let textLayer = page.querySelector('.textLayer'); if (textLayer) textLayer.classList.add('redacted-page-preview'); }); } function addRedactedThumbnailPreview(sidebarPagesSelector) { document.querySelectorAll(sidebarPagesSelector).forEach((thumbnail) => { thumbnail.classList.add('redacted-thumbnail-preview'); let thumbnailImage = thumbnail.querySelector('.thumbnailImage'); if (thumbnailImage) thumbnailImage.classList.add('redacted-thumbnail-image-preview'); }); } function removeRedactedPagePreview() { document.querySelectorAll('.textLayer').forEach((textLayer) => textLayer.classList.remove('redacted-page-preview')); document.querySelectorAll('#thumbnailView > a > div.thumbnail').forEach((thumbnail) => { thumbnail.classList.remove('redacted-thumbnail-preview'); let thumbnailImage = thumbnail.querySelector('.thumbnailImage'); if (thumbnailImage) thumbnailImage.classList.remove('redacted-thumbnail-image-preview'); }); } function extractPagesDetailed(pagesInput, totalPageCount) { let parts = pagesInput.split(',').filter((s) => s); let pagesDetailed = { numbers: new Set(), functions: new Set(), ranges: new Set(), all: false, }; for (let part of parts) { let trimmedPart = part.trim(); if ('all' == trimmedPart) { pagesDetailed.all = true; return pagesDetailed; } else if (isValidFunction(trimmedPart)) { pagesDetailed.functions.add(formatNFunction(trimmedPart)); } else if (trimmedPart.includes('-')) { let range = trimmedPart .replaceAll(' ', '') .split('-') .filter((s) => s); if (range && range.length == 2 && range[0].trim() > 0 && range[1].trim() > 0) pagesDetailed.ranges.add({ low: range[0].trim(), high: range[1].trim(), }); } else if (isPageNumber(trimmedPart)) { pagesDetailed.numbers.add(trimmedPart <= totalPageCount ? trimmedPart : totalPageCount); } } return pagesDetailed; } function formatNFunction(expression) { let result = insertMultiplicationBeforeN(expression.replaceAll(' ', '')); let multiplyByOpeningRoundBracketPattern = /([0-9n)])\(/g; // example: n(n-1), 9(n-1), (n-1)(n-2) result = result.replaceAll(multiplyByOpeningRoundBracketPattern, '$1*('); let multiplyByClosingRoundBracketPattern = /\)([0-9n)])/g; // example: (n-1)n, (n-1)9, (n-1)(n-2) result = result.replaceAll(multiplyByClosingRoundBracketPattern, ')*$1'); return result; } function insertMultiplicationBeforeN(expression) { let result = expression.replaceAll(/(\d)n/g, '$1*n'); while (result.match(/nn/)) { result = result.replaceAll(/nn/g, 'n*n'); // From nn -> n*n } return result; } function validatePages(pages) { let parts = pages.split(',').filter((s) => s); let errors = []; for (let part of parts) { let trimmedPart = part.trim(); if ('all' == trimmedPart) continue; else if (trimmedPart.includes('n')) { if (!isValidFunction(trimmedPart)) errors.push(`${trimmedPart} is an invalid function, it should consist of digits 0-9, n, *, -, /, (, ), \\.`); } else if (trimmedPart.includes('-')) { let range = trimmedPart.split('-').filter((s) => s); if (!range || range.length != 2) errors.push(`${trimmedPart} is an invalid range, it should consist of from-to, example: 1-5`); else if (range[0].trim() <= 0 || range[1].trim() <= 0) errors.push(`${trimmedPart} has invalid range(s), page numbers should be positive.`); } else if (!isPageNumber(trimmedPart)) { errors.push(`${trimmedPart} is invalid, it should either be a function, page number or a range.`); } } return {errors}; } function isPageNumber(page) { return /^[0-9]*$/.test(page); } function isValidFunction(part) { return part.includes('n') && /[0-9n+\-*/() ]+$/.test(part); } function hideContainer(container) { container?.classList.add('d-none'); } const RedactionModes = Object.freeze({ DRAWING: Symbol('drawing'), TEXT: Symbol('text'), NONE: Symbol('none'), }); function removePDFJSButtons() { document.getElementById('print')?.remove(); document.getElementById('download')?.remove(); document.getElementById('editorStamp')?.remove(); document.getElementById('editorFreeText')?.remove(); document.getElementById('editorInk')?.remove(); document.getElementById('secondaryToolbarToggle')?.remove(); document.getElementById('openFile')?.remove(); } function hideInitialPage() { document.body.style.overflowY = 'hidden'; let redactionsFormContainer = document.getElementById('redactionFormContainer'); for ( let el = redactionsFormContainer.previousElementSibling; el && el instanceof HTMLBRElement; el = el.previousElementSibling ) { el.classList.add('d-none'); } redactionsFormContainer.classList.add('d-none'); const footer = document.getElementsByTagName('footer')[0]; // Check if the parent of the footer has the id "viewerContainer" if (footer.parentElement && footer.parentElement.id !== 'viewerContainer') { footer.classList.add('d-none'); } } window.addEventListener('load', (e) => { let isChromium = !!window.chrome || (!!navigator.userAgentData && navigator.userAgentData.brands.some((data) => data.brand == 'Chromium')); let isSafari = /constructor/i.test(window.HTMLElement) || (function (p) { return p.toString() === '[object SafariRemoteNotification]'; })(!window['safari'] || (typeof safari !== 'undefined' && window['safari'].pushNotification)); let isWebkit = navigator.userAgent.search(/webkit/i) > 0; let isGecko = navigator.userAgent.search(/gecko/i) > 0; let isFirefox = typeof InstallTrigger !== 'undefined'; let hiddenInput = document.getElementById('fileInput'); let outerContainer = document.getElementById('outerContainer'); let printContainer = document.getElementById('printContainer'); let toolbarViewerRight = document.getElementById('toolbarViewerRight'); let showMoreBtn = document.getElementById('showMoreBtn'); window.onresize = (e) => { if (window.innerWidth > 1125 && showMoreBtn.classList.contains('toggled')) { showMoreBtn.click(); } else if (window.innerWidth > 1125 && toolbarViewerRight.hasAttribute('style')) { toolbarViewerRight.style.removeProperty('display'); } }; showMoreBtn.onclick = (e) => { if (showMoreBtn.classList.contains('toggled')) { toolbarViewerRight.style.display = 'none'; showMoreBtn.classList.remove('toggled'); } else { toolbarViewerRight.style.display = 'flex'; showMoreBtn.classList.add('toggled'); } }; let viewer = document.getElementById('viewer'); hiddenInput.files = undefined; let redactionMode = RedactionModes.NONE; let redactions = []; let redactionsInput = document.getElementById('redactions-input'); let redactionsPalette = document.getElementById('redactions-palette'); let redactionsPaletteInput = redactionsPalette.querySelector('input'); let redactionsPaletteContainer = document.getElementById('redactionsPaletteContainer'); let redactedPagesDetails = { numbers: new Set(), ranges: new Set(), functions: new Set(), all: false, }; let pageBasedRedactionBtn = document.getElementById('pageBasedRedactionBtn'); let pageBasedRedactionOverlay = document.getElementById('pageBasedRedactionOverlay'); pageBasedRedactionBtn.onclick = (e) => pageBasedRedactionOverlay.classList.remove('d-none'); pageBasedRedactionOverlay.querySelector('input[type=text]').onchange = (e) => { let input = e.target; let parentElement = input.parentElement; resetFieldFeedbackMessages(input, parentElement); let value = input.value.trim(); let {errors} = validatePages(value); if (errors && errors.length > 0) { applyPageRedactionBtn.disabled = 'true'; displayFieldErrorMessages(input, errors); } else { applyPageRedactionBtn.removeAttribute('disabled'); input.classList.add('is-valid'); } }; let applyPageRedactionBtn = document.getElementById('applyPageRedactionBtn'); applyPageRedactionBtn.onclick = (e) => { pageBasedRedactionOverlay.querySelectorAll('input').forEach((input) => { const id = input.getAttribute('data-for'); if (id == 'pageNumbers') { let {errors} = validatePages(input.value); resetFieldFeedbackMessages(input, input.parentElement); if (errors?.length > 0) { applyPageRedactionBtn.disabled = true; displayFieldErrorMessages(input, errors); } else { pageBasedRedactionOverlay.classList.add('d-none'); input.classList.remove('is-valid'); let totalPagesCount = PDFViewerApplication.pdfViewer.pagesCount; let pagesDetailed = extractPagesDetailed(input.value, totalPagesCount); redactedPagesDetails = pagesDetailed; addPageRedactionPreviewToPages(pagesDetailed, totalPagesCount); } } else if (id == 'pageRedactColor') setPageRedactionColor(input.value); let formInput = document.getElementById(id); if (formInput) formInput.value = input.value; }); }; let closePageRedactionBtn = document.getElementById('closePageRedactionBtn'); closePageRedactionBtn.onclick = (e) => { pageBasedRedactionOverlay.classList.add('d-none'); pageBasedRedactionOverlay.querySelectorAll('input').forEach((input) => { const id = input.getAttribute('data-for'); if (id == 'pageNumbers') { resetFieldFeedbackMessages(input, input.parentElement); } let formInput = document.getElementById(id); if (formInput) input.value = formInput.value; }); }; let pdfToImageCheckbox = document.getElementById('convertPDFToImage'); let pdfToImageBtn = document.getElementById('pdfToImageBtn'); pdfToImageBtn.onclick = (e) => { pdfToImageBtn.classList.toggle('btn-success'); pdfToImageBtn.classList.toggle('btn-danger'); pdfToImageCheckbox.checked = !pdfToImageCheckbox.checked; }; let fileChooser = document.getElementsByClassName('custom-file-chooser')[0]; let fileChooserInput = fileChooser.querySelector(`#${fileChooser.getAttribute('data-bs-element-id')}`); let uploadButton = document.getElementById('uploadBtn'); uploadButton.onclick = (e) => fileChooserInput.click(); document.addEventListener('file-input-change', (e) => { redactions = []; _setRedactionsInput(redactions); }); let submitBtn = document.getElementById('submitBtn'); let downloadBtn = document.getElementById('downloadBtn'); let downloadBtnIcon = document.getElementById('downloadBtnIcon'); downloadBtn.onclick = (e) => { submitBtn.click(); setTimeout(_showOrHideLoadingSpinner, 100); // wait 100 milliseconds so that submitBtn would be disabled }; function _showOrHideLoadingSpinner() { if (!submitBtn.disabled) { downloadBtnIcon.innerHTML = 'download'; downloadBtnIcon.classList.remove('spin-animation'); return; } downloadBtnIcon.innerHTML = 'progress_activity'; downloadBtnIcon.classList.add('spin-animation'); setTimeout(_showOrHideLoadingSpinner, 500); } redactionsPaletteContainer.onclick = (e) => redactionsPalette.click(); function clearSelection() { if (window.getSelection) { if (window.getSelection().empty) { // Chrome window.getSelection().empty(); } else if (window.getSelection().removeAllRanges) { // Firefox window.getSelection().removeAllRanges(); } } else if (document.selection) { // IE? document.selection.empty(); } } viewer.onmouseup = (e) => { if (redactionMode !== RedactionModes.TEXT) return; const containsText = window.getSelection() && window.getSelection().toString() != ''; if(containsText){ redactTextSelection(); clearSelection(); } }; redactionsPaletteInput.onchange = (e) => { let color = e.target.value; redactionsPalette.style.setProperty('--palette-color', color); }; document.addEventListener('file-input-change', (e) => { let fileChooser = document.getElementsByClassName('custom-file-chooser')[0]; let fileChooserInput = fileChooser.querySelector(`#${fileChooser.getAttribute('data-bs-element-id')}`); hiddenInput.files = fileChooserInput.files; if (!hiddenInput.files || hiddenInput.files.length === 0) { hideContainer(outerContainer); hideContainer(printContainer); } else { outerContainer?.classList.remove('d-none'); printContainer?.classList.remove('d-none'); hideInitialPage(); } hiddenInput.dispatchEvent(new Event('change', {bubbles: true})); }); PDFViewerApplication.downloadOrSave = doNothing; PDFViewerApplication.triggerPrinting = doNothing; let redactionContainersDivs = {}; PDFViewerApplication.eventBus.on('pagerendered', (e) => { removePDFJSButtons(); let textSelectionRedactionBtn = document.getElementById('man-text-select-redact'); let drawRedactionBtn = document.getElementById('man-shape-redact'); textSelectionRedactionBtn.onclick = _handleTextSelectionRedactionBtnClick; drawRedactionBtn.onclick = _handleDrawRedactionBtnClick; let layer = e.source.textLayer.div; layer.setAttribute('data-page', e.pageNumber); if (redactedPagesDetails.all || redactedPagesDetails.numbers.has(e.pageNumber)) { layer.classList.add('redacted-page-preview'); } else { layer.classList.remove('redacted-page-preview'); } zoomScaleValue = e.source.scale ? e.source.scale : e.source.pageScale; document.documentElement.style.setProperty('--zoom-scale', zoomScaleValue); let redactionsContainer = document.getElementById(`redactions-container-${e.pageNumber}`); if (!redactionsContainer && !redactionContainersDivs[`${e.pageNumber}`]) { redactionsContainer = document.createElement('div'); redactionsContainer.style.position = 'relative'; redactionsContainer.style.height = '100%'; redactionsContainer.style.width = '100%'; redactionsContainer.id = `redactions-container-${e.pageNumber}`; redactionsContainer.style.setProperty('z-index', 'unset'); layer.appendChild(redactionsContainer); redactionContainersDivs[`${e.pageNumber}`] = redactionsContainer; } else if (!redactionsContainer && redactionContainersDivs[`${e.pageNumber}`]) { redactionsContainer = redactionContainersDivs[`${e.pageNumber}`]; layer.appendChild(redactionsContainer); // Dispatch event to update text layer references for elements' events redactionsContainer.querySelectorAll('.selected-wrapper').forEach((area) => area.dispatchEvent( new CustomEvent('textLayer-reference-changed', { bubbles: true, detail: {textLayer: layer}, }) ) ); } document.onpointerup = (e) => { if (drawingLayer && e.target != drawingLayer && e.button == 0) drawingLayer.dispatchEvent(new Event('external-pointerup')); }; initDraw(layer, redactionsContainer); enableTextRedactionMode(); function _handleTextSelectionRedactionBtnClick(e) { if (textSelectionRedactionBtn.classList.contains('toggled')) { resetTextSelection(); } else { enableTextRedactionMode(); } } function enableTextRedactionMode() { if(!textSelectionRedactionBtn.classList.contains('toggled')){ textSelectionRedactionBtn.classList.add('toggled'); } resetDrawRedactions(); redactionMode = RedactionModes.TEXT; }; function resetTextSelection() { textSelectionRedactionBtn.classList.remove('toggled'); redactionMode = RedactionModes.NONE; clearSelection(); } function _handleDrawRedactionBtnClick(e) { if (drawRedactionBtn.classList.contains('toggled')) { resetDrawRedactions(); } else { resetTextSelection(); drawRedactionBtn.classList.add('toggled'); document.documentElement.style.setProperty('--textLayer-pointer-events', 'none'); document.documentElement.style.setProperty('--textLayer-user-select', 'none'); redactionMode = RedactionModes.DRAWING; } } function resetDrawRedactions() { redactionMode = RedactionModes.NONE; drawRedactionBtn.classList.remove('toggled'); document.documentElement.style.setProperty('--textLayer-pointer-events', 'auto'); document.documentElement.style.setProperty('--textLayer-user-select', 'auto'); window.dispatchEvent(new CustomEvent('reset-drawing', {bubbles: true})); } function initDraw(canvas, redactionsContainer) { let mouse = { x: 0, y: 0, startX: 0, startY: 0, }; let element = null; let drawnRedaction = null; window.addEventListener('reset-drawing', (e) => { _clearDrawing(); canvas.style.cursor = 'default'; document.documentElement.style.setProperty('--textLayer-pointer-events', 'auto'); document.documentElement.style.setProperty('--textLayer-user-select', 'auto'); }); window.addEventListener('drawing-entered', (e) => { let target = e.detail?.target; if (canvas === target) return; _clearDrawing(); }); window.addEventListener('cancel-drawing', (e) => { _clearDrawing(); canvas.style.cursor = 'default'; }); function setMousePosition(e) { if (isChromium || isSafari || isWebkit) { mouse.x = e.offsetX; mouse.y = e.offsetY; } else if (isFirefox || isGecko) { mouse.x = e.layerX; mouse.y = e.layerY; } else { let rect = (e.target || e.srcElement).getBoundingClientRect(); mouse.x = e.clientX - rect.left; mouse.y = e.clientY - rect.top; } } window.onkeydown = (e) => { if (e.key === 'Escape' && redactionMode === RedactionModes.DRAWING) { window.dispatchEvent(new CustomEvent('cancel-drawing', {bubbles: true})); } }; canvas.onpointerenter = (e) => { window.dispatchEvent( new CustomEvent('drawing-entered', { bubbles: true, detail: {target: canvas}, }) ); }; canvas.onpointerup = (e) => { let isLeftClick = e.button == 0; if (!isLeftClick) return; if (element !== null) { _saveAndResetDrawnRedaction(); console.log('finished.'); } }; canvas.addEventListener('external-pointerup', (e) => { if (element != null) { _saveAndResetDrawnRedaction(); } }); canvas.onpointerleave = (e) => { let ev = copyEvent(e, 'pointerleave'); let {left, top} = calculateMouseCoordinateToRotatedBox(canvas, e); ev.layerX = left; ev.offsetX = left; ev.layerY = top; ev.offsetY = top; setMousePosition(ev); if (element !== null) { draw(); } }; canvas.onpointerdown = (e) => { let isLeftClick = e.button == 0; if (!isLeftClick) return; if (element == null) { if (redactionMode !== RedactionModes.DRAWING) { console.warn('Drawing attempt when redaction mode is', redactionMode.description); return; } console.log('begun.'); _captureAndDrawStartingPointOfDrawnRedaction(); } }; canvas.onpointermove = function (e) { setMousePosition(e); if (element !== null) { draw(); } }; function draw() { let scaleFactor = _getScaleFactor(); let width = Math.abs(mouse.x - mouse.startX); element.style.width = _toCalcZoomPx(_scaleToDisplay(width)); let height = Math.abs(mouse.y - mouse.startY); element.style.height = _toCalcZoomPx(_scaleToDisplay(height)); let left = mouse.x - mouse.startX < 0 ? mouse.x : mouse.startX; element.style.left = _toCalcZoomPx(_scaleToDisplay(left)); let top = mouse.y - mouse.startY < 0 ? mouse.y : mouse.startY; element.style.top = _toCalcZoomPx(_scaleToDisplay(top)); if (drawnRedaction) { drawnRedaction.left = _scaleToPDF(left, scaleFactor); drawnRedaction.top = _scaleToPDF(top, scaleFactor); drawnRedaction.width = _scaleToPDF(width, scaleFactor); drawnRedaction.height = _scaleToPDF(height, scaleFactor); } } function _clearDrawing() { if (element) element.remove(); if (drawingLayer == canvas) drawingLayer = null; element = null; drawnRedaction = null; } function _saveAndResetDrawnRedaction() { if (!element) return; if ( !element.style.height || element.style.height.includes('(0px * var') || !element.style.width || element.style.width.includes('(0px * var') ) { element.remove(); } else { element.classList.add('selected-wrapper'); element.classList.remove('rectangle'); addRedactionOverlay(element, drawnRedaction, canvas); redactions.push(drawnRedaction); _setRedactionsInput(redactions); } drawingLayer = null; element = null; drawnRedaction = null; canvas.style.cursor = 'default'; } function _captureAndDrawStartingPointOfDrawnRedaction() { mouse.startX = mouse.x; mouse.startY = mouse.y; element = document.createElement('div'); element.className = 'rectangle'; drawingLayer = canvas; let left = mouse.x; let top = mouse.y; element.style.left = _toCalcZoomPx(_scaleToDisplay(left)); element.style.top = _toCalcZoomPx(_scaleToDisplay(top)); let scaleFactor = _getScaleFactor(); let color = redactionsPalette.style.getPropertyValue('--palette-color'); element.style.setProperty('--palette-color', color); drawnRedaction = { left: _scaleToPDF(left, scaleFactor), top: _scaleToPDF(top, scaleFactor), width: 0.0, height: 0.0, color: color, pageNumber: parseInt(canvas.getAttribute('data-page')), element: element, id: UUID.uuidv4(), }; redactionsContainer.appendChild(element); canvas.style.cursor = 'crosshair'; } } }); PDFViewerApplication.eventBus.on('rotationchanging', (e) => { if (!activeOverlay) return; hideOverlay(); }); function _getScaleFactor() { return parseFloat(viewer.style.getPropertyValue('--scale-factor')); } function getTextLayer(element) { let current = element; while (current) { if (current instanceof HTMLDivElement && current.classList.contains('textLayer')) return current; current = current.parentElement; } return current; } document.onclick = (e) => { if ( (e.target && e.target.classList.contains('selected-wrapper') && e.target.firstChild == activeOverlay) || e.target == activeOverlay ) return; if (activeOverlay) hideOverlay(); }; document.addEventListener('keydown', (e) => { if (e.key === 'Delete' && activeOverlay) { activeOverlay.querySelector('.delete-icon')?.dispatchEvent(new Event('click', {bubbles: true})); return; } const isRedactionShortcut = e.ctrlKey && (e.key == 's' || e.key == 'S' || e.code == 'KeyS'); if (!isRedactionShortcut || redactionMode !== RedactionModes.TEXT) return; redactTextSelection(); }); function rotateTextBox(rect, textLayerRect, angle) { let left, top, width, height; if (!angle || angle == 0) { left = rect.left - textLayerRect.left; top = rect.top - textLayerRect.top; width = rect.width; height = rect.height; } else if (angle == 90) { left = rect.top - textLayerRect.top; top = textLayerRect.right - rect.right; width = rect.height; height = rect.width; } else if (angle == 180) { left = textLayerRect.right - rect.right; top = textLayerRect.bottom - rect.bottom; width = rect.width; height = rect.height; } else if (angle == 270) { left = textLayerRect.bottom - rect.bottom; top = rect.left - textLayerRect.left; width = rect.height; height = rect.width; } return {left, top, width, height}; } function redactTextSelection() { let selection = window.getSelection(); if (!selection || selection.rangeCount <= 0) return; let range = selection.getRangeAt(0); let textLayer = getTextLayer(range.startContainer); if (!textLayer) return; const pageNumber = textLayer.getAttribute('data-page'); let redactionsArea = textLayer.querySelector(`#redactions-container-${pageNumber}`); let textLayerRect = textLayer.getBoundingClientRect(); let rects = range.getClientRects(); let scaleFactor = _getScaleFactor(); let color = redactionsPalette.style.getPropertyValue('--palette-color'); let angle = textLayer.getAttribute('data-main-rotation'); for (const rect of rects) { if (!rect || !rect.width || !rect.height) continue; let redactionElement = document.createElement('div'); redactionElement.classList.add('selected-wrapper'); let {left, top, width, height} = rotateTextBox(rect, textLayerRect, angle); let leftDisplayScaled = _scaleToDisplay(left); let topDisplayScaled = _scaleToDisplay(top); let widthDisplayScaled = _scaleToDisplay(width); let heightDisplayScaled = _scaleToDisplay(height); let redactionInfo = { left: _scaleToPDF(left, scaleFactor), top: _scaleToPDF(top, scaleFactor), width: _scaleToPDF(width, scaleFactor), height: _scaleToPDF(height, scaleFactor), pageNumber: parseInt(pageNumber), color: color, element: redactionElement, id: UUID.uuidv4(), }; redactions.push(redactionInfo); redactionElement.style.left = _toCalcZoomPx(leftDisplayScaled); redactionElement.style.top = _toCalcZoomPx(topDisplayScaled); redactionElement.style.width = _toCalcZoomPx(widthDisplayScaled); redactionElement.style.height = _toCalcZoomPx(heightDisplayScaled); redactionElement.style.setProperty('--palette-color', color); redactionsArea.appendChild(redactionElement); addRedactionOverlay(redactionElement, redactionInfo, textLayer); } _setRedactionsInput(redactions); } function _scaleToDisplay(value) { return value / zoomScaleValue; } function _scaleToPDF(value, scaleFactor) { if (!scaleFactor) scaleFactor = document.documentElement.getPropertyValue('--scale-factor'); return value / scaleFactor; } function _toCalcZoomPx(val) { return `calc(${val}px * var(--zoom-scale))`; } function _setRedactionsInput(redactions) { let stringifiedRedactions = JSON.stringify( redactions.filter(_nonEmptyRedaction).map((red) => ({ x: red.left, y: red.top, width: red.width, height: red.height, color: red.color, page: red.pageNumber, })) ); redactionsInput.value = stringifiedRedactions; } function addRedactionOverlay(redactionElement, redactionInfo, textLayer) { let redactionOverlay = document.createElement('div'); let deleteBtn = $( `` )[0]; deleteBtn.onclick = (e) => { redactions = redactions.filter((red) => redactionInfo.id != red.id); redactionElement.remove(); _setRedactionsInput(redactions); activeOverlay = null; }; let colorPaletteLabel = $( `` )[0]; let colorPaletteInput = $(` `)[0]; colorPaletteLabel.appendChild(colorPaletteInput); colorPaletteLabel.onclick = (e) => { if (colorPaletteLabel === e.target) { e.stopPropagation(); } }; colorPaletteInput.onchange = (e) => { let color = e.target.value; redactionElement.style.setProperty('--palette-color', color); let redactionIdx = redactions.findIndex((red) => redactionInfo.id === red.id); if (redactionIdx < 0) return; redactions[redactionIdx].color = color; _setRedactionsInput(redactions); }; redactionOverlay.appendChild(deleteBtn); redactionOverlay.appendChild(colorPaletteLabel); redactionOverlay.classList.add('redaction-overlay'); redactionOverlay.style.display = 'none'; redactionElement.addEventListener('textLayer-reference-changed', (e) => { textLayer = e.detail.textLayer; }); redactionElement.onclick = (e) => { if (e.target != redactionElement) return; if (activeOverlay) hideOverlay(); redactionElement.classList.add('active-redaction'); activeOverlay = redactionOverlay; _adjustActiveOverlayCoordinates(); }; redactionElement.appendChild(redactionOverlay); // Adjust active overlay coordinates to avoid placing the overlay out of page bounds function _adjustActiveOverlayCoordinates() { activeOverlay.style.visibility = 'hidden'; activeOverlay.style.display = 'flex'; textLayer = textLayer || getTextLayer(redactionElement); let angle = parseInt(textLayer.getAttribute('data-main-rotation')); if (textLayer) redactionOverlay.style.transform = `rotate(${angle * -1}deg)`; activeOverlay.style.removeProperty('left'); activeOverlay.style.removeProperty('top'); let textRect = textLayer.getBoundingClientRect(); let overlayRect = redactionOverlay.getBoundingClientRect(); let leftOffset = 0, topOffset = 0; if (overlayRect.right > textRect.right) { leftOffset = textRect.right - overlayRect.right; } else if (overlayRect.left < textRect.left) { leftOffset = textRect.left - overlayRect.left; } if (overlayRect.top < textRect.top) { topOffset = textRect.top - overlayRect.top; } else if (overlayRect.bottom > textRect.bottom) { topOffset = textRect.bottom - overlayRect.bottom; } switch (angle) { case 90: [leftOffset, topOffset] = [topOffset, -leftOffset]; break; case 180: [leftOffset, topOffset] = [-leftOffset, -topOffset]; break; case 270: [leftOffset, topOffset] = [-topOffset, leftOffset]; break; } if (leftOffset != 0) activeOverlay.style.left = `calc(50% + ${leftOffset}px`; if (topOffset != 0) activeOverlay.style.top = `calc(100% + ${topOffset}px`; activeOverlay.style.visibility = 'unset'; } } }); function calculateMouseCoordinateToRotatedBox(canvas, e) { let textRect = canvas.getBoundingClientRect(); let left, top = 0; let angle = parseInt(canvas.getAttribute('data-main-rotation')); switch (angle) { case 0: left = clamp(e.pageX - textRect.left, 0, textRect.width); top = clamp(e.pageY - textRect.top, 0, textRect.height); break; case 90: left = clamp(e.pageY - textRect.top, 0, textRect.height); top = clamp(textRect.right - e.pageX, 0, textRect.width); break; case 180: left = clamp(textRect.right - e.pageX, 0, textRect.width); top = clamp(textRect.bottom - e.pageY, 0, textRect.width); break; case 270: left = clamp(textRect.bottom - e.pageY, 0, textRect.height); top = clamp(e.pageX - textRect.left, 0, textRect.width); break; } return {left, top}; } function clamp(value, min, max) { return Math.max(min, Math.min(value, max)); } function addPageRedactionPreviewToPages(pagesDetailed, totalPagesCount) { if (pagesDetailed.all) { addRedactedPagePreview('#viewer > .page'); addRedactedThumbnailPreview('#thumbnailView > a > div.thumbnail'); } else { removeRedactedPagePreview(); setPageNumbersFromRange(pagesDetailed, totalPagesCount); setPageNumbersFromNFunctions(pagesDetailed, totalPagesCount); let pageNumbers = Array.from(pagesDetailed.numbers); if (pageNumbers?.length > 0) { let pagesSelector = pageNumbers.map((number) => `#viewer > .page[data-page-number="${number}"]`).join(','); addRedactedPagePreview(pagesSelector); let thumbnailSelector = pageNumbers .map((number) => `#thumbnailView > a > div.thumbnail[data-page-number="${number}"]`) .join(','); addRedactedThumbnailPreview(thumbnailSelector); } } } function resetFieldFeedbackMessages(input, parentElement) { if (parentElement) parentElement.querySelectorAll('.invalid-feedback').forEach((feedback) => feedback.remove()); if (input) { input.classList.remove('is-invalid'); input.classList.remove('is-valid'); } } function displayFieldErrorMessages(input, errors) { input.classList.add('is-invalid'); errors.forEach((error) => { let element = document.createElement('div'); element.classList.add('invalid-feedback'); element.classList.add('list-styling'); element.textContent = error; input.parentElement.appendChild(element); }); } function setPageRedactionColor(color) { document.documentElement.style.setProperty('--page-redaction-color', color); } function setPageNumbersFromNFunctions(pagesDetailed, totalPagesCount) { pagesDetailed.functions.forEach((fun) => { if (!isValidFunction(fun)) return; for (let n = 1; n <= totalPagesCount; n++) { let pageNumber = eval(fun); if (!pageNumber || pageNumber <= 0 || pageNumber > totalPagesCount) continue; pagesDetailed.numbers.add(pageNumber); } }); } function setPageNumbersFromRange(pagesDetailed, totalPagesCount) { pagesDetailed.ranges.forEach((range) => { for (let i = range.low; i <= range.high && i <= totalPagesCount; i++) { pagesDetailed.numbers.add(i); } }); } function hideOverlay() { activeOverlay.style.display = 'none'; activeOverlay.parentElement.classList.remove('active-redaction'); activeOverlay = null; } function _isEmptyRedaction(redaction) { return ( redaction.left == null || redaction.top == null || redaction.width == null || redaction.height == null || redaction.pageNumber == null ); } function _nonEmptyRedaction(redaction) { return !_isEmptyRedaction(redaction); } function copyEvent(e, type) { if (type == 'pointerleave') return { layerX: e.layerX, layerY: e.layerY, pageX: e.pageX, pageY: e.pageY, clientX: e.clientX, clientY: e.clientY, button: e.button, height: e.height, width: e.width, offsetX: e.offsetX, offsetY: e.offsetY, pointerId: e.pointerId, pointerType: e.pointerType, type: e.type, screenX: e.screenX, screenY: e.screenY, tiltX: e.tiltX, tiltY: e.tiltY, x: e.x, y: e.y, altKey: e.altKey, ctrlKey: e.ctrlKey, isPrimary: e.isPrimary, isTrusted: e.isTrusted, metaKey: e.metaKey, pressure: e.pressure, returnValue: e.returnValue, shiftKey: e.shiftKey, timeStamp: e.timeStamp, which: e.which, twist: e.twist, tangentialPressure: e.tangentialPressure, target: e.target, srcElement: e.srcElement, relatedTarget: e.relatedTarget, rangeOffset: e.rangeOffset, rangeParent: e.rangeParent, explicitOriginalTarget: e.explicitOriginalTarget, eventPhase: e.eventPhase, detail: e.detail, defaultPrevented: e.defaultPrevented, currentTarget: e.currentTarget, buttons: e.buttons, azimuthAngle: e.azimuthAngle, altitudeAngle: e.altitudeAngle, }; return {}; }