Feature: Support manual redaction (#2433)

# Description

## Manual Redaction:

- ### Text Selection-based redaction:
-
![image](https://github.com/user-attachments/assets/e59c5e6c-ef52-4f54-a98e-fc26e3226c8e)
- Users can now redact currently selected text by selecting the text
then clicking `ctrl + s` shortcut or by pressing on **apply/save/disk
icon** in the toolbar.
- Users can delete/cancel the redacted area by clicking on the box
containing the text, then clicking on `delete/trash` icon or by using
the shortcut `delete`.
- Users can customize the color of the redacted area/text (after the
redaction was applied) by simply clicking on the box containing the
text/area then clicking on the `color palette` icon and choosing the
color they want.
- Users can choose to select the color of redaction before redacting
text or applying changes (this only affects newly created redaction
areas, to change the color of an existing one; check the previous bullet
point).


- ### Draw/Area-based redaction:
-
![image](https://github.com/user-attachments/assets/e2968ae3-ebaf-497e-b3bd-0c8c8f4ee157)
- Users can now redact an area in the page by selecting the then
clicking `ctrl + s` shortcut or by pressing on **apply/save/disk icon**
in the toolbar.
- Users can delete/cancel the redacted area by clicking on the drawn
box, then clicking on `delete/trash` icon or by using the shortcut
`delete` (requires temporarily turning off drawing mode).
- Users can customize the color of the redacted area (after the
redaction was applied) by simply clicking on the box containing the area
then clicking on the `color palette` icon and choosing the color they
want.
- Users can choose to select the color of redaction before drawing the
box or applying changes (this only affects newly created redaction
areas, to change the color of an existing one; check the previous bullet
point).

 - ### Page-based redaction:
-
![image](https://github.com/user-attachments/assets/aba59432-23e7-4fe6-aa28-872c23a91242)
- Users can now redact **ENTIRE** pages by specifying the page
number(s), range(s) or functions.
- Users can customize the color of page-based redaction (doesn't affect
text-based nor draw-based redactions).

### Redaction modes:
There are three modes of redaction/operation currently supported
  - Text Selection-based redaction (TEXT)
  - Draw/Area-based redaction (DRAWING)
  - None - by simply not choosing any of the above modes (NONE).

## How to use:

- **Text Selection-based redaction:** click on this icon in the toolbar
![image](https://github.com/user-attachments/assets/52cc31ef-6946-482c-84a2-1ddb79646dd8)
to enable `text-selection redaction mode` then select the text you want
to redact then press `ctrl + s` or click on the disk/save icon
![image](https://github.com/user-attachments/assets/f2bdf2f2-ee07-4682-bb9a-95e13a1004cf).
- **Draw/Area-based redaction:** click on this icon in the toolbar
![image](https://github.com/user-attachments/assets/fe00dca9-761e-47a0-a748-2041830dc73e)
to enable `draw/area-based redaction` then `left mouse click (LMB)` on
the starting point of the rectangle, then once you are satisfied with
the rectangle's placement/dimensions then `left mouse click (LMB)` again
to apply the redaction.
- **Example:** `Left mouse click (LMB)` then move mouse to the right
then bottom then `Left mouse click (LMB)`.
- Note: Red box/rectangle borders indicate that you have not yet saved
(you need to left click on the page to save)
![image](https://github.com/user-attachments/assets/5ce5f789-9d6f-4984-8555-e8fef2a3e3cc)
once saved the borders will become green
![image](https://github.com/user-attachments/assets/85cabb9f-e7ee-4268-90cd-80493b625466)
(they also become clickable/hover-able when drawing mode is off).
- **Page-based redactions:**: Insert the page number(s), range(s) and/or
functions (separated by `,`) then select your preferred color and click
on `Redact` to submit.

![image](https://github.com/user-attachments/assets/ed8a0a98-32b2-4ae1-a3c7-c54bfe0fea66)

- **Color Customizations:**
- You can change the redaction color for new redactions by clicking on
this icon in the toolbar
![image](https://github.com/user-attachments/assets/bad573ee-0545-4329-b131-2022f970f134).
- You can change the redaction color for existing redactions by hovering
over the redaction box then clicking on it (`Left mouse click LMB`) then
clicking on color palette (highlighted in red in the picture)
![image](https://github.com/user-attachments/assets/22281a81-2cd9-4771-9a93-a75b6dd93433)
then select your preferred color.

- **Deletions:**
- You can delete a redacted area by hovering over the redaction box then
clicking on it (`Left mouse click LMB`) then clicking on the trash icon
(highlighted in red in the picture)
![image](https://github.com/user-attachments/assets/f0347279-8211-4b1c-a91d-c1fcb929cc5d).

## Card in the home page:

![image](https://github.com/user-attachments/assets/b3fb16eb-5ff0-4548-9f22-b1b8fe162c8b)

Closes #465

## Checklist

- [x] I have read the [Contribution
Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md)
- [x] I have performed a self-review of my own code
- [x] I have attached images of the change if it is UI based
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] If my code has heavily changed functionality I have updated
relevant docs on [Stirling-PDFs doc
repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/)
- [ ] My changes generate no new warnings
- [ ] I have read the section [Add New Translation
Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags)
(for new translation tags only)

---------

Co-authored-by: Anthony Stirling <77850077+Frooodle@users.noreply.github.com>
This commit is contained in:
Omar Ahmed Hassan
2025-01-06 13:38:44 +02:00
committed by GitHub
parent 79f6598508
commit 22af79a279
14 changed files with 7549 additions and 9 deletions

View File

@@ -0,0 +1,322 @@
:root {
--page-redaction-color: #000000;
}
.textLayer span::selection,
.textLayer span::-moz-selection {
background-color: rgba(0, 100, 0, 0.26);
}
.selected-wrapper {
position: absolute;
outline: 2px solid darkgreen;
outline-offset: -2px;
z-index: 10;
}
.selected-wrapper:hover:not(:has(.redaction-overlay:hover)) {
outline-color: var(--palette-color, #000000);
background-color: var(--palette-color, #000000);
z-index: 10;
transition: background-color 0.065s linear;
cursor: pointer;
}
.redaction-overlay {
display: flex;
position: absolute;
left: 50%;
top: 100%;
min-width: 25px;
max-width: 90px;
min-height: 25px;
flex-wrap: nowrap;
column-gap: 5px;
row-gap: 2px;
border-radius: 2px;
padding: 2px;
box-sizing: border-box;
background-color: rgb(0 96 170);
outline: 1px solid gray;
translate: -50% -100%;
}
.redaction-overlay svg {
height: 25px;
width: 25px;
max-width: 35px;
max-height: 35px;
fill: rgba(255, 255, 255, 0.904);
user-select: none;
}
.redaction-overlay svg:hover {
cursor: pointer;
background-color: rgb(3, 63, 109);
fill: rgba(226, 226, 226, 0.904);
}
.textLayer div,
.textLayer div > * {
user-select: none;
}
.rectangle {
border: 2px solid #ff0000;
position: absolute;
}
html {
--textLayer-pointer-events: auto;
--textLayer-user-select: auto;
}
.textLayer * {
pointer-events: var(--textLayer-pointer-events);
user-select: var(--textLayer-user-select);
}
#showMoreBtnIcon::before {
left: 5px;
top: 5px;
}
#showMoreBtn {
display: flex;
justify-content: center;
align-items: center;
}
#man-text-select-redact, #man-shape-redact, #downloadBtn, #uploadBtn, #pageBasedRedactionBtn, #pdfToImageBtn, #showMoreBtn {
height: var(--toolButton-height);
width: var(--toolButton-width);
border-radius: var(--toolButton-border-radius);
transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
user-select: none;
}
#text-selection, #shape-selection, #downloadBtnIcon, #uploadBtnIcon, #pageBasedRedactionBtnIcon, #pdfToImageBtnIcon, #showMoreBtnIcon {
position: relative;
font-size: var(--toolButton-icon-font-size);
}
:is(#man-shape-redact, #man-text-select-redact, #sidebarToggle, #viewThumbnail, #viewOutline, #showMoreBtn).toggled {
background-color: rgb(50, 159, 243);
color: rgb(255 255 255);
outline:rgb(50, 159, 243) !important;
border-color: rgb(50, 159, 243) !important;
}
:is(#man-shape-redact, #man-text-select-redact, #redactionsPaletteContainer, #downloadBtn, #uploadBtn, #pageBasedRedactionBtn, #pdfToImageBtn, #showMoreBtn):hover {
background-color: rgba(6, 114, 197, 0.82);
color: rgb(255 255 255);
outline:rgba(6, 114, 197, 0.82) !important;
border-color: rgba(6, 114, 197, 0.82) !important;
}
#redactionsPaletteContainer {
height: var(--toolButton-height);
width: var(--toolButton-width);
border-radius: var(--toolButton-border-radius);
overflow: hidden;
}
#redactionsPaletteContainer *, #showMoreBtn * {
user-select: none;
pointer-events: none;
}
#redactions-palette {
display: inline;
position: relative;
border-bottom: 8px solid var(--palette-color);
border-radius: inherit;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
font-size: var(--toolButton-icon-font-size);
}
#redactions-palette::before {
position: absolute;
content: '';
height: 6px;
width: 100%;
left: 0;
bottom: 0px;
background-color: var(--palette-color);
}
#redactions-palette > input[type=color] {
visibility: hidden;
position: absolute;
left: 0;
top: var(--toolButton-height);
height: 0;
}
#apply-redaction {
height: var(--toolButton-height);
width: var(--toolButton-width);
border-radius: var(--toolButton-border-radius);
}
#apply-redaction[disabled=true], #apply-redaction:disabled:not([disabled=false]) {
color: rgb(147, 149, 153);
box-shadow: none !important;
}
#apply-redaction:is(:hover):not([disabled=true], :disabled:not([disabled=false])) {
cursor: pointer;
background-color: rgba(6, 114, 197, 0.82);
color: rgb(255 255 255);
outline:rgba(6, 114, 197, 0.82) !important;
border-color: rgba(6, 114, 197, 0.82) !important;
}
.toolbar-btn-hover:hover {
cursor: pointer;
background-color: rgba(6, 114, 197, 0.82) !important;
color: rgb(255 255 255) !important;
outline:rgba(6, 114, 197, 0.82) !important;
border-color: rgba(6, 114, 197, 0.82) !important;
}
#apply-redaction-icon {
font-size: var(--toolButton-icon-font-size);
}
#apply-redaction > span {
user-select: none;
pointer-events: none;
}
#pageRedactColor, input[data-for=pageRedactColor] {
flex: 1;
padding: 1px;
}
#pageRedactColor:is(:hover, :focus-within), input[data-for=pageRedactColor]:is(:hover, :focus-within) {
cursor: pointer;
}
.palette-color {
border-bottom: 3px solid var(--palette-color);
}
.palette-color:is(:hover, :focus-within) {
cursor: pointer;
background-color: var(--button-hover-color);
}
.splitToolbarButton > .btn-primary, .splitToolbarButton > .btn-secondary, .splitToolbarButton > .toolbarButton {
margin-left: 3px;
margin-right: 3px;
}
.spin-animation {
-webkit-animation: spin 2s linear infinite; /* Safari */
-moz-animation: spin 2s linear infinite;
-o-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
@-webkit-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@-moz-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@-o-keyframes spin {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.active-redaction {
z-index: 30 !important;
}
#pageBasedRedactionOverlay {
position: absolute;
left: 50%;
top: 50%;
background-color: var(--md-sys-color-surface);
color: var(--md-sys-color-on-surface);
border-radius: 3rem;
z-index: 100;
transform: translate(-50%, -50%);
}
.list-styling {
display: list-item !important;
margin-left: 15px;
list-style-type: disc;
}
.redacted-page {
--page-redaction-color: none;
}
.redacted-page-preview {
border: 2px solid blue;
}
.redacted-page-preview:hover {
background-color: var(--page-redaction-color) !important;
}
.redacted-page-preview * {
user-select: none;
pointer-events: none;
}
.overlay-colorpicker-window {
position: absolute;
left: 0;
top: 24px;
height: 0;
visibility: hidden;
}
.redacted-thumbnail-image-preview {
border: 2px solid blue;
}
.redacted-thumbnail-preview {
position: relative;
}
.redacted-thumbnail-preview:hover::after {
content: '';
background-color: var(--page-redaction-color);
position: absolute;
left: 0;
top: 0;
height: var(--thumbnail-height);
width: var(--thumbnail-width);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4630,7 +4630,7 @@ if (DESCRIPTORS && !('size' in URLSearchParamsPrototype)) {
/************************************************************************/
/******/ // The module cache
/******/ var __webpack_module_cache__ = {};
/******/
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
@@ -4644,14 +4644,14 @@ if (DESCRIPTORS && !('size' in URLSearchParamsPrototype)) {
/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
/******/
/******/ // Execute the module function
/******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
@@ -4664,12 +4664,12 @@ if (DESCRIPTORS && !('size' in URLSearchParamsPrototype)) {
/******/ }
/******/ };
/******/ })();
/******/
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/
/************************************************************************/
var __webpack_exports__ = globalThis.pdfjsLib = {};
// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk.
@@ -6385,8 +6385,8 @@ function setLayerDimensions(div, viewport, mustFlip = false, mustRotate = true)
const useRound = util_FeatureTest.isCSSRoundSupported;
const w = `var(--scale-factor) * ${pageWidth}px`,
h = `var(--scale-factor) * ${pageHeight}px`;
const widthStr = useRound ? `round(${w}, 1px)` : `calc(${w})`,
heightStr = useRound ? `round(${h}, 1px)` : `calc(${h})`;
const widthStr = useRound ? `round(up, ${w}, 1px)` : `calc(${w})`,
heightStr = useRound ? `round(up, ${h}, 1px)` : `calc(${h})`;
if (!mustFlip || viewport.rotation % 180 === 0) {
style.width = widthStr;
style.height = heightStr;
@@ -24317,4 +24317,4 @@ var __webpack_exports__updateTextLayer = __webpack_exports__.updateTextLayer;
var __webpack_exports__version = __webpack_exports__.version;
export { __webpack_exports__AbortException as AbortException, __webpack_exports__AnnotationEditorLayer as AnnotationEditorLayer, __webpack_exports__AnnotationEditorParamsType as AnnotationEditorParamsType, __webpack_exports__AnnotationEditorType as AnnotationEditorType, __webpack_exports__AnnotationEditorUIManager as AnnotationEditorUIManager, __webpack_exports__AnnotationLayer as AnnotationLayer, __webpack_exports__AnnotationMode as AnnotationMode, __webpack_exports__CMapCompressionType as CMapCompressionType, __webpack_exports__ColorPicker as ColorPicker, __webpack_exports__DOMSVGFactory as DOMSVGFactory, __webpack_exports__DrawLayer as DrawLayer, __webpack_exports__FeatureTest as FeatureTest, __webpack_exports__GlobalWorkerOptions as GlobalWorkerOptions, __webpack_exports__ImageKind as ImageKind, __webpack_exports__InvalidPDFException as InvalidPDFException, __webpack_exports__MissingPDFException as MissingPDFException, __webpack_exports__OPS as OPS, __webpack_exports__Outliner as Outliner, __webpack_exports__PDFDataRangeTransport as PDFDataRangeTransport, __webpack_exports__PDFDateString as PDFDateString, __webpack_exports__PDFWorker as PDFWorker, __webpack_exports__PasswordResponses as PasswordResponses, __webpack_exports__PermissionFlag as PermissionFlag, __webpack_exports__PixelsPerInch as PixelsPerInch, __webpack_exports__RenderingCancelledException as RenderingCancelledException, __webpack_exports__TextLayer as TextLayer, __webpack_exports__UnexpectedResponseException as UnexpectedResponseException, __webpack_exports__Util as Util, __webpack_exports__VerbosityLevel as VerbosityLevel, __webpack_exports__XfaLayer as XfaLayer, __webpack_exports__build as build, __webpack_exports__createValidAbsoluteUrl as createValidAbsoluteUrl, __webpack_exports__fetchData as fetchData, __webpack_exports__getDocument as getDocument, __webpack_exports__getFilenameFromUrl as getFilenameFromUrl, __webpack_exports__getPdfFilenameFromUrl as getPdfFilenameFromUrl, __webpack_exports__getXfaPageViewport as getXfaPageViewport, __webpack_exports__isDataScheme as isDataScheme, __webpack_exports__isPdfFile as isPdfFile, __webpack_exports__noContextMenu as noContextMenu, __webpack_exports__normalizeUnicode as normalizeUnicode, __webpack_exports__renderTextLayer as renderTextLayer, __webpack_exports__setLayerDimensions as setLayerDimensions, __webpack_exports__shadow as shadow, __webpack_exports__updateTextLayer as updateTextLayer, __webpack_exports__version as version };
//# sourceMappingURL=pdf.mjs.map
//# sourceMappingURL=pdf.mjs.map