Merge pull request #19201 from calixteman/fix_resize_stamp

[Editor] When resizing a stamp annotation, the opposite corner must stay fixed
This commit is contained in:
calixteman 2024-12-10 11:00:01 +01:00 committed by GitHub
commit 44421d36f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 139 additions and 125 deletions

View File

@ -40,8 +40,6 @@ class StampEditor extends AnnotationEditor {
#canvas = null; #canvas = null;
#observer = null;
#resizeTimeoutId = null; #resizeTimeoutId = null;
#isSvg = false; #isSvg = false;
@ -305,8 +303,6 @@ class StampEditor extends AnnotationEditor {
this._uiManager.imageManager.deleteId(this.#bitmapId); this._uiManager.imageManager.deleteId(this.#bitmapId);
this.#canvas?.remove(); this.#canvas?.remove();
this.#canvas = null; this.#canvas = null;
this.#observer?.disconnect();
this.#observer = null;
if (this.#resizeTimeoutId) { if (this.#resizeTimeoutId) {
clearTimeout(this.#resizeTimeoutId); clearTimeout(this.#resizeTimeoutId);
this.#resizeTimeoutId = null; this.#resizeTimeoutId = null;
@ -398,9 +394,34 @@ class StampEditor extends AnnotationEditor {
); );
} }
this._uiManager.addShouldRescale(this);
return this.div; return this.div;
} }
/** @inheritdoc */
_onResized() {
// We used a CSS-zoom during the resizing, but now it's resized we can
// rescale correctly the bitmap to fit the new dimensions.
this.onScaleChanging();
}
onScaleChanging() {
if (!this.parent) {
return;
}
if (this.#resizeTimeoutId !== null) {
clearTimeout(this.#resizeTimeoutId);
}
// The user's zooming the page, there is no need to redraw the bitmap at
// each step, hence we wait a bit before redrawing it.
const TIME_TO_WAIT = 200;
this.#resizeTimeoutId = setTimeout(() => {
this.#resizeTimeoutId = null;
this.#drawBitmap();
}, TIME_TO_WAIT);
}
#createCanvas() { #createCanvas() {
const { div } = this; const { div } = this;
let { width, height } = this.#bitmap; let { width, height } = this.#bitmap;
@ -433,6 +454,15 @@ class StampEditor extends AnnotationEditor {
canvas.setAttribute("role", "img"); canvas.setAttribute("role", "img");
this.addContainer(canvas); this.addContainer(canvas);
this.width = width / pageWidth;
this.height = height / pageHeight;
if (this._initialOptions?.isCentered) {
this.center();
} else {
this.fixAndSetPosition();
}
this._initialOptions = null;
if ( if (
!this._uiManager.useNewAltTextWhenAddingImage || !this._uiManager.useNewAltTextWhenAddingImage ||
!this._uiManager.useNewAltTextFlow || !this._uiManager.useNewAltTextFlow ||
@ -440,8 +470,7 @@ class StampEditor extends AnnotationEditor {
) { ) {
div.hidden = false; div.hidden = false;
} }
this.#drawBitmap(width, height); this.#drawBitmap();
this.#createObserver();
if (!this.#hasBeenAddedInUndoStack) { if (!this.#hasBeenAddedInUndoStack) {
this.parent.addUndoableEditor(this); this.parent.addUndoableEditor(this);
this.#hasBeenAddedInUndoStack = true; this.#hasBeenAddedInUndoStack = true;
@ -584,37 +613,6 @@ class StampEditor extends AnnotationEditor {
return { canvas, width, height, imageData }; return { canvas, width, height, imageData };
} }
/**
* When the dimensions of the div change the inner canvas must
* renew its dimensions, hence it must redraw its own contents.
* @param {number} width - the new width of the div
* @param {number} height - the new height of the div
* @returns
*/
#setDimensions(width, height) {
const [parentWidth, parentHeight] = this.parentDimensions;
this.width = width / parentWidth;
this.height = height / parentHeight;
if (this._initialOptions?.isCentered) {
this.center();
} else {
this.fixAndSetPosition();
}
this._initialOptions = null;
if (this.#resizeTimeoutId !== null) {
clearTimeout(this.#resizeTimeoutId);
}
// When the user is resizing the editor we just use CSS to scale the image
// to avoid redrawing it too often.
// And once the user stops resizing the editor we redraw the image in
// rescaling it correctly (see this.#scaleBitmap).
const TIME_TO_WAIT = 200;
this.#resizeTimeoutId = setTimeout(() => {
this.#resizeTimeoutId = null;
this.#drawBitmap(width, height);
}, TIME_TO_WAIT);
}
#scaleBitmap(width, height) { #scaleBitmap(width, height) {
const { width: bitmapWidth, height: bitmapHeight } = this.#bitmap; const { width: bitmapWidth, height: bitmapHeight } = this.#bitmap;
@ -660,18 +658,21 @@ class StampEditor extends AnnotationEditor {
return bitmap; return bitmap;
} }
#drawBitmap(width, height) { #drawBitmap() {
const [parentWidth, parentHeight] = this.parentDimensions;
const { width, height } = this;
const outputScale = new OutputScale(); const outputScale = new OutputScale();
const scaledWidth = Math.ceil(width * outputScale.sx); const scaledWidth = Math.ceil(width * parentWidth * outputScale.sx);
const scaledHeight = Math.ceil(height * outputScale.sy); const scaledHeight = Math.ceil(height * parentHeight * outputScale.sy);
const canvas = this.#canvas; const canvas = this.#canvas;
if ( if (
!canvas || !canvas ||
(canvas.width === scaledWidth && canvas.height === scaledHeight) (canvas.width === scaledWidth && canvas.height === scaledHeight)
) { ) {
return; return;
} }
canvas.width = scaledWidth; canvas.width = scaledWidth;
canvas.height = scaledHeight; canvas.height = scaledHeight;
@ -746,32 +747,6 @@ class StampEditor extends AnnotationEditor {
return structuredClone(this.#bitmap); return structuredClone(this.#bitmap);
} }
/**
* Create the resize observer.
*/
#createObserver() {
if (!this._uiManager._signal) {
// This method is called after the canvas has been created but the canvas
// creation is async, so it's possible that the viewer has been closed.
return;
}
this.#observer = new ResizeObserver(entries => {
const rect = entries[0].contentRect;
if (rect.width && rect.height) {
this.#setDimensions(rect.width, rect.height);
}
});
this.#observer.observe(this.div);
this._uiManager._signal.addEventListener(
"abort",
() => {
this.#observer?.disconnect();
this.#observer = null;
},
{ once: true }
);
}
/** @inheritdoc */ /** @inheritdoc */
static async deserialize(data, parent, uiManager) { static async deserialize(data, parent, uiManager) {
let initialData = null; let initialData = null;

View File

@ -19,7 +19,7 @@ import {
copy, copy,
copyToClipboard, copyToClipboard,
createPromise, createPromise,
dragAndDropAnnotation, dragAndDrop,
firstPageOnTop, firstPageOnTop,
getEditors, getEditors,
getEditorSelector, getEditorSelector,
@ -954,19 +954,14 @@ describe("FreeText Editor", () => {
const serialized = await getSerialized(page); const serialized = await getSerialized(page);
expect(serialized).withContext(`In ${browserName}`).toEqual([]); expect(serialized).withContext(`In ${browserName}`).toEqual([]);
const editorRect = await getRect(page, getEditorSelector(0)); const editorSelector = getEditorSelector(0);
const editorRect = await getRect(page, editorSelector);
// Select the annotation we want to move. // Select the annotation we want to move.
await page.mouse.click(editorRect.x + 2, editorRect.y + 2); await page.mouse.click(editorRect.x + 2, editorRect.y + 2);
await waitForSelectedEditor(page, getEditorSelector(0)); await waitForSelectedEditor(page, editorSelector);
await dragAndDropAnnotation( await dragAndDrop(page, editorSelector, [[100, 100]]);
page,
editorRect.x + editorRect.width / 2,
editorRect.y + editorRect.height / 2,
100,
100
);
await waitForSerialized(page, 1); await waitForSerialized(page, 1);
}) })
); );
@ -2335,29 +2330,29 @@ describe("FreeText Editor", () => {
const allPositions = []; const allPositions = [];
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const editorSelector = getEditorSelector(i);
await page.mouse.click(rect.x + 10 + 30 * i, rect.y + 100 + 5 * i); await page.mouse.click(rect.x + 10 + 30 * i, rect.y + 100 + 5 * i);
await page.waitForSelector(getEditorSelector(i), { await page.waitForSelector(editorSelector, {
visible: true, visible: true,
}); });
await page.type( await page.type(
`${getEditorSelector(i)} .internal`, `${editorSelector} .internal`,
String.fromCharCode(65 + i) String.fromCharCode(65 + i)
); );
// Commit. // Commit.
await page.keyboard.press("Escape"); await page.keyboard.press("Escape");
await page.waitForSelector( await page.waitForSelector(`${editorSelector} .overlay.enabled`);
`${getEditorSelector(i)} .overlay.enabled`
);
allPositions.push(await getRect(page, getEditorSelector(i))); allPositions.push(await getRect(page, editorSelector));
} }
await selectAll(page); await selectAll(page);
await dragAndDropAnnotation(page, rect.x + 161, rect.y + 126, 39, 74); await dragAndDrop(page, getEditorSelector(4), [[39, 74]]);
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const pos = await getRect(page, getEditorSelector(i)); const editorSelector = getEditorSelector(i);
const pos = await getRect(page, editorSelector);
const oldPos = allPositions[i]; const oldPos = allPositions[i];
expect(Math.abs(Math.round(pos.x - oldPos.x) - 39)) expect(Math.abs(Math.round(pos.x - oldPos.x) - 39))
.withContext(`In ${browserName}`) .withContext(`In ${browserName}`)

