annotation.ts 9.1 KB

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