Merge pull request #18179 from nicolo-ribaudo/zooming-utilities
[api-minor] Simplify API to implement zoom in custom viewers
This commit is contained in:
commit
0cec644372
@ -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;
|
||||||
|
|
||||||
|
|||||||
81
web/app.js
81
web/app.js
@ -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,44 +2756,26 @@ 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) {
|
||||||
if (!PDFViewerApplication._touchInfo) {
|
if (!PDFViewerApplication._touchInfo) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user