diff --git a/src/core/annotation.js b/src/core/annotation.js index 95028cb5e456bd..78c63c0bf2f8b3 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) { + 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.thickness = 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.thickness = width; const maxWidth = (rect[2] - rect[0]) / 2; const maxHeight = (rect[3] - rect[1]) / 2; @@ -4283,6 +4290,8 @@ 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"; + this.data.noHTML = false; const rawInkLists = dict.getArray("InkList"); if (!Array.isArray(rawInkLists)) { @@ -4534,6 +4543,9 @@ 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; const quadPoints = (this.data.quadPoints = getQuadPoints(dict, null)); if (quadPoints) { @@ -4573,11 +4585,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 dff737b6ca6d72..72f402e0240b7a 100644 --- a/src/display/annotation_layer.js +++ b/src/display/annotation_layer.js @@ -2810,7 +2810,15 @@ 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; + } + + get _isEditable() { + return this.data.isEditable; } render() { @@ -2860,6 +2868,10 @@ class InkAnnotationElement extends AnnotationElement { } this.container.append(svg); + + if (this._isEditable) { + this._editOnDoubleClick(); + } return this.container; } @@ -2879,6 +2891,7 @@ class HighlightAnnotationElement extends AnnotationElement { ignoreBorder: true, createQuadrilaterals: true, }); + this.annotationEditorType = AnnotationEditorType.HIGHLIGHT; } render() { @@ -2887,6 +2900,8 @@ class HighlightAnnotationElement extends AnnotationElement { } this.container.classList.add("highlightAnnotation"); + this._editOnDoubleClick(); + return this.container; } } @@ -3250,6 +3265,7 @@ class AnnotationLayer { export { AnnotationLayer, FreeTextAnnotationElement, + HighlightAnnotationElement, InkAnnotationElement, StampAnnotationElement, }; diff --git a/src/display/draw_layer.js b/src/display/draw_layer.js index c5beb358933beb..0b114bbe1d2796 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 52d76d35609463..54ab234352d1a7 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -325,8 +325,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 c0c0127008e7bb..971a16a9e24e9b 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -1377,6 +1377,7 @@ class AnnotationEditor { data.rect, pageHeight ); + editor.x = x / pageWidth; editor.y = y / pageHeight; editor.width = width / pageWidth; @@ -1779,7 +1780,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 36ca75cc51463e..d89a418e59c2d8 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; #boundKeydown = this.#keydown.bind(this); @@ -113,7 +119,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; @@ -412,7 +418,9 @@ class HighlightEditor extends AnnotationEditor { /** @inheritdoc */ onceAdded() { - this.parent.addUndoableEditor(this); + if (!this.annotationElementId) { + this.parent.addUndoableEditor(this); + } this.div.focus(); } @@ -772,29 +780,112 @@ class HighlightEditor extends AnnotationEditor { /** @inheritdoc */ static deserialize(data, parent, uiManager) { + let initialData = null; + if (data instanceof HighlightAnnotationElement) { + const { + data: { quadPoints, rect, rotation, id, color }, + parent: { + page: { pageNumber }, + }, + } = data; + initialData = data = { + annotationType: AnnotationEditorType.HIGHLIGHT, + color: Array.from(color), + 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: { 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 } = 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; + 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; } @@ -806,10 +897,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, @@ -821,6 +920,28 @@ class HighlightEditor extends AnnotationEditor { rotation: this.#getRotation(), structTreeParentId: this._structTreeParentId, }; + + if (this.annotationElementId && !this.#hasElementChanged(serialized)) { + return null; + } + + serialized.id = this.annotationElementId; + console.log(serialized); + 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 c61663e0330f42..01ac3195cbcc25 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -32,6 +32,7 @@ import { scrollIntoView, setCaretAt, switchToEditor, + waitForAnnotationModeChanged, waitForSerialized, } from "./test_utils.mjs"; @@ -1932,4 +1933,98 @@ 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 page.click("[data-annotation-id='687R']", { count: 2 }); + await awaitPromise(modeChangedHandle); + const editorSelector = getEditorSelector(5); + await page.waitForSelector(editorSelector); + + await page.waitForSelector( + `${editorSelector} .editToolbar button.colorPicker` + ); + await page.click(`${editorSelector} .editToolbar button.colorPicker`); + await page.waitForSelector( + `${editorSelector} .editToolbar button[title = "Red"]` + ); + await page.click( + `${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='690R']", { count: 2 }); + await awaitPromise(modeChangedHandle); + const editorSelector = getEditorSelector(6); + await page.waitForSelector(editorSelector); + + await page.waitForSelector( + `${editorSelector} .editToolbar button.colorPicker` + ); + await page.click(`${editorSelector} .editToolbar button.colorPicker`); + await page.waitForSelector( + `${editorSelector} .editToolbar button[title = "Red"]` + ); + await page.click( + `${editorSelector} .editToolbar button[title = "Red"]` + ); + await page.waitForSelector( + `.page[data-page-number = "1"] svg.highlight[fill = "#FF0102"]` + ); + }) + ); + }); + }); }); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index b353f8d31926dc..b03c377fe05c76 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -660,3 +660,5 @@ !issue18099_reduced.pdf !file_pdfjs_test.pdf !issue18536.pdf +!highlights.pdf +!highlight.pdf diff --git a/test/pdfs/highlight.pdf b/test/pdfs/highlight.pdf new file mode 100755 index 00000000000000..93f32f5c27b84c 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 00000000000000..09e7ec9a03ed0a Binary files /dev/null and b/test/pdfs/highlights.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 8845acb9923113..10fb74ec234a55 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -10186,5 +10186,275 @@ "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 + } + } + }, + { + "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 + } + } } ]