Merge pull request #20951 from calixteman/bug2021392

Correctly scroll the search result in the viewport with rotated pdfs (bug 2021392)
This commit is contained in:
calixteman 2026-03-22 21:36:46 +01:00 committed by GitHub
commit 2643125a12
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 141 additions and 45 deletions

View File

@ -177,4 +177,56 @@ describe("find bar", () => {
); );
}); });
}); });
describe("Check that the search results are correctly visible in rotated PDFs (bug 2021392)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"hello_world_rotated.pdf",
".textLayer",
"page-fit"
);
});
afterEach(async () => {
await closePages(pages);
});
it("must scroll each match into the viewport when navigating search results", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#viewFindButton");
await page.waitForSelector("#findInput", { visible: true });
await page.type("#findInput", "hello");
await page.waitForSelector("#findInput[data-status='']");
for (let i = 0; i < 5; i++) {
if (i > 0) {
await page.click("#findNextButton");
await page.waitForSelector("#findInput[data-status='']");
}
// Verify we are on the expected match number.
const resultElement =
await page.waitForSelector("#findResultsCount");
const resultText = await resultElement.evaluate(
el => el.textContent
);
expect(resultText)
.withContext(`In ${browserName}, match ${i + 1}`)
.toEqual(`${FSI}${i + 1}${PDI} of ${FSI}5${PDI} matches`);
// The selected highlight must be visible in the viewport.
const selected = await page.waitForSelector(
".textLayer .highlight.selected"
);
expect(await selected.isIntersectingViewport())
.withContext(`In ${browserName}, match ${i + 1}`)
.toBeTrue();
}
})
);
});
});
}); });

View File

@ -891,3 +891,4 @@
!extractPages_null_in_array.pdf !extractPages_null_in_array.pdf
!issue20930.pdf !issue20930.pdf
!text_rise_eol_bug.pdf !text_rise_eol_bug.pdf
!hello_world_rotated.pdf

View File

@ -0,0 +1,74 @@
%PDF-1.4
%âãÏÓ
8 0 obj
<< /Length 43 >>
stream
BT /F1 36 Tf 210 370 Td (Hello world) Tj ET
endstream
endobj
9 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R
/MediaBox [0 0 612 792]
/Rotate 90
/Contents 8 0 R
/Resources << /Font << /F1 9 0 R >> >>
>>
endobj
4 0 obj
<< /Type /Page /Parent 2 0 R
/MediaBox [0 0 612 792]
/Rotate 90
/Contents 8 0 R
/Resources << /Font << /F1 9 0 R >> >>
>>
endobj
5 0 obj
<< /Type /Page /Parent 2 0 R
/MediaBox [0 0 612 792]
/Rotate 90
/Contents 8 0 R
/Resources << /Font << /F1 9 0 R >> >>
>>
endobj
6 0 obj
<< /Type /Page /Parent 2 0 R
/MediaBox [0 0 612 792]
/Rotate 90
/Contents 8 0 R
/Resources << /Font << /F1 9 0 R >> >>
>>
endobj
7 0 obj
<< /Type /Page /Parent 2 0 R
/MediaBox [0 0 612 792]
/Rotate 90
/Contents 8 0 R
/Resources << /Font << /F1 9 0 R >> >>
>>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R 4 0 R 5 0 R 6 0 R 7 0 R] /Count 5 >>
endobj
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
xref
0 10
0000000000 65535 f
0000001009 00000 n
0000000928 00000 n
0000000183 00000 n
0000000332 00000 n
0000000481 00000 n
0000000630 00000 n
0000000779 00000 n
0000000015 00000 n
0000000108 00000 n
trailer
<< /Size 10 /Root 1 0 R >>
startxref
1058
%%EOF

View File

