annotation.ts 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. /* eslint-disable @typescript-eslint/camelcase */
  2. import fetch from 'isomorphic-unfetch';
  3. import _ from 'lodash';
  4. import config from '../config';
  5. import apiPath from '../constants/apiPath';
  6. import { LINE_TYPE } from '../constants';
  7. import { AnnotationType, Position, ViewportType } from '../constants/type';
  8. import { xmlParser } from './dom';
  9. import { getPdfPage, renderTextLayer } from './pdf';
  10. const EXTEND_RANGE = 2;
  11. const FRACTIONDIGITS = 2;
  12. const normalizeRound = (num: number, fractionDigits?: number): number => {
  13. const frac = fractionDigits || FRACTIONDIGITS;
  14. return Math.round(num * (10 ** frac)) / (10 ** frac);
  15. };
  16. type Props = {
  17. color: string;
  18. type: string;
  19. opacity: number;
  20. scale: number;
  21. }
  22. const resetPositionValue = (
  23. {
  24. top, left, bottom, right,
  25. }: Position,
  26. scale: number,
  27. ): Position => ({
  28. top: top / scale,
  29. left: left / scale,
  30. bottom: bottom / scale,
  31. right: right / scale,
  32. });
  33. export const getAnnotationWithSelection = ({
  34. color, type, opacity, scale,
  35. }: Props): AnnotationType[] | null => {
  36. const selection: any = document.getSelection();
  37. if (!selection.rangeCount) return [];
  38. const {
  39. startContainer,
  40. startOffset,
  41. endContainer,
  42. endOffset,
  43. } = selection.getRangeAt(0);
  44. const appendInfo: AnnotationType[] = [];
  45. const startElement = startContainer.parentNode as HTMLElement;
  46. const endElement = endContainer.parentNode as HTMLElement;
  47. const startPage = startElement?.parentNode?.parentNode as HTMLElement;
  48. const endPage = endElement?.parentNode?.parentNode as HTMLElement;
  49. const startPageNum = parseInt(startPage.getAttribute('data-page-num') as string, 10);
  50. const endPageNum = parseInt(endPage.getAttribute('data-page-num') as string, 10);
  51. const textLayer = startPage.querySelector('[data-id="text-layer"]') as HTMLElement;
  52. const pageHeight = startPage.offsetHeight;
  53. if (startPageNum !== endPageNum) return null;
  54. if (startOffset === endOffset && startOffset === endOffset) return null;
  55. const startEle = startElement.cloneNode(true) as HTMLElement;
  56. const endEle = endElement.cloneNode(true) as HTMLElement;
  57. const startText = startElement.innerText.substring(0, startOffset);
  58. const endText = endEle.innerText.substring(endOffset);
  59. startEle.innerText = startText;
  60. endEle.innerText = endText;
  61. textLayer.appendChild(startEle);
  62. textLayer.appendChild(endEle);
  63. const startEleWidth = startEle.offsetWidth;
  64. const endEleWidth = endEle.offsetWidth;
  65. textLayer.removeChild(startEle);
  66. textLayer.removeChild(endEle);
  67. const info: AnnotationType = {
  68. obj_type: '',
  69. obj_attr: {
  70. page: 0,
  71. bdcolor: '',
  72. position: [],
  73. transparency: 0,
  74. },
  75. };
  76. const position: Position[] = [];
  77. // left to right and up to down select
  78. let startX = startElement.offsetLeft + startEleWidth;
  79. let startY = startElement.offsetTop - EXTEND_RANGE;
  80. let endX = endElement.offsetLeft + endElement.offsetWidth - endEleWidth;
  81. let endY = endElement.offsetTop + endElement.offsetHeight + EXTEND_RANGE;
  82. if (startX > endX && startY >= endY) {
  83. // right to left and down to up select
  84. startX = endElement.offsetLeft + startEleWidth;
  85. startY = endElement.offsetTop - EXTEND_RANGE;
  86. endX = startElement.offsetLeft + startElement.offsetWidth - endEleWidth;
  87. endY = startElement.offsetTop + startElement.offsetHeight + EXTEND_RANGE;
  88. }
  89. // @ts-ignore
  90. const textElements = [...textLayer.childNodes];
  91. textElements.forEach((ele: any) => {
  92. const {
  93. offsetTop, offsetLeft, offsetHeight, offsetWidth,
  94. } = ele;
  95. const offsetRight = offsetLeft + offsetWidth;
  96. const offsetBottom = offsetTop + offsetHeight;
  97. let coords = {
  98. top: 0, left: 0, right: 0, bottom: 0,
  99. };
  100. if (offsetTop >= startY && offsetBottom <= endY) {
  101. if (startElement === endElement) {
  102. // start and end same element
  103. coords = {
  104. top: offsetTop,
  105. bottom: offsetBottom,
  106. left: startX,
  107. right: endX,
  108. };
  109. } else if (startElement === ele) {
  110. // start element
  111. coords = {
  112. top: offsetTop,
  113. bottom: offsetBottom,
  114. left: startX,
  115. right: offsetRight,
  116. };
  117. } else if (endElement === ele) {
  118. // end element
  119. coords = {
  120. top: offsetTop,
  121. bottom: offsetBottom,
  122. left: offsetLeft,
  123. right: endX,
  124. };
  125. } else if (
  126. (offsetLeft >= startX && offsetRight <= endX)
  127. || (offsetTop > (startY + 5) && offsetBottom < (endY - 5))
  128. || (offsetLeft >= startX && offsetBottom <= startY + offsetHeight)
  129. || (offsetRight <= endX && offsetTop >= endX - offsetHeight)
  130. ) {
  131. // middle element
  132. coords = {
  133. top: offsetTop,
  134. bottom: offsetBottom,
  135. left: offsetLeft,
  136. right: offsetRight,
  137. };
  138. }
  139. if (coords.top && coords.left) {
  140. coords = {
  141. ...coords,
  142. top: pageHeight - coords.top,
  143. bottom: pageHeight - coords.bottom,
  144. };
  145. position.push(resetPositionValue(coords, scale));
  146. }
  147. }
  148. });
  149. info.obj_type = LINE_TYPE[type];
  150. info.obj_attr = {
  151. page: startPageNum - 1,
  152. bdcolor: color,
  153. position,
  154. transparency: opacity * 0.01,
  155. };
  156. appendInfo.push(info);
  157. return appendInfo;
  158. };
  159. export const fetchXfdf = (token: string): Promise<any> => (
  160. fetch(`${config.API_HOST}${apiPath.getXfdf}?f=${token}`).then(res => res.text())
  161. );
  162. export const parseAnnotationFromXml = (xmlString: string): AnnotationType[] => {
  163. const xmlDoc = xmlParser(xmlString);
  164. const elements = xmlDoc.firstElementChild || xmlDoc.firstChild;
  165. let annotations = elements.childNodes[1].childNodes;
  166. annotations = Array.prototype.slice.call(annotations);
  167. const filterAnnotations = annotations.reduce((acc: any[], cur: any) => {
  168. if (
  169. cur.tagName === 'highlight'
  170. || cur.tagName === 'underline'
  171. || cur.tagName === 'strikeout'
  172. || cur.tagName === 'squiggly'
  173. ) {
  174. let tempArray: any[] = [];
  175. if (cur.attributes.coords) {
  176. const coords = cur.attributes.coords.value.split(',');
  177. tempArray = _.chunk(coords, 8);
  178. }
  179. const position = tempArray.map((ele: string[]) => ({
  180. top: parseInt(ele[5] as string, 10),
  181. bottom: parseInt(ele[1], 10),
  182. left: parseInt(ele[0], 10),
  183. right: parseInt(ele[2], 10),
  184. }));
  185. acc.push({
  186. obj_type: LINE_TYPE[cur.tagName],
  187. obj_attr: {
  188. page: parseInt(cur.attributes.page.value, 10),
  189. bdcolor: cur.attributes.color.value,
  190. position,
  191. transparency: parseFloat(cur.attributes.opacity.value),
  192. },
  193. });
  194. }
  195. return acc;
  196. }, []);
  197. return filterAnnotations;
  198. };
  199. // eslint-disable-next-line consistent-return
  200. const getEleText = (coord: any, elements: any, viewport: any, scale: any): string => {
  201. const top = normalizeRound(viewport.height - coord.top * scale);
  202. const left = normalizeRound(coord.left * scale);
  203. const bottom = normalizeRound(viewport.height - coord.bottom * scale);
  204. const right = normalizeRound(coord.right * scale);
  205. for (let i = 0, len = elements.length; i <= len; i += 1) {
  206. const element = elements[i];
  207. if (element) {
  208. const eleTop = normalizeRound(element.offsetTop);
  209. const eleLeft = normalizeRound(element.offsetLeft);
  210. const eleRight = normalizeRound(element.offsetLeft + element.offsetWidth);
  211. if (eleTop >= top && eleTop <= bottom) {
  212. const textLength = element.innerText.length;
  213. const width = element.offsetWidth;
  214. if (eleLeft < left && eleRight > right) {
  215. const distanceL = left - eleLeft;
  216. const rateL = distanceL / width;
  217. const start = Math.floor(textLength * rateL);
  218. const distanceR = eleRight - right;
  219. const rateR = distanceR / width;
  220. const end = Math.floor(textLength - (textLength * rateR));
  221. return ` ${element.innerText.slice(start, end)}`;
  222. }
  223. if (eleLeft < left && eleRight > left) {
  224. const distance = left - eleLeft;
  225. const rate = distance / width;
  226. const start = Math.floor(textLength * rate);
  227. return ` ${element.innerText.slice(start)}`;
  228. }
  229. if (eleRight > right && eleLeft < right) {
  230. const distance = eleRight - right;
  231. const rate = distance / width;
  232. const end = Math.floor(textLength - (textLength * rate));
  233. return ` ${element.innerText.slice(0, end)}`;
  234. }
  235. if (eleLeft >= left && eleRight <= right) {
  236. return ` ${element.innerText}`;
  237. }
  238. }
  239. }
  240. }
  241. return '';
  242. };
  243. export const getAnnotationText = async ({
  244. viewport, scale, page, coords, pdf,
  245. }: {
  246. viewport: ViewportType;
  247. scale: number;
  248. page: number;
  249. coords: Position[];
  250. pdf: any;
  251. }): Promise<any> => {
  252. const pageContainer = document.getElementById(`page_${page}`) as HTMLElement;
  253. const textLayer = pageContainer.querySelector('[data-id="text-layer"]') as HTMLElement;
  254. const pdfPage = await getPdfPage(pdf, page);
  255. if (!textLayer.childNodes.length) {
  256. await renderTextLayer({
  257. textLayer,
  258. pdfPage,
  259. viewport,
  260. });
  261. }
  262. // @ts-ignore
  263. const textElements = [...textLayer.childNodes];
  264. let text = '';
  265. for (let i = 0, len = coords.length; i < len; i += 1) {
  266. const coord = coords[i];
  267. text += getEleText(coord, textElements, viewport, scale);
  268. }
  269. return text;
  270. };