diff --git a/src/core/annotation.js b/src/core/annotation.js index 95028cb5e456b..c77b2b8c96fba 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -705,6 +705,11 @@ class Annotation { this.data.pageIndex = params.pageIndex; } + const it = dict.get("IT"); + if (it instanceof Name) { + this.data.it = it.name; + } + this._isOffscreenCanvasSupported = params.evaluatorOptions.isOffscreenCanvasSupported; this._fallbackFontDict = null; @@ -1377,6 +1382,7 @@ class Annotation { class AnnotationBorderStyle { constructor() { this.width = 1; + this.rawWidth = 1; this.style = AnnotationBorderStyleType.SOLID; this.dashArray = [3]; this.horizontalCornerRadius = 0; @@ -1407,6 +1413,7 @@ class AnnotationBorderStyle { } if (typeof width === "number") { if (width > 0) { + this.rawWidth = width; const maxWidth = (rect[2] - rect[0]) / 2; const maxHeight = (rect[3] - rect[1]) / 2; @@ -4283,6 +4290,10 @@ class InkAnnotation extends MarkupAnnotation { const { dict, xref } = params; this.data.annotationType = AnnotationType.INK; this.data.inkLists = []; + this.data.isEditable = !this.data.noHTML && this.data.it === "InkHighlight"; + // We want to be able to add mouse listeners to the annotation. + this.data.noHTML = false; + this.data.opacity = dict.get("CA") || 1; const rawInkLists = dict.getArray("InkList"); if (!Array.isArray(rawInkLists)) { @@ -4534,6 +4545,10 @@ class HighlightAnnotation extends MarkupAnnotation { const { dict, xref } = params; this.data.annotationType = AnnotationType.HIGHLIGHT; + this.data.isEditable = !this.data.noHTML; + // We want to be able to add mouse listeners to the annotation. + this.data.noHTML = false; + this.data.opacity = dict.get("CA") || 1; const quadPoints = (this.data.quadPoints = getQuadPoints(dict, null)); if (quadPoints) { @@ -4573,11 +4588,15 @@ class HighlightAnnotation extends MarkupAnnotation { } } - static createNewDict(annotation, xref, { apRef, ap }) { + static createNewDict(annotation, xref, { apRef, ap, oldAnnotation }) { const { color, opacity, rect, rotation, user, quadPoints } = annotation; - const highlight = new Dict(xref); + const highlight = oldAnnotation || new Dict(xref); highlight.set("Type", Name.get("Annot")); highlight.set("Subtype", Name.get("Highlight")); + highlight.set( + oldAnnotation ? "M" : "CreationDate", + `D:${getModificationDate()}` + ); highlight.set("CreationDate", `D:${getModificationDate()}`); highlight.set("Rect", rect); highlight.set("F", 4); diff --git a/src/display/annotation_layer.js b/src/display/annotation_layer.js index 9d8ad8dceeef2..a0b4139102c35 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -2807,7 +2807,11 @@ class InkAnnotationElement extends AnnotationElement { // Use the polyline SVG element since it allows us to use coordinates // directly and to draw both straight lines and curves. this.svgElementName = "svg:polyline"; - this.annotationEditorType = AnnotationEditorType.INK; + + this.annotationEditorType = + this.data.it === "InkHighlight" + ? AnnotationEditorType.HIGHLIGHT + : AnnotationEditorType.INK; } render() { @@ -2857,6 +2861,10 @@ class InkAnnotationElement extends AnnotationElement { } this.container.append(svg); + + if (this._isEditable) { + this._editOnDoubleClick(); + } return this.container; } @@ -2876,6 +2884,7 @@ class HighlightAnnotationElement extends AnnotationElement { ignoreBorder: true, createQuadrilaterals: true, }); + this.annotationEditorType = AnnotationEditorType.HIGHLIGHT; } render() { @@ -2884,6 +2893,8 @@ class HighlightAnnotationElement extends AnnotationElement { } this.container.classList.add("highlightAnnotation"); + this._editOnDoubleClick(); + return this.container; } } @@ -3247,6 +3258,7 @@ class AnnotationLayer { export { AnnotationLayer, FreeTextAnnotationElement, + HighlightAnnotationElement, InkAnnotationElement, StampAnnotationElement, }; diff --git a/src/display/draw_layer.js b/src/display/draw_layer.js index c5beb358933be..0b114bbe1d279 100644 --- a/src/display/draw_layer.js +++ b/src/display/draw_layer.js @@ -225,6 +225,10 @@ class DrawLayer { this.#mapping.get(id).classList.remove(className); } + getSVGRoot(id) { + return this.#mapping.get(id); + } + remove(id) { if (this.#parent === null) { return; diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 75a190809578e..5bd3e778c7dd4 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -323,8 +323,10 @@ class AnnotationEditorLayer { editor = changedAnnotations.get(id); if (editor) { this.#uiManager.addChangedExistingAnnotation(editor); - editor.renderAnnotationElement(editable); - editor.show(false); + if (editor.renderAnnotationElement(editable)) { + // Content has changed, so we need to hide the editor. + editor.show(false); + } } editable.show(); } diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 596374b782496..abafe48025701 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -1367,6 +1367,7 @@ class AnnotationEditor { data.rect, pageHeight ); + editor.x = x / pageWidth; editor.y = y / pageHeight; editor.width = width / pageWidth; @@ -1765,7 +1766,7 @@ class AnnotationEditor { /** * Render an annotation in the annotation layer. * @param {Object} annotation - * @returns {HTMLElement} + * @returns {HTMLElement|null} */ renderAnnotationElement(annotation) { let content = annotation.container.querySelector(".annotationContent"); diff --git a/src/display/editor/highlight.js b/src/display/editor/highlight.js index 154777e352db6..0afcc6ad728bc 100644 --- a/src/display/editor/highlight.js +++ b/src/display/editor/highlight.js @@ -21,6 +21,10 @@ import { } from "../../shared/util.js"; import { bindEvents, KeyboardManager } from "./tools.js"; import { FreeOutliner, Outliner } from "./outliner.js"; +import { + HighlightAnnotationElement, + InkAnnotationElement, +} from "../annotation_layer.js"; import { AnnotationEditor } from "./editor.js"; import { ColorPicker } from "./color_picker.js"; import { noContextMenu } from "../display_utils.js"; @@ -51,6 +55,8 @@ class HighlightEditor extends AnnotationEditor { #id = null; + #initialData = null; + #isFreeHighlight = false; #lastPoint = null; @@ -111,7 +117,7 @@ class HighlightEditor extends AnnotationEditor { this.#isFreeHighlight = true; this.#createFreeOutlines(params); this.#addToDrawLayer(); - } else { + } else if (this.#boxes) { this.#anchorNode = params.anchorNode; this.#anchorOffset = params.anchorOffset; this.#focusNode = params.focusNode; @@ -316,15 +322,22 @@ class HighlightEditor extends AnnotationEditor { * @param {string} color */ #updateColor(color) { - const setColor = col => { + const setColorAndOpacity = (col, opa) => { this.color = col; this.parent?.drawLayer.changeColor(this.#id, col); this.#colorPicker?.updateColor(col); + this.#opacity = opa; + this.parent?.drawLayer.changeOpacity(this.#id, opa); }; const savedColor = this.color; + const savedOpacity = this.#opacity; this.addCommands({ - cmd: setColor.bind(this, color), - undo: setColor.bind(this, savedColor), + cmd: setColorAndOpacity.bind( + this, + color, + HighlightEditor._defaultOpacity + ), + undo: setColorAndOpacity.bind(this, savedColor, savedOpacity), post: this._uiManager.updateUI.bind(this._uiManager, this), mustExec: true, type: AnnotationEditorParamsType.HIGHLIGHT_COLOR, @@ -410,7 +423,9 @@ class HighlightEditor extends AnnotationEditor { /** @inheritdoc */ onceAdded() { - this.parent.addUndoableEditor(this); + if (!this.annotationElementId) { + this.parent.addUndoableEditor(this); + } this.div.focus(); } @@ -769,29 +784,114 @@ class HighlightEditor extends AnnotationEditor { /** @inheritdoc */ static deserialize(data, parent, uiManager) { + let initialData = null; + if (data instanceof HighlightAnnotationElement) { + const { + data: { quadPoints, rect, rotation, id, color, opacity }, + parent: { + page: { pageNumber }, + }, + } = data; + initialData = data = { + annotationType: AnnotationEditorType.HIGHLIGHT, + color: Array.from(color), + opacity, + quadPoints, + boxes: null, + pageIndex: pageNumber - 1, + rect: rect.slice(0), + rotation, + id, + deleted: false, + }; + } else if (data instanceof InkAnnotationElement) { + const { + data: { + inkLists, + rect, + rotation, + id, + color, + borderStyle: { rawWidth: thickness }, + }, + parent: { + page: { pageNumber }, + }, + } = data; + initialData = data = { + annotationType: AnnotationEditorType.HIGHLIGHT, + color: Array.from(color), + thickness, + inkLists, + boxes: null, + pageIndex: pageNumber - 1, + rect: rect.slice(0), + rotation, + id, + deleted: false, + }; + } + + const { color, quadPoints, inkLists, opacity } = data; const editor = super.deserialize(data, parent, uiManager); - const { - rect: [blX, blY, trX, trY], - color, - quadPoints, - } = data; editor.color = Util.makeHexColor(...color); - editor.#opacity = data.opacity; + editor.#opacity = opacity || 1; + if (inkLists) { + editor.#thickness = data.thickness; + } + editor.annotationElementId = data.id || null; + editor.#initialData = initialData; const [pageWidth, pageHeight] = editor.pageDimensions; - editor.width = (trX - blX) / pageWidth; - editor.height = (trY - blY) / pageHeight; - const boxes = (editor.#boxes = []); - for (let i = 0; i < quadPoints.length; i += 8) { - boxes.push({ - x: (quadPoints[4] - trX) / pageWidth, - y: (trY - (1 - quadPoints[i + 5])) / pageHeight, - width: (quadPoints[i + 2] - quadPoints[i]) / pageWidth, - height: (quadPoints[i + 5] - quadPoints[i + 1]) / pageHeight, + const [pageX, pageY] = editor.pageTranslation; + + if (quadPoints) { + const boxes = (editor.#boxes = []); + for (let i = 0; i < quadPoints.length; i += 8) { + boxes.push({ + x: (quadPoints[i] - pageX) / pageWidth, + y: 1 - (quadPoints[i + 1] - pageY) / pageHeight, + width: (quadPoints[i + 2] - quadPoints[i]) / pageWidth, + height: (quadPoints[i + 1] - quadPoints[i + 5]) / pageHeight, + }); + } + editor.#createOutlines(); + editor.#addToDrawLayer(); + editor.rotate(editor.rotation); + } else if (inkLists) { + editor.#isFreeHighlight = true; + const points = inkLists[0]; + const point = { + x: points[0] - pageX, + y: pageHeight - (points[1] - pageY), + }; + const outliner = new FreeOutliner( + point, + [0, 0, pageWidth, pageHeight], + 1, + editor.#thickness / 2, + true, + 0.001 + ); + for (let i = 0, ii = points.length; i < ii; i += 2) { + point.x = points[i] - pageX; + point.y = pageHeight - (points[i + 1] - pageY); + outliner.add(point); + } + const { id, clipPathId } = parent.drawLayer.highlight( + outliner, + editor.color, + editor._defaultOpacity, + /* isPathUpdatable = */ true + ); + editor.#createFreeOutlines({ + highlightOutlines: outliner.getOutlines(), + highlightId: id, + clipPathId, }); + editor.#addToDrawLayer(); } - editor.#createOutlines(); return editor; } @@ -803,10 +903,18 @@ class HighlightEditor extends AnnotationEditor { return null; } + if (this.deleted) { + return { + pageIndex: this.pageIndex, + id: this.annotationElementId, + deleted: true, + }; + } + const rect = this.getRect(0, 0); const color = AnnotationEditor._colorManager.convert(this.color); - return { + const serialized = { annotationType: AnnotationEditorType.HIGHLIGHT, color, opacity: this.#opacity, @@ -818,6 +926,27 @@ class HighlightEditor extends AnnotationEditor { rotation: this.#getRotation(), structTreeParentId: this._structTreeParentId, }; + + if (this.annotationElementId && !this.#hasElementChanged(serialized)) { + return null; + } + + serialized.id = this.annotationElementId; + return serialized; + } + + #hasElementChanged(serialized) { + const { color } = this.#initialData; + return serialized.color.some((c, i) => c !== color[i]); + } + + /** @inheritdoc */ + renderAnnotationElement(annotation) { + annotation.updateEdited({ + rect: this.getRect(0, 0), + }); + + return null; } static canCreateNewEmptyEditor() { diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index e793ec825b198..e4fc2eee4d89f 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -32,6 +32,9 @@ import { scrollIntoView, setCaretAt, switchToEditor, + waitAndClick, + waitForAnnotationModeChanged, + waitForSelectedEditor, waitForSerialized, } from "./test_utils.mjs"; @@ -1921,4 +1924,100 @@ describe("Highlight Editor", () => { ); }); }); + + describe("Highlight (edit existing in double clicking on it)", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait( + "highlights.pdf", + ".annotationEditorLayer", + null, + null, + { + highlightEditorColors: + "yellow=#FFFF00,green=#00FF00,blue=#0000FF,pink=#FF00FF,red=#FF0102", + } + ); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must change the color of an highlight", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const modeChangedHandle = await waitForAnnotationModeChanged(page); + await waitAndClick(page, "[data-annotation-id='687R']", { count: 2 }); + await awaitPromise(modeChangedHandle); + await page.waitForSelector("#highlightParamsToolbarContainer"); + + const editorSelector = getEditorSelector(5); + await page.waitForSelector(editorSelector); + + await waitAndClick( + page, + `${editorSelector} .editToolbar button.colorPicker` + ); + await waitAndClick( + page, + `${editorSelector} .editToolbar button[title = "Red"]` + ); + await page.waitForSelector( + `.page[data-page-number = "1"] svg.highlight[fill = "#FF0102"]` + ); + }) + ); + }); + }); + + describe("Free Highlight (edit existing in double clicking on it)", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait( + "highlights.pdf", + ".annotationEditorLayer", + null, + null, + { + highlightEditorColors: + "yellow=#FFFF00,green=#00FF00,blue=#0000FF,pink=#FF00FF,red=#FF0102", + } + ); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must change the color of a free highlight", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const modeChangedHandle = await waitForAnnotationModeChanged(page); + await page.click("[data-annotation-id='693R']", { count: 2 }); + await awaitPromise(modeChangedHandle); + await page.waitForSelector("#highlightParamsToolbarContainer"); + + const editorSelector = getEditorSelector(6); + await page.waitForSelector(editorSelector); + await page.focus(editorSelector); + await waitForSelectedEditor(page, editorSelector); + + await waitAndClick( + page, + `${editorSelector} .editToolbar button.colorPicker` + ); + await waitAndClick( + page, + `${editorSelector} .editToolbar button[title = "Red"]` + ); + await page.waitForSelector( + `.page[data-page-number = "1"] svg.highlight[fill = "#FF0102"]` + ); + }) + ); + }); + }); }); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index eb31ad44b2c69..3d14f5b92fadc 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -194,6 +194,11 @@ async function clearInput(page, selector, waitForInputEvent = false) { : action(); } +async function waitAndClick(page, selector, clickOptions = {}) { + await page.waitForSelector(selector, { visible: true }); + await page.click(selector, clickOptions); +} + function getSelector(id) { return `[data-element-id="${id}"]`; } @@ -800,6 +805,7 @@ export { serializeBitmapDimensions, setCaretAt, switchToEditor, + waitAndClick, waitForAnnotationEditorLayer, waitForAnnotationModeChanged, waitForEntryInStorage, diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 98581ffbafbc0..0ea0abe0585c2 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -662,3 +662,5 @@ !file_pdfjs_test.pdf !issue18536.pdf !issue18561.pdf +!highlights.pdf +!highlight.pdf diff --git a/test/pdfs/highlight.pdf b/test/pdfs/highlight.pdf new file mode 100755 index 0000000000000..93f32f5c27b84 Binary files /dev/null and b/test/pdfs/highlight.pdf differ diff --git a/test/pdfs/highlights.pdf b/test/pdfs/highlights.pdf new file mode 100755 index 0000000000000..09e7ec9a03ed0 Binary files /dev/null and b/test/pdfs/highlights.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 57f5a9f7aa011..c0151de5e59ae 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -10202,5 +10202,277 @@ "rounds": 1, "link": true, "type": "eq" + }, + { + "id": "highlight-update-print", + "file": "pdfs/highlight.pdf", + "md5": "74671e2d9541931a606e886114bf3efa", + "rounds": 1, + "type": "eq", + "print": true, + "annotationStorage": { + "pdfjs_internal_editor_0": { + "annotationType": 9, + "color": [83, 255, 188], + "opacity": 1, + "thickness": 12, + "quadPoints": [ + 224.95899963378906, 649.6790161132812, 257.0950012207031, + 649.6790161132812, 224.95899963378906, 665.8670043945312, + 257.0950012207031, 665.8670043945312 + ], + "outlines": [ + [ + 224.62632386, 649.4661241, 224.62632386, 666.0718055, 257.44364369, + 666.0718055, 257.44364369, 649.4661241 + ] + ], + "pageIndex": 0, + "rect": [224.62632386, 649.4661241, 257.44364369, 666.0718055], + "rotation": 0, + "structTreeParentId": null, + "id": "24R" + }, + "pdfjs_internal_editor_1": { + "annotationType": 9, + "color": [128, 235, 255], + "opacity": 1, + "thickness": 12, + "quadPoints": null, + "outlines": { + "outline": [ + null, + null, + null, + null, + 231.02000427246094, + 575.9500122070312, + 234.9244860593802, + 575.8426675799172, + 235.67976763594078, + 575.8027971554493, + 236.50453905085675, + 575.765994157359, + 237.32931046577278, + 575.7291911592689, + 238.15139102232476, + 575.6967454939373, + 238.97078072051278, + 575.6686571613642, + null, + null, + null, + null, + 240.19986526779482, + 575.6265246625046, + null, + null, + null, + null, + 248.19520403340397, + 575.3534715117306, + null, + null, + null, + null, + 248.6047837595648, + 587.3464796601443, + null, + null, + null, + null, + 240.60944499395563, + 587.6195328109183, + 238.6632878655088, + 587.6857348021831, + 237.88825461323634, + 587.7160188758909, + 237.11665096007022, + 587.7501062268005, + 236.3450473069041, + 587.7841935777101, + 235.50103901924535, + 587.8260330788092, + 234.5846260970941, + 587.875624730098, + null, + null, + null, + null, + 231.02000427246094, + 587.9500122070312 + ], + "points": [ + [ + 231.02000427246094, 581.9500122070312, 241.21000671386722, + 581.9500122070312, 243.60000610351562, 581.3499755859375, 246, + 581.3499755859375, 248.39999389648438, 581.3499755859375, + 248.39999389648438, 581.3499755859375 + ] + ] + }, + "pageIndex": 0, + "rect": [ + 230.69150427246095, 575.1494715117307, 248.9332837595648, + 588.1540122070312 + ], + "rotation": 0, + "structTreeParentId": null, + "id": "41R" + } + } + }, + { + "id": "highlight-update-save-print", + "file": "pdfs/highlight.pdf", + "md5": "74671e2d9541931a606e886114bf3efa", + "rounds": 1, + "type": "eq", + "save": true, + "print": true, + "annotationStorage": { + "pdfjs_internal_editor_0": { + "annotationType": 9, + "color": [83, 255, 188], + "opacity": 1, + "thickness": 12, + "quadPoints": [ + 224.95899963378906, 649.6790161132812, 257.0950012207031, + 649.6790161132812, 224.95899963378906, 665.8670043945312, + 257.0950012207031, 665.8670043945312 + ], + "outlines": [ + [ + 224.62632386, 649.4661241, 224.62632386, 666.0718055, 257.44364369, + 666.0718055, 257.44364369, 649.4661241 + ] + ], + "pageIndex": 0, + "rect": [224.62632386, 649.4661241, 257.44364369, 666.0718055], + "rotation": 0, + "structTreeParentId": null, + "id": "24R" + }, + "pdfjs_internal_editor_1": { + "annotationType": 9, + "color": [128, 235, 255], + "opacity": 1, + "thickness": 12, + "quadPoints": null, + "outlines": { + "outline": [ + null, + null, + null, + null, + 231.02000427246094, + 575.9500122070312, + 234.9244860593802, + 575.8426675799172, + 235.67976763594078, + 575.8027971554493, + 236.50453905085675, + 575.765994157359, + 237.32931046577278, + 575.7291911592689, + 238.15139102232476, + 575.6967454939373, + 238.97078072051278, + 575.6686571613642, + null, + null, + null, + null, + 240.19986526779482, + 575.6265246625046, + null, + null, + null, + null, + 248.19520403340397, + 575.3534715117306, + null, + null, + null, + null, + 248.6047837595648, + 587.3464796601443, + null, + null, + null, + null, + 240.60944499395563, + 587.6195328109183, + 238.6632878655088, + 587.6857348021831, + 237.88825461323634, + 587.7160188758909, + 237.11665096007022, + 587.7501062268005, + 236.3450473069041, + 587.7841935777101, + 235.50103901924535, + 587.8260330788092, + 234.5846260970941, + 587.875624730098, + null, + null, + null, + null, + 231.02000427246094, + 587.9500122070312 + ], + "points": [ + [ + 231.02000427246094, 581.9500122070312, 241.21000671386722, + 581.9500122070312, 243.60000610351562, 581.3499755859375, 246, + 581.3499755859375, 248.39999389648438, 581.3499755859375, + 248.39999389648438, 581.3499755859375 + ] + ] + }, + "pageIndex": 0, + "rect": [ + 230.69150427246095, 575.1494715117307, 248.9332837595648, + 588.1540122070312 + ], + "rotation": 0, + "structTreeParentId": null, + "id": "41R" + } + } + }, + { + "id": "highlight-delete-print", + "file": "pdfs/highlight.pdf", + "md5": "74671e2d9541931a606e886114bf3efa", + "rounds": 1, + "type": "eq", + "print": true, + "annotationStorage": { + "pdfjs_internal_editor_0": { + "annotationType": 9, + "id": "24R", + "deleted": true, + "pageIndex": 0 + } + } + }, + { + "id": "highlight-delete-save-print", + "file": "pdfs/highlight.pdf", + "md5": "74671e2d9541931a606e886114bf3efa", + "rounds": 1, + "type": "eq", + "print": true, + "save": true, + "annotationStorage": { + "pdfjs_internal_editor_1": { + "annotationType": 9, + "id": "41R", + "deleted": true, + "pageIndex": 0 + } + } } ]