mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 01:01:49 +10:00
We were previously omitting cmaps meaning that when signing documents with certain UTF-8 characters or CJK characters they would appear as outlined squares in the pdf viewer despite the actual pdf looking as expected with the characters displaying correctly.
273 lines
7.9 KiB
TypeScript
273 lines
7.9 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
|
|
|
import { msg } from '@lingui/core/macro';
|
|
import { useLingui } from '@lingui/react';
|
|
import { Trans } from '@lingui/react/macro';
|
|
import type { EnvelopeItem } from '@prisma/client';
|
|
import { base64 } from '@scure/base';
|
|
import { Loader } from 'lucide-react';
|
|
import { type PDFDocumentProxy } from 'pdfjs-dist';
|
|
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
|
|
|
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
// import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
|
// import 'react-pdf/dist/esm/Page/TextLayer.css';
|
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
|
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
|
|
|
|
import { cn } from '../lib/utils';
|
|
import { useToast } from './use-toast';
|
|
|
|
export type LoadedPDFDocument = PDFDocumentProxy;
|
|
|
|
/**
|
|
* This imports the worker from the `pdfjs-dist` package.
|
|
*/
|
|
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
|
'pdfjs-dist/build/pdf.worker.min.js',
|
|
import.meta.url,
|
|
).toString();
|
|
|
|
const pdfViewerOptions = {
|
|
cMapUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/static/cmaps`,
|
|
};
|
|
|
|
export type OnPDFViewerPageClick = (_event: {
|
|
pageNumber: number;
|
|
numPages: number;
|
|
originalEvent: React.MouseEvent<HTMLDivElement, MouseEvent>;
|
|
pageHeight: number;
|
|
pageWidth: number;
|
|
pageX: number;
|
|
pageY: number;
|
|
}) => void | Promise<void>;
|
|
|
|
const PDFLoader = () => (
|
|
<>
|
|
<Loader className="text-documenso h-12 w-12 animate-spin" />
|
|
|
|
<p className="text-muted-foreground mt-4">
|
|
<Trans>Loading document...</Trans>
|
|
</p>
|
|
</>
|
|
);
|
|
|
|
export type PDFViewerProps = {
|
|
className?: string;
|
|
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
|
token: string | undefined;
|
|
version: 'original' | 'signed';
|
|
onDocumentLoad?: (_doc: LoadedPDFDocument) => void;
|
|
onPageClick?: OnPDFViewerPageClick;
|
|
overrideData?: string;
|
|
[key: string]: unknown;
|
|
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'onPageClick'>;
|
|
|
|
export const PDFViewer = ({
|
|
className,
|
|
envelopeItem,
|
|
token,
|
|
version,
|
|
onDocumentLoad,
|
|
onPageClick,
|
|
overrideData,
|
|
...props
|
|
}: PDFViewerProps) => {
|
|
const { _ } = useLingui();
|
|
const { toast } = useToast();
|
|
|
|
const $el = useRef<HTMLDivElement>(null);
|
|
|
|
const [isDocumentBytesLoading, setIsDocumentBytesLoading] = useState(false);
|
|
const [documentBytes, setDocumentBytes] = useState<Uint8Array | null>(
|
|
overrideData ? base64.decode(overrideData) : null,
|
|
);
|
|
|
|
const [width, setWidth] = useState(0);
|
|
const [numPages, setNumPages] = useState(0);
|
|
const [pdfError, setPdfError] = useState(false);
|
|
|
|
const isLoading = isDocumentBytesLoading || !documentBytes;
|
|
|
|
const onDocumentLoaded = (doc: LoadedPDFDocument) => {
|
|
setNumPages(doc.numPages);
|
|
onDocumentLoad?.(doc);
|
|
};
|
|
|
|
const onDocumentPageClick = (
|
|
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
|
|
pageNumber: number,
|
|
) => {
|
|
const $el = event.target instanceof HTMLElement ? event.target : null;
|
|
|
|
if (!$el) {
|
|
return;
|
|
}
|
|
|
|
const $page = $el.closest(PDF_VIEWER_PAGE_SELECTOR);
|
|
|
|
if (!$page) {
|
|
return;
|
|
}
|
|
|
|
const { height, width, top, left } = $page.getBoundingClientRect();
|
|
|
|
const pageX = event.clientX - left;
|
|
const pageY = event.clientY - top;
|
|
|
|
if (onPageClick) {
|
|
void onPageClick({
|
|
pageNumber,
|
|
numPages,
|
|
originalEvent: event,
|
|
pageHeight: height,
|
|
pageWidth: width,
|
|
pageX,
|
|
pageY,
|
|
});
|
|
}
|
|
};
|
|
|
|
useEffect(() => {
|
|
if ($el.current) {
|
|
const $current = $el.current;
|
|
|
|
const { width } = $current.getBoundingClientRect();
|
|
|
|
setWidth(width);
|
|
|
|
const onResize = () => {
|
|
const { width } = $current.getBoundingClientRect();
|
|
|
|
setWidth(width);
|
|
};
|
|
|
|
window.addEventListener('resize', onResize);
|
|
|
|
return () => {
|
|
window.removeEventListener('resize', onResize);
|
|
};
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (overrideData) {
|
|
const bytes = base64.decode(overrideData);
|
|
|
|
setDocumentBytes(bytes);
|
|
return;
|
|
}
|
|
|
|
const fetchDocumentBytes = async () => {
|
|
try {
|
|
setIsDocumentBytesLoading(true);
|
|
|
|
const documentUrl = getEnvelopeItemPdfUrl({
|
|
type: 'view',
|
|
envelopeItem: envelopeItem,
|
|
token,
|
|
});
|
|
|
|
const bytes = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
|
|
|
setDocumentBytes(new Uint8Array(bytes));
|
|
|
|
setIsDocumentBytesLoading(false);
|
|
} catch (err) {
|
|
console.error(err);
|
|
|
|
toast({
|
|
title: _(msg`Error`),
|
|
description: _(msg`An error occurred while loading the document.`),
|
|
variant: 'destructive',
|
|
});
|
|
}
|
|
};
|
|
|
|
void fetchDocumentBytes();
|
|
}, [envelopeItem.envelopeId, envelopeItem.id, token, version, toast, overrideData]);
|
|
|
|
return (
|
|
<div ref={$el} className={cn('overflow-hidden', className)} {...props}>
|
|
{isLoading ? (
|
|
<div
|
|
className={cn(
|
|
'flex h-[80vh] max-h-[60rem] w-full flex-col items-center justify-center overflow-hidden rounded',
|
|
)}
|
|
>
|
|
<PDFLoader />
|
|
</div>
|
|
) : (
|
|
<>
|
|
<PDFDocument
|
|
file={documentBytes.buffer}
|
|
className={cn('w-full overflow-hidden rounded', {
|
|
'h-[80vh] max-h-[60rem]': numPages === 0,
|
|
})}
|
|
onLoadSuccess={(d) => onDocumentLoaded(d)}
|
|
// Uploading a invalid document causes an error which doesn't appear to be handled by the `error` prop.
|
|
// Therefore we add some additional custom error handling.
|
|
onSourceError={() => {
|
|
setPdfError(true);
|
|
}}
|
|
externalLinkTarget="_blank"
|
|
loading={
|
|
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
|
|
{pdfError ? (
|
|
<div className="text-muted-foreground text-center">
|
|
<p>
|
|
<Trans>Something went wrong while loading the document.</Trans>
|
|
</p>
|
|
<p className="mt-1 text-sm">
|
|
<Trans>Please try again or contact our support.</Trans>
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<PDFLoader />
|
|
)}
|
|
</div>
|
|
}
|
|
error={
|
|
<div className="dark:bg-background flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50">
|
|
<div className="text-muted-foreground text-center">
|
|
<p>
|
|
<Trans>Something went wrong while loading the document.</Trans>
|
|
</p>
|
|
<p className="mt-1 text-sm">
|
|
<Trans>Please try again or contact our support.</Trans>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
// options={pdfViewerOptions}
|
|
>
|
|
{Array(numPages)
|
|
.fill(null)
|
|
.map((_, i) => (
|
|
<div key={i} className="last:-mb-2">
|
|
<div className="border-border overflow-hidden rounded border will-change-transform">
|
|
<PDFPage
|
|
pageNumber={i + 1}
|
|
width={width}
|
|
renderAnnotationLayer={false}
|
|
renderTextLayer={false}
|
|
loading={() => ''}
|
|
onClick={(e) => onDocumentPageClick(e, i + 1)}
|
|
/>
|
|
</div>
|
|
<p className="text-muted-foreground/80 my-2 text-center text-[11px]">
|
|
<Trans>
|
|
Page {i + 1} of {numPages}
|
|
</Trans>
|
|
</p>
|
|
</div>
|
|
))}
|
|
</PDFDocument>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PDFViewer;
|