Skip to content

Commit 0cec644

Browse files
Merge pull request #18179 from nicolo-ribaudo/zooming-utilities
[api-minor] Simplify API to implement zoom in custom viewers
2 parents 027ada8 + b7933d8 commit 0cec644

File tree

3 files changed

+136
-95
lines changed

3 files changed

+136
-95
lines changed

test/integration/viewer_spec.mjs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,90 @@ import {
1717
awaitPromise,
1818
closePages,
1919
createPromise,
20+
getSpanRectFromText,
2021
loadAndWait,
2122
} from "./test_utils.mjs";
2223

2324
describe("PDF viewer", () => {
25+
describe("Zoom origin", () => {
26+
let pages;
27+
28+
beforeAll(async () => {
29+
pages = await loadAndWait(
30+
"tracemonkey.pdf",
31+
".textLayer .endOfContent",
32+
"page-width",
33+
null,
34+
{ page: 2 }
35+
);
36+
});
37+
38+
afterAll(async () => {
39+
await closePages(pages);
40+
});
41+
42+
async function getTextAt(page, pageNumber, coordX, coordY) {
43+
await page.waitForFunction(
44+
pageNum =>
45+
!document.querySelector(
46+
`.page[data-page-number="${pageNum}"] > .textLayer`
47+
).hidden,
48+
{},
49+
pageNumber
50+
);
51+
return page.evaluate(
52+
(x, y) => document.elementFromPoint(x, y)?.textContent,
53+
coordX,
54+
coordY
55+
);
56+
}
57+
58+
it("supports specifiying a custom origin", async () => {
59+
await Promise.all(
60+
pages.map(async ([browserName, page]) => {
61+
// We use this text span of page 2 because:
62+
// - it's in the visible area even when zooming at page-width
63+
// - it's small, so it easily catches if the page moves too much
64+
// - it's in a "random" position: not near the center of the
65+
// viewport, and not near the borders
66+
const text = "guards";
67+
68+
const rect = await getSpanRectFromText(page, 2, text);
69+
const originX = rect.x + rect.width / 2;
70+
const originY = rect.y + rect.height / 2;
71+
72+
await page.evaluate(
73+
origin => {
74+
window.PDFViewerApplication.pdfViewer.increaseScale({
75+
scaleFactor: 2,
76+
origin,
77+
});
78+
},
79+
[originX, originY]
80+
);
81+
const textAfterZoomIn = await getTextAt(page, 2, originX, originY);
82+
expect(textAfterZoomIn)
83+
.withContext(`In ${browserName}, zoom in`)
84+
.toBe(text);
85+
86+
await page.evaluate(
87+
origin => {
88+
window.PDFViewerApplication.pdfViewer.decreaseScale({
89+
scaleFactor: 0.8,
90+
origin,
91+
});
92+
},
93+
[originX, originY]
94+
);
95+
const textAfterZoomOut = await getTextAt(page, 2, originX, originY);
96+
expect(textAfterZoomOut)
97+
.withContext(`In ${browserName}, zoom out`)
98+
.toBe(text);
99+
})
100+
);
101+
});
102+
});
103+
24104
describe("Zoom with the mouse wheel", () => {
25105
let pages;
26106

web/app.js

Lines changed: 17 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -743,26 +743,24 @@ const PDFViewerApplication = {
743743
return this._initializedCapability.promise;
744744
},
745745

746-
zoomIn(steps, scaleFactor) {
746+
updateZoom(steps, scaleFactor, origin) {
747747
if (this.pdfViewer.isInPresentationMode) {
748748
return;
749749
}
750-
this.pdfViewer.increaseScale({
750+
this.pdfViewer.updateScale({
751751
drawingDelay: AppOptions.get("defaultZoomDelay"),
752752
steps,
753753
scaleFactor,
754+
origin,
754755
});
755756
},
756757

757-
zoomOut(steps, scaleFactor) {
758-
if (this.pdfViewer.isInPresentationMode) {
759-
return;
760-
}
761-
this.pdfViewer.decreaseScale({
762-
drawingDelay: AppOptions.get("defaultZoomDelay"),
763-
steps,
764-
scaleFactor,
765-
});
758+
zoomIn() {
759+
this.updateZoom(1);
760+
},
761+
762+
zoomOut() {
763+
this.updateZoom(-1);
766764
},
767765

768766
zoomReset() {
@@ -2124,16 +2122,6 @@ const PDFViewerApplication = {
21242122
return newFactor;
21252123
},
21262124

2127-
_centerAtPos(previousScale, x, y) {
2128-
const { pdfViewer } = this;
2129-
const scaleDiff = pdfViewer.currentScale / previousScale - 1;
2130-
if (scaleDiff !== 0) {
2131-
const [top, left] = pdfViewer.containerTopLeft;
2132-
pdfViewer.container.scrollLeft += (x - left) * scaleDiff;
2133-
pdfViewer.container.scrollTop += (y - top) * scaleDiff;
2134-
}
2135-
},
2136-
21372125
/**
21382126
* Should be called *after* all pages have loaded, or if an error occurred,
21392127
* to unblock the "load" event; see https://bugzilla.mozilla.org/show_bug.cgi?id=1618553
@@ -2610,6 +2598,7 @@ function webViewerWheel(evt) {
26102598
evt.deltaX === 0 &&
26112599
(Math.abs(scaleFactor - 1) < 0.05 || isBuiltInMac) &&
26122600
evt.deltaZ === 0;
2601+
const origin = [evt.clientX, evt.clientY];
26132602

26142603
if (
26152604
isPinchToZoom ||
@@ -2628,20 +2617,13 @@ function webViewerWheel(evt) {
26282617
return;
26292618
}
26302619

2631-
const previousScale = pdfViewer.currentScale;
26322620
if (isPinchToZoom && supportsPinchToZoom) {
26332621
scaleFactor = PDFViewerApplication._accumulateFactor(
2634-
previousScale,
2622+
pdfViewer.currentScale,
26352623
scaleFactor,
26362624
"_wheelUnusedFactor"
26372625
);
2638-
if (scaleFactor < 1) {
2639-
PDFViewerApplication.zoomOut(null, scaleFactor);
2640-
} else if (scaleFactor > 1) {
2641-
PDFViewerApplication.zoomIn(null, scaleFactor);
2642-
} else {
2643-
return;
2644-
}
2626+
PDFViewerApplication.updateZoom(null, scaleFactor, origin);
26452627
} else {
26462628
const delta = normalizeWheelEventDirection(evt);
26472629

@@ -2673,19 +2655,8 @@ function webViewerWheel(evt) {
26732655
);
26742656
}
26752657

2676-
if (ticks < 0) {
2677-
PDFViewerApplication.zoomOut(-ticks);
2678-
} else if (ticks > 0) {
2679-
PDFViewerApplication.zoomIn(ticks);
2680-
} else {
2681-
return;
2682-
}
2658+
PDFViewerApplication.updateZoom(ticks, null, origin);
26832659
}
2684-
2685-
// After scaling the page via zoomIn/zoomOut, the position of the upper-
2686-
// left corner is restored. When the mouse wheel is used, the position
2687-
// under the cursor should be restored instead.
2688-
PDFViewerApplication._centerAtPos(previousScale, evt.clientX, evt.clientY);
26892660
}
26902661
}
26912662

@@ -2785,42 +2756,24 @@ function webViewerTouchMove(evt) {
27852756

27862757
evt.preventDefault();
27872758

2759+
const origin = [(page0X + page1X) / 2, (page0Y + page1Y) / 2];
27882760
const distance = Math.hypot(page0X - page1X, page0Y - page1Y) || 1;
27892761
const pDistance = Math.hypot(pTouch0X - pTouch1X, pTouch0Y - pTouch1Y) || 1;
2790-
const previousScale = pdfViewer.currentScale;
27912762
if (supportsPinchToZoom) {
27922763
const newScaleFactor = PDFViewerApplication._accumulateFactor(
2793-
previousScale,
2764+
pdfViewer.currentScale,
27942765
distance / pDistance,
27952766
"_touchUnusedFactor"
27962767
);
2797-
if (newScaleFactor < 1) {
2798-
PDFViewerApplication.zoomOut(null, newScaleFactor);
2799-
} else if (newScaleFactor > 1) {
2800-
PDFViewerApplication.zoomIn(null, newScaleFactor);
2801-
} else {
2802-
return;
2803-
}
2768+
PDFViewerApplication.updateZoom(null, newScaleFactor, origin);
28042769
} else {
28052770
const PIXELS_PER_LINE_SCALE = 30;
28062771
const ticks = PDFViewerApplication._accumulateTicks(
28072772
(distance - pDistance) / PIXELS_PER_LINE_SCALE,
28082773
"_touchUnusedTicks"
28092774
);
2810-
if (ticks < 0) {
2811-
PDFViewerApplication.zoomOut(-ticks);
2812-
} else if (ticks > 0) {
2813-
PDFViewerApplication.zoomIn(ticks);
2814-
} else {
2815-
return;
2816-
}
2775+
PDFViewerApplication.updateZoom(ticks, null, origin);
28172776
}
2818-
2819-
PDFViewerApplication._centerAtPos(
2820-
previousScale,
2821-
(page0X + page1X) / 2,
2822-
(page0Y + page1Y) / 2
2823-
);
28242777
}
28252778

28262779
function webViewerTouchEnd(evt) {

web/pdf_viewer.js

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1219,7 +1219,7 @@ class PDFViewer {
12191219
#setScaleUpdatePages(
12201220
newScale,
12211221
newValue,
1222-
{ noScroll = false, preset = false, drawingDelay = -1 }
1222+
{ noScroll = false, preset = false, drawingDelay = -1, origin = null }
12231223
) {
12241224
this._currentScaleValue = newValue.toString();
12251225

@@ -1252,6 +1252,7 @@ class PDFViewer {
12521252
}, drawingDelay);
12531253
}
12541254

1255+
const previousScale = this._currentScale;
12551256
this._currentScale = newScale;
12561257

12571258
if (!noScroll) {
@@ -1275,6 +1276,15 @@ class PDFViewer {
12751276
destArray: dest,
12761277
allowNegativeOffset: true,
12771278
});
1279+
if (Array.isArray(origin)) {
1280+
// If the origin of the scaling transform is specified, preserve its
1281+
// location on screen. If not specified, scaling will fix the top-left
1282+
// corner of the visible PDF area.
1283+
const scaleDiff = newScale / previousScale - 1;
1284+
const [top, left] = this.containerTopLeft;
1285+
this.container.scrollLeft += (origin[0] - left) * scaleDiff;
1286+
this.container.scrollTop += (origin[1] - top) * scaleDiff;
1287+
}
12781288
}
12791289

12801290
this.eventBus.dispatch("scalechanging", {
@@ -2122,54 +2132,52 @@ class PDFViewer {
21222132
* @property {number} [drawingDelay]
21232133
* @property {number} [scaleFactor]
21242134
* @property {number} [steps]
2135+
* @property {Array} [origin] x and y coordinates of the scale
2136+
* transformation origin.
21252137
*/
21262138

21272139
/**
2128-
* Increase the current zoom level one, or more, times.
2140+
* Changes the current zoom level by the specified amount.
21292141
* @param {ChangeScaleOptions} [options]
21302142
*/
2131-
increaseScale({ drawingDelay, scaleFactor, steps } = {}) {
2143+
updateScale({ drawingDelay, scaleFactor = null, steps = null, origin }) {
2144+
if (steps === null && scaleFactor === null) {
2145+
throw new Error(
2146+
"Invalid updateScale options: either `steps` or `scaleFactor` must be provided."
2147+
);
2148+
}
21322149
if (!this.pdfDocument) {
21332150
return;
21342151
}
21352152
let newScale = this._currentScale;
2136-
if (scaleFactor > 1) {
2153+
if (scaleFactor > 0 && scaleFactor !== 1) {
21372154
newScale = Math.round(newScale * scaleFactor * 100) / 100;
2138-
} else {
2139-
steps ??= 1;
2155+
} else if (steps) {
2156+
const delta = steps > 0 ? DEFAULT_SCALE_DELTA : 1 / DEFAULT_SCALE_DELTA;
2157+
const round = steps > 0 ? Math.ceil : Math.floor;
2158+
steps = Math.abs(steps);
21402159
do {
2141-
newScale =
2142-
Math.ceil((newScale * DEFAULT_SCALE_DELTA).toFixed(2) * 10) / 10;
2143-
} while (--steps > 0 && newScale < MAX_SCALE);
2160+
newScale = round((newScale * delta).toFixed(2) * 10) / 10;
2161+
} while (--steps > 0);
21442162
}
2145-
this.#setScale(Math.min(MAX_SCALE, newScale), {
2146-
noScroll: false,
2147-
drawingDelay,
2148-
});
2163+
newScale = Math.max(MIN_SCALE, Math.min(MAX_SCALE, newScale));
2164+
this.#setScale(newScale, { noScroll: false, drawingDelay, origin });
2165+
}
2166+
2167+
/**
2168+
* Increase the current zoom level one, or more, times.
2169+
* @param {ChangeScaleOptions} [options]
2170+
*/
2171+
increaseScale(options = {}) {
2172+
this.updateScale({ ...options, steps: options.steps ?? 1 });
21492173
}
21502174

21512175
/**
21522176
* Decrease the current zoom level one, or more, times.
21532177
* @param {ChangeScaleOptions} [options]
21542178
*/
2155-
decreaseScale({ drawingDelay, scaleFactor, steps } = {}) {
2156-
if (!this.pdfDocument) {
2157-
return;
2158-
}
2159-
let newScale = this._currentScale;
2160-
if (scaleFactor > 0 && scaleFactor < 1) {
2161-
newScale = Math.round(newScale * scaleFactor * 100) / 100;
2162-
} else {
2163-
steps ??= 1;
2164-
do {
2165-
newScale =
2166-
Math.floor((newScale / DEFAULT_SCALE_DELTA).toFixed(2) * 10) / 10;
2167-
} while (--steps > 0 && newScale > MIN_SCALE);
2168-
}
2169-
this.#setScale(Math.max(MIN_SCALE, newScale), {
2170-
noScroll: false,
2171-
drawingDelay,
2172-
});
2179+
decreaseScale(options = {}) {
2180+
this.updateScale({ ...options, steps: -(options.steps ?? 1) });
21732181
}
21742182

21752183
#updateContainerHeightCss(height = this.container.clientHeight) {

0 commit comments

Comments
 (0)