/* eslint-disable @typescript-eslint/camelcase */ import _ from 'lodash'; import { LINE_TYPE } from '../constants'; import { AnnotationType, PositionType, ViewportType } from '../constants/type'; import { xmlParser } from './dom'; import { getPdfPage, renderTextLayer } from './pdf'; const EXTEND_RANGE = 2; const FRACTIONDIGITS = 2; const normalizeRound = (num: number, fractionDigits?: number): number => { const frac = fractionDigits || FRACTIONDIGITS; return Math.round(num * (10 ** frac)) / (10 ** frac); }; type Props = { color: string; type: string; opacity: number; scale: number; } const resetPositionValue = ( { top, left, bottom, right, }: PositionType, scale: number, ): PositionType => ({ top: top / scale, left: left / scale, bottom: bottom / scale, right: right / scale, }); export const getAnnotationWithSelection = ({ color, type, opacity, scale, }: Props): AnnotationType[] | null => { const selection: any = document.getSelection(); if (!selection.rangeCount) return []; const { startContainer, startOffset, endContainer, endOffset, } = selection.getRangeAt(0); const appendInfo: AnnotationType[] = []; const startElement = startContainer.parentNode as HTMLElement; const endElement = endContainer.parentNode as HTMLElement; const startPage = startElement?.parentNode?.parentNode as HTMLElement; const endPage = endElement?.parentNode?.parentNode as HTMLElement; const startPageNum = parseInt(startPage.getAttribute('data-page-num') as string, 10); const endPageNum = parseInt(endPage.getAttribute('data-page-num') as string, 10); const textLayer = startPage.querySelector('[data-id="text-layer"]') as HTMLElement; const pageHeight = startPage.offsetHeight; if (startPageNum !== endPageNum) return null; if (startOffset === endOffset && startOffset === endOffset) return null; const startEle = startElement.cloneNode(true) as HTMLElement; const endEle = endElement.cloneNode(true) as HTMLElement; const startText = startElement.innerText.substring(0, startOffset); const endText = endEle.innerText.substring(endOffset); startEle.innerText = startText; endEle.innerText = endText; textLayer.appendChild(startEle); textLayer.appendChild(endEle); const startEleWidth = startEle.offsetWidth; const endEleWidth = endEle.offsetWidth; textLayer.removeChild(startEle); textLayer.removeChild(endEle); const info: AnnotationType = { obj_type: '', obj_attr: { page: 0, bdcolor: '', position: [], transparency: 0, }, }; const position: PositionType[] = []; // left to right and up to down select let startX = startElement.offsetLeft + startEleWidth; let startY = startElement.offsetTop - EXTEND_RANGE; let endX = endElement.offsetLeft + endElement.offsetWidth - endEleWidth; let endY = endElement.offsetTop + endElement.offsetHeight + EXTEND_RANGE; if (startX > endX && startY >= endY) { // right to left and down to up select startX = endElement.offsetLeft + startEleWidth; startY = endElement.offsetTop - EXTEND_RANGE; endX = startElement.offsetLeft + startElement.offsetWidth - endEleWidth; endY = startElement.offsetTop + startElement.offsetHeight + EXTEND_RANGE; } // @ts-ignore const textElements = [...textLayer.childNodes]; textElements.forEach((ele: any) => { const { offsetTop, offsetLeft, offsetHeight, offsetWidth, } = ele; const offsetRight = offsetLeft + offsetWidth; const offsetBottom = offsetTop + offsetHeight; let coords = { top: 0, left: 0, right: 0, bottom: 0, }; if (offsetTop >= startY && offsetBottom <= endY) { if (startElement === endElement) { // start and end same element coords = { top: offsetTop, bottom: offsetBottom, left: startX, right: endX, }; } else if (startElement === ele) { // start element coords = { top: offsetTop, bottom: offsetBottom, left: startX, right: offsetRight, }; } else if (endElement === ele) { // end element coords = { top: offsetTop, bottom: offsetBottom, left: offsetLeft, right: endX, }; } else if ( (offsetLeft >= startX && offsetRight <= endX) || (offsetTop > (startY + 5) && offsetBottom < (endY - 5)) || (offsetLeft >= startX && offsetBottom <= startY + offsetHeight + 5) || (offsetRight <= endX && offsetTop >= endX - offsetHeight - 5) ) { // middle element coords = { top: offsetTop, bottom: offsetBottom, left: offsetLeft, right: offsetRight, }; } if (coords.top && coords.left) { coords = { ...coords, top: pageHeight - coords.top, bottom: pageHeight - coords.bottom, }; position.push(resetPositionValue(coords, scale)); } } }); info.obj_type = LINE_TYPE[type]; info.obj_attr = { page: startPageNum - 1, bdcolor: color, position, transparency: opacity * 0.01, }; appendInfo.push(info); return appendInfo; }; export const parseAnnotationFromXml = (xmlString: string): AnnotationType[] => { if (!xmlString) return []; const xmlDoc = xmlParser(xmlString); const elements = xmlDoc.firstElementChild || xmlDoc.firstChild; let annotations = elements.childNodes[1].childNodes; annotations = Array.prototype.slice.call(annotations); const filterAnnotations = annotations.reduce((acc: any[], cur: any) => { if ( cur.tagName === 'highlight' || cur.tagName === 'underline' || cur.tagName === 'strikeout' || cur.tagName === 'squiggly' ) { let tempArray: any[] = []; if (cur.attributes.coords) { const coords = cur.attributes.coords.value.split(','); tempArray = _.chunk(coords, 8); } const position = tempArray.map((ele: string[]) => ({ top: parseInt(ele[5] as string, 10), bottom: parseInt(ele[1], 10), left: parseInt(ele[0], 10), right: parseInt(ele[2], 10), })); acc.push({ obj_type: LINE_TYPE[cur.tagName], obj_attr: { page: parseInt(cur.attributes.page.value, 10), bdcolor: cur.attributes.color.value, position, transparency: parseFloat(cur.attributes.opacity.value), }, }); } return acc; }, []); return filterAnnotations; }; // eslint-disable-next-line consistent-return const getEleText = (coord: any, elements: any, viewport: any, scale: any): string => { const top = normalizeRound(viewport.height - coord.top * scale); const left = normalizeRound(coord.left * scale); const bottom = normalizeRound(viewport.height - coord.bottom * scale); const right = normalizeRound(coord.right * scale); for (let i = 0, len = elements.length; i <= len; i += 1) { const element = elements[i]; if (element) { const eleTop = normalizeRound(element.offsetTop); const eleLeft = normalizeRound(element.offsetLeft); const eleRight = normalizeRound(element.offsetLeft + element.offsetWidth); if (eleTop >= top && eleTop <= bottom) { const textLength = element.innerText.length; const width = element.offsetWidth; if (eleLeft < left && eleRight > right) { const distanceL = left - eleLeft; const rateL = distanceL / width; const start = Math.floor(textLength * rateL); const distanceR = eleRight - right; const rateR = distanceR / width; const end = Math.floor(textLength - (textLength * rateR)); return ` ${element.innerText.slice(start, end)}`; } if (eleLeft < left && eleRight > left) { const distance = left - eleLeft; const rate = distance / width; const start = Math.floor(textLength * rate); return ` ${element.innerText.slice(start)}`; } if (eleRight > right && eleLeft < right) { const distance = eleRight - right; const rate = distance / width; const end = Math.floor(textLength - (textLength * rate)); return ` ${element.innerText.slice(0, end)}`; } if (eleLeft >= left && eleRight <= right) { return ` ${element.innerText}`; } } } } return ''; }; export const getAnnotationText = async ({ viewport, scale, page, coords, pdf, }: { viewport: ViewportType; scale: number; page: number; coords: PositionType[]; pdf: any; }): Promise => { const pageContainer = document.getElementById(`page_${page}`) as HTMLElement; const textLayer = pageContainer.querySelector('[data-id="text-layer"]') as HTMLElement; const pdfPage = await getPdfPage(pdf, page); if (!textLayer.childNodes.length) { await renderTextLayer({ textLayer, pdfPage, viewport, }); } // @ts-ignore const textElements = [...textLayer.childNodes]; let text = ''; for (let i = 0, len = coords.length; i < len; i += 1) { const coord = coords[i]; text += getEleText(coord, textElements, viewport, scale); } return text; };