Lessons Learned: Extracting Page Numbers from PDF.js
When working on a project that required retrieving the current page number from an embedded PDF viewer, I encountered several obstacles that took considerable time to resolve. This article documents the challenges I faced and the solutions I eventually implemented.
Initial Setup
The PDF viewer was hosted at an endpoint configured by the backend team, which deployed PDF.js on the server. I won't go into detail about the server-side configuration since I'm primarily a frontend developer.
My initial approach was to download PDF.js and serve it from the frontend. This quickly revealed a critical issue: cross-origin resource sharing (CORS) restrictions prevented communication between the embedded viewer and the parent page.
Problem 1: Accessing the iframe Context
After embedding the PDF using an iframe, I attempted to access the contentWindow and retrieve the PDFViewerApplication instance. However, the initial method failed because standard iframe paths don't establish the necessary connection with the PDF.js application layer. While the PDF displayed correctly, I coulnd't interact with the underlying API.
Problem 2: CORS Errors When Self-Hosting
I then tried hosting PDF.js locally by placing it in the public static directory and using absolute paths in the source code. This approach successfully exposed the PDFViewerApplication object, but triggered CORS errors. Despite extensive searching, I couldn't find a satisfactory workaround for this issue.
Problem 3: Timing Issues with Async Loading
After deploying to production, a new problem emerged: the iframe became unreachable, even though it worked perfectly in the local environment. The root cause was a race condition between code execution and page rendering. The script attempted to access the iframe before it finished loading, resulting in an undefined reference.
Adding a setInterval timer to poll for the iframe's readiness seemed like a reasonable solution. However, another issue surfaced: the timer variable was being cleared before the entire code block executed, causing the interval to terminate prematurely.
Final Working Solution
The following implementation handles these timing complexities effectively:
<iframe
v-else-if="resourceType === 'pdf'"
id="previewPDF"
ref="pdfPreview"
:src="`https://example.com/pdf/${resource_id}`"
width="100%"
height="100%"
class="iframe-content"
></iframe>
retrievePdfPages() {
if (this.pollingTimer) {
clearInterval(this.pollingTimer);
}
this.currentPageIndex = 1;
const pdfContainer = this.$refs.pdfPreview.contentWindow;
console.log('PDF frame reference:', pdfContainer);
console.log('PDFViewerApplication available:', pdfContainer.PDFViewerApplication);
this.pollingTimer = setInterval(() => {
this.iterationCount++;
console.log('Attempt:', this.iterationCount);
if (this.iterationCount > 24) {
clearInterval(this.pollingTimer);
return;
}
if (pdfContainer && pdfContainer.PDFViewerApplication) {
clearInterval(this.pollingTimer);
const totalPages = pdfContainer.PDFViewerApplication.pagesCount;
if (totalPages === 0 || totalPages === undefined) {
console.info("Document loading in progress...");
} else {
if (totalPages < 2) {
console.log(`Page 1 of 1, progress: 100%`);
} else {
this.currentPageIndex = pdfContainer.PDFViewerApplication.page;
console.log("Initial load complete, page:", this.currentPageIndex);
pdfContainer.document
.getElementById("viewerContainer")
.addEventListener("scroll", event => {
const pdfData = pdfContainer.PDFViewerApplication;
if (this.currentPageIndex !== pdfData.page) {
this.currentPageIndex = pdfData.page;
console.log("Scroll detected, current page:", this.currentPageIndex);
this.fetchPageInfo(this.resource_id, this.currentPageIndex);
}
});
}
}
}
}, 500);
}
The key improvements in this implementation include proper variable scoping for the timer and a polling mechanism that gracefully handles the asynchronous nature of iframe loading.