diff options
author | Bob Zeleznik <zzzman@gmail.com> | 2019-06-18 02:19:07 -0400 |
---|---|---|
committer | Bob Zeleznik <zzzman@gmail.com> | 2019-06-18 02:19:07 -0400 |
commit | 62c781c0c79ac395c5e117d208a90485ff1ba599 (patch) | |
tree | 0198c16e5299470d21a4f3d0fc0720b2ab975cce /src | |
parent | 4dc8c03562a0473becb895824740da487e16e771 (diff) |
faster loading of PDFs
Diffstat (limited to 'src')
-rw-r--r-- | src/client/views/nodes/PDFBox.tsx | 1 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 340 |
2 files changed, 116 insertions, 225 deletions
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 655c12ab3..3aaa5749d 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -69,7 +69,6 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen loaded = (nw: number, nh: number, np: number) => { if (this.props.Document) { let doc = this.props.Document.proto ? this.props.Document.proto : this.props.Document; - console.log("pages = " + np); doc.numPages = np; if (doc.nativeWidth && doc.nativeHeight) return; let oldaspect = NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth, 1); diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index d74a16f3f..5149e48fd 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -43,16 +43,7 @@ export class PDFViewer extends React.Component<IPDFViewerProps> { @action componentDidMount() { - const pdfUrl = this.props.url; - console.log("pdf starting to load") - let promise = Pdfjs.getDocument(pdfUrl).promise; - - promise.then((pdf: Pdfjs.PDFDocumentProxy) => { - runInAction(() => { - console.log("pdf url received"); - this._pdf = pdf; - }); - }); + Pdfjs.getDocument(this.props.url).promise.then(pdf => runInAction(() => this._pdf = pdf)); } render() { @@ -83,12 +74,8 @@ class Viewer extends React.Component<IViewerProps> { // _visibleElements is the array of JSX elements that gets rendered @observable.shallow private _visibleElements: JSX.Element[] = []; // _isPage is an array that tells us whether or not an index is rendered as a page or as a placeholder - @observable private _isPage: boolean[] = []; + @observable private _isPage: string[] = []; @observable private _pageSizes: { width: number, height: number }[] = []; - @observable private _startIndex: number = 0; - @observable private _endIndex: number = 1; - @observable private _loaded: boolean = false; - @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>; @observable private _annotations: Doc[] = []; @observable private _pointerEvents: "all" | "none" = "all"; @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>(); @@ -97,7 +84,6 @@ class Viewer extends React.Component<IViewerProps> { private _annotationLayer: React.RefObject<HTMLDivElement>; private _reactionDisposer?: IReactionDisposer; private _annotationReactionDisposer?: IReactionDisposer; - private _pagesLoaded: number = 0; private _dropDisposer?: DragManager.DragDropDisposer; constructor(props: IViewerProps) { @@ -106,77 +92,62 @@ class Viewer extends React.Component<IViewerProps> { this._annotationLayer = React.createRef(); } + componentDidUpdate = (prevProps: IViewerProps) => { + if (this.scrollY !== prevProps.scrollY && this._visibleElements.length) { + this.renderPages(this.startIndex, this.endIndex, false); + } + } + @action componentDidMount = () => { let wasSelected = this.props.parent.props.active(); - // reaction for when document gets (de)selected this._reactionDisposer = reaction( - () => [this.props.parent.props.active(), this.startIndex], - () => { - // if deselected, render images in place of pdf - if (wasSelected && !this.props.parent.props.active()) { - this.saveThumbnail(); - } - // if selected, render pdf - else if (!wasSelected && this.props.parent.props.active()) { - this.renderPages(this.startIndex, this.endIndex, true); - } + () => [this.props.parent.props.active(), this.startIndex, this.props.pdf], + async () => { + await this.initialLoad(); wasSelected = this.props.parent.props.active(); - this._pointerEvents = wasSelected ? "none" : "all"; - }, - { fireImmediately: true } - ); + runInAction(() => this._pointerEvents = wasSelected ? "none" : "all"); + this.renderPages(this.startIndex, this.endIndex, false); + }, { fireImmediately: true }); - if (this.props.parent.Document) { - this._annotationReactionDisposer = reaction( - () => DocListCast(this.props.parent.Document.annotations), - () => { - let annotations = DocListCast(this.props.parent.Document.annotations); - if (annotations && annotations.length) { - this.renderAnnotations(annotations, true); - } - }, - { fireImmediately: true } - ); - } + this._annotationReactionDisposer = reaction( + () => this.props.parent.Document && DocListCast(this.props.parent.Document.annotations), + (annotations: Doc[]) => + annotations && annotations.length && this.renderAnnotations(annotations, true), + { fireImmediately: true }); + } - setTimeout(() => { - // this.renderPages(this.startIndex, this.endIndex, true); - this.initialLoad(); - }, 1000); + componentWillUnmount = () => { + this._reactionDisposer && this._reactionDisposer(); + this._annotationReactionDisposer && this._annotationReactionDisposer(); } @action - initialLoad = () => { - let pdf = this.props.pdf; - if (pdf) { - this._pageSizes = Array<{ width: number, height: number }>(pdf.numPages); - let rendered = 0; - for (let i = 0; i < pdf.numPages; i++) { - pdf.getPage(i + 1).then( - (page: Pdfjs.PDFPageProxy) => { - runInAction(() => { - this._pageSizes[i] = { width: page.view[2] * scale, height: page.view[3] * scale }; - }); - console.log(`page ${i} size retreieved`); - rendered++; - if (rendered === pdf!.numPages - 1) { - this.saveThumbnail(); - } - } - ); + initialLoad = async () => { + if (this.props.pdf && this._pageSizes.length === 0) { + let pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages); + for (let i = 0; i < this.props.pdf.numPages; i++) { + await this.props.pdf.getPage(i + 1).then(page => runInAction(() => { + pageSizes[i] = { width: page.view[2] * scale, height: page.view[3] * scale }; + if (i === 0) this.props.loaded(pageSizes[i].width, pageSizes[i].height, this.props.pdf!.numPages); + })); } + runInAction(() => { + this._pageSizes = pageSizes; + let divs = Array.from(Array(this._pageSizes.length).keys()).map(i => ( + <div key={`pdfviewer-placeholder-${i}`} className="pdfviewer-placeholder" + style={{ width: this._pageSizes[i] ? this._pageSizes[i].width : 0, height: this._pageSizes[i] ? this._pageSizes[i].height : 0 }} /> + )); + this._isPage = Array.from(Array(this._pageSizes.length).map(p => "none")); + this._visibleElements = new Array<JSX.Element>(...divs); + }) } } private mainCont = (div: HTMLDivElement | null) => { - if (this._dropDisposer) { - this._dropDisposer(); - } + this._dropDisposer && this._dropDisposer(); if (div) { - this._dropDisposer = DragManager.MakeDropTarget(div, { - handlers: { drop: this.drop.bind(this) } - }); + this._dropDisposer = div && DragManager.MakeDropTarget(div, { handlers: { drop: this.drop.bind(this) } }); } } @@ -222,154 +193,102 @@ class Viewer extends React.Component<IViewerProps> { e.stopPropagation(); } } - - componentWillUnmount = () => { - if (this._reactionDisposer) { - this._reactionDisposer(); - } - if (this._annotationReactionDisposer) { - this._annotationReactionDisposer(); - } - } - + /** + * Called by the Page class when it gets rendered, initializes the lists and + * puts a placeholder with all of the correct page sizes when all of the pages have been loaded. + */ @action - saveThumbnail = async () => { - // file address of the pdf - const address: string = this.props.url; - for (let i = 0; i < this._visibleElements.length; i++) { - if (this._isPage[i]) { - // change the address to be the file address of the PNG version of each page - let res = JSON.parse(await rp.get(DocServer.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${i + 1}.PNG`))); - let thisAddress = res.path; - let nWidth = parseInt(res.width); - let nHeight = parseInt(res.height); - // replace page with image - runInAction(() => - this._visibleElements[i] = <img key={thisAddress} style={{ width: `${nWidth * scale}px`, height: `${nHeight * scale}px` }} src={thisAddress} />); - } - } - } - - @computed get scrollY(): number { - return this.props.scrollY; - } - - @computed get startIndex(): number { - return Math.max(0, this.getIndex(this.scrollY) - this._pageBuffer); - } - - @computed get endIndex(): number { - let width = this._pageSizes.map(i => i ? i.width : 0); - return Math.min(this.props.pdf ? this.props.pdf.numPages - 1 : 0, this.getIndex(this.scrollY + Math.max(...width)) + this._pageBuffer); - } - - componentDidUpdate = (prevProps: IViewerProps) => { - if (this.scrollY !== prevProps.scrollY || this._pdf !== this.props.pdf) { - this._pdf = this.props.pdf; - // render pages if the scorll position changes - console.log(`START: ${this.startIndex}, END: ${this.endIndex}`); - this.renderPages(this.startIndex, this.endIndex); - } + pageLoaded = (index: number, page: Pdfjs.PDFPageViewport): void => { + this.props.pdf && this.props.loaded && this.props.loaded(page.width, page.height, this.props.pdf.numPages); } - @action - private renderAnnotations = (annotations: Doc[], removeOldAnnotations: boolean): void => { - if (removeOldAnnotations) { - this._annotations = annotations; - } - else { - this._annotations.push(...annotations); - this._annotations = new Array<Doc>(...this._annotations); + getPlaceholderPage = (page: number) => { + if (this._isPage[page] !== "none") { + this._isPage[page] = "none"; + this._visibleElements[page] = ( + <div key={`pdfviewer-placeholder-${page}`} className="pdfviewer-placeholder" style={{ width: this._pageSizes[page] ? this._pageSizes[page].width : 0, height: this._pageSizes[page] ? this._pageSizes[page].height : 0 }} /> + ); } } - - /** - * @param startIndex: where to start rendering pages - * @param endIndex: where to end rendering pages - * @param forceRender: (optional), force pdfs to re-render, even if the page already exists - */ @action - renderPages = (startIndex: number, endIndex: number, forceRender: boolean = false) => { - let numPages = this.props.pdf ? this.props.pdf.numPages : 0; - if (!this.props.pdf) { - return; - } - - if (this._pageSizes.length !== numPages) { - this._pageSizes = new Array(numPages).map(i => ({ width: 0, height: 0 })); - } - - // this is only for an initial render to get all of the pages rendered - if (this._visibleElements.length !== numPages) { - let divs = Array.from(Array(numPages).keys()).map(i => i < 5 ? ( + getRenderedPage = (page: number) => { + if (this._isPage[page] !== "page") { + this._isPage[page] = "page"; + this._visibleElements[page] = ( <Page pdf={this.props.pdf} - page={i} - numPages={numPages} - key={`${this.props.pdf ? this.props.pdf.fingerprint + `-page${i + 1}` : "undefined"}`} - name={`${this.props.pdf ? this.props.pdf.fingerprint + `-page${i + 1}` : "undefined"}`} + page={page} + numPages={this.props.pdf!.numPages} + key={`${this.props.pdf ? this.props.pdf.fingerprint + `-page${page + 1}` : "undefined"}`} + name={`${this.props.pdf ? this.props.pdf.fingerprint + `-page${page + 1}` : "undefined"}`} pageLoaded={this.pageLoaded} parent={this.props.parent} - renderAnnotations={this.renderAnnotations} makePin={this.createPinAnnotation} + renderAnnotations={this.renderAnnotations} createAnnotation={this.createAnnotation} sendAnnotations={this.receiveAnnotations} makeAnnotationDocuments={this.makeAnnotationDocument} receiveAnnotations={this.sendAnnotations} {...this.props} /> - ) : - (<div key={`pdfviewer-placeholder-${i}`} className="pdfviewer-placeholder" style={{ width: this._pageSizes[i] ? this._pageSizes[i].width : 612 * scale, height: this._pageSizes[i] ? this._pageSizes[i].height : 792 * scale }} />) ); - let arr = Array.from(Array(numPages).keys()).map(i => i < 5); - this._visibleElements.push(...divs); - this._isPage.push(...arr); } + } - // if nothing changed, return - if (startIndex === this._startIndex && endIndex === this._endIndex && !forceRender) { - return; + // change the address to be the file address of the PNG version of each page + // file address of the pdf + @action + getPageImage = async (page: number) => { + let handleError = () => this.getRenderedPage(page); + if (this._isPage[page] != "image") { + this._isPage[page] = "image"; + const address = this.props.url; + let res = JSON.parse(await rp.get(DocServer.prepend(`/thumbnail${address.substring("files/".length, address.length - ".pdf".length)}-${page + 1}.PNG`))); + runInAction(() => this._visibleElements[page] = <img key={res.path} src={res.path} onError={handleError} + style={{ width: `${parseInt(res.width) * scale}px`, height: `${parseInt(res.height) * scale}px` }} />); } + } - // unrender pages outside of the pdf by replacing them with empty stand-in divs - for (let i = 0; i < numPages; i++) { - if (i < startIndex || i > endIndex) { - if (this._isPage[i]) { - this._visibleElements[i] = ( - <div key={`pdfviewer-placeholder-${i}`} className="pdfviewer-placeholder" style={{ width: this._pageSizes[i] ? this._pageSizes[i].width : 0, height: this._pageSizes[i] ? this._pageSizes[i].height : 0 }} /> - ); + @computed get scrollY(): number { return this.props.scrollY; } + + @computed get startIndex(): number { return Math.max(0, this.getPageFromScroll(this.scrollY) - this._pageBuffer); } + + @computed get endIndex(): number { + let width = this._pageSizes.map(i => i ? i.width : 0); + return Math.min(this.props.pdf ? this.props.pdf.numPages - 1 : 0, this.getPageFromScroll(this.scrollY + Math.max(...width)) + this._pageBuffer); + } + + /** + * @param startIndex: where to start rendering pages + * @param endIndex: where to end rendering pages + * @param forceRender: (optional), force pdfs to re-render, even if the page already exists + */ + @action + renderPages = (startIndex: number, endIndex: number, forceRender: boolean = false) => { + if (this.props.pdf) { + // unrender pages outside of the pdf by replacing them with empty stand-in divs + for (let i = 0; i < this.props.pdf.numPages; i++) { + if (i < startIndex || i > endIndex) { + this.getPlaceholderPage(i); + } else { + if (this.props.parent.props.active()) { + this.getRenderedPage(i); + } else { + this.getPageImage(i); + } } - this._isPage[i] = false; } } + } - // render pages for any indices that don't already have pages (force rerender will make these render regardless) - for (let i = startIndex; i <= endIndex; i++) { - if (!this._isPage[i] || (this._isPage[i] && forceRender)) { - this._visibleElements[i] = ( - <Page - pdf={this.props.pdf} - page={i} - numPages={numPages} - key={`${this.props.pdf ? this.props.pdf.fingerprint + `-page${i + 1}` : "undefined"}`} - name={`${this.props.pdf ? this.props.pdf.fingerprint + `-page${i + 1}` : "undefined"}`} - pageLoaded={this.pageLoaded} - parent={this.props.parent} - makePin={this.createPinAnnotation} - renderAnnotations={this.renderAnnotations} - createAnnotation={this.createAnnotation} - sendAnnotations={this.receiveAnnotations} - makeAnnotationDocuments={this.makeAnnotationDocument} - receiveAnnotations={this.sendAnnotations} - {...this.props} /> - ); - this._isPage[i] = true; - } + @action + private renderAnnotations = (annotations: Doc[], removeOldAnnotations: boolean): void => { + if (removeOldAnnotations) { + this._annotations = annotations; + } + else { + this._annotations.push(...annotations); + this._annotations = new Array<Doc>(...this._annotations); } - - this._startIndex = startIndex; - this._endIndex = endIndex; - - return; } @action @@ -392,7 +311,7 @@ class Viewer extends React.Component<IViewerProps> { let pinAnno = new Doc(); pinAnno.x = x; - pinAnno.y = y + this.getPageHeight(page); + pinAnno.y = y + this.getScrollFromPage(page); pinAnno.width = pinAnno.height = PinRadius; pinAnno.page = page; pinAnno.target = targetDoc; @@ -411,9 +330,7 @@ class Viewer extends React.Component<IViewerProps> { } // get the page index that the vertical offset passed in is on - getIndex = (vOffset: number) => { - // if (this._loaded) { - let numPages = this.props.pdf ? this.props.pdf.numPages : 0; + getPageFromScroll = (vOffset: number) => { let index = 0; let currOffset = vOffset; while (index < this._pageSizes.length && currOffset - (this._pageSizes[index] ? this._pageSizes[index].height : 792 * scale) > 0) { @@ -421,34 +338,10 @@ class Viewer extends React.Component<IViewerProps> { index++; } return index; - // } - return 0; } - /** - * Called by the Page class when it gets rendered, initializes the lists and - * puts a placeholder with all of the correct page sizes when all of the pages have been loaded. - */ - @action - pageLoaded = (index: number, page: Pdfjs.PDFPageViewport): void => { - if (this._loaded) { - return; - } - let numPages = this.props.pdf ? this.props.pdf.numPages : 0; - this.props.loaded(page.width, page.height, numPages); - this._pageSizes[index - 1] = { width: page.width, height: page.height }; - this._pagesLoaded++; - if (this._pagesLoaded === numPages) { - this._loaded = true; - let divs = Array.from(Array(numPages).keys()).map(i => ( - <div key={`pdfviewer-placeholder-${i}`} className="pdfviewer-placeholder" style={{ width: this._pageSizes[i] ? this._pageSizes[i].width : 0, height: this._pageSizes[i] ? this._pageSizes[i].height : 0 }} /> - )); - this._visibleElements = new Array<JSX.Element>(...divs); - this.renderPages(this.startIndex, this.endIndex, true); - } - } - getPageHeight = (index: number): number => { + getScrollFromPage = (index: number): number => { let counter = 0; if (this.props.pdf && index < this.props.pdf.numPages) { for (let i = 0; i < index; i++) { @@ -463,7 +356,7 @@ class Viewer extends React.Component<IViewerProps> { createAnnotation = (div: HTMLDivElement, page: number) => { if (this._annotationLayer.current) { if (div.style.top) { - div.style.top = (parseInt(div.style.top) + this.getPageHeight(page)).toString(); + div.style.top = (parseInt(div.style.top) + this.getScrollFromPage(page)).toString(); } this._annotationLayer.current.append(div); let savedPage = this._savedAnnotations.getValue(page); @@ -494,7 +387,6 @@ class Viewer extends React.Component<IViewerProps> { } render() { - trace(); return ( <div ref={this.mainCont} style={{ pointerEvents: "all" }}> <div className="viewer"> |