Explorar o código

Merge branch 'optimization' into develop

RoyLiu %!s(int64=5) %!d(string=hai) anos
pai
achega
c5f6b76d41

+ 73 - 0
components/PageView/index.tsx

@@ -0,0 +1,73 @@
+import React, { useEffect, useRef } from 'react';
+
+import { renderPdfPage } from '../../helpers/pdf';
+import { TypeRenderingStates, ViewportType } from '../../constants/type';
+
+import {
+  PageWrapper, CanvasWrapper, PdfCanvas, AnnotationLayer, TextLayer, LoadingLayer,
+} from './styled';
+
+type Props = {
+  pageNum: number;
+  renderingState?: TypeRenderingStates;
+  getPage: () => void;
+  viewport: ViewportType;
+  rotation: number;
+};
+
+const PageView: React.FunctionComponent<Props> = ({
+  pageNum,
+  getPage,
+  viewport,
+  renderingState = 'PAUSED',
+  rotation,
+}) => {
+  const rootEle = useRef<HTMLDivElement>(null);
+  let page: any = null;
+
+  useEffect(() => {
+    if (renderingState === 'RENDERING') {
+      renderPage();
+    } else if (page && renderingState === 'LOADING') {
+      page.cleanup();
+    }
+  }, [renderingState]);
+
+  useEffect(() => {
+    return () => {
+      if (page) page.cleanup();
+    }
+  }, []);
+
+  const renderPage = async () => {
+    page = await getPage();
+
+    await renderPdfPage({rootEle: rootEle.current!, page, viewport});
+  };
+
+  return (
+    <PageWrapper
+      ref={rootEle}
+      id={`page_${pageNum}`}
+      width={viewport.width}
+      height={viewport.height}
+      rotation={rotation}
+    >
+      {
+        renderingState === 'LOADING' ? (
+          <LoadingLayer />
+        ) : (
+          <>
+            <CanvasWrapper>
+              <PdfCanvas />
+            </CanvasWrapper>
+            <TextLayer />
+            <AnnotationLayer />
+          </>
+        )
+      }
+    </PageWrapper>
+  );
+};
+
+export default PageView;

+ 30 - 0
components/PageView/styled.ts

@@ -0,0 +1,30 @@
+import styled from 'styled-components';
+
+export const PageWrapper = styled('div')<{width: number, height: number, rotation?: number}>`
+  direction: ltr;
+  position: relative;
+  overflow: visible;
+  background-clip: content-box;
+  display: inline-block;
+  margin: 25px auto;
+
+  width: ${props => props.width}px;
+  height: ${props => props.height}px;
+  transform: rotate(${props => props.rotation}deg);
+  background-color: white;
+`;
+
+export const CanvasWrapper = styled.div``;
+
+export const PdfCanvas = styled.canvas`
+  margin: 0;
+  display: block;
+  width: 100%;
+  height: 100%;
+`;
+
+export const AnnotationLayer = styled.div``;
+
+export const TextLayer = styled.div``;
+
+export const LoadingLayer = styled.div``;

+ 118 - 0
components/Viewer/index.tsx

@@ -0,0 +1,118 @@
+import React, { forwardRef, useEffect, useState } from 'react';
+
+import PageView from '../PageView';
+import { ViewportType } from '../../constants/type';
+import { scrollIntoView } from '../../helpers/utility';
+
+import { Container, Wrapper } from './styled';
+
+type Props = {
+  totalPage: number;
+  currentPage: number;
+  viewport: ViewportType;
+  pdf: any;
+  rotation: number;
+};
+type Ref = HTMLDivElement;
+
+const Viewer = forwardRef<Ref, Props>(({
+  totalPage,
+  currentPage,
+  viewport,
+  pdf,
+  rotation,
+}, ref) => {
+  const [elements, setElement] = useState<React.ReactNode[]>([]);
+
+  const getPdfPage = async (pageNum: number) => {
+    const page = await pdf.getPage(pageNum);
+    return page;
+  };
+
+  const createPages = () => {
+    const pagesContent: React.ReactNode[] = [];
+
+    for(let i = 1; i <= totalPage; i++) {
+      const component = (
+        <PageView
+          key={`page-${i}`}
+          pageNum={i}
+          renderingState={[1, 2, 3].includes(i) ? 'RENDERING' : 'LOADING'}
+          viewport={viewport}
+          getPage={() => getPdfPage(i) }
+          rotation={rotation}
+        />
+      );
+      pagesContent.push(component);
+    }
+
+    setElement(pagesContent);
+  };
+
+  const updatePages = () => {
+    const renderingIndexQueue = [currentPage - 1 ,currentPage, currentPage + 1];
+    let index = currentPage - 2;
+    const end = currentPage + 2;
+
+    while (true) {
+      if (elements[index]) {
+        const pageNum = index + 1;
+
+        elements[index] = (
+          <PageView
+            key={`page-${pageNum}`}
+            pageNum={pageNum}
+            renderingState={renderingIndexQueue.includes(pageNum) ? 'RENDERING' : 'LOADING'}
+            viewport={viewport}
+            getPage={() => getPdfPage(pageNum)}
+            rotation={rotation}
+          />
+        )
+      }
+
+      index += 1;
+      if (index >= end) break;
+    }
+
+    if (elements.length) {
+      setElement([...elements]);
+    }
+  };
+
+  const changePdfContainerScale = () => {
+    if (elements.length) {
+      for(let i = 1; i <= totalPage; i++) {
+        const ele: HTMLDivElement = document.getElementById(`page_${i}`) as HTMLDivElement;
+        ele.style.width =  `${viewport.width}px`;
+        ele.style.height = `${viewport.height}px`;
+
+        if (i === currentPage) {
+          updatePages();
+          scrollIntoView(ele);
+        }
+      }
+    }
+  }
+
+  useEffect(() => {
+    createPages();
+  }, [totalPage]);
+
+  useEffect(() => {
+    changePdfContainerScale();
+  }, [viewport]);
+
+  useEffect(() => {
+    updatePages();
+  }, [currentPage, rotation]);
+
+  return (
+    <Container ref={ref}>
+      <Wrapper width={rotation / 90 % 2 === 1 ? viewport.height : viewport.width}>
+        {elements}
+      </Wrapper>
+    </Container>
+  );
+});
+
+export default Viewer;