@ -17,8 +17,8 @@
/** @typedef {import("./event_utils").EventBus} EventBus */ /** @typedef {import("./event_utils").EventBus} EventBus */
/** @typedef {import("./pdf_link_service.js").PDFLinkService} PDFLinkService */ /** @typedef {import("./pdf_link_service.js").PDFLinkService} PDFLinkService */
import { binarySearchFirstItem, scrollIntoView } from "./ui_utils.js";
import { getCharacterType, getNormalizeWithNFKC } from "./pdf_find_utils.js"; import { getCharacterType, getNormalizeWithNFKC } from "./pdf_find_utils.js";
import { binarySearchFirstItem } from "./ui_utils.js";
const FindState = { const FindState = {
FOUND: 0, FOUND: 0,
@ -28,7 +28,6 @@ const FindState = {
}; };
const FIND_TIMEOUT = 250; // ms const FIND_TIMEOUT = 250; // ms
const MATCH_SCROLL_OFFSET_TOP = -50; // px
const CHARACTERS_TO_NORMALIZE = { const CHARACTERS_TO_NORMALIZE = {
"\u2010": "-", // Hyphen "\u2010": "-", // Hyphen
@ -553,7 +552,6 @@ class PDFFindController {
/** /**
* @typedef {Object} PDFFindControllerScrollMatchIntoViewParams * @typedef {Object} PDFFindControllerScrollMatchIntoViewParams
* @property {HTMLElement} element * @property {HTMLElement} element
* @property {number} selectedLeft
* @property {number} pageIndex * @property {number} pageIndex
* @property {number} matchIndex * @property {number} matchIndex
*/ */
@ -562,12 +560,7 @@ class PDFFindController {
* Scroll the current match into view. * Scroll the current match into view.
* @param {PDFFindControllerScrollMatchIntoViewParams} * @param {PDFFindControllerScrollMatchIntoViewParams}
*/ */
scrollMatchIntoView({ scrollMatchIntoView({ element = null, pageIndex = -1, matchIndex = -1 }) {
element = null,
selectedLeft = 0,
pageIndex = -1,
matchIndex = -1,
}) {
if (!this._scrollMatches || !element) { if (!this._scrollMatches || !element) {
return; return;
} else if (matchIndex === -1 || matchIndex !== this._selected.matchIdx) { } else if (matchIndex === -1 || matchIndex !== this._selected.matchIdx) {
@ -576,11 +569,7 @@ class PDFFindController {
return; return;
} }
this._scrollMatches = false; // Ensure that scrolling only happens once. this._scrollMatches = false; // Ensure that scrolling only happens once.
const spot = { element.scrollIntoView({ block: "start", inline: "center" });
top: MATCH_SCROLL_OFFSET_TOP,
left: selectedLeft,
};
scrollIntoView(element, spot, /* scrollMatches = */ true);
} }
#reset() { #reset() {

View File

@ -195,11 +195,9 @@ class TextHighlighter {
div.append(span); div.append(span);
if (className.includes("selected")) { if (className.includes("selected")) {
const { left } = span.getClientRects()[0]; return span;
const parentLeft = div.getBoundingClientRect().left;
return left - parentLeft;
} }
return 0; return null;
} }
div.append(node); div.append(node);
@ -233,7 +231,7 @@ class TextHighlighter {
const end = match.end; const end = match.end;
const isSelected = isSelectedPage && i === selectedMatchIdx; const isSelected = isSelectedPage && i === selectedMatchIdx;
const highlightSuffix = isSelected ? " selected" : ""; const highlightSuffix = isSelected ? " selected" : "";
let selectedLeft = 0; let selectedSpan = null;
// Match inside new div. // Match inside new div.
if (!prevEnd || begin.divIdx !== prevEnd.divIdx) { if (!prevEnd || begin.divIdx !== prevEnd.divIdx) {
@ -248,14 +246,14 @@ class TextHighlighter {
} }
if (begin.divIdx === end.divIdx) { if (begin.divIdx === end.divIdx) {
selectedLeft = appendTextToDiv( selectedSpan = appendTextToDiv(
begin.divIdx, begin.divIdx,
begin.offset, begin.offset,
end.offset, end.offset,
"highlight" + highlightSuffix "highlight" + highlightSuffix
); );
} else { } else {
selectedLeft = appendTextToDiv( selectedSpan = appendTextToDiv(
begin.divIdx, begin.divIdx,
begin.offset, begin.offset,
infinity.offset, infinity.offset,
@ -271,8 +269,7 @@ class TextHighlighter {
if (isSelected) { if (isSelected) {
// Attempt to scroll the selected match into view. // Attempt to scroll the selected match into view.
findController.scrollMatchIntoView({ findController.scrollMatchIntoView({
element: textDivs[begin.divIdx], element: selectedSpan,
selectedLeft,
pageIndex: pageIdx, pageIndex: pageIdx,
matchIndex: selectedMatchIdx, matchIndex: selectedMatchIdx,
}); });

View File

@ -110,6 +110,7 @@
&.selected { &.selected {
background-color: var(--highlight-selected-bg-color); background-color: var(--highlight-selected-bg-color);
backdrop-filter: var(--highlight-selected-backdrop-filter); backdrop-filter: var(--highlight-selected-backdrop-filter);
scroll-margin-top: 50px;
} }
} }

View File

@ -13,8 +13,6 @@
* limitations under the License. * limitations under the License.
*/ */
import { MathClamp } from "pdfjs-lib";
const DEFAULT_SCALE_VALUE = "auto"; const DEFAULT_SCALE_VALUE = "auto";
const DEFAULT_SCALE = 1.0; const DEFAULT_SCALE = 1.0;
const DEFAULT_SCALE_DELTA = 1.1; const DEFAULT_SCALE_DELTA = 1.1;
@ -78,11 +76,8 @@ const AutoPrintRegExp = /\bprint\s*\(/;
* specifying the offset from the top left edge. * specifying the offset from the top left edge.
* @param {number} [spot.left] * @param {number} [spot.left]
* @param {number} [spot.top] * @param {number} [spot.top]
* @param {boolean} [scrollMatches] - When scrolling search results into view,
* ignore elements that either: Contains marked content identifiers,
* or have the CSS-rule `overflow: hidden;` set. The default value is `false`.
*/ */
function scrollIntoView(element, spot, scrollMatches = false) { function scrollIntoView(element, spot) {
// Assuming offsetParent is available (it's not available when viewer is in // Assuming offsetParent is available (it's not available when viewer is in
// hidden iframe or object). We have to scroll: if the offsetParent is not set // hidden iframe or object). We have to scroll: if the offsetParent is not set
// producing the error. See also animationStarted. // producing the error. See also animationStarted.
@ -94,11 +89,8 @@ function scrollIntoView(element, spot, scrollMatches = false) {
let offsetY = element.offsetTop + element.clientTop; let offsetY = element.offsetTop + element.clientTop;
let offsetX = element.offsetLeft + element.clientLeft; let offsetX = element.offsetLeft + element.clientLeft;
while ( while (
(parent.clientHeight === parent.scrollHeight && parent.clientHeight === parent.scrollHeight &&
parent.clientWidth === parent.scrollWidth) || parent.clientWidth === parent.scrollWidth
(scrollMatches &&
(parent.classList.contains("markedContent") ||
getComputedStyle(parent).overflow === "hidden"))
) { ) {
offsetY += parent.offsetTop; offsetY += parent.offsetTop;
offsetX += parent.offsetLeft; offsetX += parent.offsetLeft;
@ -113,17 +105,7 @@ function scrollIntoView(element, spot, scrollMatches = false) {
offsetY += spot.top; offsetY += spot.top;
} }
if (spot.left !== undefined) { if (spot.left !== undefined) {
if (scrollMatches) { offsetX += spot.left;
const elementWidth = element.getBoundingClientRect().width;
const padding = MathClamp(
(parent.clientWidth - elementWidth) / 2,
20,
400
);
offsetX += spot.left - padding;
} else {
offsetX += spot.left;
}
parent.scrollLeft = offsetX; parent.scrollLeft = offsetX;
} }
} }