import * as htmlToImage from "html-to-image";
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
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 { FieldWaiting, Opt } from '../../../fields/Field';
import { ImageField } from '../../../fields/ImageField';
import { KeyStore } from '../../../fields/KeyStore';
import { PDFField } from '../../../fields/PDFField';
import { RouteStore } from "../../../server/RouteStore";
import { Utils } from '../../../Utils';
import { Annotation } from './Annotation';
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
import "./PDFBox.scss";
import { Sticky } from './Sticky'; //you should look at sticky and annotation, because they are used here
import React = require("react")
/** ALSO LOOK AT: Annotation.tsx, Sticky.tsx
* This method renders PDF and puts all kinds of functionalities such as annotation, highlighting,
* area selection (I call it stickies), embedded ink node for directly annotating using a pen or
* mouse, and pagination.
*
*
* HOW TO USE:
* AREA selection:
* 1) Click on Area button.
* 2) click on any part of the PDF, and drag to get desired sized area shape
* 3) You can write on the area (hence the reason why it's called sticky)
* 4) to make another area, you need to click on area button AGAIN.
*
* HIGHLIGHT: (Buggy. No multiline/multidiv text highlighting for now...)
* 1) just click and drag on a text
* 2) click highlight
* 3) for annotation, just pull your cursor over to that text
* 4) another method: click on highlight first and then drag on your desired text
* 5) To make another highlight, you need to reclick on the button
*
* Draw:
* 1) click draw and select color. then just draw like there's no tomorrow.
* 2) once you finish drawing your masterpiece, just reclick on the draw button to end your drawing session.
*
* Pagination:
* 1) click on arrows. You'll notice that stickies will stay in those page. But... highlights won't.
* 2) to test this out, make few area/stickies and then click on next page then come back. You'll see that they are all saved.
*
*
* written by: Andrew Kim
*/
@observer
export class PDFBox extends React.Component {
public static LayoutString() { return FieldView.LayoutString(PDFBox); }
private _mainDiv = React.createRef()
private _pdf = React.createRef();
//very useful for keeping track of X and y position throughout the PDF Canvas
private initX: number = 0;
private initY: number = 0;
//checks if tool is on
private _toolOn: boolean = false; //checks if tool is on
private _pdfContext: any = null; //gets pdf context
private bool: Boolean = false; //general boolean debounce
private currSpan: any;//keeps track of current span (for highlighting)
private _currTool: any; //keeps track of current tool button reference
private _drawToolOn: boolean = false; //boolean that keeps track of the drawing tool
private _drawTool = React.createRef()//drawing tool button reference
private _colorTool = React.createRef(); //color button reference
private _currColor: string = "black"; //current color that user selected (for ink/pen)
private _highlightTool = React.createRef(); //highlighter button reference
private _highlightToolOn: boolean = false;
private _pdfCanvas: any;
private _reactionDisposer: Opt;
@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 this.props.doc.GetNumber(KeyStore.CurPage, 1); }
@computed private get thumbnailPage() { return this.props.doc.GetNumber(KeyStore.ThumbnailPage, -1); }
componentDidMount() {
this._reactionDisposer = reaction(
() => [this.curPage, this.thumbnailPage],
() => {
if (this.curPage > 0 && this.thumbnailPage > 0 && this.curPage != this.thumbnailPage) {
this.saveThumbnail();
this._interactive = true;
}
},
{ fireImmediately: true });
}
componentWillUnmount() {
if (this._reactionDisposer) {
this._reactionDisposer();
}
}
/**
* selection tool used for area highlighting (stickies). Kinda temporary
*/
selectionTool = () => {
this._toolOn = true;
}
/**
* when user draws on the canvas. When mouse pointer is down
*/
drawDown = (e: PointerEvent) => {
this.initX = e.offsetX;
this.initY = e.offsetY;
this._pdfContext.beginPath();
this._pdfContext.lineTo(this.initX, this.initY);
this._pdfContext.strokeStyle = this._currColor;
this._pdfCanvas.addEventListener("pointermove", this.drawMove);
this._pdfCanvas.addEventListener("pointerup", this.drawUp);
}
//when user drags
drawMove = (e: PointerEvent): void => {
//x and y mouse movement
let x = this.initX += e.movementX,
y = this.initY += e.movementY;
//connects the point
this._pdfContext.lineTo(x, y);
this._pdfContext.stroke();
}
drawUp = (e: PointerEvent) => {
this._pdfContext.closePath();
this._pdfCanvas.removeEventListener("pointermove", this.drawMove);
this._pdfCanvas.removeEventListener("pointerdown", this.drawDown);
this._pdfCanvas.addEventListener("pointerdown", this.drawDown);
}
/**
* highlighting helper function
*/
makeEditableAndHighlight = (colour: string) => {
var range, sel = window.getSelection();
if (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.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) => {
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) {
if (!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 =
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._toolOn) {
let mouse = e.nativeEvent;
this.initX = mouse.offsetX;
this.initY = mouse.offsetY;
}
}
/**
* controls area highlighting and partially highlighting. Kinda temporary
*/
@action
onPointerUp = (e: React.PointerEvent) => {
if (this._highlightToolOn) {
this.highlight("rgba(76, 175, 80, 0.3)"); //highlights to this default color.
this._highlightToolOn = false;
}
if (this._toolOn) {
let mouse = e.nativeEvent;
let finalX = mouse.offsetX;
let finalY = mouse.offsetY;
let width = Math.abs(finalX - this.initX); //width
let height = Math.abs(finalY - this.initY); //height
//these two if statements are bidirectional dragging. You can drag from any point to another point and generate sticky
if (finalX < this.initX) {
this.initX = finalX;
}
if (finalY < this.initY) {
this.initY = finalY;
}
if (this._mainDiv.current) {
let sticky =
this._pageInfo.area.push(sticky);
}
this._toolOn = false;
}
this._interactive = true;
}
/**
* starts drawing the line when user presses down.
*/
onDraw = () => {
if (this._currTool != null) {
this._currTool.style.backgroundColor = "grey";
}
if (this._drawTool.current) {
this._currTool = this._drawTool.current;
if (this._drawToolOn) {
this._drawToolOn = false;
this._pdfCanvas.removeEventListener("pointerdown", this.drawDown);
this._pdfCanvas.removeEventListener("pointerup", this.drawUp);
this._pdfCanvas.removeEventListener("pointermove", this.drawMove);
this._drawTool.current.style.backgroundColor = "grey";
} else {
this._drawToolOn = true;
this._pdfCanvas.addEventListener("pointerdown", this.drawDown);
this._drawTool.current.style.backgroundColor = "cyan";
}
}
}
/**
* for changing color (for ink/pen)
*/
onColorChange = (e: React.PointerEvent) => {
if (e.currentTarget.innerHTML == "Red") {
this._currColor = "red";
} else if (e.currentTarget.innerHTML == "Blue") {
this._currColor = "blue";
} else if (e.currentTarget.innerHTML == "Green") {
this._currColor = "green";
} else if (e.currentTarget.innerHTML == "Black") {
this._currColor = "black";
}
}
/**
* For highlighting (text drag highlighting)
*/
onHighlight = () => {
this._drawToolOn = false;
if (this._currTool != null) {
this._currTool.style.backgroundColor = "grey";
}
if (this._highlightTool.current) {
this._currTool = this._drawTool.current;
if (this._highlightToolOn) {
this._highlightToolOn = false;
this._highlightTool.current.style.backgroundColor = "grey";
} else {
this._highlightToolOn = true;
this._highlightTool.current.style.backgroundColor = "orange";
}
}
}
@action
saveThumbnail = () => {
setTimeout(() => {
var me = this;
let nwidth = me.props.doc.GetNumber(KeyStore.NativeWidth, 0);
let nheight = me.props.doc.GetNumber(KeyStore.NativeHeight, 0);
htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 1 })
.then(function (dataUrl: string) {
me.props.doc.SetData(KeyStore.Thumbnail, new URL(dataUrl), ImageField);
me.props.doc.SetNumber(KeyStore.ThumbnailPage, me.props.doc.GetNumber(KeyStore.CurPage, -1));
})
.catch(function (error: any) {
console.error('oops, something went wrong!', error);
});
}, 1000);
}
@action
onLoaded = (page: any) => {
if (this._mainDiv.current) {
this._mainDiv.current.childNodes.forEach((element) => {
if (element.nodeName == "DIV") {
element.childNodes[0].childNodes.forEach((e) => {
if (e instanceof HTMLCanvasElement) {
this._pdfCanvas = e;
this._pdfContext = e.getContext("2d")
}
})
}
})
}
// bcz: the number of pages should really be set when the document is imported.
this.props.doc.SetNumber(KeyStore.NumPages, page._transport.numPages);
if (this._perPageInfo.length == 0) { //Makes sure it only runs once
this._perPageInfo = [...Array(page._transport.numPages)]
}
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 PDF
// so this design is flawed.
var nativeWidth = this.props.doc.GetNumber(KeyStore.NativeWidth, 0);
if (!this.props.doc.GetNumber(KeyStore.NativeHeight, 0)) {
var nativeHeight = nativeWidth * r.entry.height / r.entry.width;
this.props.doc.SetNumber(KeyStore.Height, nativeHeight / nativeWidth * this.props.doc.GetNumber(KeyStore.Width, 0));
this.props.doc.SetNumber(KeyStore.NativeHeight, nativeHeight);
}
if (!this.props.doc.GetT(KeyStore.Thumbnail, ImageField)) {
this.saveThumbnail();
}
}
@computed
get pdfContent() {
let page = this.curPage;
const renderHeight = 2400;
let pdfUrl = this.props.doc.GetT(this.props.fieldKey, PDFField);
let xf = this.props.doc.GetNumber(KeyStore.NativeHeight, 0) / renderHeight;
return ;
}
@computed
get pdfRenderer() {
let proxy = this._loaded ? (null) : this.imageProxyRenderer;
let pdfUrl = this.props.doc.GetT(this.props.fieldKey, PDFField);
if ((!this._interactive && proxy) || !pdfUrl || pdfUrl == FieldWaiting) {
return proxy;
}
return [
this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element),
this._currAnno.map((element: any) => element),
this.pdfContent,
proxy
];
}
@computed
get imageProxyRenderer() {
let thumbField = this.props.doc.Get(KeyStore.Thumbnail);
if (thumbField) {
let path = thumbField == FieldWaiting || this.thumbnailPage != this.curPage ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
thumbField instanceof ImageField ? thumbField.Data.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
return
;
}
return (null);
}
render() {
return (
{this.pdfRenderer}
);
}
}