diff --git a/l10n/en-US/viewer.properties b/l10n/en-US/viewer.properties index c820d8068..db404c896 100644 --- a/l10n/en-US/viewer.properties +++ b/l10n/en-US/viewer.properties @@ -245,8 +245,8 @@ editor_free_text2.title=Text editor_free_text2_label=Text editor_ink2.title=Draw editor_ink2_label=Draw -editor_stamp.title=Add an image -editor_stamp_label=Add an image +editor_stamp1.title=Add or edit images +editor_stamp1_label=Add or edit images free_text2_default_content=Start typing… @@ -256,6 +256,8 @@ editor_free_text_size=Size editor_ink_color=Color editor_ink_thickness=Thickness editor_ink_opacity=Opacity +editor_stamp_add_image_label=Add image +editor_stamp_add_image.title=Add image # Editor aria editor_free_text2_aria_label=Text Editor diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 7c4121440..1cb53569e 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -166,7 +166,10 @@ class AnnotationEditorLayer { } } - const editor = this.#createAndAddNewEditor({ offsetX: 0, offsetY: 0 }); + const editor = this.#createAndAddNewEditor( + { offsetX: 0, offsetY: 0 }, + /* isCentered = */ false + ); editor.setInBackground(); } @@ -481,9 +484,10 @@ class AnnotationEditorLayer { /** * Create and add a new editor. * @param {PointerEvent} event + * @param {boolean} isCentered * @returns {AnnotationEditor} */ - #createAndAddNewEditor(event) { + #createAndAddNewEditor(event, isCentered) { const id = this.getNextId(); const editor = this.#createNewEditor({ parent: this, @@ -491,6 +495,7 @@ class AnnotationEditorLayer { x: event.offsetX, y: event.offsetY, uiManager: this.#uiManager, + isCentered, }); if (editor) { this.add(editor); @@ -499,6 +504,31 @@ class AnnotationEditorLayer { return editor; } + /** + * Create and add a new editor. + */ + addNewEditor() { + const { x, y, width, height } = this.div.getBoundingClientRect(); + const tlX = Math.max(0, x); + const tlY = Math.max(0, y); + const brX = Math.min(window.innerWidth, x + width); + const brY = Math.min(window.innerHeight, y + height); + const centerX = (tlX + brX) / 2 - x; + const centerY = (tlY + brY) / 2 - y; + const [offsetX, offsetY] = + this.viewport.rotation % 180 === 0 + ? [centerX, centerY] + : [centerY, centerX]; + + this.#createAndAddNewEditor( + { + offsetX, + offsetY, + }, + /* isCentered = */ true + ); + } + /** * Set the last selected editor. * @param {AnnotationEditor} editor @@ -560,7 +590,12 @@ class AnnotationEditorLayer { return; } - this.#createAndAddNewEditor(event); + if (this.#uiManager.getMode() === AnnotationEditorType.STAMP) { + this.#uiManager.unselectAll(); + return; + } + + this.#createAndAddNewEditor(event, /* isCentered = */ false); } /** diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 3c3cae425..45895b26a 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -48,6 +48,8 @@ class AnnotationEditor { #isInEditMode = false; + _initialOptions = Object.create(null); + _uiManager = null; _focusEventsAllowed = true; @@ -77,6 +79,7 @@ class AnnotationEditor { this._uiManager = parameters.uiManager; this.annotationElementId = null; this._willKeepAspectRatio = false; + this._initialOptions.isCentered = parameters.isCentered; const { rotation, @@ -154,6 +157,29 @@ class AnnotationEditor { this.div?.classList.toggle("draggable", value); } + center() { + const [pageWidth, pageHeight] = this.pageDimensions; + switch (this.parentRotation) { + case 90: + this.x -= (this.height * pageHeight) / (pageWidth * 2); + this.y += (this.width * pageWidth) / (pageHeight * 2); + break; + case 180: + this.x += this.width / 2; + this.y += this.height / 2; + break; + case 270: + this.x += (this.height * pageHeight) / (pageWidth * 2); + this.y -= (this.width * pageWidth) / (pageHeight * 2); + break; + default: + this.x -= this.width / 2; + this.y -= this.height / 2; + break; + } + this.fixAndSetPosition(); + } + /** * Add some commands into the CommandManager (undo/redo stuff). * @param {Object} params diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index 08dd5f362..7e96d518c 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -363,6 +363,10 @@ class FreeTextEditor extends AnnotationEditor { } this.enableEditMode(); this.editorDiv.focus(); + if (this._initialOptions?.isCentered) { + this.center(); + } + this._initialOptions = null; } /** @inheritdoc */ diff --git a/src/display/editor/stamp.js b/src/display/editor/stamp.js index a9a55eaa2..d121724ed 100644 --- a/src/display/editor/stamp.js +++ b/src/display/editor/stamp.js @@ -290,7 +290,12 @@ class StampEditor extends AnnotationEditor { this.width = width / parentWidth; this.height = height / parentHeight; this.setDims(width, height); - this.fixAndSetPosition(); + if (this._initialOptions?.isCentered) { + this.center(); + } else { + this.fixAndSetPosition(); + } + this._initialOptions = null; if (this.#resizeTimeoutId !== null) { clearTimeout(this.#resizeTimeoutId); } diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 380c12e69..7d041412e 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -18,6 +18,7 @@ /** @typedef {import("./annotation_editor_layer.js").AnnotationEditorLayer} AnnotationEditorLayer */ import { + AnnotationEditorParamsType, AnnotationEditorPrefix, AnnotationEditorType, FeatureTest, @@ -1144,6 +1145,10 @@ class AnnotationEditorUIManager { if (!this.#editorTypes) { return; } + if (type === AnnotationEditorParamsType.CREATE) { + this.currentLayer.addNewEditor(type); + return; + } for (const editor of this.#selectedEditors) { editor.updateParams(type, value); diff --git a/src/shared/util.js b/src/shared/util.js index 607fd2508..adce62c1d 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -78,6 +78,7 @@ const AnnotationEditorType = { const AnnotationEditorParamsType = { RESIZE: 1, + CREATE: 2, FREETEXT_SIZE: 11, FREETEXT_COLOR: 12, FREETEXT_OPACITY: 13, diff --git a/test/integration/stamp_editor_spec.js b/test/integration/stamp_editor_spec.js index 6fc3bfa44..228a2019f 100644 --- a/test/integration/stamp_editor_spec.js +++ b/test/integration/stamp_editor_spec.js @@ -15,7 +15,9 @@ const { closePages, + dragAndDropAnnotation, getEditorDimensions, + getEditorSelector, loadAndWait, serializeBitmapDimensions, waitForAnnotationEditorLayer, @@ -43,15 +45,8 @@ describe("Stamp Editor", () => { } await page.click("#editorStamp"); + await page.click("#editorStampAddImage"); - const rect = await page.$eval(".annotationEditorLayer", el => { - // With Chrome something is wrong when serializing a DomRect, - // hence we extract the values and just return them. - const { x, y } = el.getBoundingClientRect(); - return { x, y }; - }); - - await page.mouse.click(rect.x + 100, rect.y + 100); const input = await page.$("#stampEditorFileInput"); await input.uploadFile( `${path.join(__dirname, "../images/firefox_logo.png")}` @@ -87,14 +82,7 @@ describe("Stamp Editor", () => { return; } - const rect = await page.$eval(".annotationEditorLayer", el => { - // With Chrome something is wrong when serializing a DomRect, - // hence we extract the values and just return them. - const { x, y } = el.getBoundingClientRect(); - return { x, y }; - }); - - await page.mouse.click(rect.x + 100, rect.y + 100); + await page.click("#editorStampAddImage"); const input = await page.$("#stampEditorFileInput"); await input.uploadFile( `${path.join(__dirname, "../images/firefox_logo.svg")}` @@ -146,6 +134,7 @@ describe("Stamp Editor", () => { } await page.click("#editorStamp"); + await page.click("#editorStampAddImage"); const rect = await page.$eval(".annotationEditorLayer", el => { // With Chrome something is wrong when serializing a DomRect, @@ -154,7 +143,13 @@ describe("Stamp Editor", () => { return { x: right, y: bottom }; }); - await page.mouse.click(rect.x - 10, rect.y - 10); + const editorRect = await page.$eval(getEditorSelector(0), el => { + // With Chrome something is wrong when serializing a DomRect, + // hence we extract the values and just return them. + const { x, y } = el.getBoundingClientRect(); + return { x, y }; + }); + const input = await page.$("#stampEditorFileInput"); await input.uploadFile( `${path.join(__dirname, "../images/firefox_logo.png")}` @@ -162,6 +157,15 @@ describe("Stamp Editor", () => { await page.waitForTimeout(300); + await dragAndDropAnnotation( + page, + editorRect.x + 10, + editorRect.y + 10, + rect.x - 10, + rect.y - 10 + ); + await page.waitForTimeout(10); + const { left } = await getEditorDimensions(page, 0); // The image is bigger than the page, so it has been scaled down to @@ -204,14 +208,7 @@ describe("Stamp Editor", () => { await page.waitForTimeout(10); } - const rect = await page.$eval(".annotationEditorLayer", el => { - // With Chrome something is wrong when serializing a DomRect, - // hence we extract the values and just return them. - const { x, y } = el.getBoundingClientRect(); - return { x, y }; - }); - - await page.mouse.click(rect.x + 10, rect.y + 10); + await page.click("#editorStampAddImage"); await page.waitForTimeout(10); const input = await page.$("#stampEditorFileInput"); await input.uploadFile( diff --git a/web/annotation_editor_params.js b/web/annotation_editor_params.js index 60e3560f6..ddfc17d1a 100644 --- a/web/annotation_editor_params.js +++ b/web/annotation_editor_params.js @@ -31,6 +31,7 @@ class AnnotationEditorParams { editorInkColor, editorInkThickness, editorInkOpacity, + editorStampAddImage, }) { const dispatchEvent = (typeStr, value) => { this.eventBus.dispatch("switchannotationeditorparams", { @@ -54,6 +55,9 @@ class AnnotationEditorParams { editorInkOpacity.addEventListener("input", function () { dispatchEvent("INK_OPACITY", this.valueAsNumber); }); + editorStampAddImage.addEventListener("click", () => { + dispatchEvent("CREATE"); + }); this.eventBus._on("annotationeditorparamschanged", evt => { for (const [type, value] of evt.details) { diff --git a/web/toolbar.js b/web/toolbar.js index 826f59733..051fabecc 100644 --- a/web/toolbar.js +++ b/web/toolbar.js @@ -218,6 +218,7 @@ class Toolbar { editorInkButton, editorInkParamsToolbar, editorStampButton, + editorStampParamsToolbar, }) { const editorModeChanged = ({ mode }) => { toggleCheckedBtn( @@ -230,7 +231,11 @@ class Toolbar { mode === AnnotationEditorType.INK, editorInkParamsToolbar ); - toggleCheckedBtn(editorStampButton, mode === AnnotationEditorType.STAMP); + toggleCheckedBtn( + editorStampButton, + mode === AnnotationEditorType.STAMP, + editorStampParamsToolbar + ); const isDisable = mode === AnnotationEditorType.DISABLE; editorFreeTextButton.disabled = isDisable; diff --git a/web/viewer.css b/web/viewer.css index 570d6c186..0caed2f1e 100644 --- a/web/viewer.css +++ b/web/viewer.css @@ -118,6 +118,7 @@ --secondaryToolbarButton-spreadOdd-icon: url(images/secondaryToolbarButton-spreadOdd.svg); --secondaryToolbarButton-spreadEven-icon: url(images/secondaryToolbarButton-spreadEven.svg); --secondaryToolbarButton-documentProperties-icon: url(images/secondaryToolbarButton-documentProperties.svg); + --editorParams-stampAddImage-icon: url(images/toolbarButton-zoomIn.svg); } :root:dir(rtl) { @@ -576,6 +577,11 @@ body { margin-bottom: -4px; } +#editorStampParamsToolbar { + inset-inline-end: 40px; + background-color: var(--toolbar-bg-color); +} + #editorInkParamsToolbar { inset-inline-end: 68px; background-color: var(--toolbar-bg-color); @@ -586,6 +592,10 @@ body { background-color: var(--toolbar-bg-color); } +#editorStampAddImage::before { + mask-image: var(--editorParams-stampAddImage-icon); +} + .doorHanger, .doorHangerRight { border-radius: 2px; diff --git a/web/viewer.html b/web/viewer.html index 09a5726bc..cfb73d529 100644 --- a/web/viewer.html +++ b/web/viewer.html @@ -198,6 +198,14 @@ See https://github.com/adobe-type-tools/cmap-resources +
+