1255 lines
41 KiB
TypeScript
1255 lines
41 KiB
TypeScript
import React, {useEffect, useRef, useState} from "react";
|
|
import {
|
|
getDocument,
|
|
GlobalWorkerOptions,
|
|
PageViewport,
|
|
PDFDocumentProxy,
|
|
PDFPageProxy,
|
|
renderTextLayer
|
|
} from "pdfjs-dist";
|
|
import "./PDFViewer.css"
|
|
import {Button, Dropdown, Input, MenuProps, Modal, Progress, Space, Tooltip} from "antd";
|
|
import {
|
|
CaretLeftOutlined,
|
|
CaretRightOutlined, DownloadOutlined, FullscreenOutlined,
|
|
PercentageOutlined, PrinterOutlined,
|
|
ZoomInOutlined,
|
|
ZoomOutOutlined
|
|
} from "@ant-design/icons";
|
|
import html2canvas from "html2canvas";
|
|
import {PDFDocument, PDFPage} from "pdf-lib";
|
|
|
|
const currentUrl = getCurrentUrl();
|
|
console.log(currentUrl);
|
|
GlobalWorkerOptions.workerSrc = `${currentUrl}/pdf.worker.js`;
|
|
|
|
function getCurrentUrl(): string {
|
|
// window.location 객체를 통해 현재 URL 정보에 접근
|
|
const { protocol, hostname, port } = window.location;
|
|
|
|
// 포트가 80이 아닌 경우에만 포트를 포함한 URL 반환
|
|
const portSuffix = (port === '80' || port === '443') ? '' : `:${port}`;
|
|
|
|
// 조합된 URL 반환
|
|
return `${protocol}//${hostname}${portSuffix}/pdfwiz/pdfjs`;
|
|
}
|
|
|
|
interface PdfViewerProps {
|
|
fileUrl: string;
|
|
fileName?:string;
|
|
downloadFileUrl?:string;
|
|
scale?:number;
|
|
renderText?:boolean;
|
|
waterMarkText?:string;
|
|
}
|
|
|
|
async function loadPage(pdf:PDFDocumentProxy, pageNum:number, scale:number, rotate:number, container:HTMLDivElement, isRenderText:boolean, waterMarkText:string|undefined)
|
|
{
|
|
const pageLayer = document.createElement("div");
|
|
const canvas = document.createElement("canvas");
|
|
const cavasLayer = document.createElement("div");
|
|
const loadingLayer = document.createElement("div");
|
|
const drawLayer = document.createElement("div");
|
|
|
|
const loadingImage = document.createElement("img");
|
|
|
|
loadingImage.setAttribute("src", "pdfwiz/image/loading.svg");
|
|
const textLayer = isRenderText ? document.createElement("div") : null;
|
|
if (textLayer)
|
|
{
|
|
pageLayer.appendChild(textLayer);
|
|
textLayer.className = "textLayer";
|
|
}
|
|
pageLayer.className = "pageLayer";
|
|
cavasLayer.className = "loading_canvasWrapper";
|
|
|
|
pageLayer.setAttribute("data-pageIdx", `${pageNum}`);
|
|
pageLayer.setAttribute("id", `page-${pageNum}`);
|
|
pageLayer.setAttribute("data-pageLoad", `false`);
|
|
|
|
drawLayer.className = "drawLayer";
|
|
drawLayer.setAttribute("data-pageIdx", `${pageNum}`);
|
|
drawLayer.setAttribute("id", `draw-${pageNum}`);
|
|
|
|
if (waterMarkText)
|
|
{
|
|
const waterMarkLayer = document.createElement("div");
|
|
waterMarkLayer.className = "waterMarkLayer";
|
|
drawLayer.appendChild(waterMarkLayer);
|
|
}
|
|
|
|
pageLayer.appendChild(drawLayer);
|
|
|
|
container.appendChild(pageLayer);
|
|
cavasLayer.appendChild(canvas);
|
|
pageLayer.appendChild(cavasLayer);
|
|
loadingLayer.appendChild(loadingImage);
|
|
loadingLayer.className = "loading";
|
|
cavasLayer.appendChild(loadingLayer);
|
|
|
|
const page = await pdf.getPage(pageNum);
|
|
const viewport = page.getViewport({
|
|
scale: scale
|
|
,rotation:rotate|0
|
|
})
|
|
|
|
canvas.width = viewport.width;
|
|
canvas.height = viewport.height;
|
|
|
|
const context = canvas.getContext("2d");
|
|
if (!context) return;
|
|
|
|
|
|
if(pageNum <= 3)
|
|
{
|
|
const isLoad = pageLayer.getAttribute("data-pageLoad") === "true" ? true : false;
|
|
if(!isLoad)
|
|
{
|
|
pageLayer.setAttribute("data-pageLoad", `true`);
|
|
await renderPage(page, context, viewport, textLayer).finally(() => {
|
|
cavasLayer.className = "canvasWrapper";
|
|
if (loadingImage && cavasLayer.contains(loadingLayer)) {
|
|
cavasLayer.removeChild(loadingLayer);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function renderPage(page:PDFPageProxy, context:CanvasRenderingContext2D, viewport:PageViewport, textLayer:HTMLDivElement|null)
|
|
{
|
|
const renderContext = {
|
|
canvasContext: context,
|
|
viewport: viewport,
|
|
};
|
|
|
|
await page.render(renderContext).promise;
|
|
|
|
if (textLayer)
|
|
{
|
|
page.getTextContent().then(textContent => {
|
|
const txtLayer = renderTextLayer(
|
|
{
|
|
textContentSource: textContent,
|
|
container: textLayer,
|
|
viewport: viewport,
|
|
isOffscreenCanvasSupported:true,
|
|
|
|
}
|
|
).promise;
|
|
}).finally(()=>{
|
|
const parent = textLayer.parentElement;
|
|
if (parent != null)
|
|
{
|
|
const drawLayer = parent.querySelector(".drawLayer");
|
|
const pageStyle = textLayer.getAttribute("style");
|
|
if (drawLayer && pageStyle)
|
|
{
|
|
drawLayer.setAttribute("style", pageStyle);
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
export const PDFViewer = (props: PdfViewerProps) => {
|
|
const { fileUrl , scale, renderText, downloadFileUrl
|
|
, waterMarkText, fileName} = props;
|
|
|
|
const [modalVisible, setModalVisible] = useState<boolean>(false);
|
|
//const downloadUrl = downloadFileUrl || fileUrl;
|
|
//const fileTitle= fileName||fileUrl;
|
|
const [threshold, setThreshold] = useState<number>(0.7);
|
|
const [scaleValue, setScaleValue] = useState<number>(scale || 1.5);
|
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
|
const [totalPage, setTotalPage] = useState<number>(0);
|
|
|
|
const rightClickCountRef = useRef(0);
|
|
const leftClickCountRef = useRef(0);
|
|
|
|
const [observer, setObserver] = useState<IntersectionObserver>();
|
|
|
|
const [calcScaleValue, setCalcScaleValue] = useState(scaleValue);
|
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
|
const timerClickRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const [pdfDocument, setPdfDocument] = useState<PDFDocumentProxy>();
|
|
|
|
|
|
const [modalTitle, setModalTitle] = useState<string>();
|
|
|
|
const [progressVisible, setProgressVisible] = useState<boolean>(false);
|
|
const [procPercent, setProcPercent] = useState<number>(0);
|
|
|
|
const [passwordVisible, setPasswordVisible] = useState<boolean>(false);
|
|
const [pdfPassword, setPdfPassword] = useState<string>("");
|
|
const [pdfErrorMsg, setPdfErrorMsg] = useState<string>("");
|
|
|
|
|
|
const waterMarkCanvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
const isRenderText = renderText||false;
|
|
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const items:MenuProps['items'] = [
|
|
{
|
|
key: 'ACTUAL_SIZE',
|
|
label: '실제크기',
|
|
},
|
|
{
|
|
key: 'WIDTH_FIT',
|
|
label: '가로에 맞추기',
|
|
},
|
|
{
|
|
key: 'HEIGHT_FIT',
|
|
label: '세로에 맞추기',
|
|
},
|
|
{
|
|
key: '0.5',
|
|
label: '50%',
|
|
},
|
|
{
|
|
key: '0.75',
|
|
label: '70%',
|
|
},
|
|
{
|
|
key: '1.0',
|
|
label: '100%',
|
|
},
|
|
{
|
|
key: '1.25',
|
|
label: '125%',
|
|
},
|
|
{
|
|
key: '1.5',
|
|
label: '150%',
|
|
},
|
|
{
|
|
key: '2.0',
|
|
label: '200%',
|
|
},
|
|
{
|
|
key: '3.0',
|
|
label: '300%',
|
|
},
|
|
{
|
|
key: '4.0',
|
|
label: '400%',
|
|
},
|
|
];
|
|
|
|
|
|
const handleRatioMenuClick: MenuProps['onClick'] = (e) => {
|
|
const key:string = e.key;
|
|
switch (key)
|
|
{
|
|
case 'ACTUAL_SIZE' :
|
|
ZoomCtrl(1.0);
|
|
break;
|
|
case 'WIDTH_FIT' :
|
|
ZoomCtrl(getWidthFitScale());
|
|
break;
|
|
case 'HEIGHT_FIT' :
|
|
ZoomCtrl(getHeightFitScale());
|
|
break;
|
|
case '0.5' :
|
|
ZoomCtrl(0.5);
|
|
break;
|
|
case '0.75' :
|
|
ZoomCtrl(0.75);
|
|
break;
|
|
case '1.0' :
|
|
ZoomCtrl(1.0);
|
|
break;
|
|
case '1.25' :
|
|
ZoomCtrl(1.25);
|
|
break;
|
|
case '1.5' :
|
|
ZoomCtrl(1.5);
|
|
break;
|
|
case '2.0' :
|
|
ZoomCtrl(2.0);
|
|
break;
|
|
case '3.0' :
|
|
ZoomCtrl(3.0);
|
|
break;
|
|
case '4.0' :
|
|
ZoomCtrl(4.0);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
function calcThreshold(stdScaleValue:number) :number
|
|
{
|
|
if (0.5 >= stdScaleValue )
|
|
{
|
|
return 1.0;
|
|
}
|
|
else if (0.75 >= stdScaleValue && 1.0 < stdScaleValue)
|
|
{
|
|
return 0.9;
|
|
}
|
|
else if (1.0 >= stdScaleValue && 1.25 < stdScaleValue)
|
|
{
|
|
return 0.8;
|
|
}
|
|
else if (1.25 >= stdScaleValue && 1.5 < stdScaleValue)
|
|
{
|
|
return 0.7;
|
|
}
|
|
else if (1.5 >= stdScaleValue && 1.75 < stdScaleValue)
|
|
{
|
|
return 0.6;
|
|
}
|
|
else if (1.75 >= stdScaleValue && 2.0 < stdScaleValue)
|
|
{
|
|
return 0.5;
|
|
}
|
|
else if (2.0 >= stdScaleValue && 3.0 < stdScaleValue)
|
|
{
|
|
return 0.4;
|
|
}
|
|
else if (4.0 >= stdScaleValue )
|
|
{
|
|
return 0.3;
|
|
}
|
|
else
|
|
{
|
|
return 0.65
|
|
}
|
|
}
|
|
|
|
const raioMenuProps = {
|
|
items,
|
|
onClick: handleRatioMenuClick,
|
|
};
|
|
|
|
|
|
|
|
function enterFullScreen(elementId: string): void {
|
|
const element = document.getElementById(elementId) as HTMLElement & {
|
|
requestFullscreen?: () => Promise<void>;
|
|
webkitRequestFullscreen?: () => Promise<void>;
|
|
mozRequestFullScreen?: () => Promise<void>;
|
|
msRequestFullscreen?: () => Promise<void>;
|
|
};
|
|
|
|
if (!element) {
|
|
console.error(`Element with id ${elementId} not found.`);
|
|
return;
|
|
}
|
|
|
|
const onFullScreenEntered = () => {
|
|
// 여기에 원하는 로직을 추가하세요.
|
|
console.log("전체 화면 모드에 진입하였습니다.");
|
|
ZoomCtrl(getWindowHeightFitScale());
|
|
// 이벤트 리스너 제거
|
|
document.removeEventListener('fullscreenchange', onFullScreenEntered);
|
|
};
|
|
|
|
// 이벤트 리스너 추가
|
|
document.addEventListener('fullscreenchange', onFullScreenEntered);
|
|
|
|
if (element.requestFullscreen) {
|
|
element.requestFullscreen();
|
|
} else if (element.webkitRequestFullscreen) {
|
|
element.webkitRequestFullscreen();
|
|
} else if (element.mozRequestFullScreen) {
|
|
element.mozRequestFullScreen();
|
|
} else if (element.msRequestFullscreen) {
|
|
element.msRequestFullscreen();
|
|
} else {
|
|
console.error("Fullscreen API is not supported.");
|
|
}
|
|
}
|
|
|
|
|
|
function scrollToDiv(pageNum:number) {
|
|
const divId = `page-${pageNum}`;
|
|
const element = document.getElementById(divId);
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: 'smooth' });
|
|
} else {
|
|
console.error(`Element with id ${divId} not found.`);
|
|
}
|
|
}
|
|
|
|
function movePage(pageNum:number)
|
|
{
|
|
if(pageNum < 1)
|
|
{
|
|
pageNum = 1;
|
|
}
|
|
else if(pageNum > totalPage)
|
|
{
|
|
pageNum = totalPage;
|
|
}
|
|
scrollToDiv(pageNum);
|
|
return pageNum;
|
|
}
|
|
|
|
function getWidthFitScale()
|
|
{
|
|
const parentObj = document.getElementById("pdf_content");
|
|
if(parentObj)
|
|
{
|
|
const parentWidth = parentObj.offsetWidth;
|
|
console.log("부모 : " + parentWidth);
|
|
const pageLayer = parentObj.querySelector(`.pageLayer[data-pageidx="${currentPage}"]`);
|
|
if (pageLayer) {
|
|
const canvas = pageLayer.querySelector('canvas');
|
|
if (canvas)
|
|
{
|
|
console.log("자식 : " + (canvas.width / calcScaleValue) );
|
|
return parentWidth / ((canvas.width / calcScaleValue) + 15) ;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function getHeightFitScale()
|
|
{
|
|
const parentObj = document.getElementById("pdf_content");
|
|
if(parentObj)
|
|
{
|
|
const parentHeight = parentObj.offsetHeight;
|
|
const pageLayer = parentObj.querySelector(`.pageLayer[data-pageidx="${currentPage}"]`);
|
|
if (pageLayer) {
|
|
const canvas = pageLayer.querySelector('canvas');
|
|
if (canvas)
|
|
{
|
|
return parentHeight / ((canvas.height / calcScaleValue) + 30);
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function getWindowHeightFitScale()
|
|
{
|
|
const parentObj = document.getElementById("pdf_content");
|
|
if(parentObj)
|
|
{
|
|
const pageLayer = parentObj.querySelector(`.pageLayer[data-pageidx="${currentPage}"]`);
|
|
if (pageLayer) {
|
|
const canvas = pageLayer.querySelector('canvas');
|
|
if (canvas)
|
|
{
|
|
return window.screen.height / ((canvas.height / calcScaleValue) + 15);
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function zoomIn(stdValue:number)
|
|
{
|
|
const maxZoom:number = 4.0;
|
|
let calsZoom:number = stdValue + 0.25;
|
|
|
|
if (calsZoom > maxZoom)
|
|
{
|
|
calsZoom = maxZoom;
|
|
}
|
|
return calsZoom;
|
|
}
|
|
|
|
function zoomOut(stdValue:number)
|
|
{
|
|
|
|
const minZoom:number = 0.5;
|
|
let calsZoom:number = stdValue - 0.25;
|
|
if (calsZoom < minZoom)
|
|
{
|
|
calsZoom = minZoom;
|
|
}
|
|
return calsZoom;
|
|
|
|
}
|
|
|
|
function drawLoadingPage(pageLayer:Element, isLoadingIcon:boolean)
|
|
{
|
|
pageLayer.setAttribute("data-pageLoad", "false");
|
|
const cavasLayer = pageLayer.querySelector('.canvasWrapper') as HTMLDivElement;
|
|
if (cavasLayer)
|
|
{
|
|
cavasLayer.className = "loading_canvasWrapper";
|
|
|
|
if (isLoadingIcon)
|
|
{
|
|
const loadingLayer = document.createElement("div");
|
|
const loadingImage = document.createElement("img");
|
|
loadingImage.setAttribute("src", "pdf-loading.svg");
|
|
|
|
loadingLayer.appendChild(loadingImage);
|
|
loadingLayer.className = "loading";
|
|
cavasLayer.appendChild(loadingLayer);
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawLayerShowNHide(isShow:boolean)
|
|
{
|
|
if (containerRef.current)
|
|
{
|
|
const drawLayers = containerRef.current.querySelectorAll(".drawLayer");
|
|
drawLayers.forEach((layer) => {
|
|
if (!isShow)
|
|
{
|
|
(layer as HTMLElement).style.display = 'none';
|
|
}
|
|
else
|
|
{
|
|
(layer as HTMLElement).style.display = 'block';
|
|
}
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
const ZoomCtrl = (calcZoom:number) => {
|
|
|
|
if(calcZoom === calcScaleValue)
|
|
{
|
|
return;
|
|
}
|
|
if (observer)
|
|
{
|
|
observer.disconnect();
|
|
}
|
|
|
|
console.log("cal : " + calcZoom);
|
|
console.log("calsc : " + calcScaleValue);
|
|
console.log("cur : " + currentPage);
|
|
|
|
|
|
const targetPageIdx = currentPage;
|
|
const startIdx = Math.max(1, targetPageIdx - 3);
|
|
const endIdx = targetPageIdx + 3;
|
|
|
|
if (containerRef.current)
|
|
{
|
|
drawLayerShowNHide(false);
|
|
setCalcScaleValue(calcZoom);
|
|
|
|
const pdfViewer = containerRef.current;
|
|
|
|
for (let i = 1; i <= totalPage; i++) {
|
|
const pageLayer = pdfViewer.querySelector(`.pageLayer[data-pageidx="${i}"]`);
|
|
if (pageLayer) {
|
|
const canvas = pageLayer.querySelector('canvas');
|
|
if (canvas) {
|
|
if ( i >= startIdx && i <= endIdx)
|
|
{
|
|
const tempCanvas = document.createElement('canvas');
|
|
const tempCtx = tempCanvas.getContext('2d');
|
|
if (!tempCtx) {
|
|
console.error("Failed to get 2d context from tempCanvas.");
|
|
return;
|
|
}
|
|
tempCanvas.width = canvas.width;
|
|
tempCanvas.height = canvas.height;
|
|
tempCtx.drawImage(canvas, 0, 0);
|
|
canvas.width = (canvas.width / calcScaleValue) * calcZoom;
|
|
canvas.height = (canvas.height / calcScaleValue) * calcZoom;
|
|
|
|
const ctx = canvas.getContext('2d')!;
|
|
ctx.drawImage(tempCanvas, 0, 0, tempCanvas.width, tempCanvas.height, 0, 0, canvas.width, canvas.height);
|
|
}
|
|
else
|
|
{
|
|
drawLoadingPage(pageLayer, true);
|
|
canvas.width = (canvas.width / calcScaleValue) * calcZoom;
|
|
canvas.height = (canvas.height / calcScaleValue) * calcZoom;
|
|
}
|
|
|
|
console.log("can : " + canvas.width);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (timerRef.current) {
|
|
clearTimeout(timerRef.current);
|
|
}
|
|
|
|
timerRef.current = setTimeout(() => {
|
|
if (containerRef.current) {
|
|
setCalcScaleValue(calcZoom);
|
|
|
|
const pdfViewer = containerRef.current;
|
|
|
|
for (let i = startIdx; i <= endIdx; i++) {
|
|
const pageLayer = pdfViewer.querySelector(`.pageLayer[data-pageidx="${i}"]`);
|
|
if (pageLayer) {
|
|
drawLoadingPage(pageLayer, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
const container = containerRef.current;
|
|
container!.style.setProperty('--scale-factor', `${calcZoom}`); // 원하는 값으로 설정
|
|
|
|
setScaleValue(calcZoom);
|
|
setThreshold(calcThreshold(calcZoom));
|
|
|
|
console.log("타임 : " + calcZoom);
|
|
}, 500);
|
|
}
|
|
|
|
const observerOptions = {
|
|
root: null,
|
|
rootMargin: '0px 0px 30px 0px',
|
|
threshold: threshold,
|
|
}
|
|
|
|
function createObserver(pdfDocument:PDFDocumentProxy)
|
|
{
|
|
if (observer)
|
|
{
|
|
observer.disconnect();
|
|
}
|
|
|
|
const newObserver = new IntersectionObserver((entries, observer) => {
|
|
entries.forEach(async entry => {
|
|
// 관찰 대상이 viewport 안에 들어온 경우 image 로드
|
|
if (entry.isIntersecting) {
|
|
const pageLayer = entry.target;
|
|
const pageIdx: number = Number(pageLayer.getAttribute("data-pageidx"));
|
|
setCurrentPage(pageIdx);
|
|
await drawPdfPage(pdfDocument, pageLayer);
|
|
}
|
|
})
|
|
}, observerOptions);
|
|
|
|
setObserver(newObserver);
|
|
|
|
const pdfPages = document.querySelectorAll('.pageLayer');
|
|
pdfPages.forEach((pdfPage) => {
|
|
newObserver.observe(pdfPage);
|
|
})
|
|
|
|
return () => {
|
|
newObserver.disconnect();
|
|
};
|
|
}
|
|
|
|
async function drawPdfPage(pdfDocument:PDFDocumentProxy, pageLayer: Element) {
|
|
|
|
console.log(scaleValue);
|
|
if (pdfDocument) {
|
|
const pageIdx: number = Number(pageLayer.getAttribute("data-pageidx"));
|
|
const cavasLayer = pageLayer.querySelector('.loading_canvasWrapper') as HTMLDivElement;
|
|
const isLoad = pageLayer.getAttribute("data-pageLoad") === "true" ? true : false;
|
|
|
|
if (cavasLayer && !isLoad) {
|
|
pageLayer.setAttribute("data-pageLoad", `true`);
|
|
const page = await pdfDocument.getPage(pageIdx);
|
|
const textLayer = pageLayer.querySelector('.textLayer') as HTMLDivElement;
|
|
const canvas = pageLayer.querySelector('canvas');
|
|
const loadingLayer = pageLayer.querySelector('.loading');
|
|
const context = canvas!.getContext("2d");
|
|
if (!context) return;
|
|
|
|
const viewport = page.getViewport({
|
|
scale: scaleValue
|
|
, rotation: 0
|
|
})
|
|
|
|
await renderPage(page, context, viewport, textLayer).finally(() => {
|
|
cavasLayer.className = "canvasWrapper";
|
|
if (loadingLayer && cavasLayer.contains(loadingLayer)) {
|
|
cavasLayer.removeChild(loadingLayer);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
async function runZoom() {
|
|
if (containerRef.current && pdfDocument) {
|
|
|
|
const pdfViewer = containerRef.current;
|
|
const targetPageIdx = currentPage;
|
|
const startIdx = Math.max(1, targetPageIdx - 3);
|
|
const endIdx = targetPageIdx + 3;
|
|
for (let i = startIdx; i <= endIdx; i++) {
|
|
const pageLayer = pdfViewer.querySelector(`.pageLayer[data-pageidx="${i}"]`);
|
|
if (pageLayer)
|
|
{
|
|
await drawPdfPage(pdfDocument, pageLayer);
|
|
}
|
|
|
|
}
|
|
|
|
createObserver(pdfDocument);
|
|
}
|
|
}
|
|
|
|
async function loadPdfFromUrl() {
|
|
const response = await fetch(fileUrl);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch PDF from ${fileUrl}. Status: ${response.statusText}`);
|
|
}
|
|
const pdfBytes = new Uint8Array(await response.arrayBuffer());
|
|
return pdfBytes;
|
|
}
|
|
|
|
async function getPdfBlob() {
|
|
const warterMark = document.querySelector("#draw-1");
|
|
if (warterMark != null && pdfDocument) {
|
|
const waterMarkCanvas = await html2canvas(warterMark as HTMLElement, {
|
|
backgroundColor: null,
|
|
});
|
|
//패스워드가 있으면 이미지를 만들고 없으면 기존거에 워터마크만 추가한다.
|
|
const isSecurity = pdfPassword.length > 0;
|
|
let pdfDoc:PDFDocument = isSecurity ? await PDFDocument.create() : await PDFDocument.load(await pdfDocument.getData());
|
|
const waterMarkData = waterMarkCanvas.toDataURL("image/png");
|
|
const waterMarkImage = pdfDoc.embedPng(waterMarkData);
|
|
|
|
for (let i=1; i<=totalPage; i++)
|
|
{
|
|
setProcPercent(calculateProgress(i, totalPage, 100));
|
|
let pdfPage:PDFPage|null = null;
|
|
if (isSecurity)
|
|
{
|
|
const page = await pdfDocument.getPage(i);
|
|
const viewport = page.getViewport({ scale: 2.0 });
|
|
|
|
const canvas = document.createElement('canvas');
|
|
const context = canvas.getContext('2d');
|
|
canvas.height = viewport.height;
|
|
canvas.width = viewport.width;
|
|
|
|
if (context && viewport) {
|
|
const renderContext = {
|
|
canvasContext: context,
|
|
viewport: viewport
|
|
};
|
|
await page.render(renderContext).promise;
|
|
pdfPage = pdfDoc.addPage([viewport.width, viewport.height]);
|
|
const pdfPageIamge = pdfDoc.embedJpg(canvas.toDataURL("image/jpeg"));
|
|
pdfPage.drawImage(await pdfPageIamge, {
|
|
x: 0,
|
|
y: 0,
|
|
width: viewport.width,
|
|
height: viewport.height
|
|
});
|
|
}
|
|
|
|
}
|
|
else
|
|
{
|
|
pdfPage = pdfDoc.getPages()[i-1];
|
|
}
|
|
|
|
//워터마크 삽입
|
|
if (pdfPage && waterMarkImage)
|
|
{
|
|
pdfPage.drawImage(await waterMarkImage, {
|
|
x: 0,
|
|
y: 0,
|
|
width: pdfPage.getWidth(),
|
|
height: pdfPage.getHeight(),
|
|
});
|
|
}
|
|
}
|
|
|
|
const modifiedPdfBytes = await pdfDoc.save();
|
|
|
|
setProcPercent(100);
|
|
const blob = new Blob([modifiedPdfBytes], {type: 'application/pdf'});
|
|
return blob;
|
|
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
const downloadPdfDocument = async () => {
|
|
|
|
const blob = await getPdfBlob();
|
|
if (blob)
|
|
{
|
|
const pdfUrl = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = pdfUrl;
|
|
link.download = 'overlayed.pdf';
|
|
link.click();
|
|
|
|
URL.revokeObjectURL(pdfUrl);
|
|
|
|
hideModal();
|
|
|
|
}
|
|
};
|
|
|
|
const printPdfDocument = async () => {
|
|
|
|
const blob = await getPdfBlob();
|
|
if (blob)
|
|
{
|
|
const pdfUrl = URL.createObjectURL(blob);
|
|
const printIframe = document.getElementById('printFrame') as HTMLIFrameElement;
|
|
|
|
printIframe.src = pdfUrl;
|
|
const printWindow = printIframe.contentWindow;
|
|
if (printWindow) {
|
|
printIframe.onload = function () {
|
|
setTimeout(function () {
|
|
printWindow.print();
|
|
hideModal();
|
|
URL.revokeObjectURL(pdfUrl);
|
|
}, 500);
|
|
};
|
|
}
|
|
}
|
|
};
|
|
|
|
function calculateProgress(currentPage:number, totalPage:number, totalPercent:number) {
|
|
const progress = (currentPage / totalPage) * totalPercent;
|
|
console.log(progress);
|
|
return Math.min(Math.round(progress), totalPercent); // 진행률이 100%를 초과하지 않도록 합니다.
|
|
}
|
|
|
|
function showModal(revModalTitle:string, isProgress:boolean)
|
|
{
|
|
if (isProgress)
|
|
{
|
|
setProgressVisible(true);
|
|
}
|
|
setProcPercent(0);
|
|
setModalTitle(revModalTitle);
|
|
setModalVisible(true);
|
|
}
|
|
|
|
function hideModal()
|
|
{
|
|
setPdfErrorMsg("");
|
|
setModalVisible(false);
|
|
setModalTitle("");
|
|
setProcPercent(0);
|
|
setProgressVisible(false);
|
|
setPasswordVisible(false);
|
|
}
|
|
|
|
function PrevPage()
|
|
{
|
|
const movePageNum = currentPage-1;
|
|
setCurrentPage(movePage(movePageNum < 1 ? 1: movePageNum ));
|
|
}
|
|
|
|
function NextPage()
|
|
{
|
|
const movePageNum = currentPage+1;
|
|
setCurrentPage(movePage(movePageNum > totalPage ? totalPage : movePageNum ));
|
|
}
|
|
|
|
|
|
const handleDoubleClick = (event:React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
event.preventDefault();
|
|
|
|
leftClickCountRef.current += 1;
|
|
|
|
if (leftClickCountRef.current === 1) {
|
|
timerClickRef.current = setTimeout(() => {
|
|
leftClickCountRef.current = 0; // 클릭 횟수 초기화
|
|
}, 250);
|
|
} else if (leftClickCountRef.current === 2) {
|
|
if (timerClickRef.current) {
|
|
clearTimeout(timerClickRef.current);
|
|
}
|
|
|
|
ZoomCtrl( zoomIn( calcScaleValue || scaleValue ) );
|
|
leftClickCountRef.current = 0; // 클릭 횟수 초기화
|
|
}
|
|
};
|
|
|
|
const handleRightClick = (event:React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
|
event.preventDefault();
|
|
|
|
rightClickCountRef.current += 1;
|
|
|
|
if (rightClickCountRef.current === 1) {
|
|
timerClickRef.current = setTimeout(() => {
|
|
rightClickCountRef.current = 0; // 클릭 횟수 초기화
|
|
}, 250);
|
|
} else if (rightClickCountRef.current === 2) {
|
|
if (timerClickRef.current) {
|
|
clearTimeout(timerClickRef.current);
|
|
}
|
|
ZoomCtrl( zoomOut( calcScaleValue || scaleValue ) );
|
|
rightClickCountRef.current = 0; // 클릭 횟수 초기화
|
|
}
|
|
};
|
|
|
|
const handlePasswordSubmit = () => {
|
|
loadPdf(pdfPassword).then(r => { });
|
|
|
|
};
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
if (waterMarkText && waterMarkCanvasRef.current) {
|
|
const canvas = waterMarkCanvasRef.current;
|
|
const ctx = canvas.getContext('2d');
|
|
const fontSize = 70 * scaleValue;
|
|
const padding = fontSize;
|
|
if (ctx) {
|
|
|
|
ctx.font = `${fontSize}px Arial`;
|
|
const textWidth = ctx.measureText(waterMarkText).width;
|
|
const textHeight = fontSize; // 이 값은 폰트 크기에 따라 조절하시길 바랍니다.
|
|
|
|
canvas.width = textWidth + padding;
|
|
canvas.height = textHeight + padding * 2;
|
|
|
|
ctx.font = `${fontSize}px Arial`; // 캔버스 크기가 변경되면 폰트 설정도 초기화되므로 다시 설정해야 합니다.
|
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.10)';
|
|
ctx.fillText(waterMarkText, 0, textHeight - 5); // y 좌표를 조절하여 텍스트가 캔버스 내에 잘 들어가도록 합니다.
|
|
|
|
//setWaterMarkImgURl(canvas.toDataURL('image/png'));
|
|
const waterMarkImgURl = canvas.toDataURL('image/png');
|
|
if(waterMarkImgURl)
|
|
{
|
|
const style = document.createElement('style');
|
|
style.innerHTML = `
|
|
.pdfViewer .waterMarkLayer::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: 0px;
|
|
left: 0px;
|
|
width: 200%;
|
|
height: 200%;
|
|
background-image: url(${waterMarkImgURl});
|
|
background-repeat: repeat;
|
|
transform: rotate(-45deg);
|
|
background-color: transparent;
|
|
}`;
|
|
document.head.appendChild(style);
|
|
}
|
|
}
|
|
}
|
|
}, [waterMarkText, scaleValue]);
|
|
|
|
|
|
const loadPdf = async (password: string | null | undefined) => {
|
|
console.log("PDFViewer component rendered");
|
|
|
|
const container = containerRef.current;
|
|
|
|
if (!container) return;
|
|
|
|
// Clear the container's contents whenever fileUrl changes
|
|
container.innerHTML = "";
|
|
|
|
const loadOption: any = {
|
|
url: fileUrl,
|
|
cMapUrl: `${currentUrl}/cmaps/`,
|
|
cMapPacked: true,
|
|
enableXfa: true,
|
|
};
|
|
|
|
if (password) {
|
|
loadOption.password = password;
|
|
}
|
|
|
|
let isError = false;
|
|
|
|
try {
|
|
const pdfDocumentProxy: PDFDocumentProxy = await getDocument(loadOption).promise;
|
|
|
|
setPdfDocument(pdfDocumentProxy);
|
|
setTotalPage(pdfDocumentProxy.numPages);
|
|
|
|
container.style.setProperty('--scale-factor', `${scaleValue}`); // 원하는 값으로 설정
|
|
|
|
for (let pageNum = 1; pageNum <= pdfDocumentProxy.numPages; pageNum++) {
|
|
await loadPage(pdfDocumentProxy, pageNum, scaleValue, 0, container, isRenderText, waterMarkText);
|
|
}
|
|
|
|
createObserver(pdfDocumentProxy);
|
|
|
|
} catch (error:any) {
|
|
isError = true
|
|
if (error.name === 'PasswordException' && !password) {
|
|
showModal("보안 PDF", false);
|
|
setPasswordVisible(true);
|
|
}
|
|
else if (error.name === 'PasswordException' && error.message === 'Incorrect Password' && password) {
|
|
|
|
setPdfErrorMsg("PDF 암호가 잘못되었습니다.");
|
|
showModal("보안 PDF", false);
|
|
setPasswordVisible(true);
|
|
}
|
|
else {
|
|
console.error(error);
|
|
}
|
|
}
|
|
finally {
|
|
if (!isError && password)
|
|
hideModal();
|
|
}
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
setTotalPage(0);
|
|
setCurrentPage(1);
|
|
setPdfErrorMsg("");
|
|
setPdfPassword("");
|
|
|
|
loadPdf(null);
|
|
|
|
}, [fileUrl]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
runZoom().then().finally( () =>{
|
|
drawLayerShowNHide(true);
|
|
});
|
|
}, [ scaleValue ]);
|
|
|
|
|
|
useEffect(() => {
|
|
const handleKeyDown = (event:KeyboardEvent) => {
|
|
|
|
switch (event.key) {
|
|
case 'PageUp':
|
|
PrevPage();
|
|
console.log("up");
|
|
event.preventDefault(); // 기본 동작 방지
|
|
break;
|
|
case 'PageDown':
|
|
NextPage();
|
|
console.log("down");
|
|
event.preventDefault(); // 기본 동작 방지
|
|
break;
|
|
case '+':
|
|
ZoomCtrl( zoomIn( calcScaleValue || scaleValue ) );
|
|
break;
|
|
case '-':
|
|
ZoomCtrl( zoomOut( calcScaleValue || scaleValue ) );
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
|
return () => {
|
|
window.removeEventListener('keydown', handleKeyDown);
|
|
};
|
|
}, [currentPage, scaleValue, calcScaleValue, totalPage]); // currentPage가 변경될 때마다 이벤트 리스너 갱신
|
|
|
|
return (
|
|
|
|
<div
|
|
style={{
|
|
width: '100%',
|
|
}}
|
|
>
|
|
<nav style={{
|
|
margin:'0px 0px 4px 0px',
|
|
padding:'9px 8px 9px 0px',
|
|
height: '50px',
|
|
display:'flex',
|
|
justifyContent: "space-between",
|
|
alignItems:'center',
|
|
borderRadius: "8px", // 라운드 처리 추가
|
|
border: '1px solid #dadada',
|
|
|
|
background: "white",
|
|
}}>
|
|
<div style={{ width:'34%', marginLeft: '8px' }}>
|
|
|
|
<Space.Compact style={{
|
|
width:'100%',
|
|
justifyContent:'left',
|
|
alignItems:'center',
|
|
display:'flex'
|
|
}} block>
|
|
<Tooltip title="이전페이지" arrow={false}>
|
|
<Button type="primary" icon={<CaretLeftOutlined />} onClick={() => {
|
|
PrevPage();
|
|
}}/>
|
|
</Tooltip>
|
|
<Input
|
|
type="number"
|
|
style={{
|
|
marginLeft : "10px",
|
|
marginRight : "5px",
|
|
width : "65px"
|
|
}} size="small" value={currentPage}
|
|
onChange={e => setCurrentPage(Number(e.target.value))}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
setCurrentPage(movePage(currentPage));
|
|
}
|
|
}}
|
|
/>
|
|
/
|
|
<Input style={{
|
|
marginLeft : "5px",
|
|
marginRight : "10px",
|
|
width : "40px"
|
|
}} size="small" value={totalPage} readOnly/>
|
|
<Tooltip title="다음페이지" arrow={false}>
|
|
<Button type="primary" icon={<CaretRightOutlined />} onClick={() => {
|
|
|
|
NextPage();
|
|
|
|
}}/>
|
|
</Tooltip>
|
|
|
|
|
|
|
|
</Space.Compact>
|
|
|
|
|
|
</div>
|
|
<div style={{ width:'33%', textAlign: 'center', flexGrow: 1 }}>
|
|
<Space.Compact style={{
|
|
width:'100%',
|
|
justifyContent:'center',
|
|
}} block>
|
|
<Tooltip title="확대" arrow={false}>
|
|
<Button type="primary" icon={<ZoomInOutlined />} onClick={() => {
|
|
|
|
ZoomCtrl( zoomIn( calcScaleValue || scaleValue ) );
|
|
//zoonIn();
|
|
}}/>
|
|
</Tooltip>
|
|
|
|
<Tooltip title="비율로보기">
|
|
<Dropdown
|
|
placement="bottomCenter"
|
|
menu={raioMenuProps}
|
|
>
|
|
<Button type="primary" icon={<PercentageOutlined />}/>
|
|
</Dropdown >
|
|
</Tooltip>
|
|
<Tooltip title="축소" arrow={false}>
|
|
<Button type="primary" icon={<ZoomOutOutlined />} onClick={() => {
|
|
//zoonOut();
|
|
ZoomCtrl( zoomOut( calcScaleValue || scaleValue ) );
|
|
}}/>
|
|
</Tooltip>
|
|
</Space.Compact>
|
|
</div>
|
|
<div style={{ width:'33%', marginRight: '8px',textAlign: 'right' }}>
|
|
<Space.Compact style={{
|
|
width:'100%',
|
|
justifyContent:'flex-end',
|
|
}} block>
|
|
<Tooltip title="전체화면" arrow={false}>
|
|
<Button type="primary" icon={<FullscreenOutlined />} onClick={() => {
|
|
enterFullScreen("pdf_content");
|
|
}}/>
|
|
</Tooltip>
|
|
<Tooltip title="다운로드" arrow={false}>
|
|
<Button type="primary" icon={<DownloadOutlined />} onClick={() => {
|
|
showModal("다운로드 준비중..", true);
|
|
downloadPdfDocument().finally(() => {
|
|
});
|
|
}
|
|
}/>
|
|
</Tooltip>
|
|
<Tooltip title="인쇄" arrow={false}>
|
|
<Button type="primary" icon={<PrinterOutlined />} onClick={() => {
|
|
showModal("인쇄 준비중..", true);
|
|
printPdfDocument().finally(() => {
|
|
});
|
|
}}/>
|
|
</Tooltip>
|
|
</Space.Compact>
|
|
</div>
|
|
</nav>
|
|
<div
|
|
id="pdf_content"
|
|
style={{
|
|
border: '1px solid rgba(0, 0, 0, 0.1)',
|
|
display: 'flex',
|
|
height: 'calc( 100vh - 120px )',
|
|
}}
|
|
|
|
>
|
|
<div
|
|
style={{
|
|
width:'100%',
|
|
overflow:"auto"
|
|
}}
|
|
>
|
|
<div onClick={handleDoubleClick}
|
|
onContextMenu={handleRightClick}
|
|
className="pdfViewer" ref={containerRef} />
|
|
</div>
|
|
<canvas ref={waterMarkCanvasRef} style={{ display: 'none' }} />
|
|
<iframe id="printFrame"/>
|
|
|
|
<Modal
|
|
centered
|
|
open={modalVisible}
|
|
title={modalTitle}
|
|
footer={null}
|
|
closable={false}
|
|
>
|
|
{progressVisible && <Progress percent={procPercent} status="active" />}
|
|
{
|
|
passwordVisible &&
|
|
<>
|
|
<div style={{
|
|
fontWeight:"bold",
|
|
marginLeft:"10px",
|
|
marginBottom : "10px",
|
|
color:"red"}}>{pdfErrorMsg}</div>
|
|
<Input
|
|
placeholder="문서가 비밀번호로 보호되고 있습니다. 비밀번호를 입력하세요."
|
|
type="password"
|
|
style={{
|
|
margin : "0px",
|
|
width : "100%",
|
|
height : "35px"
|
|
}} size="small"
|
|
value={pdfPassword}
|
|
onChange={(e) => {setPdfPassword(e.target.value)}}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
handlePasswordSubmit();
|
|
}
|
|
}}
|
|
/>
|
|
|
|
</>
|
|
|
|
}
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|