Browse Source

update annotation module

RoyLiu 5 years ago
parent
commit
534226a0ff

+ 40 - 14
components/Annotation/index.tsx

@@ -1,11 +1,11 @@
-/* eslint-disable @typescript-eslint/camelcase */
 import React from 'react';
 
-import { AnnotationType } from '../../constants/type';
+import { ViewportType, AnnotationType } from '../../constants/type';
 import AnnotationSelector from '../AnnotationSelector';
+import Markup from '../Markup';
 
 import {
-  Markup, Popper,
+  Popper,
 } from './styled';
 
 type Props = AnnotationType & {
@@ -15,6 +15,7 @@ type Props = AnnotationType & {
   onUpdate: (data: any) => void;
   onDelete: () => void;
   scale: number;
+  viewport: ViewportType;
 };
 
 const Annotation: React.FunctionComponent<Props> = ({
@@ -26,25 +27,48 @@ const Annotation: React.FunctionComponent<Props> = ({
   onUpdate,
   onDelete,
   scale,
+  viewport,
 }: any) => {
   const {
     page, position, bdcolor, transparency,
   } = obj_attr;
 
+  const processPosition = (type: string, ele: any): any => {
+    switch (type) {
+      case 'Squiggly':
+        return {
+          top: `${viewport.height - ele.top * scale}px`,
+          left: `${ele.left * scale}px`,
+          width: `${(ele.right - ele.left) * scale}px`,
+          height: `${(ele.top - ele.bottom) * scale + 5}px`,
+        };
+      default:
+        return {
+          top: `${viewport.height - ele.top * scale}px`,
+          left: `${ele.left * scale}px`,
+          width: `${(ele.right - ele.left) * scale}px`,
+          height: `${(ele.top - ele.bottom) * scale + 2}px`,
+        };
+    }
+  };
+
   return (
     <>
       {
-        position.map((ele: any, index: number) => (
-          <Markup
-            key={`block_${page + index}`}
-            position={ele}
-            scale={scale}
-            bdcolor={bdcolor}
-            opacity={transparency}
-            markupType={obj_type}
-            isCovered={isCovered}
-          />
-        ))
+        position.map((ele: any, index: number) => {
+          const pos = processPosition(obj_type, ele);
+
+          return (
+            <Markup
+              key={`block_${page + index}`}
+              position={pos}
+              bdcolor={bdcolor}
+              opacity={transparency}
+              markupType={obj_type}
+              isCovered={isCovered}
+            />
+          );
+        })
       }
       {
         !isCollapse ? (
@@ -52,6 +76,8 @@ const Annotation: React.FunctionComponent<Props> = ({
             <AnnotationSelector
               onUpdate={onUpdate}
               onDelete={onDelete}
+              colorProps={bdcolor}
+              opacityProps={transparency * 100}
             />
           </Popper>
         ) : ''

+ 1 - 60
components/Annotation/styled.ts

@@ -1,63 +1,4 @@
-import styled, { css } from 'styled-components';
-
-const MarkupStyle: Record<string, any> = {
-  Highlight: css<{isCovered: boolean; bdcolor: string}>`
-    background-color: ${props => (props.isCovered ? '#297fb8' : props.bdcolor)};
-  `,
-  Underline: css<{isCovered: boolean; bdcolor: string}>`
-    border-bottom: 2px solid ${props => (props.isCovered ? '#297fb8' : props.bdcolor)};
-  `,
-  Squiggly: css<{isCovered: boolean; bdcolor: string}>`
-    overflow: hidden;
-    
-    &:before {
-      content: '';
-      position: absolute;
-      background: radial-gradient(ellipse, transparent, transparent 8px, ${props => (props.isCovered ? '#297fb8' : props.bdcolor)} 9px, ${props => (props.isCovered ? '#297fb8' : props.bdcolor)} 10px, transparent 11px);
-      background-size: 22px 26px;
-      width: 100%;
-      height: 5px;
-      left: 0;
-      bottom: 3px;
-    }
-    &:after {
-      content: '';
-      position: absolute;
-      background: radial-gradient(ellipse, transparent, transparent 8px, ${props => (props.isCovered ? '#297fb8' : props.bdcolor)} 9px, ${props => (props.isCovered ? '#297fb8' : props.bdcolor)} 10px, transparent 11px);
-      background-size: 22px 26px;
-      width: 100%;
-      height: 5px;
-      bottom: -1px;
-      left: 11px;
-      background-position: 0px -22px;
-    }
-  `,
-  StrikeOut: css<{isCovered: boolean; bdcolor: string}>`
-    &:after {
-      content: '';
-      position: absolute;
-      height: 2px;
-      width: 100%;
-      left: 0;
-      top: 40%;
-      background-color: ${props => (props.isCovered ? '#297fb8' : props.bdcolor)};
-    };
-  `,
-};
-
-export const Markup = styled('div')<{position: Record<string, any>; bdcolor: string; markupType: string; opacity: number; isCovered: boolean; scale: number}>`
-  position: absolute;
-  cursor: pointer;
-  opacity: ${props => props.opacity};
-
-  ${props => css`
-    top: ${props.position.top * props.scale}px;
-    left: ${props.position.left * props.scale}px;
-    width: ${(props.position.right * props.scale) - (props.position.left * props.scale)}px;
-    height: ${(props.position.bottom * props.scale) - (props.position.top * props.scale) + 2}px;
-  `}
-  ${props => MarkupStyle[props.markupType]}
-`;
+import styled from 'styled-components';
 
 export const Popper = styled('div')<{position: Record<string, any>}>`
   position: absolute;

+ 82 - 0
components/AnnotationItem/index.tsx

@@ -0,0 +1,82 @@
+import React, { useEffect, useState } from 'react';
+
+import Divider from '../Divider';
+import Markup from '../Markup';
+import { scrollIntoView } from '../../helpers/utility';
+
+import {
+  PageNumber, AnnotationBox, Content, Inner, Text,
+} from './styled';
+
+type Props = {
+  type: string;
+  page?: number;
+  bdcolor: string;
+  getText: () => Promise<any>;
+  showPageNum?: boolean;
+  transparency: number;
+}
+
+const AnnotationItem = ({
+  type,
+  page,
+  showPageNum,
+  bdcolor,
+  getText,
+  transparency,
+}: Props): React.ReactElement => {
+  const [content, setContent] = useState([]);
+
+  const handleClick = (): void => {
+    const ele: HTMLElement | null = document.getElementById(`page_${page}`);
+
+    if (ele) {
+      scrollIntoView(ele);
+    }
+  };
+
+  useEffect(() => {
+    getText().then((text) => {
+      let textArray = [];
+
+      if (text.includes(' ')) {
+        textArray = text.split(' ');
+      } else {
+        textArray = text.match(/.{1,12}/g);
+      }
+
+      setContent(textArray);
+    });
+  }, []);
+
+  return (
+    <>
+      {showPageNum && <Divider orientation="horizontal" />}
+      {showPageNum && <PageNumber>{`Page ${page}`}</PageNumber>}
+      <AnnotationBox onClick={handleClick}>
+        <Content>
+          {
+            content.map((textContent: string, index: number): any => {
+              const key = `key_${index}`;
+              return textContent ? (
+                <Inner key={key}>
+                  <Text>{textContent}</Text>
+                  <Markup
+                    position={{
+                      top: 0, left: 0, width: '100%', height: '100%',
+                    }}
+                    markupType={type}
+                    bdcolor={bdcolor}
+                    opacity={transparency}
+                  />
+                </Inner>
+              ) : null;
+            })
+          }
+        </Content>
+      </AnnotationBox>
+    </>
+  );
+};
+
+export default AnnotationItem;

+ 35 - 0
components/AnnotationItem/styled.ts

@@ -0,0 +1,35 @@
+import styled from 'styled-components';
+
+import { color } from '../../constants/style';
+
+export const PageNumber = styled.div`
+  font-size: 12px;
+  font-weight: bold;
+  color: ${color.primary};
+  text-align: right;
+  margin-bottom: 5px;
+`;
+
+export const AnnotationBox = styled.div`
+  border-radius: 4px;
+  border: solid 1px ${color.black38};
+  padding: 12px;
+  width: 235px;
+  margin-bottom: 12px;
+`;
+
+export const Content = styled.div`
+  text-align: left;
+  position: relative;
+`;
+
+export const Inner = styled.div`
+  display: inline-block;
+  position: relative;
+  padding: 0 2px;
+`;
+
+export const Text = styled.div`
+  z-index: 1;
+  position: relative;
+`;

+ 126 - 45
components/AnnotationList/index.tsx

@@ -1,13 +1,18 @@
-import React from 'react';
+import React, {
+  useEffect, useState, useRef, useCallback,
+} from 'react';
+import queryString from 'query-string';
 
 import Icon from '../Icon';
 import Drawer from '../Drawer';
 import Typography from '../Typography';
-import Divider from '../Divider';
-
+import Item from '../AnnotationItem';
 import {
-  AnnotationBox, PageNumber, Content, Info,
-} from './styled';
+  AnnotationType, ViewportType, ScrollStateType, Position,
+} from '../../constants/type';
+
+import { downloadFileWithUri, watchScroll } from '../../helpers/utility';
+import { getAnnotationText } from '../../helpers/annotation';
 import { Separator } from '../../global/otherStyled';
 import {
   Wrapper, Head, Body, IconWrapper,
@@ -16,47 +21,123 @@ import {
 type Props = {
   isActive?: boolean;
   close: () => void;
+  annotations: AnnotationType[];
+  viewport: ViewportType;
+  scale: number;
+  pdf: any;
 };
 
-const Annotations: React.FunctionComponent<Props> = ({
+const AnnotationsList: React.FunctionComponent<Props> = ({
   isActive = false,
   close,
-}: Props) => (
-  <Drawer anchor="right" open={isActive}>
-    <Wrapper>
-      <Head>
-        <IconWrapper>
-          <Icon glyph="right-back" onClick={close} />
-        </IconWrapper>
-        <Separator />
-        <IconWrapper>
-          <Icon glyph="sort" />
-        </IconWrapper>
-        <IconWrapper>
-          <Icon glyph="annotation-export" />
-        </IconWrapper>
-        <IconWrapper>
-          <Icon glyph="import" />
-        </IconWrapper>
-      </Head>
-      <Body>
-        <Typography light>2 Annotations</Typography>
-        <Divider orientation="horizontal" />
-        <PageNumber>Page 1</PageNumber>
-        <AnnotationBox>
-          <Content>
-            If the Photographer fails to appear at the place and time specified above,
-            the deposit shall be refunded to the Client.
-          </Content>
-          <Info>
-            Gameboy
-            <Separator />
-            2016/07/28 18:18
-          </Info>
-        </AnnotationBox>
-      </Body>
-    </Wrapper>
-  </Drawer>
-);
-
-export default Annotations;
+  annotations,
+  viewport,
+  scale,
+  pdf,
+}: Props) => {
+  const [renderQueue, setQueue] = useState<AnnotationType[]>([]);
+  const containerRef = useRef<HTMLDivElement>(null);
+  const innerRef = useRef<HTMLDivElement>(null);
+
+  const handleExport = (): void => {
+    const parsed = queryString.parse(window.location.search);
+    const uri = `/api/v1/output.xfdf?f=${parsed.token}`;
+    downloadFileWithUri('output.xfdf', uri);
+  };
+
+  const getText = async (page: number, position: Position[]): Promise<any> => {
+    const text = await getAnnotationText({
+      pdf,
+      viewport,
+      scale,
+      page,
+      coords: position,
+    });
+    return text;
+  };
+
+  const scrollUpdate = useCallback((state: ScrollStateType): void => {
+    const innerHeight = innerRef.current?.offsetHeight || 0;
+    const wrapperHeight = containerRef.current?.offsetHeight || 0;
+
+    if (
+      wrapperHeight + state.lastY >= innerHeight
+      && renderQueue.length !== annotations.length
+    ) {
+      const start = renderQueue.length;
+      const end = renderQueue.length + 10;
+      const newQueue = [...renderQueue, ...annotations.slice(start, end)];
+      setQueue(newQueue);
+    }
+  }, [renderQueue, annotations]);
+
+  useEffect(() => {
+    const state = watchScroll(containerRef.current, scrollUpdate);
+
+    return (): void => {
+      state.subscriber.unsubscribe();
+    };
+  }, [scrollUpdate]);
+
+  useEffect(() => {
+    if (isActive) {
+      setQueue(annotations.slice(0, 10));
+    }
+  }, [isActive]);
+
+  return (
+    <Drawer anchor="right" open={isActive}>
+      <Wrapper>
+        <Head>
+          <IconWrapper>
+            <Icon glyph="right-back" onClick={close} />
+          </IconWrapper>
+          <Separator />
+          <IconWrapper>
+            <Icon glyph="sort" />
+          </IconWrapper>
+          <IconWrapper onClick={handleExport}>
+            <Icon glyph="annotation-export" />
+          </IconWrapper>
+          <IconWrapper>
+            <Icon glyph="import" />
+          </IconWrapper>
+        </Head>
+        <Body ref={containerRef}>
+          <div ref={innerRef}>
+            <Typography light align="left">
+              {`${annotations.length} Annotations`}
+            </Typography>
+            {isActive && renderQueue.map((ele, index) => {
+              const key = `annot_item_${index}`;
+              const {
+                obj_type,
+                obj_attr: {
+                  page,
+                  bdcolor,
+                  position,
+                  transparency,
+                },
+              } = ele;
+              const actualPage = page + 1;
+              const prevPage = index > 0 ? annotations[index - 1].obj_attr.page + 1 : -1;
+              return (
+                <Item
+                  key={key}
+                  type={obj_type}
+                  page={actualPage}
+                  bdcolor={bdcolor}
+                  transparency={transparency}
+                  getText={(): Promise<any> => getText(actualPage, position)}
+                  showPageNum={actualPage !== prevPage}
+                />
+              );
+            })}
+          </div>
+        </Body>
+      </Wrapper>
+    </Drawer>
+  );
+};
+
+export default AnnotationsList;

+ 0 - 49
components/AnnotationList/styled.ts

@@ -1,49 +0,0 @@
-import styled from 'styled-components';
-
-import { color } from '../../constants/style';
-
-export const Wrapper = styled.div`
-  margin-top: 60px;
-  padding: 24px 8px;
-  background-color: white;
-`;
-
-export const Head = styled.div`
-  display: flex;
-`;
-
-export const Body = styled.div`
-  padding: 8px;
-`;
-
-export const IconWrapper = styled.span`
-  display: inline-block;
-  padding: 8px;
-`;
-
-export const PageNumber = styled.div`
-  font-size: 12px;
-  font-weight: bold;
-  color: ${color.primary};
-  text-align: right;
-  margin-bottom: 5px;
-`;
-
-export const AnnotationBox = styled.div`
-  border-radius: 4px;
-  border: solid 1px ${color.black38};
-  padding: 12px 12px 8px;
-  width: 235px;
-`;
-
-export const Content = styled.div`
-  background-color: ${color['lemon-yellow']};
-`;
-
-export const Info = styled.div`
-  margin-top: 8px;
-  display: flex;
-  color: ${color.black38};
-  font-size: 12px;
-  font-weight: bold;
-`;

+ 7 - 3
components/AnnotationSelector/index.tsx

@@ -15,18 +15,22 @@ import {
 type Props = {
   onUpdate: (data: any) => void;
   onDelete: () => void;
-  color?: string;
+  colorProps?: string;
+  opacityProps?: number;
 };
 
 const index: React.FunctionComponent<Props> = ({
   onUpdate,
   onDelete,
+  colorProps,
+  opacityProps,
 }: Props) => {
   const [openSlider, setSlider] = useState(false);
   const [openDialog, setDialog] = useState(false);
-  const [opacity, setOpacity] = useState(100);
+  const [opacity, setOpacity] = useState(opacityProps);
 
   const handleClick = (color: string): void => {
+    console.log(color);
     onUpdate({ color });
   };
 
@@ -61,7 +65,7 @@ const index: React.FunctionComponent<Props> = ({
           </>
         ) : (
           <>
-            <ColorSelector onClick={handleClick} />
+            <ColorSelector color={colorProps} onClick={handleClick} />
             <Subtitle>opacity</Subtitle>
             <Button
               appearance="dark"

+ 29 - 0
components/Markup/index.tsx

@@ -0,0 +1,29 @@
+import React from 'react';
+
+import { Markup } from './styled';
+
+type Props = {
+  position: Record<string, any>;
+  bdcolor: string;
+  opacity: number;
+  markupType: string;
+  isCovered?: boolean;
+};
+
+const index = ({
+  position,
+  bdcolor,
+  opacity,
+  markupType,
+  isCovered,
+}: Props): React.ReactElement => (
+  <Markup
+    position={position}
+    bdcolor={bdcolor}
+    opacity={opacity}
+    markupType={markupType}
+    isCovered={isCovered}
+  />
+);
+
+export default index;

+ 60 - 0
components/Markup/styled.ts

@@ -0,0 +1,60 @@
+import styled, { css } from 'styled-components';
+
+const MarkupStyle: Record<string, any> = {
+  Highlight: css<{isCovered: boolean; bdcolor: string}>`
+    background-color: ${props => (props.isCovered ? '#297fb8' : props.bdcolor)};
+  `,
+  Underline: css<{isCovered: boolean; bdcolor: string}>`
+    border-bottom: 2px solid ${props => (props.isCovered ? '#297fb8' : props.bdcolor)};
+  `,
+  Squiggly: css<{isCovered: boolean; bdcolor: string}>`
+    overflow: hidden;
+    
+    &:before {
+      content: '';
+      position: absolute;
+      background: radial-gradient(ellipse, transparent, transparent 8px, ${props => (props.isCovered ? '#297fb8' : props.bdcolor)} 9px, ${props => (props.isCovered ? '#297fb8' : props.bdcolor)} 10px, transparent 11px);
+      background-size: 22px 26px;
+      width: 100%;
+      height: 5px;
+      left: 0;
+      bottom: 2px;
+    }
+    &:after {
+      content: '';
+      position: absolute;
+      background: radial-gradient(ellipse, transparent, transparent 8px, ${props => (props.isCovered ? '#297fb8' : props.bdcolor)} 9px, ${props => (props.isCovered ? '#297fb8' : props.bdcolor)} 10px, transparent 11px);
+      background-size: 22px 26px;
+      width: 100%;
+      height: 5px;
+      bottom: -1px;
+      left: 11px;
+      background-position: 0px -22px;
+    }
+  `,
+  StrikeOut: css<{isCovered: boolean; bdcolor: string}>`
+    &:after {
+      content: '';
+      position: absolute;
+      height: 2px;
+      width: 100%;
+      left: 0;
+      top: 40%;
+      background-color: ${props => (props.isCovered ? '#297fb8' : props.bdcolor)};
+    };
+  `,
+};
+
+export const Markup = styled('div')<{position: Record<string, any>; bdcolor: string; markupType: string; opacity: number; isCovered?: boolean}>`
+  position: absolute;
+  cursor: pointer;
+  opacity: ${props => props.opacity};
+
+  ${props => css`
+    top: ${props.position.top};
+    left: ${props.position.left};
+    width: ${props.position.width};
+    height: ${props.position.height};
+  `}
+  ${props => MarkupStyle[props.markupType]}
+`;

+ 7 - 6
constants/type.ts

@@ -21,6 +21,7 @@ export type ScrollStateType = {
   down: boolean;
   lastX: number;
   lastY: number;
+  subscriber: any;
 };
 
 export type SelectOptionType = {
@@ -37,11 +38,11 @@ export type Position = {
 };
 
 export type AnnotationType = {
-  obj_type?: string;
-  obj_attr?: {
-    page?: number;
-    bdcolor?: string;
-    position?: Position[];
-    transparency?: number;
+  obj_type: string;
+  obj_attr: {
+    page: number;
+    bdcolor: string;
+    position: Position[];
+    transparency: number;
   };
 };

+ 2 - 1
containers/Annotation.tsx

@@ -23,7 +23,7 @@ const Annotation: React.FunctionComponent<Props> = ({
   const [isCovered, setMouseOver] = useState(false);
   const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
   const [cursorPosition, ref] = useCursorPosition();
-  const [{ annotations }, dispatch] = useStore();
+  const [{ viewport, annotations }, dispatch] = useStore();
   const { updateAnnotation } = useActions(dispatch);
 
   const handleClick = (): void => {
@@ -83,6 +83,7 @@ const Annotation: React.FunctionComponent<Props> = ({
         mousePosition={mousePosition}
         onUpdate={handleUpdate}
         onDelete={handleDelete}
+        viewport={viewport}
       />
     </div>
   );

+ 30 - 0
containers/AnnotationList.tsx

@@ -0,0 +1,30 @@
+import React from 'react';
+
+import useStore from '../store';
+import useActions from '../actions';
+
+import AnnotationListComp from '../components/AnnotationList';
+
+const AnnotationList: React.FunctionComponent = () => {
+  const [{
+    navbarState,
+    annotations,
+    viewport,
+    scale,
+    pdf,
+  }, dispatch] = useStore();
+  const { setNavbar } = useActions(dispatch);
+
+  return (
+    <AnnotationListComp
+      annotations={annotations}
+      viewport={viewport}
+      scale={scale}
+      pdf={pdf}
+      isActive={navbarState === 'annotations'}
+      close={(): void => { setNavbar(''); }}
+    />
+  );
+};
+
+export default AnnotationList;

+ 141 - 25
helpers/annotation.ts

@@ -1,12 +1,21 @@
 /* eslint-disable @typescript-eslint/camelcase */
 import fetch from 'isomorphic-unfetch';
+import _ from 'lodash';
 
 import config from '../config';
 import apiPath from '../constants/apiPath';
 import { LINE_TYPE } from '../constants';
-import { AnnotationType, Position } from '../constants/type';
+import { AnnotationType, Position, ViewportType } from '../constants/type';
 import { xmlParser } from './dom';
-import { chunk } from './utility';
+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;
@@ -47,6 +56,7 @@ export const getAnnotationWithSelection = ({
   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;
@@ -68,24 +78,34 @@ export const getAnnotationWithSelection = ({
   textLayer.removeChild(startEle);
   textLayer.removeChild(endEle);
 
-  const info: AnnotationType = {};
+  const info: AnnotationType = {
+    obj_type: '',
+    obj_attr: {
+      page: 0,
+      bdcolor: '',
+      position: [],
+      transparency: 0,
+    },
+  };
   const position: Position[] = [];
 
   // left to right and up to down select
   let startX = startElement.offsetLeft + startEleWidth;
-  let startY = startElement.offsetTop;
+  let startY = startElement.offsetTop - EXTEND_RANGE;
   let endX = endElement.offsetLeft + endElement.offsetWidth - endEleWidth;
-  let endY = endElement.offsetTop + endElement.offsetHeight;
+  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;
+    startY = endElement.offsetTop - EXTEND_RANGE;
     endX = startElement.offsetLeft + startElement.offsetWidth - endEleWidth;
-    endY = startElement.offsetTop + startElement.offsetHeight;
+    endY = startElement.offsetTop + startElement.offsetHeight + EXTEND_RANGE;
   }
+  // @ts-ignore
+  const textElements = [...textLayer.childNodes];
 
-  textLayer.childNodes.forEach((ele: any) => {
+  textElements.forEach((ele: any) => {
     const {
       offsetTop, offsetLeft, offsetHeight, offsetWidth,
     } = ele;
@@ -104,40 +124,52 @@ export const getAnnotationWithSelection = ({
           left: startX,
           right: endX,
         };
-      } else if (
-        (offsetTop > startY && offsetBottom < endY) || (offsetLeft >= startX && offsetRight <= endX)
-      ) {
-        // middle element
+      } else if (startElement === ele) {
+        // start element
         coords = {
           top: offsetTop,
           bottom: offsetBottom,
-          left: offsetLeft,
+          left: startX,
           right: offsetRight,
         };
-      } else if (offsetTop === startY) {
-        // start line element
+      } else if (endElement === ele) {
+        // end element
         coords = {
           top: offsetTop,
           bottom: offsetBottom,
-          left: offsetLeft <= startX ? startX : offsetLeft,
-          right: offsetRight,
+          left: offsetLeft,
+          right: endX,
         };
-      } else if (offsetBottom === endY) {
-        // end line element
+      } else if (
+        (offsetLeft >= startX && offsetRight <= endX)
+        || (offsetTop > (startY + 5) && offsetBottom < (endY - 5))
+        || (offsetLeft >= startX && offsetBottom <= startY + offsetHeight)
+        || (offsetRight <= endX && offsetTop >= endX - offsetHeight)
+      ) {
+        // middle element
         coords = {
           top: offsetTop,
           bottom: offsetBottom,
           left: offsetLeft,
-          right: offsetRight >= endX ? endX : offsetRight,
+          right: offsetRight,
+        };
+      }
+
+      if (coords.top && coords.left) {
+        coords = {
+          ...coords,
+          top: pageHeight - coords.top,
+          bottom: pageHeight - coords.bottom,
         };
+
+        position.push(resetPositionValue(coords, scale));
       }
-      position.push(resetPositionValue(coords, scale));
     }
   });
 
   info.obj_type = LINE_TYPE[type];
   info.obj_attr = {
-    page: startPageNum,
+    page: startPageNum - 1,
     bdcolor: color,
     position,
     transparency: opacity * 0.01,
@@ -153,8 +185,11 @@ export const fetchXfdf = (token: string): Promise<any> => (
 
 export const parseAnnotationFromXml = (xmlString: string): AnnotationType[] => {
   const xmlDoc = xmlParser(xmlString);
-  let annotations = xmlDoc.firstElementChild.children[0].children;
+  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'
@@ -162,10 +197,10 @@ export const parseAnnotationFromXml = (xmlString: string): AnnotationType[] => {
       || cur.tagName === 'strikeout'
       || cur.tagName === 'squiggly'
     ) {
-      let tempArray = [];
+      let tempArray: any[] = [];
       if (cur.attributes.coords) {
         const coords = cur.attributes.coords.value.split(',');
-        tempArray = chunk(coords, 8);
+        tempArray = _.chunk(coords, 8);
       }
 
       const position = tempArray.map((ele: string[]) => ({
@@ -190,3 +225,84 @@ export const parseAnnotationFromXml = (xmlString: string): AnnotationType[] => {
 
   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: Position[];
+  pdf: any;
+}): Promise<any> => {
+  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;
+};

+ 42 - 19
helpers/pdf.ts

@@ -1,9 +1,12 @@
 // @ts-ignore
 import pdfjs from 'pdfjs-dist';
+// @ts-ignore
+import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.entry';
 import { ProgressType, ViewportType } from '../constants/type';
 import { objIsEmpty } from './utility';
+import { delay } from './time';
 
-pdfjs.GlobalWorkerOptions.workerSrc = '../static/pdf.worker.js';
+pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
 
 let normalizationRegex: any = null;
 const CHARACTERS_TO_NORMALIZE: {[index: string]: any} = {
@@ -43,13 +46,31 @@ export const fetchPdf = async (
   return {};
 };
 
+export const renderTextLayer = async ({
+  pdfPage,
+  textLayer,
+  viewport,
+}: {
+  pdfPage: any;
+  textLayer: HTMLElement;
+  viewport: ViewportType;
+}): Promise<any> => {
+  const textContent = await pdfPage.getTextContent();
+  pdfjs.renderTextLayer({
+    textContent,
+    container: textLayer,
+    viewport,
+    textDivs: [],
+  });
+};
+
 export const renderPdfPage = async ({
   rootEle,
-  page,
+  pdfPage,
   viewport,
 }: {
   rootEle: HTMLElement;
-  page: any;
+  pdfPage: any;
   viewport: ViewportType;
 }): Promise<any> => {
   if (rootEle) {
@@ -66,21 +87,20 @@ export const renderPdfPage = async ({
         viewport,
       };
 
-      if (!objIsEmpty(page)) {
-        const renderTask = page.render(renderContext);
-        textLayer.innerHTML = '';
-
-        page.getTextContent().then((textContent: any) => {
-          pdfjs.renderTextLayer({
-            textContent,
-            container: textLayer,
-            viewport,
-            textDivs: [],
-          });
-        });
+      if (!objIsEmpty(pdfPage)) {
+        const renderTask = pdfPage.render(renderContext);
 
         await renderTask.promise;
       }
+
+      textLayer.innerHTML = '';
+      await delay(200);
+
+      await renderTextLayer({
+        pdfPage,
+        textLayer,
+        viewport,
+      });
     }
   }
 };
@@ -99,11 +119,14 @@ export const calcFindPhraseMatch = (pageContent: string, query: string): number[
   const queryLen = query.length;
   let matchIdx = -queryLen;
 
-  while (query) {
-    matchIdx = pageContent.indexOf(query, matchIdx + queryLen);
-    if (matchIdx === -1) break;
-    matches.push(matchIdx);
+  if (pageContent) {
+    while (query) {
+      matchIdx = pageContent.indexOf(query, matchIdx + queryLen);
+      if (matchIdx === -1) break;
+      matches.push(matchIdx);
+    }
   }
+
   return matches;
 };