View File

@ -17,7 +17,7 @@ import {
awaitPromise, awaitPromise,
closePages, closePages,
createPromise, createPromise,
dragAndDropAnnotation, dragAndDrop,
getAnnotationSelector, getAnnotationSelector,
getEditors, getEditors,
getEditorSelector, getEditorSelector,
@ -822,13 +822,7 @@ describe("Ink Editor", () => {
await page.mouse.click(editorRect.x + 2, editorRect.y + 2); await page.mouse.click(editorRect.x + 2, editorRect.y + 2);
await waitForSelectedEditor(page, edgeB); await waitForSelectedEditor(page, edgeB);
await dragAndDropAnnotation( await dragAndDrop(page, edgeB, [[100, 100]]);
page,
editorRect.x + editorRect.width / 2,
editorRect.y + editorRect.height / 2,
100,
100
);
await waitForSerialized(page, 1); await waitForSerialized(page, 1);
}) })
); );

View File

@ -16,11 +16,12 @@
import { import {
applyFunctionToEditor, applyFunctionToEditor,
awaitPromise, awaitPromise,
cleanupEditing,
clearInput, clearInput,
closePages, closePages,
copy, copy,
copyToClipboard, copyToClipboard,
dragAndDropAnnotation, dragAndDrop,
getAnnotationSelector, getAnnotationSelector,
getEditorDimensions, getEditorDimensions,
getEditors, getEditors,
@ -120,13 +121,7 @@ describe("Stamp Editor", () => {
}); });
afterEach(async () => { afterEach(async () => {
for (const [, page] of pages) { await cleanupEditing(pages, switchToStamp);
await page.evaluate(() => {
window.uiManager.reset();
});
// Disable editing mode.
await switchToStamp(page, /* disable */ true);
}
}); });
afterAll(async () => { afterAll(async () => {
@ -216,13 +211,23 @@ describe("Stamp Editor", () => {
let pages; let pages;
beforeAll(async () => { beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer", 50); pages = await loadAndWait("empty.pdf", ".annotationEditorLayer", 50, {
eventBusSetup: eventBus => {
eventBus.on("annotationeditoruimanager", ({ uiManager }) => {
window.uiManager = uiManager;
});
},
});
}); });
afterAll(async () => { afterAll(async () => {
await closePages(pages); await closePages(pages);
}); });
afterEach(async () => {
await cleanupEditing(pages, switchToStamp);
});
it("must check that an added image stay within the page", async () => { it("must check that an added image stay within the page", async () => {
await Promise.all( await Promise.all(
pages.map(async ([browserName, page]) => { pages.map(async ([browserName, page]) => {
@ -239,14 +244,13 @@ describe("Stamp Editor", () => {
await input.uploadFile( await input.uploadFile(
`${path.join(__dirname, "../images/firefox_logo.png")}` `${path.join(__dirname, "../images/firefox_logo.png")}`
); );
await waitForImage(page, getEditorSelector(i)); const editorSelector = getEditorSelector(i);
await page.waitForSelector(`${getEditorSelector(i)} .altText`); await waitForImage(page, editorSelector);
await page.waitForSelector(`${editorSelector} .altText`);
for (let j = 0; j < 4; j++) { for (let j = 0; j < 4; j++) {
await page.keyboard.press("Escape"); await page.keyboard.press("Escape");
await page.waitForSelector( await page.waitForSelector(`${editorSelector} .resizers.hidden`);
`${getEditorSelector(i)} .resizers.hidden`
);
const handle = await waitForAnnotationEditorLayer(page); const handle = await waitForAnnotationEditorLayer(page);
await page.evaluate(() => { await page.evaluate(() => {
@ -255,10 +259,10 @@ describe("Stamp Editor", () => {
await awaitPromise(handle); await awaitPromise(handle);
await page.focus(".stampEditor"); await page.focus(".stampEditor");
await waitForSelectedEditor(page, getEditorSelector(i)); await waitForSelectedEditor(page, editorSelector);
await page.waitForSelector( await page.waitForSelector(
`${getEditorSelector(i)} .resizers:not(.hidden)` `${editorSelector} .resizers:not(.hidden)`
); );
const stampRect = await getRect(page, ".stampEditor"); const stampRect = await getRect(page, ".stampEditor");
@ -285,6 +289,44 @@ describe("Stamp Editor", () => {
}) })
); );
}); });
it("must check that the opposite corner doesn't move", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToStamp(page);
await page.click("#editorStampAddImage");
const input = await page.$("#stampEditorFileInput");
await input.uploadFile(
`${path.join(__dirname, "../images/firefox_logo.png")}`
);
const editorSelector = getEditorSelector(0);
await waitForImage(page, editorSelector);
await page.waitForSelector(`${editorSelector} .resizer.topLeft`);
const baseRect = await getRect(page, editorSelector);
const bRX = baseRect.x + baseRect.width;
const bRY = baseRect.y + baseRect.height;
await dragAndDrop(page, `${editorSelector} .resizer.topLeft`, [
[-10, -10],
[20, 20],
[-10, -10],
[20, 20],
]);
const newRect = await getRect(page, editorSelector);
const newBRX = newRect.x + newRect.width;
const newBRY = newRect.y + newRect.height;
expect(Math.abs(bRX - newBRX) <= 1)
.withContext(`In ${browserName}`)
.toBeTrue();
expect(Math.abs(bRY - newBRY) <= 1)
.withContext(`In ${browserName}`)
.toBeTrue();
})
);
});
}); });
describe("Alt text dialog", () => { describe("Alt text dialog", () => {
@ -1441,7 +1483,8 @@ describe("Stamp Editor", () => {
const modeChangedHandle = await waitForAnnotationModeChanged(page); const modeChangedHandle = await waitForAnnotationModeChanged(page);
await page.click(getAnnotationSelector("25R"), { count: 2 }); await page.click(getAnnotationSelector("25R"), { count: 2 });
await awaitPromise(modeChangedHandle); await awaitPromise(modeChangedHandle);
await waitForSelectedEditor(page, getEditorSelector(0)); const editorSelector = getEditorSelector(0);
await waitForSelectedEditor(page, editorSelector);
const editorIds = await getEditors(page, "stamp"); const editorIds = await getEditors(page, "stamp");
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(5); expect(editorIds.length).withContext(`In ${browserName}`).toEqual(5);
@ -1451,22 +1494,13 @@ describe("Stamp Editor", () => {
const serialized = await getSerialized(page); const serialized = await getSerialized(page);
expect(serialized).withContext(`In ${browserName}`).toEqual([]); expect(serialized).withContext(`In ${browserName}`).toEqual([]);
const editorRect = await page.$eval(getEditorSelector(0), el => { const editorRect = await getRect(page, editorSelector);
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
});
// Select the annotation we want to move. // Select the annotation we want to move.
await page.mouse.click(editorRect.x + 2, editorRect.y + 2); await page.mouse.click(editorRect.x + 2, editorRect.y + 2);
await waitForSelectedEditor(page, getEditorSelector(0)); await waitForSelectedEditor(page, editorSelector);
await dragAndDropAnnotation( await dragAndDrop(page, editorSelector, [[100, 100]]);
page,
editorRect.x + editorRect.width / 2,
editorRect.y + editorRect.height / 2,
100,
100
);
await waitForSerialized(page, 1); await waitForSerialized(page, 1);
}) })
); );

View File

@ -522,10 +522,15 @@ async function serializeBitmapDimensions(page) {
}); });
} }
async function dragAndDropAnnotation(page, startX, startY, tX, tY) { async function dragAndDrop(page, selector, translations) {
const rect = await getRect(page, selector);
const startX = rect.x + rect.width / 2;
const startY = rect.y + rect.height / 2;
await page.mouse.move(startX, startY); await page.mouse.move(startX, startY);
await page.mouse.down(); await page.mouse.down();
await page.mouse.move(startX + tX, startY + tY); for (const [tX, tY] of translations) {
await page.mouse.move(startX + tX, startY + tY);
}
await page.mouse.up(); await page.mouse.up();
await page.waitForSelector("#viewer:not(.noUserSelect)"); await page.waitForSelector("#viewer:not(.noUserSelect)");
} }
@ -809,16 +814,27 @@ function isCanvasWhite(page, pageNumber, rectangle) {
); );
} }
async function cleanupEditing(pages, switcher) {
for (const [, page] of pages) {
await page.evaluate(() => {
window.uiManager.reset();
});
// Disable editing mode.
await switcher(page, /* disable */ true);
}
}
export { export {
applyFunctionToEditor, applyFunctionToEditor,
awaitPromise, awaitPromise,
cleanupEditing,
clearInput, clearInput,
closePages, closePages,
closeSinglePage, closeSinglePage,
copy, copy,
copyToClipboard, copyToClipboard,
createPromise, createPromise,
dragAndDropAnnotation, dragAndDrop,
firstPageOnTop, firstPageOnTop,
getAnnotationSelector, getAnnotationSelector,
getAnnotationStorage, getAnnotationStorage,