import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import * as Pdfjs from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; import * as rp from "request-promise"; import { Dictionary } from "typescript-collections"; import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc"; import { Id } from "../../../new_fields/FieldSymbols"; import { List } from "../../../new_fields/List"; import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../new_fields/Types"; import { emptyFunction } from "../../../Utils"; import { DocServer } from "../../DocServer"; import { Docs, DocUtils, DocumentOptions } from "../../documents/Documents"; import { DocumentManager } from "../../util/DocumentManager"; import { DragManager } from "../../util/DragManager"; import { DocumentView } from "../nodes/DocumentView"; import { PDFBox, handleBackspace } from "../nodes/PDFBox"; import Page from "./Page"; import "./PDFViewer.scss"; import React = require("react"); import PDFMenu from "./PDFMenu"; import { UndoManager } from "../../util/UndoManager"; import { CompileScript, CompiledScript, CompileResult } from "../../util/Scripting"; import { ScriptField } from "../../../new_fields/ScriptField"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import Annotation from "./Annotation"; import { KeyCodes } from "../../northstar/utils/KeyCodes"; const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer"); export const scale = 2; interface IPDFViewerProps { url: string; loaded: (nw: number, nh: number, np: number) => void; scrollY: number; parent: PDFBox; } /** * Wrapper that loads the PDF and cascades the pdf down */ @observer export class PDFViewer extends React.Component { @observable _pdf: Opt; private _mainDiv = React.createRef(); @action componentDidMount() { Pdfjs.getDocument(this.props.url).promise.then(pdf => runInAction(() => this._pdf = pdf)); } render() { return (
{!this._pdf ? (null) : }
); } } interface IViewerProps { pdf: Pdfjs.PDFDocumentProxy; loaded: (nw: number, nh: number, np: number) => void; scrollY: number; parent: PDFBox; mainCont: React.RefObject; url: string; } /** * Handles rendering and virtualization of the pdf */ @observer export class Viewer extends React.Component { // _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: string[] = []; @observable private _pageSizes: { width: number, height: number }[] = []; @observable private _annotations: Doc[] = []; @observable private _savedAnnotations: Dictionary = new Dictionary(); @observable private _script: CompileResult | undefined; @observable private _searching: boolean = false; @observable public Index: number = -1; private _pageBuffer: number = 1; private _annotationLayer: React.RefObject = React.createRef(); private _reactionDisposer?: IReactionDisposer; private _annotationReactionDisposer?: IReactionDisposer; private _dropDisposer?: DragManager.DragDropDisposer; private _filterReactionDisposer?: IReactionDisposer; private _activeReactionDisposer?: IReactionDisposer; private _viewer: React.RefObject; private _mainCont: React.RefObject; private _pdfViewer: any; // private _textContent: Pdfjs.TextContent[] = []; private _pdfFindController: any; private _searchString: string = ""; private _rendered: boolean = false; private _pageIndex: number = -1; private _matchIndex: number = 0; constructor(props: IViewerProps) { super(props); let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField); this._script = scriptfield ? scriptfield.script : CompileScript("return true"); this._viewer = React.createRef(); this._mainCont = React.createRef(); } componentDidUpdate = (prevProps: IViewerProps) => { if (this.scrollY !== prevProps.scrollY) { this.renderPages(); } } @action componentDidMount = () => { this._reactionDisposer = reaction( () => [this.props.parent.props.active(), this.startIndex, this._pageSizes.length ? this.endIndex : 0], async () => { await this.initialLoad(); this.renderPages(); }, { fireImmediately: true }); this._annotationReactionDisposer = reaction( () => { return this.props.parent && this.props.parent.fieldExtensionDoc && DocListCast(this.props.parent.fieldExtensionDoc.annotations); }, (annotations: Doc[]) => { annotations && annotations.length && this.renderAnnotations(annotations, true); }, { fireImmediately: true }); this._activeReactionDisposer = reaction( () => this.props.parent.props.active(), () => { runInAction(() => { if (!this.props.parent.props.active()) { this._searching = false; this._pdfFindController = null; if (this._viewer.current) { let cns = this._viewer.current.childNodes; for (let i = cns.length - 1; i >= 0; i--) { cns.item(i).remove(); } } } }); } ); if (this.props.parent.props.ContainingCollectionView) { this._filterReactionDisposer = reaction( () => this.props.parent.Document.filterScript, () => { runInAction(() => { let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField); this._script = scriptfield ? scriptfield.script : CompileScript("return true"); if (this.props.parent.props.ContainingCollectionView) { let fieldDoc = Doc.resolvedFieldDataDoc(this.props.parent.props.ContainingCollectionView.props.DataDoc ? this.props.parent.props.ContainingCollectionView.props.DataDoc : this.props.parent.props.ContainingCollectionView.props.Document, this.props.parent.props.ContainingCollectionView.props.fieldKey, "true"); let ccvAnnos = DocListCast(fieldDoc.annotations); ccvAnnos.forEach(d => { if (this._script && this._script.compiled) { let run = this._script.run(d); if (run.success) { d.opacity = run.result ? 1 : 0; } } }); } this.Index = -1; }); } ); } if (this._mainCont.current) { this._dropDisposer = this._mainCont.current && DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } }); } } componentWillUnmount = () => { this._reactionDisposer && this._reactionDisposer(); this._annotationReactionDisposer && this._annotationReactionDisposer(); this._filterReactionDisposer && this._filterReactionDisposer(); this._dropDisposer && this._dropDisposer(); } scrollTo(y: number) { this.props.parent.scrollTo(y); } @action initialLoad = async () => { if (this._pageSizes.length === 0) { let pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages); this._isPage = Array(this.props.pdf.numPages); // this._textContent = Array(this.props.pdf.numPages); const proms: Pdfjs.PDFPromise[] = []; for (let i = 0; i < this.props.pdf.numPages; i++) { proms.push(this.props.pdf.getPage(i + 1).then(page => runInAction(() => { pageSizes[i] = { width: (page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]) * scale, height: (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]) * scale }; // let x = page.getViewport(scale); // page.getTextContent().then((text: Pdfjs.TextContent) => { // // let tc = new Pdfjs.TextContentItem() // // let tc = {str: } // this._textContent[i] = text; // // text.items.forEach(t => { // // tcStr += t.str; // // }) // }); // pageSizes[i] = { width: x.width, height: x.height }; }))); } await Promise.all(proms); runInAction(() => Array.from(Array((this._pageSizes = pageSizes).length).keys()).map(this.getPlaceholderPage)); this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages); // this.props.loaded(Math.max(...pageSizes.map(i => i.width)), pageSizes[0].height, this.props.pdf.numPages); let startY = NumCast(this.props.parent.Document.startY); let ccv = this.props.parent.props.ContainingCollectionView; if (ccv) { ccv.props.Document.panY = startY; } this.props.parent.Document.scrollY = 0; this.props.parent.Document.scrollY = startY + 1; } } @action makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string): Doc => { let annoDocs: Doc[] = []; let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {}); mainAnnoDoc.title = "Annotation on " + StrCast(this.props.parent.Document.title); mainAnnoDoc.pdfDoc = this.props.parent.props.Document; let minY = Number.MAX_VALUE; this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => { for (let anno of value) { let annoDoc = new Doc(); if (anno.style.left) annoDoc.x = parseInt(anno.style.left) / scale; if (anno.style.top) { annoDoc.y = parseInt(anno.style.top) / scale; minY = Math.min(parseInt(anno.style.top), minY); } if (anno.style.height) annoDoc.height = parseInt(anno.style.height) / scale; if (anno.style.width) annoDoc.width = parseInt(anno.style.width) / scale; annoDoc.page = key; annoDoc.target = sourceDoc; annoDoc.group = mainAnnoDoc; annoDoc.color = color; annoDoc.type = AnnotationTypes.Region; annoDocs.push(annoDoc); anno.remove(); } }); mainAnnoDoc.y = Math.max(minY, 0); mainAnnoDoc.annotations = new List(annoDocs); if (sourceDoc) { DocUtils.MakeLink(sourceDoc, mainAnnoDoc, undefined, `Annotation from ${StrCast(this.props.parent.Document.title)}`, "", StrCast(this.props.parent.Document.title)); } this._savedAnnotations.clear(); this.Index = -1; return mainAnnoDoc; } drop = async (e: Event, de: DragManager.DropEvent) => { if (de.data instanceof DragManager.LinkDragData) { let sourceDoc = de.data.linkSourceDocument; let destDoc = this.makeAnnotationDocument(sourceDoc, 1, "red"); let targetAnnotations = DocListCast(this.props.parent.fieldExtensionDoc.annotations); if (targetAnnotations) { targetAnnotations.push(destDoc); this.props.parent.fieldExtensionDoc.annotations = new List(targetAnnotations); } else { this.props.parent.fieldExtensionDoc.annotations = new List([destDoc]); } e.stopPropagation(); } } /** * 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 => { this.props.loaded(page.width, page.height, this.props.pdf.numPages); } @action getPlaceholderPage = (page: number) => { if (this._isPage[page] !== "none") { this._isPage[page] = "none"; this._visibleElements[page] = (
); } } @action getRenderedPage = (page: number) => { if (this._isPage[page] !== "page") { this._isPage[page] = "page"; this._visibleElements[page] = ( ); } } // 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; try { 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] = ); } catch (e) { } } } @computed get scrollY(): number { return this.props.scrollY; } // startIndex: where to start rendering pages @computed get startIndex(): number { return Math.max(0, this.getPageFromScroll(this.scrollY) - this._pageBuffer); } // endIndex: where to end rendering pages @computed get endIndex(): number { return Math.min(this.props.pdf.numPages - 1, this.getPageFromScroll(this.scrollY + this._pageSizes[0].height) + this._pageBuffer); } @action renderPages = () => { for (let i = 0; i < this.props.pdf.numPages; i++) { if (i < this.startIndex || i > this.endIndex) { this.getPlaceholderPage(i); // pages outside of the pdf use empty stand-in divs } else { if (this.props.parent.props.active()) { this.getRenderedPage(i); } else { this.getPageImage(i); } } } } @action private renderAnnotations = (annotations: Doc[], removeOldAnnotations: boolean): void => { if (removeOldAnnotations) { this._annotations = annotations; } else { this._annotations.push(...annotations); this._annotations = new Array(...this._annotations); } } @action receiveAnnotations = (annotations: HTMLDivElement[], page: number) => { if (page === -1) { this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove())); this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, annotations)); } else { this._savedAnnotations.setValue(page, annotations); } } sendAnnotations = (page: number): HTMLDivElement[] | undefined => { return this._savedAnnotations.getValue(page); } // get the page index that the vertical offset passed in is on getPageFromScroll = (vOffset: number) => { let index = 0; let currOffset = vOffset; while (index < this._pageSizes.length && currOffset - this._pageSizes[index].height > 0) { currOffset -= this._pageSizes[index++].height; } return index; } getScrollFromPage = (index: number): number => { let counter = 0; for (let i = 0; i < Math.min(this.props.pdf.numPages, index); i++) { counter += this._pageSizes[i].height; } return counter; } createAnnotation = (div: HTMLDivElement, page: number) => { if (this._annotationLayer.current) { if (div.style.top) { div.style.top = (parseInt(div.style.top) + this.getScrollFromPage(page)).toString(); } this._annotationLayer.current.append(div); let savedPage = this._savedAnnotations.getValue(page); if (savedPage) { savedPage.push(div); this._savedAnnotations.setValue(page, savedPage); } else { this._savedAnnotations.setValue(page, [div]); } } } renderAnnotation = (anno: Doc, index: number): JSX.Element => { return ; } @action pointerDown = () => { // this._searching = false; } @action search = (searchString: string) => { if (this._pdfViewer._pageViewsReady) { this._pdfFindController.executeCommand('find', { caseSensitive: false, findPrevious: undefined, highlightAll: true, phraseSearch: true, query: searchString }); } else { let container = this._mainCont.current; if (container) { container.addEventListener("pagesloaded", () => { console.log("rendered"); this._pdfFindController.executeCommand('find', { caseSensitive: false, findPrevious: undefined, highlightAll: true, phraseSearch: true, query: searchString }); this._rendered = true; }); container.addEventListener("pagerendered", () => { console.log("rendered"); this._pdfFindController.executeCommand('find', { caseSensitive: false, findPrevious: undefined, highlightAll: true, phraseSearch: true, query: searchString }); this._rendered = true; }); } } // let viewer = this._viewer.current; // if (!this._pdfFindController) { // if (container && viewer) { // let simpleLinkService = new SimpleLinkService(); // let pdfViewer = new PDFJSViewer.PDFViewer({ // container: container, // viewer: viewer, // linkService: simpleLinkService // }); // simpleLinkService.setPdf(this.props.pdf); // container.addEventListener("pagesinit", () => { // pdfViewer.currentScaleValue = 1; // }); // container.addEventListener("pagerendered", () => { // console.log("rendered"); // this._pdfFindController.executeCommand('find', // { // caseSensitive: false, // findPrevious: undefined, // highlightAll: true, // phraseSearch: true, // query: searchString // }); // }); // pdfViewer.setDocument(this.props.pdf); // this._pdfFindController = new PDFJSViewer.PDFFindController(pdfViewer); // // this._pdfFindController._linkService = pdfLinkService; // pdfViewer.findController = this._pdfFindController; // } // } // else { // this._pdfFindController.executeCommand('find', // { // caseSensitive: false, // findPrevious: undefined, // highlightAll: true, // phraseSearch: true, // query: searchString // }); // } } searchStringChanged = (e: React.ChangeEvent) => { this._searchString = e.currentTarget.value; } @action toggleSearch = (e: React.MouseEvent) => { e.stopPropagation(); this._searching = !this._searching; if (this._searching) { let container = this._mainCont.current; let viewer = this._viewer.current; if (!this._pdfFindController) { if (container && viewer) { let simpleLinkService = new SimpleLinkService(); this._pdfViewer = new PDFJSViewer.PDFViewer({ container: container, viewer: viewer, linkService: simpleLinkService }); simpleLinkService.setPdf(this.props.pdf); container.addEventListener("pagesinit", () => { this._pdfViewer.currentScaleValue = 1; }); container.addEventListener("pagerendered", () => { console.log("rendered"); this._rendered = true; }); this._pdfViewer.setDocument(this.props.pdf); this._pdfFindController = new PDFJSViewer.PDFFindController(this._pdfViewer); // this._pdfFindController._linkService = pdfLinkService; this._pdfViewer.findController = this._pdfFindController; } } } else { this._pdfFindController = null; if (this._viewer.current) { let cns = this._viewer.current.childNodes; for (let i = cns.length - 1; i >= 0; i--) { cns.item(i).remove(); } } } } @action prevAnnotation = (e: React.MouseEvent) => { e.stopPropagation(); // if (this.Index > 0) { // this.Index--; // } this.Index = Math.max(this.Index - 1, 0); } @action nextAnnotation = (e: React.MouseEvent) => { e.stopPropagation(); let compiled = this._script; let filtered = this._annotations.filter(anno => { if (compiled && compiled.compiled) { let run = compiled.run({ this: anno }); if (run.success) { return run.result; } } return true; }); this.Index = Math.min(this.Index + 1, filtered.length - 1); } nextResult = () => { // if (this._viewer.current) { // let results = this._pdfFindController.pageMatches; // if (results && results.length) { // if (this._pageIndex === this.props.pdf.numPages && this._matchIndex === results[this._pageIndex].length - 1) { // return; // } // if (this._pageIndex === -1 || this._matchIndex === results[this._pageIndex].length - 1) { // this._matchIndex = 0; // this._pageIndex++; // } // else { // this._matchIndex++; // } // this._pdfFindController._nextMatch() // let nextMatch = this._viewer.current.children[this._pageIndex].children[1].children[results[this._pageIndex][this._matchIndex]]; // rconsole.log(nextMatch); // this.props.parent.scrollTo(nextMatch.getBoundingClientRect().top); // nextMatch.setAttribute("style", nextMatch.getAttribute("style") ? nextMatch.getAttribute("style") + ", background-color: green" : "background-color: green"); // } // } } render() { let compiled = this._script; return (
{this._visibleElements}
{this._annotations.filter(anno => { if (compiled && compiled.compiled) { let run = compiled.run({ this: anno }); if (run.success) { return run.result; } } return true; }).sort((a: Doc, b: Doc) => NumCast(a.y) - NumCast(b.y)) .map((anno: Doc, index: number) => this.renderAnnotation(anno, index))}
e.stopPropagation()} style={{ bottom: -this.props.scrollY, left: `${this._searching ? 0 : 100}%` }}> {/* */} e.keyCode === KeyCodes.ENTER ? this.search(this._searchString) : e.keyCode === KeyCodes.BACKSPACE ? e.stopPropagation() : true} placeholder="Search" className="pdfViewer-overlaySearchBar" onChange={this.searchStringChanged} />
); } } export enum AnnotationTypes { Region } class SimpleLinkService { externalLinkTarget: any = null; externalLinkRel: any = null; pdf: any = null; navigateTo(dest: any) { } getDestinationHash(dest: any) { return "#"; } getAnchorUrl(hash: any) { return "#"; } setHash(hash: any) { } executeNamedAction(action: any) { } cachePageRef(pageNum: any, pageRef: any) { } get pagesCount() { return this.pdf ? this.pdf.numPages : 0; } get page() { return 0; } setPdf(pdf: any) { this.pdf = pdf; } get rotation() { return 0; } set rotation(value: any) { } }