diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/documents/Documents.ts | 9 | ||||
-rw-r--r-- | src/client/views/nodes/DocumentContentsView.tsx | 1 | ||||
-rw-r--r-- | src/client/views/nodes/PDFBox.tsx | 363 | ||||
-rw-r--r-- | src/client/views/pdf/PDFBox2.tsx | 28 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.scss | 27 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 349 |
6 files changed, 440 insertions, 337 deletions
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index ab61b915c..be7356a09 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -33,6 +33,11 @@ import { DocServer } from "../DocServer"; import { StrokeData, InkField } from "../../new_fields/InkField"; import { dropActionType } from "../util/DragManager"; import { DateField } from "../../new_fields/DateField"; +<<<<<<< HEAD +import { PDFBox2 } from "../views/pdf/PDFBox2"; +import { schema } from "prosemirror-schema-basic"; +======= +>>>>>>> 7d3ef1c914cc1cc0b6c05b14773a8b83e1b95c96 import { UndoManager } from "../util/UndoManager"; import { RouteStore } from "../../server/RouteStore"; var requestImageSize = require('request-image-size'); @@ -174,8 +179,8 @@ export namespace Docs { return textProto; } function CreatePdfPrototype(): Doc { - let pdfProto = setupPrototypeOptions(pdfProtoId, "PDF_PROTO", CollectionPDFView.LayoutString("annotations"), - { x: 0, y: 0, nativeWidth: 1200, width: 300, backgroundLayout: PDFBox.LayoutString(), curPage: 1 }); + let pdfProto = setupPrototypeOptions(pdfProtoId, "PDF_PROTO", PDFBox.LayoutString(), + { x: 0, y: 0, width: 300, height: 300, backgroundLayout: PDFBox.LayoutString(), curPage: 1 }); return pdfProto; } function CreateWebPrototype(): Doc { diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 02396c3af..c2caabb92 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -23,6 +23,7 @@ import { FieldViewProps } from "./FieldView"; import { Without, OmitKeys } from "../../../Utils"; import { Cast, StrCast, NumCast } from "../../../new_fields/Types"; import { List } from "../../../new_fields/List"; +import { PDFBox2 } from "../pdf/PDFBox2"; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? type BindingProps = Without<FieldViewProps, 'fieldKey'>; diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index aa29a7170..83f69f7f9 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -4,12 +4,8 @@ import { observer } from "mobx-react"; import 'react-image-lightbox/style.css'; import Measure from "react-measure"; //@ts-ignore -import { Document, Page } from "react-pdf"; -import 'react-pdf/dist/Page/AnnotationLayer.css'; -import { Id } from "../../../new_fields/FieldSymbols"; -import { makeInterface } from "../../../new_fields/Schema"; -import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; -import { ImageField, PdfField } from "../../../new_fields/URLField"; +// import { Document, Page } from "react-pdf"; +// import 'react-pdf/dist/Page/AnnotationLayer.css'; import { RouteStore } from "../../../server/RouteStore"; import { Utils } from '../../../Utils'; import { DocServer } from "../../DocServer"; @@ -23,7 +19,12 @@ import { pageSchema } from "./ImageBox"; import "./PDFBox.scss"; var path = require('path'); import React = require("react"); -import { ContextMenu } from "../ContextMenu"; +import { SelectionManager } from "../../util/SelectionManager"; +import { Cast, FieldValue, NumCast } from "../../../new_fields/Types"; +import { Opt, HeightSym, Doc } from "../../../new_fields/Doc"; +import { makeInterface } from "../../../new_fields/Schema"; +import { ImageField, PdfField } from "../../../new_fields/URLField"; +import { PDFViewer } from "../pdf/PDFViewer"; /** ALSO LOOK AT: Annotation.tsx, Sticky.tsx * This method renders PDF and puts all kinds of functionalities such as annotation, highlighting, @@ -55,349 +56,41 @@ const PdfDocument = makeInterface(positionSchema, pageSchema); export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) { public static LayoutString() { return FieldView.LayoutString(PDFBox); } - private _mainDiv = React.createRef<HTMLDivElement>(); - private renderHeight = 2400; - - @observable private _renderAsSvg = true; @observable private _alt = false; + @observable private _scrollY: number = 0; - private _reactionDisposer?: IReactionDisposer; - - @observable private _perPageInfo: Object[] = []; //stores pageInfo - @observable private _pageInfo: any = { area: [], divs: [], anno: [] }; //divs is array of objects linked to anno - - @observable private _currAnno: any = []; - @observable private _interactive: boolean = false; - @observable private _loaded: boolean = false; - - @computed private get curPage() { return NumCast(this.Document.curPage, 1); } - @computed private get thumbnailPage() { return NumCast(this.props.Document.thumbnailPage, -1); } - - componentDidMount() { - let wasSelected = this.props.isSelected(); - this._reactionDisposer = reaction( - () => [this.props.isSelected(), this.curPage], - () => { - if (this.curPage > 0 && !this.props.isTopMost && this.curPage !== this.thumbnailPage && wasSelected && !this.props.isSelected()) { - this.saveThumbnail(); - } - wasSelected = this._interactive = this.props.isSelected(); - }, - { fireImmediately: true }); - - } - - componentWillUnmount() { - if (this._reactionDisposer) this._reactionDisposer(); - } - - /** - * highlighting helper function - */ - makeEditableAndHighlight = (colour: string) => { - var range, sel = window.getSelection(); - if (sel && sel.rangeCount && sel.getRangeAt) { - range = sel.getRangeAt(0); - } - document.designMode = "on"; - if (!document.execCommand("HiliteColor", false, colour)) { - document.execCommand("HiliteColor", false, colour); - } - - if (range && sel) { - sel.removeAllRanges(); - sel.addRange(range); - - let obj: Object = { parentDivs: [], spans: [] }; - //@ts-ignore - if (range.commonAncestorContainer.className === 'react-pdf__Page__textContent') { //multiline highlighting case - obj = this.highlightNodes(range.commonAncestorContainer.childNodes); - } else { //single line highlighting case - let parentDiv = range.commonAncestorContainer.parentElement; - if (parentDiv) { - if (parentDiv.className === 'react-pdf__Page__textContent') { //when highlight is overwritten - obj = this.highlightNodes(parentDiv.childNodes); - } else { - parentDiv.childNodes.forEach((child) => { - if (child.nodeName === 'SPAN') { - //@ts-ignore - obj.parentDivs.push(parentDiv); - //@ts-ignore - child.id = "highlighted"; - //@ts-ignore - obj.spans.push(child); - // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler - } - }); - } - } - } - this._pageInfo.divs.push(obj); - - } - document.designMode = "off"; - } - - highlightNodes = (nodes: NodeListOf<ChildNode>) => { - let temp = { parentDivs: [], spans: [] }; - nodes.forEach((div) => { - div.childNodes.forEach((child) => { - if (child.nodeName === 'SPAN') { - //@ts-ignore - temp.parentDivs.push(div); - //@ts-ignore - child.id = "highlighted"; - //@ts-ignore - temp.spans.push(child); - // child.addEventListener("mouseover", this.onEnter); //adds mouseover annotation handler - } - }); - - }); - return temp; - } - - /** - * when the cursor enters the highlight, it pops out annotation. ONLY WORKS FOR SINGLE DIV LINES - */ - @action - onEnter = (e: any) => { - let span: HTMLSpanElement = e.toElement; - let index: any; - this._pageInfo.divs.forEach((obj: any) => { - obj.spans.forEach((element: any) => { - if (element === span && !index) { - index = this._pageInfo.divs.indexOf(obj); - } - }); - }); - - if (this._pageInfo.anno.length >= index + 1) { - if (this._currAnno.length === 0) { - this._currAnno.push(this._pageInfo.anno[index]); - } - } else { - if (this._currAnno.length === 0) { //if there are no current annotation - let div = span.offsetParent; - //@ts-ignore - let divX = div.style.left; - //@ts-ignore - let divY = div.style.top; - //slicing "px" from the end - divX = divX.slice(0, divX.length - 2); //gets X of the DIV element (parent of Span) - divY = divY.slice(0, divY.length - 2); //gets Y of the DIV element (parent of Span) - let annotation = <Annotation key={Utils.GenerateGuid()} Span={span} X={divX} Y={divY - 300} Highlights={this._pageInfo.divs} Annotations={this._pageInfo.anno} CurrAnno={this._currAnno} />; - this._pageInfo.anno.push(annotation); - this._currAnno.push(annotation); - } - } - - } - - /** - * highlight function for highlighting actual text. This works fine. - */ - highlight = (color: string) => { - if (window.getSelection()) { - try { - if (!document.execCommand("hiliteColor", false, color)) { - this.makeEditableAndHighlight(color); - } - } catch (ex) { - this.makeEditableAndHighlight(color); - } - } - } - - /** - * controls the area highlighting (stickies) Kinda temporary - */ - onPointerDown = (e: React.PointerEvent) => { - if (this.props.isSelected() && !InkingControl.Instance.selectedTool && e.buttons === 1) { - if (e.altKey) { - this._alt = true; - } else { - if (e.metaKey) { - e.stopPropagation(); - } - } - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointerup", this.onPointerUp); - } - if (this.props.isSelected() && e.buttons === 2) { - runInAction(() => this._alt = true); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointerup", this.onPointerUp); + getHeight = (): number => { + if (this.props.Document) { + let doc = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + console.log(doc); + return NumCast(doc.height); } + return 0; } - /** - * controls area highlighting and partially highlighting. Kinda temporary - */ - @action - onPointerUp = (e: PointerEvent) => { - this._alt = false; - document.removeEventListener("pointerup", this.onPointerUp); - if (this.props.isSelected()) { - this.highlight("rgba(76, 175, 80, 0.3)"); //highlights to this default color. + loaded = (nw: number, nh: number) => { + if (this.props.Document) { + let doc = this.props.Document.proto ? this.props.Document.proto : this.props.Document; + doc.nativeWidth = nw; + doc.nativeHeight = nh; } - this._interactive = true; - } - - - @action - saveThumbnail = () => { - this.props.Document.thumbnailPage = FieldValue(this.Document.curPage, -1); - this._renderAsSvg = false; - setTimeout(() => { - runInAction(() => this._smallRetryCount = this._mediumRetryCount = this._largeRetryCount = 0); - let nwidth = FieldValue(this.Document.nativeWidth, 0); - let nheight = FieldValue(this.Document.nativeHeight, 0); - htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 0.8 }) - .then(action((dataUrl: string) => { - SearchBox.convertDataUri(dataUrl, "icon" + this.Document[Id] + "_" + this.curPage).then((returnedFilename) => { - if (returnedFilename) { - let url = DocServer.prepend(returnedFilename); - this.props.Document.thumbnail = new ImageField(new URL(url)); - } - runInAction(() => this._renderAsSvg = true); - }) - })) - .catch(function (error: any) { - console.error('oops, something went wrong!', error); - }); - }, 1250); } @action - onLoaded = (page: any) => { - // bcz: the number of pages should really be set when the document is imported. - this.props.Document.numPages = page._transport.numPages; - if (this._perPageInfo.length === 0) { //Makes sure it only runs once - this._perPageInfo = [...Array(page._transport.numPages)]; + onScroll = (e: React.UIEvent<HTMLDivElement>) => { + if (e.currentTarget) { + this._scrollY = e.currentTarget.scrollTop; } - this._loaded = true; } - @action - setScaling = (r: any) => { - // bcz: the nativeHeight should really be set when the document is imported. - // also, the native dimensions could be different for different pages of the canvas - // so this design is flawed. - var nativeWidth = FieldValue(this.Document.nativeWidth, 0); - if (!FieldValue(this.Document.nativeHeight, 0)) { - var nativeHeight = nativeWidth * r.offset.height / r.offset.width; - this.props.Document.height = nativeHeight / nativeWidth * FieldValue(this.Document.width, 0); - this.props.Document.nativeHeight = nativeHeight; - } - } - @computed - get pdfPage() { - return <Page height={this.renderHeight} renderTextLayer={false} pageNumber={this.curPage} onLoadSuccess={this.onLoaded} />; - } - @computed - get pdfContent() { - let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField); - if (!pdfUrl) { - return <p>No pdf url to render</p>; - } - let pdfpage = this.pdfPage; - let body = this.Document.nativeHeight ? - pdfpage : - <Measure offset onResize={this.setScaling}> - {({ measureRef }) => - <div className="pdfBox-page" ref={measureRef}> - {pdfpage} - </div> - } - </Measure>; - let xf = (this.Document.nativeHeight || 0) / this.renderHeight; - return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}> - <Document file={window.origin + RouteStore.corsProxy + `/${pdfUrl.url}`} renderMode={this._renderAsSvg || this.props.isTopMost ? "svg" : "canvas"}> - {body} - </Document> - </div >; - } - - @computed - get pdfRenderer() { - let pdfUrl = Cast(this.props.Document[this.props.fieldKey], PdfField); - let proxy = this.imageProxyRenderer; - if ((!this._interactive && proxy && (!this.props.ContainingCollectionView || !this.props.ContainingCollectionView.props.isTopMost)) || !pdfUrl) { - return proxy; - } - return [ - proxy, - this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element), - this._currAnno.map((element: any) => element), - this.pdfContent - ]; - } - - choosePath(url: URL) { - if (url.protocol === "data" || url.href.indexOf(window.location.origin) === -1) - return url.href; - let ext = path.extname(url.href); - return url.href.replace(ext, this._curSuffix + ext); - } - @observable _smallRetryCount = 1; - @observable _mediumRetryCount = 1; - @observable _largeRetryCount = 1; - @action retryPath = () => { - if (this._curSuffix === "_s") this._smallRetryCount++; - if (this._curSuffix === "_m") this._mediumRetryCount++; - if (this._curSuffix === "_l") this._largeRetryCount++; - } - @action onError = () => { - let timeout = this._curSuffix === "_s" ? this._smallRetryCount : this._curSuffix === "_m" ? this._mediumRetryCount : this._largeRetryCount; - if (timeout < 10) - setTimeout(this.retryPath, Math.min(10000, timeout * 5)); - } - _curSuffix = "_m"; - - @computed - get imageProxyRenderer() { - let thumbField = this.props.Document.thumbnail; - if (thumbField && this._renderAsSvg && NumCast(this.props.Document.thumbnailPage, 0) === this.Document.curPage) { - - // let transform = this.props.ScreenToLocalTransform().inverse(); - let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50; - // var [sptX, sptY] = transform.transformPoint(0, 0); - // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight()); - // let w = bptX - sptX; - - let path = thumbField instanceof ImageField ? thumbField.url.href : "http://cs.brown.edu/people/bcz/prairie.jpg"; - // this._curSuffix = ""; - // if (w > 20) { - let field = thumbField; - // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s"; - // else if (w < 400 && this._mediumRetryCount < 10) this._curSuffix = "_m"; - // else if (this._largeRetryCount < 10) this._curSuffix = "_l"; - if (field instanceof ImageField) path = this.choosePath(field.url); - // } - return <img className="pdfBox-thumbnail" key={path + (this._mediumRetryCount).toString()} src={path} onError={this.onError} />; - } - return (null); - } - @action onKeyDown = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = true); - @action onKeyUp = (e: React.KeyboardEvent) => e.key === "Alt" && (this._alt = false); - onContextMenu = (e: React.MouseEvent): void => { - let field = Cast(this.Document[this.props.fieldKey], PdfField); - if (field) { - let url = field.url.href; - ContextMenu.Instance.addItem({ - description: "Copy path", event: () => { - Utils.CopyText(url); - }, icon: "expand-arrows-alt" - }); - } - } render() { + trace(); + const pdfUrl = window.origin + RouteStore.corsProxy + "/https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf"; let classname = "pdfBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : ""); return ( - <div className={classname} tabIndex={0} ref={this._mainDiv} onPointerDown={this.onPointerDown} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onContextMenu={this.onContextMenu} > - {this.pdfRenderer} - </div > + <div onScroll={this.onScroll} style={{ overflow: "scroll", height: `${NumCast(this.props.Document.nativeWidth ? this.props.Document.nativeWidth : 300)}px` }} onWheel={(e: React.WheelEvent) => e.stopPropagation()} className={classname}> + <PDFViewer url={pdfUrl} loaded={this.loaded} scrollY={this._scrollY} parent={this} /> + </div> ); } diff --git a/src/client/views/pdf/PDFBox2.tsx b/src/client/views/pdf/PDFBox2.tsx new file mode 100644 index 000000000..71825c260 --- /dev/null +++ b/src/client/views/pdf/PDFBox2.tsx @@ -0,0 +1,28 @@ +import React = require("react"); +import { FieldViewProps, FieldView } from "../nodes/FieldView"; +import { DocComponent } from "../DocComponent"; +import { makeInterface } from "../../../new_fields/Schema"; +import { positionSchema } from "../nodes/DocumentView"; +import { pageSchema } from "../nodes/ImageBox"; +import { PDFViewer } from "./PDFViewer"; +import { RouteStore } from "../../../server/RouteStore"; +import { InkingControl } from "../InkingControl"; +import { observer } from "mobx-react"; +import { trace } from "mobx"; + +type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>; +const PdfDocument = makeInterface(positionSchema, pageSchema); + +@observer +export class PDFBox2 extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) { + public static LayoutString() { return FieldView.LayoutString(PDFBox2); } + + render() { + trace(); + const pdfUrl = "https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf"; + let classname = "pdfBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool); + return ( + <PDFViewer url={pdfUrl} /> + ) + } +}
\ No newline at end of file diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss new file mode 100644 index 000000000..9d41a1bb0 --- /dev/null +++ b/src/client/views/pdf/PDFViewer.scss @@ -0,0 +1,27 @@ +.textLayer { + div { + user-select: text; + } +} + +.viewer-button-cont { + position: absolute; + display: flex; + justify-content: space-evenly; + align-items: center; +} + +.viewer-previousPage, +.viewer-nextPage { + background: grey; + font-weight: bold; + opacity: 0.5; + padding: 0 10px; + border-radius: 5px; +} + +.textLayer { + user-select: auto; +} + +.viewer {}
\ No newline at end of file diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx new file mode 100644 index 000000000..d510ba91c --- /dev/null +++ b/src/client/views/pdf/PDFViewer.tsx @@ -0,0 +1,349 @@ +import { observer } from "mobx-react"; +import React = require("react"); +import { observable, action, runInAction, computed, IReactionDisposer, reaction } from "mobx"; +import { RouteStore } from "../../../server/RouteStore"; +import * as Pdfjs from "pdfjs-dist"; +import * as htmlToImage from "html-to-image"; +import { Opt } from "../../../new_fields/Doc"; +import "./PDFViewer.scss"; +import "pdfjs-dist/web/pdf_viewer.css"; +import { number } from "prop-types"; +import { JSXElement } from "babel-types"; +import { PDFBox } from "../nodes/PDFBox"; +import { NumCast, FieldValue } from "../../../new_fields/Types"; +import { SearchBox } from "../SearchBox"; +import { Utils } from "../../../Utils"; + +interface IPDFViewerProps { + url: string; + loaded: (nw: number, nh: number) => void; + scrollY: number; + parent: PDFBox; +} + +@observer +export class PDFViewer extends React.Component<IPDFViewerProps> { + @observable _pdf: Opt<Pdfjs.PDFDocumentProxy>; + + @action + componentDidMount() { + // const pdfUrl = window.origin + RouteStore.corsProxy + "/https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf"; + const pdfUrl = this.props.url; + let promise = Pdfjs.getDocument(pdfUrl).promise; + + promise.then((pdf: Pdfjs.PDFDocumentProxy) => { + runInAction(() => this._pdf = pdf); + }); + } + + render() { + return ( + <div> + <Viewer pdf={this._pdf} loaded={this.props.loaded} scrollY={this.props.scrollY} parent={this.props.parent} /> + </div> + ); + } +} + +interface IViewerProps { + pdf: Opt<Pdfjs.PDFDocumentProxy>; + loaded: (nw: number, nh: number) => void; + scrollY: number; + parent: PDFBox; +} + +@observer +class Viewer extends React.Component<IViewerProps> { + @observable.shallow private _visibleElements: JSX.Element[] = []; + @observable private _isPage: boolean[] = []; + @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 _renderAsSvg = true; + + private _pageBuffer: number = 1; + private _reactionDisposer?: IReactionDisposer; + private _mainDiv = React.createRef<HTMLDivElement>(); + + @computed private get thumbnailPage() { return NumCast(this.props.parent.Document.thumbnailPage, -1); } + + componentDidMount() { + let wasSelected = this.props.parent.props.isSelected(); + this._reactionDisposer = reaction( + () => [this.props.parent.props.isSelected(), this.startIndex], + () => { + if (this.startIndex > 0 && !this.props.parent.props.isTopMost && this.startIndex !== this.thumbnailPage && wasSelected && !this.props.parent.props.isSelected()) { + this.saveThumbnail(); + } + wasSelected = this.props.parent.props.isSelected(); + }, + { fireImmediately: true } + ); + let numPages = this.props.pdf ? this.props.pdf.numPages : 0; + this.renderPages(0, numPages - 1, true); + } + + saveThumbnail = () => { + this.props.parent.props.Document.thumbnailPage = FieldValue(this.props.parent.Document.curPage, -1); + this._renderAsSvg = false; + setTimeout(() => { + let nwidth = FieldValue(this.props.parent.Document.nativeWidth, 0); + let nheight = FieldValue(this.props.parent.Document.nativeHeight, 0); + htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 0.8 }) + .then(action((dataUrl: string) => { + })); + }, 1250); + } + + @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.width); + 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; + this.renderPages(this.startIndex, this.endIndex); + } + } + + @action + renderPages = (startIndex: number, endIndex: number, forceRender: boolean = false) => { + let numPages = this.props.pdf ? this.props.pdf.numPages : 0; + + if (this._visibleElements.length !== numPages) { + let divs = Array.from(Array(numPages).keys()).map(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} + {...this.props} /> + )); + let arr = Array.from(Array(numPages).keys()).map(i => false); + this._visibleElements.push(...divs); + this._isPage.push(...arr); + } + + if (startIndex === this._startIndex && endIndex === this._endIndex && !forceRender) { + return; + } + + for (let i = startIndex; i <= endIndex; i++) { + if (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} + {...this.props} /> + ); + this._isPage[i] = true; + } + else if (!this._isPage[i]) { + 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} + {...this.props} /> + ); + this._isPage[i] = true; + } + } + + 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 }} /> + ); + this._isPage[i] = false; + } + } + } + + return; + } + + getIndex = (vOffset: number) => { + if (this._loaded) { + let index = 0; + let currOffset = vOffset; + while (currOffset - this._pageSizes[index].height > 0) { + currOffset -= this._pageSizes[index].height; + index++; + } + return index; + } + return 0; + } + + @action + pageLoaded = (index: number, page: Pdfjs.PDFPageViewport): void => { + let numPages = this.props.pdf ? this.props.pdf.numPages : 0; + this.props.loaded(page.width, page.height); + if (index > this._pageSizes.length) { + this._pageSizes.push({ width: page.width, height: page.height }); + } + else { + this._pageSizes[index - 1] = { width: page.width, height: page.height }; + } + if (index === 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); + } + } + + render() { + console.log(`START: ${this.startIndex}`); + console.log(`END: ${this.endIndex}`) + let numPages = this.props.pdf ? this.props.pdf.numPages : 0; + return ( + <div className="viewer" ref={this._mainDiv}> + {/* {Array.from(Array(numPages).keys()).map((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} + {...this.props} + /> + ))} */} + {this._visibleElements} + </div> + ); + } +} + +interface IPageProps { + pdf: Opt<Pdfjs.PDFDocumentProxy>; + name: string; + numPages: number; + page: number; + pageLoaded: (index: number, page: Pdfjs.PDFPageViewport) => void; +} + +@observer +class Page extends React.Component<IPageProps> { + @observable _state: string = "N/A"; + @observable _width: number = 0; + @observable _height: number = 0; + @observable _page: Opt<Pdfjs.PDFPageProxy>; + canvas: React.RefObject<HTMLCanvasElement>; + textLayer: React.RefObject<HTMLDivElement>; + @observable _currPage: number = this.props.page + 1; + + constructor(props: IPageProps) { + super(props); + this.canvas = React.createRef(); + this.textLayer = React.createRef(); + } + + componentDidMount() { + console.log(this.props.pdf); + if (this.props.pdf) { + this.update(this.props.pdf); + } + } + + componentDidUpdate() { + if (this.props.pdf) { + this.update(this.props.pdf); + } + } + + private update = (pdf: Pdfjs.PDFDocumentProxy) => { + if (pdf) { + this.loadPage(pdf); + } + else { + this._state = "loading"; + } + } + + private loadPage = (pdf: Pdfjs.PDFDocumentProxy) => { + if (this._state === "rendering" || this._page) return; + + pdf.getPage(this._currPage).then( + (page: Pdfjs.PDFPageProxy) => { + this._state = "rendering"; + this.renderPage(page); + } + ); + } + + @action + private renderPage = (page: Pdfjs.PDFPageProxy) => { + let scale = 1; + let viewport = page.getViewport(scale); + let canvas = this.canvas.current; + if (canvas) { + let context = canvas.getContext("2d"); + canvas.width = viewport.width; + this._width = viewport.width; + canvas.height = viewport.height; + this._height = viewport.height; + this.props.pageLoaded(this._currPage, viewport); + if (context) { + page.render({ canvasContext: context, viewport: viewport }); + page.getTextContent().then((res: Pdfjs.TextContent) => { + //@ts-ignore + let textLayer = Pdfjs.renderTextLayer({ + textContent: res, + container: this.textLayer.current, + viewport: viewport + }); + // textLayer._render(); + this._state = "rendered"; + }); + + this._page = page; + } + } + } + + onPointerDown = (e: React.PointerEvent) => { + console.log("down"); + e.stopPropagation(); + } + + onPointerMove = (e: React.PointerEvent) => { + console.log("move") + e.stopPropagation(); + } + + render() { + return ( + <div onPointerDown={this.onPointerDown} onPointerMove={this.onPointerMove} className={this.props.name} style={{ "width": this._width, "height": this._height }}> + <div className="canvasContainer"> + <canvas ref={this.canvas} /> + </div> + <div className="textlayer" ref={this.textLayer} style={{ "position": "relative", "top": `-${this._height}px`, "height": `${this._height}px` }} /> + </div> + ); + } +}
\ No newline at end of file |