Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/react-pdf/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"make-cancellable-promise": "^2.0.0",
"make-event-props": "^2.0.0",
"merge-refs": "^2.0.0",
"pdfjs-dist": "5.4.296",
"pdfjs-dist": "6.0.227",
"tiny-invariant": "^1.0.0",
"warning": "^4.0.0"
},
Expand Down
18 changes: 4 additions & 14 deletions packages/react-pdf/src/Document.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { beforeAll, describe, expect, it, vi } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { page, userEvent } from 'vitest/browser';
import { render } from 'vitest-browser-react';
import { createRef } from 'react';

import Document from './Document.js';
import DocumentContext from './DocumentContext.js';
import { pdfjs } from './index.test.js';
import Page from './Page.js';

import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../test-utils.js';

import type { PDFDocumentProxy } from 'pdfjs-dist';
import type LinkService from './LinkService.js';
import type { ScrollPageIntoViewArgs } from './shared/types.js';

Expand Down Expand Up @@ -48,17 +46,9 @@ async function waitForAsync() {
}

describe('Document', () => {
// Object with basic loaded PDF information that shall match after successful loading
const desiredLoadedPdf: Partial<PDFDocumentProxy> = {};
const desiredLoadedPdf2: Partial<PDFDocumentProxy> = {};

beforeAll(async () => {
const pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise;
desiredLoadedPdf._pdfInfo = pdf._pdfInfo;

const pdf2 = await pdfjs.getDocument({ data: pdfFile2.arrayBuffer }).promise;
desiredLoadedPdf2._pdfInfo = pdf2._pdfInfo;
});
// Assert public callback fields, not PDF.js internal _pdfInfo properties.
const desiredLoadedPdf = { numPages: 4 };
const desiredLoadedPdf2 = { numPages: 5 };

describe('loading', () => {
it('loads a file and calls onSourceSuccess and onLoadSuccess callbacks via data URI properly', async () => {
Expand Down
90 changes: 24 additions & 66 deletions packages/react-pdf/src/LinkService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventBus, SimpleLinkService } from 'pdfjs-dist/web/pdf_viewer.mjs';
import invariant from 'tiny-invariant';

import type { PDFDocumentProxy } from 'pdfjs-dist';
import type { IPDFLinkService } from 'pdfjs-dist/types/web/interfaces.js';
import type {
Dest,
ExternalLinkRel,
Expand All @@ -31,78 +31,56 @@ type PDFViewer = {
scrollPageIntoView: (args: ScrollPageIntoViewArgs) => void;
};

export default class LinkService implements IPDFLinkService {
externalLinkEnabled: boolean;
externalLinkRel?: ExternalLinkRel;
externalLinkTarget?: ExternalLinkTarget;
isInPresentationMode: boolean;
pdfDocument?: PDFDocumentProxy | null;
pdfViewer?: PDFViewer | null;
export default class LinkService extends SimpleLinkService {
declare pdfDocument: PDFDocumentProxy | null;
declare pdfViewer: PDFViewer | null;

#externalLinkRel?: ExternalLinkRel;
#externalLinkTarget?: ExternalLinkTarget;

constructor() {
this.externalLinkEnabled = true;
this.externalLinkRel = undefined;
this.externalLinkTarget = undefined;
this.isInPresentationMode = false;
this.pdfDocument = undefined;
this.pdfViewer = undefined;
super({ eventBus: new EventBus() });
}

setDocument(pdfDocument: PDFDocumentProxy): void {
override setDocument(pdfDocument: PDFDocumentProxy): void {
this.pdfDocument = pdfDocument;
}

setViewer(pdfViewer: PDFViewer): void {
override setViewer(pdfViewer: PDFViewer): void {
this.pdfViewer = pdfViewer;
}

setExternalLinkRel(externalLinkRel?: ExternalLinkRel): void {
this.externalLinkRel = externalLinkRel;
this.#externalLinkRel = externalLinkRel;
}

setExternalLinkTarget(externalLinkTarget?: ExternalLinkTarget): void {
this.externalLinkTarget = externalLinkTarget;
}

setHash(): void {
// Intentionally empty
this.#externalLinkTarget = externalLinkTarget;
}

setHistory(): void {
// Intentionally empty
}

get pagesCount(): number {
override get pagesCount(): number {
return this.pdfDocument ? this.pdfDocument.numPages : 0;
}

get page(): number {
override get page(): number {
invariant(this.pdfViewer, 'PDF viewer is not initialized.');

return this.pdfViewer.currentPageNumber || 0;
}

set page(value: number) {
override set page(value: number) {
invariant(this.pdfViewer, 'PDF viewer is not initialized.');

this.pdfViewer.currentPageNumber = value;
}

get rotation(): number {
return 0;
}

set rotation(_value) {
// Intentionally empty
}

addLinkAttributes(link: HTMLAnchorElement, url: string, newWindow: boolean): void {
override addLinkAttributes(link: HTMLAnchorElement, url: string, newWindow?: boolean): void {
link.href = url;
link.rel = this.externalLinkRel || DEFAULT_LINK_REL;
link.target = newWindow ? '_blank' : this.externalLinkTarget || '';
link.rel = this.#externalLinkRel || DEFAULT_LINK_REL;
link.target = newWindow ? '_blank' : this.#externalLinkTarget || '';
}

goToDestination(dest: Dest): Promise<void> {
override goToDestination(dest: Dest): Promise<void> {
return new Promise<ResolvedDest | null>((resolve) => {
invariant(this.pdfDocument, 'PDF document not loaded.');

Expand Down Expand Up @@ -156,7 +134,7 @@ export default class LinkService implements IPDFLinkService {
});
}

goToPage(pageNumber: number): void {
override goToPage(pageNumber: number): void {
const pageIndex = pageNumber - 1;

invariant(this.pdfViewer, 'PDF viewer is not initialized.');
Expand All @@ -172,30 +150,6 @@ export default class LinkService implements IPDFLinkService {
});
}

goToXY(): void {
// Intentionally empty
}

cachePageRef(): void {
// Intentionally empty
}

getDestinationHash(): string {
return '#';
}

getAnchorUrl(): string {
return '#';
}

executeNamedAction(): void {
// Intentionally empty
}

executeSetOCGState(): void {
// Intentionally empty
}

isPageVisible(): boolean {
return true;
}
Expand All @@ -207,4 +161,8 @@ export default class LinkService implements IPDFLinkService {
navigateTo(dest: Dest): void {
this.goToDestination(dest);
}

override async executeSetOCGState(): Promise<void> {
// Intentionally empty
}
}
29 changes: 7 additions & 22 deletions packages/react-pdf/src/Page.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import silentlyFailingPdf from '../../../__mocks__/_silently_failing_pdf.js';

import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../test-utils.js';

import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import type { DocumentContextType, PageCallback } from './shared/types.js';

const pdfFile = await loadPDF('../../__mocks__/_pdf.pdf');
Expand Down Expand Up @@ -57,35 +57,20 @@ describe('Page', () => {
let pdf4: PDFDocumentProxy;
let pdf5: PDFDocumentProxy;

// Object with basic loaded page information that shall match after successful loading
const desiredLoadedPage: Partial<PDFPageProxy> = {};
const desiredLoadedPage2: Partial<PDFPageProxy> = {};
const desiredLoadedPage3: Partial<PDFPageProxy> = {};
// Assert public callback fields, not PDF.js internal _page* properties.
const desiredLoadedPage = { pageNumber: 1 };
const desiredLoadedPage2 = { pageNumber: 2 };
const desiredLoadedPage3 = { pageNumber: 1 };

// Callbacks used in registerPage and unregisterPage callbacks
let registerPageArguments: [number, HTMLDivElement];
let unregisterPageArguments: [number];
const registerPageArguments: [number, HTMLDivElement] = [0, expect.any(HTMLDivElement)];
const unregisterPageArguments: [number] = [0];

beforeAll(async () => {
pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise;

const page = await pdf.getPage(1);
desiredLoadedPage._pageIndex = page._pageIndex;
desiredLoadedPage._pageInfo = page._pageInfo;

const page2 = await pdf.getPage(2);
desiredLoadedPage2._pageIndex = page2._pageIndex;
desiredLoadedPage2._pageInfo = page2._pageInfo;

pdf2 = await pdfjs.getDocument({ data: pdfFile2.arrayBuffer }).promise;

const page3 = await pdf2.getPage(1);
desiredLoadedPage3._pageIndex = page3._pageIndex;
desiredLoadedPage3._pageInfo = page3._pageInfo;

registerPageArguments = [page._pageIndex, expect.any(HTMLDivElement)];
unregisterPageArguments = [page._pageIndex];

pdf4 = await pdfjs.getDocument({ data: pdfFile4.arrayBuffer }).promise;

pdf5 = await pdfjs.getDocument({ data: pdfFile5.arrayBuffer }).promise;
Expand Down
16 changes: 6 additions & 10 deletions packages/react-pdf/src/Page/AnnotationLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,18 +188,14 @@ export default function AnnotationLayer(): React.ReactElement {

layer.innerHTML = '';

try {
new pdfjs.AnnotationLayer(annotationLayerParameters).render(renderParameters);
const cancellable = makeCancellable(
new pdfjs.AnnotationLayer(annotationLayerParameters).render(renderParameters),
);
const runningTask = cancellable;

// Intentional immediate callback
onRenderSuccess();
} catch (error) {
onRenderError(error);
}
cancellable.promise.then(onRenderSuccess).catch(onRenderError);

return () => {
// TODO: Cancel running task?
};
return () => cancelRunningTask(runningTask);
},
[
annotations,
Expand Down
25 changes: 22 additions & 3 deletions packages/react-pdf/src/Page/TextLayer.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,22 @@ function getTextItems(container: HTMLElement) {
return wrapper.querySelectorAll('[role="presentation"]');
}

/**
* PDF.js 6 may render more `[role="presentation"]` nodes than `textContent.items.length`
* when marked content is included.
*/
async function getRenderedTextItemCount(page: PDFPageProxy): Promise<number> {
const layer = document.createElement('div');
const textLayer = new pdfjs.TextLayer({
container: layer,
textContentSource: page.streamTextContent({ includeMarkedContent: true }),
viewport: page.getViewport({ scale: 1 }),
});
await textLayer.render();

return layer.querySelectorAll('[role="presentation"]').length;
}

describe('TextLayer', () => {
// Loaded page
let page: PDFPageProxy;
Expand All @@ -49,6 +65,7 @@ describe('TextLayer', () => {
// Loaded page text items
let desiredTextItems: TextContent['items'];
let desiredTextItems2: TextContent['items'];
let desiredRenderedTextItemCount: number;

beforeAll(async () => {
const pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise;
Expand All @@ -57,6 +74,8 @@ describe('TextLayer', () => {
const textContent = await page.getTextContent();
desiredTextItems = textContent.items;

desiredRenderedTextItemCount = await getRenderedTextItemCount(page);

page2 = await pdf.getPage(2);
const textContent2 = await page2.getTextContent();
desiredTextItems2 = textContent2.items;
Expand Down Expand Up @@ -150,7 +169,7 @@ describe('TextLayer', () => {

const textItems = getTextItems(container);

expect(textItems).toHaveLength(desiredTextItems.length);
expect(textItems).toHaveLength(desiredRenderedTextItemCount);
});

it('renders text content properly given customTextRenderer', async () => {
Expand All @@ -171,7 +190,7 @@ describe('TextLayer', () => {

const textItems = getTextItems(container);

expect(textItems).toHaveLength(desiredTextItems.length);
expect(textItems).toHaveLength(desiredRenderedTextItemCount);
});

it('maps textContent items to actual TextLayer children properly', async () => {
Expand Down Expand Up @@ -225,7 +244,7 @@ describe('TextLayer', () => {

const textItems = getTextItems(container);

expect(textItems).toHaveLength(desiredTextItems.length);
expect(textItems).toHaveLength(desiredRenderedTextItemCount);

expect(customTextRenderer).toHaveBeenCalledTimes(desiredTextItems.length);
expect(customTextRenderer).toHaveBeenCalledWith(
Expand Down
22 changes: 5 additions & 17 deletions packages/react-pdf/src/Thumbnail.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import silentlyFailingPdf from '../../../__mocks__/_silently_failing_pdf.js';

import { loadPDF, makeAsyncCallback, muteConsole, restoreConsole } from '../../../test-utils.js';

import type { PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist';
import type { PDFDocumentProxy } from 'pdfjs-dist';
import type { DocumentContextType, PageCallback } from './shared/types.js';

const pdfFile = await loadPDF('../../__mocks__/_pdf.pdf');
Expand Down Expand Up @@ -51,29 +51,17 @@ describe('Thumbnail', () => {
let pdf: PDFDocumentProxy;
let pdf2: PDFDocumentProxy;

// Object with basic loaded page information that shall match after successful loading
const desiredLoadedThumbnail: Partial<PDFPageProxy> = {};
const desiredLoadedThumbnail2: Partial<PDFPageProxy> = {};
const desiredLoadedThumbnail3: Partial<PDFPageProxy> = {};
// Assert public callback fields, not PDF.js internal _page* properties.
const desiredLoadedThumbnail = { pageNumber: 1 };
const desiredLoadedThumbnail2 = { pageNumber: 2 };
const desiredLoadedThumbnail3 = { pageNumber: 1 };

const linkService = new LinkService();

beforeAll(async () => {
pdf = await pdfjs.getDocument({ data: pdfFile.arrayBuffer }).promise;

const page = await pdf.getPage(1);
desiredLoadedThumbnail._pageIndex = page._pageIndex;
desiredLoadedThumbnail._pageInfo = page._pageInfo;

const page2 = await pdf.getPage(2);
desiredLoadedThumbnail2._pageIndex = page2._pageIndex;
desiredLoadedThumbnail2._pageInfo = page2._pageInfo;

pdf2 = await pdfjs.getDocument({ data: pdfFile2.arrayBuffer }).promise;

const page3 = await pdf2.getPage(1);
desiredLoadedThumbnail3._pageIndex = page3._pageIndex;
desiredLoadedThumbnail3._pageInfo = page3._pageInfo;
});

describe('loading', () => {
Expand Down
Loading