Merge pull request #18179 from nicolo-ribaudo/zooming-utilities

[api-minor] Simplify API to implement zoom in custom viewers
This commit is contained in:
Jonas Jenwald 2024-05-28 16:39:06 +02:00 committed by GitHub
commit 0cec644372
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 142 additions and 101 deletions

View File

@ -17,10 +17,90 @@ import {
awaitPromise, awaitPromise,
closePages, closePages,
createPromise, createPromise,
getSpanRectFromText,
loadAndWait, loadAndWait,
} from "./test_utils.mjs"; } from "./test_utils.mjs";
describe("PDF viewer", () => { describe("PDF viewer", () => {
describe("Zoom origin", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".textLayer .endOfContent",
"page-width",
null,
{ page: 2 }
);
});
afterAll(async () => {
await closePages(pages);
});
async function getTextAt(page, pageNumber, coordX, coordY) {
await page.waitForFunction(
pageNum =>
!document.querySelector(
`.page[data-page-number="${pageNum}"] > .textLayer`
).hidden,
{},
pageNumber
);
return page.evaluate(
(x, y) => document.elementFromPoint(x, y)?.textContent,
coordX,
coordY
);
}
it("supports specifiying a custom origin", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// We use this text span of page 2 because:
// - it's in the visible area even when zooming at page-width
// - it's small, so it easily catches if the page moves too much
// - it's in a "random" position: not near the center of the
// viewport, and not near the borders
const text = "guards";
const rect = await getSpanRectFromText(page, 2, text);
const originX = rect.x + rect.width / 2;
const originY = rect.y + rect.height / 2;
await page.evaluate(
origin => {
window.PDFViewerApplication.pdfViewer.increaseScale({
scaleFactor: 2,
origin,
});
},
[originX, originY]
);
const textAfterZoomIn = await getTextAt(page, 2, originX, originY);
expect(textAfterZoomIn)
.withContext(`In ${browserName}, zoom in`)
.toBe(text);
await page.evaluate(
origin => {
window.PDFViewerApplication.pdfViewer.decreaseScale({
scaleFactor: 0.8,
origin,
});
},
[originX, originY]
);
const textAfterZoomOut = await getTextAt(page, 2, originX, originY);
expect(textAfterZoomOut)
.withContext(`In ${browserName}, zoom out`)
.toBe(text);
})
);
});
});
describe("Zoom with the mouse wheel", () => { describe("Zoom with the mouse wheel", () => {
let pages; let pages;

View File

@ -743,26 +743,24 @@ const PDFViewerApplication = {
return this._initializedCapability.promise; return this._initializedCapability.promise;
}, },
zoomIn(steps, scaleFactor) { updateZoom(steps, scaleFactor, origin) {
if (this.pdfViewer.isInPresentationMode) { if (this.pdfViewer.isInPresentationMode) {
return; return;
} }
this.pdfViewer.increaseScale({ this.pdfViewer.updateScale({
drawingDelay: AppOptions.get("defaultZoomDelay"), drawingDelay: AppOptions.get("defaultZoomDelay"),
steps, steps,
scaleFactor, scaleFactor,
origin,
}); });
}, },
zoomOut(steps, scaleFactor) { zoomIn() {
if (this.pdfViewer.isInPresentationMode) { this.updateZoom(1);
return; },
}
this.pdfViewer.decreaseScale({ zoomOut() {
drawingDelay: AppOptions.get("defaultZoomDelay"), this.updateZoom(-1);
steps,
scaleFactor,
});
}, },
zoomReset() { zoomReset() {
@ -2124,16 +2122,6 @@ const PDFViewerApplication = {
return newFactor; return newFactor;
}, },
_centerAtPos(previousScale, x, y) {
const { pdfViewer } = this;
const scaleDiff = pdfViewer.currentScale / previousScale - 1;
if (scaleDiff !== 0) {
const [top, left] = pdfViewer.containerTopLeft;
pdfViewer.container.scrollLeft += (x - left) * scaleDiff;
pdfViewer.container.scrollTop += (y - top) * scaleDiff;
}
},
/** /**
* Should be called *after* all pages have loaded, or if an error occurred, * Should be called *after* all pages have loaded, or if an error occurred,
* to unblock the "load" event; see https://bugzilla.mozilla.org/show_bug.cgi?id=1618553 * to unblock the "load" event; see https://bugzilla.mozilla.org/show_bug.cgi?id=1618553
@ -2610,6 +2598,7 @@ function webViewerWheel(evt) {
evt.deltaX === 0 && evt.deltaX === 0 &&
(Math.abs(scaleFactor - 1) < 0.05 || isBuiltInMac) && (Math.abs(scaleFactor - 1) < 0.05 || isBuiltInMac) &&
evt.deltaZ === 0; evt.deltaZ === 0;
const origin = [evt.clientX, evt.clientY];
if ( if (
isPinchToZoom || isPinchToZoom ||
@ -2628,20 +2617,13 @@ function webViewerWheel(evt) {
return; return;
} }
const previousScale = pdfViewer.currentScale;
if (isPinchToZoom && supportsPinchToZoom) { if (isPinchToZoom && supportsPinchToZoom) {
scaleFactor = PDFViewerApplication._accumulateFactor( scaleFactor = PDFViewerApplication._accumulateFactor(
previousScale, pdfViewer.currentScale,
scaleFactor, scaleFactor,
"_wheelUnusedFactor" "_wheelUnusedFactor"
); );
if (scaleFactor < 1) { PDFViewerApplication.updateZoom(null, scaleFactor, origin);
PDFViewerApplication.zoomOut(null, scaleFactor);
} else if (scaleFactor > 1) {
PDFViewerApplication.zoomIn(null, scaleFactor);
} else {
return;
}
} else { } else {
const delta = normalizeWheelEventDirection(evt); const delta = normalizeWheelEventDirection(evt);
@ -2673,20 +2655,9 @@ function webViewerWheel(evt) {
); );
} }
if (ticks < 0) { PDFViewerApplication.updateZoom(ticks, null, origin);
PDFViewerApplication.zoomOut(-ticks);
} else if (ticks > 0) {
PDFViewerApplication.zoomIn(ticks);
} else {
return;
} }
} }
// After scaling the page via zoomIn/zoomOut, the position of the upper-
// left corner is restored. When the mouse wheel is used, the position
// under the cursor should be restored instead.
PDFViewerApplication._centerAtPos(previousScale, evt.clientX, evt.clientY);
}
} }
function webViewerTouchStart(evt) { function webViewerTouchStart(evt) {
@ -2785,42 +2756,24 @@ function webViewerTouchMove(evt) {
evt.preventDefault(); evt.preventDefault();
const origin = [(page0X + page1X) / 2, (page0Y + page1Y) / 2];
const distance = Math.hypot(page0X - page1X, page0Y - page1Y) || 1; const distance = Math.hypot(page0X - page1X, page0Y - page1Y) || 1;
const pDistance = Math.hypot(pTouch0X - pTouch1X, pTouch0Y - pTouch1Y) || 1; const pDistance = Math.hypot(pTouch0X - pTouch1X, pTouch0Y - pTouch1Y) || 1;
const previousScale = pdfViewer.currentScale;
if (supportsPinchToZoom) { if (supportsPinchToZoom) {
const newScaleFactor = PDFViewerApplication._accumulateFactor( const newScaleFactor = PDFViewerApplication._accumulateFactor(
previousScale, pdfViewer.currentScale,
distance / pDistance, distance / pDistance,
"_touchUnusedFactor" "_touchUnusedFactor"
); );
if (newScaleFactor < 1) { PDFViewerApplication.updateZoom(null, newScaleFactor, origin);
PDFViewerApplication.zoomOut(null, newScaleFactor);
} else if (newScaleFactor > 1) {
PDFViewerApplication.zoomIn(null, newScaleFactor);
} else {
return;
}
} else { } else {
const PIXELS_PER_LINE_SCALE = 30; const PIXELS_PER_LINE_SCALE = 30;
const ticks = PDFViewerApplication._accumulateTicks( const ticks = PDFViewerApplication._accumulateTicks(
(distance - pDistance) / PIXELS_PER_LINE_SCALE, (distance - pDistance) / PIXELS_PER_LINE_SCALE,
"_touchUnusedTicks" "_touchUnusedTicks"
); );
if (ticks < 0) { PDFViewerApplication.updateZoom(ticks, null, origin);
PDFViewerApplication.zoomOut(-ticks);
} else if (ticks > 0) {
PDFViewerApplication.zoomIn(ticks);
} else {
return;
} }
}
PDFViewerApplication._centerAtPos(
previousScale,
(page0X + page1X) / 2,
(page0Y + page1Y) / 2
);
} }
function webViewerTouchEnd(evt) { function webViewerTouchEnd(evt) {

View File

@ -1219,7 +1219,7 @@ class PDFViewer {
#setScaleUpdatePages( #setScaleUpdatePages(
newScale, newScale,
newValue, newValue,
{ noScroll = false, preset = false, drawingDelay = -1 } { noScroll = false, preset = false, drawingDelay = -1, origin = null }
) { ) {
this._currentScaleValue = newValue.toString(); this._currentScaleValue = newValue.toString();
@ -1252,6 +1252,7 @@ class PDFViewer {
}, drawingDelay); }, drawingDelay);
} }
const previousScale = this._currentScale;
this._currentScale = newScale; this._currentScale = newScale;
if (!noScroll) { if (!noScroll) {
@ -1275,6 +1276,15 @@ class PDFViewer {
destArray: dest, destArray: dest,
allowNegativeOffset: true, allowNegativeOffset: true,
}); });
if (Array.isArray(origin)) {
// If the origin of the scaling transform is specified, preserve its
// location on screen. If not specified, scaling will fix the top-left
// corner of the visible PDF area.
const scaleDiff = newScale / previousScale - 1;
const [top, left] = this.containerTopLeft;
this.container.scrollLeft += (origin[0] - left) * scaleDiff;
this.container.scrollTop += (origin[1] - top) * scaleDiff;
}
} }
this.eventBus.dispatch("scalechanging", { this.eventBus.dispatch("scalechanging", {
@ -2122,54 +2132,52 @@ class PDFViewer {
* @property {number} [drawingDelay] * @property {number} [drawingDelay]
* @property {number} [scaleFactor] * @property {number} [scaleFactor]
* @property {number} [steps] * @property {number} [steps]
* @property {Array} [origin] x and y coordinates of the scale
* transformation origin.
*/ */
/**
* Changes the current zoom level by the specified amount.
* @param {ChangeScaleOptions} [options]
*/
updateScale({ drawingDelay, scaleFactor = null, steps = null, origin }) {
if (steps === null && scaleFactor === null) {
throw new Error(
"Invalid updateScale options: either `steps` or `scaleFactor` must be provided."
);
}
if (!this.pdfDocument) {
return;
}
let newScale = this._currentScale;
if (scaleFactor > 0 && scaleFactor !== 1) {
newScale = Math.round(newScale * scaleFactor * 100) / 100;
} else if (steps) {
const delta = steps > 0 ? DEFAULT_SCALE_DELTA : 1 / DEFAULT_SCALE_DELTA;
const round = steps > 0 ? Math.ceil : Math.floor;
steps = Math.abs(steps);
do {
newScale = round((newScale * delta).toFixed(2) * 10) / 10;
} while (--steps > 0);
}
newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, newScale));
this.#setScale(newScale, { noScroll: false, drawingDelay, origin });
}
/** /**
* Increase the current zoom level one, or more, times. * Increase the current zoom level one, or more, times.
* @param {ChangeScaleOptions} [options] * @param {ChangeScaleOptions} [options]
*/ */
increaseScale({ drawingDelay, scaleFactor, steps } = {}) { increaseScale(options = {}) {
if (!this.pdfDocument) { this.updateScale({ ...options, steps: options.steps ?? 1 });
return;
}
let newScale = this._currentScale;
if (scaleFactor > 1) {
newScale = Math.round(newScale * scaleFactor * 100) / 100;
} else {
steps ??= 1;
do {
newScale =
Math.ceil((newScale * DEFAULT_SCALE_DELTA).toFixed(2) * 10) / 10;
} while (--steps > 0 && newScale < MAX_SCALE);
}
this.#setScale(Math.min(MAX_SCALE, newScale), {
noScroll: false,
drawingDelay,
});
} }
/** /**
* Decrease the current zoom level one, or more, times. * Decrease the current zoom level one, or more, times.
* @param {ChangeScaleOptions} [options] * @param {ChangeScaleOptions} [options]
*/ */
decreaseScale({ drawingDelay, scaleFactor, steps } = {}) { decreaseScale(options = {}) {
if (!this.pdfDocument) { this.updateScale({ ...options, steps: -(options.steps ?? 1) });
return;
}
let newScale = this._currentScale;
if (scaleFactor > 0 && scaleFactor < 1) {
newScale = Math.round(newScale * scaleFactor * 100) / 100;
} else {
steps ??= 1;
do {
newScale =
Math.floor((newScale / DEFAULT_SCALE_DELTA).toFixed(2) * 10) / 10;
} while (--steps > 0 && newScale > MIN_SCALE);
}
this.#setScale(Math.max(MIN_SCALE, newScale), {
noScroll: false,
drawingDelay,
});
} }
#updateContainerHeightCss(height = this.container.clientHeight) { #updateContainerHeightCss(height = this.container.clientHeight) {