import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, makeObservable, 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 React from 'react'; import { ClientUtils, returnFalse, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { ComputedField } from '../../../fields/ScriptField'; import { Cast, FieldValue, ImageCast, NumCast, StrCast, toList } from '../../../fields/Types'; import { ImageField, PdfField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { DocUtils } from '../../documents/DocUtils'; import { KeyCodes } from '../../util/KeyCodes'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { CollectionFreeFormView } from '../collections/collectionFreeForm'; import { CollectionStackingView } from '../collections/CollectionStackingView'; import { ContextMenu } from '../ContextMenu'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { Colors } from '../global/globalEnums'; import { PDFViewer } from '../pdf/PDFViewer'; import { PinDocView, PinProps } from '../PinFuncs'; import { SidebarAnnos } from '../SidebarAnnos'; import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import { ImageBox } from './ImageBox'; import { OpenWhere } from './OpenWhere'; import './PDFBox.scss'; import { CreateImage } from './WebBoxRenderer'; @observer export class PDFBox extends ViewBoxAnnotatableComponent() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); } public static openSidebarWidth = 250; public static sidebarResizerWidth = 5; private _searchString: string = ''; private _initialScrollTarget: Opt; private _pdfViewer: PDFViewer | undefined; private _searchRef = React.createRef(); private _disposers: { [name: string]: IReactionDisposer } = {}; private _sidebarRef = React.createRef(); @observable private _searching: boolean = false; @observable private _pdf: Opt = undefined; @observable private _pageControls = false; @computed get pdfUrl() { return Cast(this.dataDoc[this._props.fieldKey], PdfField); } constructor(props: FieldViewProps) { super(props); makeObservable(this); const nw = Doc.NativeWidth(this.Document, this.dataDoc) || 927; const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200; !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (nh / nw)); if (this.pdfUrl) { if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) runInAction(() => { this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href); }); else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then( action(pdf => { this._pdf = pdf; }) ); } } replaceCanvases = (oldDiv: HTMLElement, newDiv: HTMLElement) => { if (oldDiv.childNodes) { for (let i = 0; i < oldDiv.childNodes.length; i++) { this.replaceCanvases(oldDiv.childNodes[i] as HTMLElement, newDiv.childNodes[i] as HTMLElement); } } if (oldDiv.className === 'pdfBox-ui' || oldDiv.className === 'pdfViewerDash-overlay-inking') { newDiv.style.display = 'none'; } if (newDiv?.style) newDiv.style.overflow = 'hidden'; if (oldDiv instanceof HTMLCanvasElement) { const canvas = oldDiv; const img = document.createElement('img'); // create a Image Element img.src = canvas.toDataURL(); // image sourcez img.style.width = canvas.style.width; img.style.height = canvas.style.height; const newCan = newDiv as HTMLCanvasElement; const parEle = newCan.parentElement as HTMLElement; parEle.removeChild(newCan); parEle.appendChild(img); } }; crop = (region: Doc | undefined, addCrop?: boolean) => { const docViewContent = this.DocumentView?.().ContentDiv; if (!region || !docViewContent) return undefined; const cropping = Doc.MakeCopy(region, true); cropping.layout_unrendered = false; // text selection have this cropping.text_inlineAnnotations = undefined; // text selections have this -- it causes them not to be rendered. cropping.backgroundColor = undefined; // text selections have this -- it causes images to be fully transparent cropping.opacity = undefined; // text selections have this -- it causes images to be fully transparent const regionData = region[DocData]; regionData.lockedPosition = true; regionData.title = 'region:' + this.Document.title; regionData.followLinkToggle = true; this.addDocument(region); const newDiv = docViewContent.cloneNode(true) as HTMLDivElement; newDiv.style.width = NumCast(this.layoutDoc._width).toString(); newDiv.style.height = NumCast(this.layoutDoc._height).toString(); this.replaceCanvases(docViewContent, newDiv); const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv); // const anchx = NumCast(cropping.x); // const anchy = NumCast(cropping.y); const anchw = NumCast(cropping._width) * (this._props.NativeDimScaling?.() || 1); const anchh = NumCast(cropping._height) * (this._props.NativeDimScaling?.() || 1); // const viewScale = 1; cropping.title = 'crop: ' + this.Document.title; cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width); cropping.y = NumCast(this.Document.y); cropping._width = anchw; cropping._height = anchh; cropping.onClick = undefined; const croppingProto = cropping[DocData]; croppingProto.annotationOn = undefined; croppingProto.isDataDoc = true; croppingProto.proto = Cast(this.Document.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO croppingProto.type = DocumentType.IMG; croppingProto.layout = ImageBox.LayoutString('data'); croppingProto.data = new ImageField(ClientUtils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')); croppingProto.data_nativeWidth = anchw; croppingProto.data_nativeHeight = anchh; if (addCrop) { DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); } this._props.bringToFront?.(cropping); CreateImage( '', document.styleSheets, htmlString, anchw, anchh, (NumCast(region.y) * this._props.PanelWidth()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']), (NumCast(region.x) * this._props.PanelWidth()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']), 4 ) .then(dataUrl => { ClientUtils.convertDataUri(dataUrl, region[Id]).then(returnedfilename => setTimeout( action(() => { croppingProto.data = new ImageField(returnedfilename); }), 500 ) ); }) .catch(error => { console.error('oops, something went wrong!', error); }); return cropping; }; updateIcon = () => { // currently we render pdf icons as text labels const docViewContent = this.DocumentView?.().ContentDiv; const filename = this.layoutDoc[Id] + '-icon' + new Date().getTime(); this._pdfViewer?._mainCont.current && docViewContent && UpdateIcon( filename, docViewContent, NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._height), this._props.PanelWidth(), this._props.PanelHeight(), NumCast(this.layoutDoc._layout_scrollTop), NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], 1), true, this.layoutDoc[Id] + '-icon', (iconFile: string, nativeWidth: number, nativeHeight: number) => { setTimeout(() => { this.dataDoc.icon = new ImageField(iconFile); this.dataDoc.icon_nativeWidth = nativeWidth; this.dataDoc.icon_nativeHeight = nativeHeight; }, 500); } ); }; componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); } componentDidMount() { this._props.setContentViewBox?.(this); this._disposers.select = reaction( () => this._props.isSelected(), () => { document.removeEventListener('keydown', this.onKeyDown); this._props.isSelected() && document.addEventListener('keydown', this.onKeyDown); }, { fireImmediately: true } ); this._disposers.scroll = reaction( () => this.layoutDoc.layout_scrollTop, () => { if (!(ComputedField.WithoutComputed(() => FieldValue(this.Document[this.SidebarKey + '_panY'])) instanceof ComputedField)) { this.Document[this.SidebarKey + '_panY'] = ComputedField.MakeFunction('this.layout_scrollTop'); } this.layoutDoc[this.SidebarKey + '_freeform_scale'] = 1; this.layoutDoc[this.SidebarKey + '_freeform_panX'] = 0; } ); } sidebarAddDocTab = (docIn: Doc | Doc[], where: OpenWhere) => { const docs = toList(docIn); if (docs.some(doc => DocListCast(this.Document[this._props.fieldKey + '_sidebar']).includes(doc)) && !this.SidebarShown) { this.toggleSidebar(false); return true; } return this._props.addDocTab(docs, where); }; focus = (anchor: Doc, options: FocusViewOptions) => { this._initialScrollTarget = anchor; return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.config_scrollTop)), options); }; getView = async (doc: Doc, options: FocusViewOptions) => { if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) { options.didMove = true; this.toggleSidebar(false); } return new Promise>(res => { DocumentView.addViewRenderedCb(doc, dv => res(dv)); }); }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { let ele: Opt; if (this._pdfViewer?.selectionContent()) { ele = document.createElement('div'); ele.append(this._pdfViewer.selectionContent()!); } const docAnchor = () => Docs.Create.ConfigDocument({ title: StrCast(this.Document.title + '@' + (NumCast(this.layoutDoc._layout_scrollTop) ?? 0).toFixed(0)), annotationOn: this.Document, }); const visibleAnchor = this._pdfViewer?._getAnchor?.(this._pdfViewer.savedAnnotations(), true); const anchor = visibleAnchor ?? docAnchor(); PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true, pannable: true } }, this.Document); anchor.text = ele?.textContent ?? ''; anchor.text_html = ele?.innerHTML; addAsAnnotation && this.addDocument(anchor); return anchor; }; @action loaded = (nw: number, nh: number, np: number) => { this.dataDoc[this._props.fieldKey + '_numPages'] = np; Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), (nw * 96) / 72)); Doc.SetNativeHeight(this.dataDoc, (nh * 96) / 72); this.layoutDoc._height = NumCast(this.layoutDoc._width) / (Doc.NativeAspect(this.dataDoc) || 1); !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (nh / nw)); }; override search = action((searchString: string, bwd?: boolean, clear: boolean = false) => { if (!this._searching && !clear) { this._searching = true; setTimeout(() => { this._searchRef.current?.focus(); this._searchRef.current?.select(); this._searchRef.current?.setRangeText(searchString); }); } return this._pdfViewer?.search(searchString, bwd, clear) || false; }); public prevAnnotation = () => this._pdfViewer?.prevAnnotation(); public nextAnnotation = () => this._pdfViewer?.nextAnnotation(); public backPage = () => { this.Document._layout_curPage = Math.max(1, (NumCast(this.Document._layout_curPage) || 1) - 1); return true; }; public forwardPage = () => { this.Document._layout_curPage = Math.min(NumCast(this.dataDoc[this._props.fieldKey + '_numPages']), (NumCast(this.Document._layout_curPage) || 1) + 1); return true; }; public gotoPage = (p: number) => { this.Document._layout_curPage = p; }; @undoBatch onKeyDown = action((e: KeyboardEvent) => { let processed = false; switch (e.key) { case 'PageDown': processed = this.forwardPage(); break; case 'PageUp': processed = this.backPage(); break; default: } if (processed) { e.stopImmediatePropagation(); e.preventDefault(); } }); setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; if (this._initialScrollTarget) { this.focus(this._initialScrollTarget, { instant: true }); this._initialScrollTarget = undefined; } }; searchStringChanged = (e: React.ChangeEvent) => { this._searchString = e.currentTarget.value; }; // adding external documents; to sidebar key // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"_annotation") sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => { if (!this.layoutDoc._show_sidebar) this.toggleSidebar(); return this.addDocument(doc, sidebarKey); }; sidebarBtnDown = (e: React.PointerEvent, onButton: boolean) => { // onButton determines whether the width of the pdf box changes, or just the ratio of the sidebar to the pdf const batch = UndoManager.StartBatch('sidebar'); setupMoveUpEvents( this, e, (moveEv, down, delta) => { const localDelta = this._props .ScreenToLocalTransform() .scale(this._props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this._props.NativeDimScaling?.() || 1)) / nativeWidth; if (ratio >= 1) { this.layoutDoc.nativeWidth = nativeWidth * ratio; onButton && (this.layoutDoc._width = NumCast(this.layoutDoc._width) + localDelta[0]); this.layoutDoc._show_sidebar = nativeWidth !== this.layoutDoc._nativeWidth; } return false; }, (clickEv, movement, isClick) => !isClick && batch.end(), () => { onButton && this.toggleSidebar(); batch.end(); } ); }; @observable _previewNativeWidth: Opt = undefined; @observable _previewWidth: Opt = undefined; toggleSidebar = action((preview: boolean = false) => { const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); const sideratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth; const pdfratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth + PDFBox.sidebarResizerWidth : 0) + NumCast(this.layoutDoc._width)) / NumCast(this.layoutDoc._width); const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); if (preview) { this._previewNativeWidth = nativeWidth * sideratio; this._previewWidth = (NumCast(this.layoutDoc._width) * nativeWidth * sideratio) / curNativeWidth; this._showSidebar = true; } else { this.layoutDoc.nativeWidth = nativeWidth * pdfratio; this.layoutDoc._width = (NumCast(this.layoutDoc._width) * nativeWidth * pdfratio) / curNativeWidth; this.layoutDoc._show_sidebar = nativeWidth !== this.layoutDoc._nativeWidth; } }); settingsPanel() { const pageBtns = ( <> ); const searchTitle = `${!this._searching ? 'Open' : 'Close'} Search Bar`; const curPage = NumCast(this.Document._layout_curPage) || 1; return !this._props.isContentActive() || this._pdfViewer?.isAnnotating ? null : (
([KeyCodes.BACKSPACE, KeyCodes.DELETE].includes(e.keyCode) ? e.stopPropagation() : true)} onPointerDown={e => e.stopPropagation()} style={{ display: this._props.isContentActive() ? 'flex' : 'none' }}>
e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
99 ? 4 : 3}ch`, pointerEvents: 'all' }} onChange={e => { this.Document._layout_curPage = Number(e.currentTarget.value); }} onKeyDown={e => e.stopPropagation()} onClick={action(() => { this._pageControls = !this._pageControls; })} /> {this._pageControls ? pageBtns : null}
{this.sidebarHandle}
); } sidebarWidth = () => { if (!this.SidebarShown) return 0; if (this._previewWidth) return PDFBox.sidebarResizerWidth + PDFBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target) const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc); return PDFBox.sidebarResizerWidth + nativeDiff * (this._props.NativeDimScaling?.() || 1); }; @undoBatch toggleSidebarType = () => { this.dataDoc[this.SidebarKey + '_type_collection'] = this.dataDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform; }; specificContextMenu = (): void => { const cm = ContextMenu.Instance; const options = cm.findByDescription('Options...'); const optionItems = options?.subitems ?? []; !Doc.noviceMode && optionItems.push({ description: 'Toggle Sidebar Type', event: this.toggleSidebarType, icon: 'expand-arrows-alt' }); !Doc.noviceMode && optionItems.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' }); // optionItems.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" }); !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'asterisk' }); const help = cm.findByDescription('Help...'); const helpItems = help?.subitems ?? []; helpItems.push({ description: 'Copy path', event: () => this.pdfUrl && ClientUtils.CopyText(ClientUtils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' }); !help && ContextMenu.Instance.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'asterisk' }); }; @computed get renderTitleBox() { const classname = 'pdfBox' + (this._props.isContentActive() ? '-interactive' : ''); return (
{StrCast(this.Document.title)}
); } anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; @observable _showSidebar = false; @computed get SidebarShown() { return !!(this._showSidebar || this.layoutDoc._show_sidebar); } @computed get sidebarHandle() { return (
this.sidebarBtnDown(e, true)}>
); } public get SidebarKey() { return this.fieldKey + '_sidebar'; } @computed get pdfScale() { const pdfNativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); const nativeWidth = NumCast(this.layoutDoc.nativeWidth, pdfNativeWidth); const pdfRatio = pdfNativeWidth / nativeWidth; return (pdfRatio * this._props.PanelWidth()) / pdfNativeWidth; } @computed get sidebarNativeWidth() { return this.sidebarWidth() / this.pdfScale; } @computed get sidebarNativeHeight() { return this._props.PanelHeight() / this.pdfScale; } sidebarNativeWidthFunc = () => this.sidebarNativeWidth; sidebarNativeHeightFunc = () => this.sidebarNativeHeight; sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey); sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey); sidebarScreenToLocal = () => this.ScreenToLocalBoxXf().translate((this.sidebarWidth() - this._props.PanelWidth()) / this.pdfScale, 0); @computed get sidebarCollection() { const renderComponent = (tag: string) => { const ComponentTag = tag === CollectionViewType.Freeform ? CollectionFreeFormView : CollectionStackingView; return ComponentTag === CollectionStackingView ? ( ) : (
setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this._props.select(false), true)}>
); }; return (
{renderComponent(StrCast(this.layoutDoc[this.SidebarKey + '_type_collection']))}
); } @computed get renderPdfView() { TraceMobx(); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const scale = previewScale * (this._props.NativeDimScaling?.() || 1); return !this._pdf ? null : (
600 ? (NumCast(this.Document._height) * this._props.PanelWidth()) / NumCast(this.Document._width) : undefined, }}>
this.sidebarBtnDown(e, false)} />
{this.sidebarCollection}
{this.settingsPanel()}
); } static pdfcache = new Map(); static pdfpromise = new Map>(); render() { TraceMobx(); const pdfView = !this._pdf ? null : this.renderPdfView; const href = this.pdfUrl?.url.href; if (!pdfView && href) { if (PDFBox.pdfcache.get(href)) setTimeout( action(() => { this._pdf = PDFBox.pdfcache.get(href); }) ); else { if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise); PDFBox.pdfpromise.get(href)?.then( action(pdf => { PDFBox.pdfcache.set(href, (this._pdf = pdf)); }) ); } } return pdfView ?? this.renderTitleBox; } } Docs.Prototypes.TemplateMap.set(DocumentType.PDF, { layout: { view: PDFBox, dataField: 'data' }, options: { acl: '', _layout_curPage: 1, _layout_fitWidth: true, _layout_nativeDimEditable: true, _layout_reflowVertical: true, systemIcon: 'BsFileEarmarkPdfFill' }, });