annotation.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. /* eslint-disable no-param-reassign */
  2. import { v4 as uuidv4 } from 'uuid';
  3. import dayjs from 'dayjs';
  4. import queryString from 'query-string';
  5. import { ANNOTATION_TYPE, FORM_TYPE } from '../constants';
  6. import { getPdfPage, renderTextLayer } from './pdf';
  7. import { getPosition, parsePositionForBackend } from './position';
  8. import { normalizeRound, floatToHex } from './utility';
  9. import { xmlParser, getElementsByTagName } from './dom';
  10. type GetFontAttributeFunc = (
  11. type: string,
  12. element: HTMLElement,
  13. ) => Record<string, unknown>;
  14. const getContent = (type: string, element: HTMLElement): string => {
  15. if (type !== 'Text' && type !== 'FreeText') return '';
  16. let content = '';
  17. const nodes = Array.prototype.slice.call(element.childNodes);
  18. nodes.forEach((ele: HTMLElement) => {
  19. if (ele.tagName === 'contents') {
  20. content = ele.innerHTML || ele.textContent || '';
  21. }
  22. });
  23. return content;
  24. };
  25. const getFontAttribute: GetFontAttributeFunc = (type, element) => {
  26. if (type !== 'FreeText') return {};
  27. const appearanceString =
  28. (element.childNodes[1] as HTMLElement).innerHTML ||
  29. element.childNodes[1].textContent ||
  30. '';
  31. const arr = appearanceString.split(' ');
  32. return {
  33. fontsize: parseInt(arr[5], 10),
  34. fontname: arr[4].substr(1),
  35. textcolor: floatToHex(
  36. parseFloat(arr[0]),
  37. parseFloat(arr[1]),
  38. parseFloat(arr[2]),
  39. ),
  40. };
  41. };
  42. export const parseAnnotationFromXml = (xmlString: string): AnnotationType[] => {
  43. if (!xmlString) return [];
  44. const xmlDoc = xmlParser(xmlString);
  45. const elements = xmlDoc.firstElementChild || xmlDoc.firstChild;
  46. const element = getElementsByTagName(elements as ChildNode, 'annots') || [];
  47. const annotations: HTMLElement[] = Array.prototype.slice.call(element);
  48. const filterAnnots = annotations.reduce(
  49. (acc: AnnotationType[], cur: HTMLElement) => {
  50. const type = ANNOTATION_TYPE[cur.tagName];
  51. const attributes = cur.attributes as ElementAttributeType;
  52. if (type) {
  53. const page = parseInt(attributes.page.value, 10);
  54. acc.push({
  55. id: uuidv4(),
  56. obj_type: type,
  57. obj_attr: {
  58. title: attributes.title ? attributes.title.value : undefined,
  59. date: attributes.date ? attributes.date.value : undefined,
  60. page,
  61. position: getPosition(type, cur),
  62. bdcolor: attributes.color ? attributes.color.value : undefined,
  63. bdwidth: attributes.width
  64. ? parseInt(attributes.width.value, 10)
  65. : 0,
  66. transparency: attributes.opacity
  67. ? parseFloat(attributes.opacity.value)
  68. : 1,
  69. content: getContent(type, cur) || undefined,
  70. fcolor: attributes['interior-color']
  71. ? attributes['interior-color'].value
  72. : undefined,
  73. ftransparency: attributes['interior-opacity']
  74. ? parseFloat(attributes['interior-opacity'].value)
  75. : undefined,
  76. is_arrow: !!attributes.tail,
  77. ...getFontAttribute(type, cur),
  78. },
  79. });
  80. }
  81. return acc;
  82. },
  83. [],
  84. );
  85. return filterAnnots;
  86. };
  87. export const parseFormElementFromXml = (
  88. xmlString: string,
  89. ): AnnotationType[] => {
  90. if (!xmlString) return [];
  91. const xmlDoc = xmlParser(xmlString);
  92. const elements = xmlDoc.firstElementChild || xmlDoc.firstChild;
  93. const element = getElementsByTagName(elements as ChildNode, 'widgets') || [];
  94. const annotations: HTMLElement[] = Array.prototype.slice.call(element);
  95. const filterForm = annotations.reduce(
  96. (acc: AnnotationType[], cur: HTMLElement) => {
  97. const type = FORM_TYPE[cur.tagName];
  98. const attributes = cur.attributes as ElementAttributeType;
  99. if (type) {
  100. const page = parseInt(attributes.page.value, 10);
  101. acc.push({
  102. id: uuidv4(),
  103. obj_type: type,
  104. obj_attr: {
  105. date: attributes.date ? attributes.date.value : undefined,
  106. page,
  107. position: getPosition(type, cur),
  108. bdcolor: attributes.color ? attributes.color.value : undefined,
  109. style: attributes.style ? attributes.style.value : undefined,
  110. bdwidth: attributes.width
  111. ? parseInt(attributes.width.value, 10)
  112. : 0,
  113. transparency: attributes.opacity
  114. ? parseFloat(attributes.opacity.value)
  115. : 1,
  116. },
  117. });
  118. }
  119. return acc;
  120. },
  121. [],
  122. );
  123. return filterForm;
  124. };
  125. // eslint-disable-next-line consistent-return
  126. const getEleText = (
  127. coord: PositionType,
  128. elements: HTMLElement[],
  129. viewport: ViewportType,
  130. scale: number,
  131. ): string => {
  132. const top = normalizeRound(viewport.height - coord.top * scale);
  133. const left = normalizeRound(coord.left * scale);
  134. const bottom = normalizeRound(viewport.height - coord.bottom * scale);
  135. const right = normalizeRound(coord.right * scale);
  136. for (let i = 0, len = elements.length; i <= len; i += 1) {
  137. const element = elements[i];
  138. if (element) {
  139. const eleTop = normalizeRound(element.offsetTop);
  140. const eleLeft = normalizeRound(element.offsetLeft);
  141. const eleRight = normalizeRound(element.offsetLeft + element.offsetWidth);
  142. if (eleTop >= top && eleTop <= bottom) {
  143. const textLength = element.innerText.length;
  144. const width = element.offsetWidth;
  145. if (eleLeft < left && eleRight > right) {
  146. const distanceL = left - eleLeft;
  147. const rateL = distanceL / width;
  148. const start = Math.floor(textLength * rateL);
  149. const distanceR = eleRight - right;
  150. const rateR = distanceR / width;
  151. const end = Math.floor(textLength - textLength * rateR);
  152. return ` ${element.innerText.slice(start, end)}`;
  153. }
  154. if (eleLeft < left && eleRight > left) {
  155. const distance = left - eleLeft;
  156. const rate = distance / width;
  157. const start = Math.floor(textLength * rate);
  158. return ` ${element.innerText.slice(start)}`;
  159. }
  160. if (eleRight > right && eleLeft < right) {
  161. const distance = eleRight - right;
  162. const rate = distance / width;
  163. const end = Math.floor(textLength - textLength * rate);
  164. return ` ${element.innerText.slice(0, end)}`;
  165. }
  166. if (eleLeft >= left && eleRight <= right) {
  167. return ` ${element.innerText}`;
  168. }
  169. }
  170. }
  171. }
  172. return '';
  173. };
  174. export const getAnnotationText = async ({
  175. viewport,
  176. scale,
  177. page,
  178. coords,
  179. pdf,
  180. }: {
  181. viewport: ViewportType;
  182. scale: number;
  183. page: number;
  184. coords: PositionType[];
  185. pdf: PdfType;
  186. }): Promise<string> => {
  187. const pageContainer = document.getElementById(`page_${page}`) as HTMLElement;
  188. const textLayer = pageContainer.querySelector(
  189. '[data-id="text-layer"]',
  190. ) as HTMLElement;
  191. const pdfPage = await getPdfPage(pdf, page);
  192. if (!textLayer.childNodes.length) {
  193. await renderTextLayer({
  194. textLayer,
  195. pdfPage,
  196. viewport,
  197. });
  198. }
  199. const textElements = Array.prototype.slice.call(textLayer.childNodes);
  200. let text = '';
  201. for (let i = 0, len = coords.length; i < len; i += 1) {
  202. const coord = coords[i];
  203. text += getEleText(coord, textElements, viewport, scale);
  204. }
  205. return text;
  206. };
  207. export const parseAnnotationObject = (
  208. {
  209. id,
  210. obj_type,
  211. obj_attr: {
  212. page,
  213. bdcolor,
  214. transparency,
  215. fcolor,
  216. ftransparency,
  217. position = { left: 0, top: 0, right: 0, bottom: 0 },
  218. content,
  219. style,
  220. bdwidth,
  221. fontname,
  222. fontsize,
  223. textcolor,
  224. is_arrow,
  225. src,
  226. },
  227. }: AnnotationType,
  228. pageHeight: number,
  229. scale: number,
  230. ): AnnotationType => ({
  231. id: id || uuidv4(),
  232. obj_type,
  233. obj_attr: {
  234. page: page - 1,
  235. bdcolor,
  236. position: parsePositionForBackend(obj_type, position, pageHeight, scale),
  237. transparency: transparency ? transparency * 0.01 : 0,
  238. content: content || undefined,
  239. style,
  240. fcolor,
  241. ftransparency: ftransparency ? ftransparency * 0.01 : 0,
  242. bdwidth,
  243. fontname,
  244. fontsize,
  245. textcolor,
  246. is_arrow,
  247. src,
  248. },
  249. });
  250. export const appendUserIdAndDate = (
  251. annotateObj: AnnotationType,
  252. ): AnnotationType => {
  253. const parsed = queryString.parse(window.location.search);
  254. if (parsed.watermark) {
  255. annotateObj.obj_attr.title = parsed.watermark;
  256. }
  257. const datetime = dayjs().format('YYYY-MM-DD_HH:mm:ss');
  258. annotateObj.obj_attr.date = datetime;
  259. return annotateObj;
  260. };