+ 19 - 0
components/Viewer/styled.ts

@@ -0,0 +1,19 @@
+import styled from 'styled-components';
+
+export const Container = styled.div`
+  position: fixed;
+  left: 267px;
+  right: 0;
+  top: 60px;
+  bottom: 0;
+  overflow: scroll;
+  text-align: center;
+  padding: 20px;
+`;
+
+export const Wrapper = styled('div')<{width: number}>`
+  padding: 20px 0;
+  display: inline-flex;
+  flex-direction: column;
+  width: ${props => props.width}px;
+`;

+ 85 - 0
containers/PdfViewer.tsx

@@ -0,0 +1,85 @@
+import React, { useEffect, useState, useRef, useCallback } from 'react';
+
+import Viewer from '../components/Viewer';
+import { fetchPdf } from '../helpers/pdf';
+import { watchScroll } from '../helpers/utility';
+import { ProgressType, ScrollStateType } from '../constants/type';
+
+import useActions from '../actions';
+import useStore from '../store';
+
+const PdfViewer: React.FunctionComponent = () => {
+  const containerRef = useRef<HTMLDivElement>(null);
+  const pageRef = useRef(1);
+  const viewportRef = useRef({height: 0, width: 0});
+  const [initialized, setInitialize] = useState(false);
+  const [{totalPage, currentPage, viewport, pdf, scale, rotation}, dispatch] = useStore();
+  const { setTotalPage, setCurrentPage, setPdf, setProgress, setViewport } = useActions(dispatch);
+
+  const pdfGenerator = async () => {
+    const pdf = await fetchPdf('../static/Quick Start Guide.pdf', getProgress);
+
+    const totalNum = pdf.numPages;
+    setTotalPage(totalNum);
+    setPdf(pdf);
+
+    await getViewport(pdf, 1);
+    setInitialize(true);
+  }
+
+  const getViewport = async (pdfEle: any, scale: number) => {
+    const page = await pdfEle.getPage(1);
+    const viewport = page.getViewport({ scale });
+    viewport.width = Math.round(viewport.width);
+    viewport.height = Math.round(viewport.height);
+    setViewport(viewport);
+  }
+
+  const getProgress = useCallback((progress: ProgressType) => {
+    setProgress(progress);
+  }, [dispatch]);
+
+  const scrollUpdate = useCallback((state: ScrollStateType) => {
+    const viewport = viewportRef.current;
+    const page = Math.round((state.lastY + viewport.height / 1.4) / (viewport.height + 50));
+    if (page !== pageRef.current) {
+      setCurrentPage(page);
+    }
+  }, [dispatch]);
+
+  useEffect(() => {
+    pdfGenerator();
+  }, []);
+
+  useEffect(() => {
+    if (containerRef.current) {
+      watchScroll(containerRef.current, scrollUpdate);
+    }
+  }, [initialized]);
+
+  useEffect(() => {
+    pageRef.current = currentPage;
+    viewportRef.current = viewport;
+  }, [currentPage, viewport]);
+
+  useEffect(() => {
+    if (pdf) {
+      getViewport(pdf, scale);
+    }
+  }, [scale]);
+
+  return (
+    initialized ? (
+      <Viewer
+        ref={containerRef}
+        totalPage={totalPage}
+        currentPage={currentPage}
+        viewport={viewport}
+        pdf={pdf}
+        rotation={rotation}
+      />
+    ) : null
+  );
+};
+
+export default PdfViewer;

+ 59 - 0
helpers/pdf.ts

@@ -0,0 +1,59 @@
+// @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';
+
+const CMAP_URL = '../../node_modules/pdfjs-dist/cmaps/';
+const CMAP_PACKED = true;
+
+pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
+
+export const fetchPdf = async (src: string, cb: (progress: ProgressType) => void) => {
+  const loadingTask = pdfjs.getDocument({
+    url: src,
+    cMapUrl: CMAP_URL,
+    cMapPacked: CMAP_PACKED,
+  });
+
+  if (cb) {
+    loadingTask.onProgress = (progress: ProgressType) => {
+      cb(progress);
+    };
+  }
+
+  const pdf = await loadingTask.promise;
+  return pdf;
+};
+
+export const renderPdfPage = async ({
+  rootEle,
+  page,
+  viewport,
+}: {
+  rootEle: HTMLDivElement;
+  page: any;
+  viewport: ViewportType;
+}) => {
+  if (rootEle) {
+    const canvas: HTMLCanvasElement = rootEle.querySelectorAll('canvas')[0] as HTMLCanvasElement;
+
+    if (canvas) {
+      const context: CanvasRenderingContext2D = canvas.getContext('2d')!;
+      canvas.height = viewport.height;
+      canvas.width = viewport.width;
+
+      const renderContext = {
+        canvasContext: context,
+        viewport,
+      };
+
+      if (!objIsEmpty(page)) {
+        const renderTask = page.render(renderContext);
+
+        await renderTask.promise;
+      }
+    }
+  }
+};