Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[api-minor] Simplify API to implement zoom in custom viewers #18179

Merged
merged 2 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions test/integration/viewer_spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,90 @@ import {
awaitPromise,
closePages,
createPromise,
getSpanRectFromText,
loadAndWait,
} from "./test_utils.mjs";

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", () => {
let pages;

Expand Down
81 changes: 17 additions & 64 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -743,26 +743,24 @@ const PDFViewerApplication = {
return this._initializedCapability.promise;
},

zoomIn(steps, scaleFactor) {
updateZoom(steps, scaleFactor, origin) {
if (this.pdfViewer.isInPresentationMode) {
return;
}
this.pdfViewer.increaseScale({
this.pdfViewer.updateScale({
drawingDelay: AppOptions.get("defaultZoomDelay"),
steps,
scaleFactor,
origin,
});
},

zoomOut(steps, scaleFactor) {
if (this.pdfViewer.isInPresentationMode) {
return;
}
this.pdfViewer.decreaseScale({
drawingDelay: AppOptions.get("defaultZoomDelay"),
steps,
scaleFactor,
});
zoomIn() {
this.updateZoom(1);
},

zoomOut() {
this.updateZoom(-1);
},

zoomReset() {
Expand Down Expand Up @@ -2124,16 +2122,6 @@ const PDFViewerApplication = {
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,
* to unblock the "load" event; see https://bugzilla.mozilla.org/show_bug.cgi?id=1618553
Expand Down Expand Up @@ -2610,6 +2598,7 @@ function webViewerWheel(evt) {
evt.deltaX === 0 &&
(Math.abs(scaleFactor - 1) < 0.05 || isBuiltInMac) &&
evt.deltaZ === 0;
const origin = [evt.clientX, evt.clientY];

if (
isPinchToZoom ||
Expand All @@ -2628,20 +2617,13 @@ function webViewerWheel(evt) {
return;
}

const previousScale = pdfViewer.currentScale;
if (isPinchToZoom && supportsPinchToZoom) {
scaleFactor = PDFViewerApplication._accumulateFactor(
previousScale,
pdfViewer.currentScale,
scaleFactor,
"_wheelUnusedFactor"
);
if (scaleFactor < 1) {
PDFViewerApplication.zoomOut(null, scaleFactor);
} else if (scaleFactor > 1) {
PDFViewerApplication.zoomIn(null, scaleFactor);
} else {
return;
}
PDFViewerApplication.updateZoom(null, scaleFactor, origin);
} else {
const delta = normalizeWheelEventDirection(evt);

Expand Down Expand Up @@ -2673,19 +2655,8 @@ function webViewerWheel(evt) {
);
}

if (ticks < 0) {
PDFViewerApplication.zoomOut(-ticks);
} else if (ticks > 0) {
PDFViewerApplication.zoomIn(ticks);
} else {
return;
}
PDFViewerApplication.updateZoom(ticks, null, origin);
}

// 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);
}
}

Expand Down Expand Up @@ -2785,42 +2756,24 @@ function webViewerTouchMove(evt) {

evt.preventDefault();

const origin = [(page0X + page1X) / 2, (page0Y + page1Y) / 2];
const distance = Math.hypot(page0X - page1X, page0Y - page1Y) || 1;
const pDistance = Math.hypot(pTouch0X - pTouch1X, pTouch0Y - pTouch1Y) || 1;
const previousScale = pdfViewer.currentScale;
if (supportsPinchToZoom) {
const newScaleFactor = PDFViewerApplication._accumulateFactor(
previousScale,
pdfViewer.currentScale,
distance / pDistance,
"_touchUnusedFactor"
);
if (newScaleFactor < 1) {
PDFViewerApplication.zoomOut(null, newScaleFactor);
} else if (newScaleFactor > 1) {
PDFViewerApplication.zoomIn(null, newScaleFactor);
} else {
return;
}
PDFViewerApplication.updateZoom(null, newScaleFactor, origin);
} else {
const PIXELS_PER_LINE_SCALE = 30;
const ticks = PDFViewerApplication._accumulateTicks(
(distance - pDistance) / PIXELS_PER_LINE_SCALE,
"_touchUnusedTicks"
);
if (ticks < 0) {
PDFViewerApplication.zoomOut(-ticks);
} else if (ticks > 0) {
PDFViewerApplication.zoomIn(ticks);
} else {
return;
}
PDFViewerApplication.updateZoom(ticks, null, origin);
}

PDFViewerApplication._centerAtPos(
previousScale,
(page0X + page1X) / 2,
(page0Y + page1Y) / 2
);
}

function webViewerTouchEnd(evt) {
Expand Down
70 changes: 39 additions & 31 deletions web/pdf_viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1219,7 +1219,7 @@ class PDFViewer {
#setScaleUpdatePages(
newScale,
newValue,
{ noScroll = false, preset = false, drawingDelay = -1 }
{ noScroll = false, preset = false, drawingDelay = -1, origin = null }
) {
this._currentScaleValue = newValue.toString();

Expand Down Expand Up @@ -1252,6 +1252,7 @@ class PDFViewer {
}, drawingDelay);
}

const previousScale = this._currentScale;
this._currentScale = newScale;

if (!noScroll) {
Expand All @@ -1275,6 +1276,15 @@ class PDFViewer {
destArray: dest,
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", {
Expand Down Expand Up @@ -2122,54 +2132,52 @@ class PDFViewer {
* @property {number} [drawingDelay]
* @property {number} [scaleFactor]
* @property {number} [steps]
* @property {Array} [origin] x and y coordinates of the scale
* transformation origin.
*/

/**
* Increase the current zoom level one, or more, times.
* Changes the current zoom level by the specified amount.
* @param {ChangeScaleOptions} [options]
*/
increaseScale({ drawingDelay, scaleFactor, steps } = {}) {
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 > 1) {
if (scaleFactor > 0 && scaleFactor !== 1) {
newScale = Math.round(newScale * scaleFactor * 100) / 100;
} else {
steps ??= 1;
} 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 =
Math.ceil((newScale * DEFAULT_SCALE_DELTA).toFixed(2) * 10) / 10;
} while (--steps > 0 && newScale < MAX_SCALE);
newScale = round((newScale * delta).toFixed(2) * 10) / 10;
} while (--steps > 0);
}
this.#setScale(Math.min(MAX_SCALE, newScale), {
noScroll: false,
drawingDelay,
});
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.
* @param {ChangeScaleOptions} [options]
*/
increaseScale(options = {}) {
this.updateScale({ ...options, steps: options.steps ?? 1 });
}

/**
* Decrease the current zoom level one, or more, times.
* @param {ChangeScaleOptions} [options]
*/
decreaseScale({ drawingDelay, scaleFactor, steps } = {}) {
if (!this.pdfDocument) {
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,
});
decreaseScale(options = {}) {
this.updateScale({ ...options, steps: -(options.steps ?? 1) });
}

#updateContainerHeightCss(height = this.container.clientHeight) {
Expand Down