lazy pdf rendering
This commit is contained in:
@@ -39,8 +39,6 @@ interface PageDescriptor {
|
|||||||
scale: number;
|
scale: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN_RENDER_WIDTH = 1200;
|
|
||||||
const MAX_RENDER_WIDTH = 2200;
|
|
||||||
const RERENDER_DELTA = 48;
|
const RERENDER_DELTA = 48;
|
||||||
|
|
||||||
const getAvailableViewportSize = (viewportNode: Element, stackNode: Element) => {
|
const getAvailableViewportSize = (viewportNode: Element, stackNode: Element) => {
|
||||||
@@ -82,8 +80,11 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
|||||||
const [pages, setPages] = useState<PageDescriptor[]>([]);
|
const [pages, setPages] = useState<PageDescriptor[]>([]);
|
||||||
const [pixelRatio, setPixelRatio] = useState(1);
|
const [pixelRatio, setPixelRatio] = useState(1);
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('contain');
|
const [viewMode, setViewMode] = useState<ViewMode>('contain');
|
||||||
|
const [visiblePages, setVisiblePages] = useState<Set<number>>(new Set());
|
||||||
const pdfRef = useRef<PDFDocumentProxy | null>(null);
|
const pdfRef = useRef<PDFDocumentProxy | null>(null);
|
||||||
const wasmUrlRef = useRef<string | null>(null);
|
const wasmUrlRef = useRef<string | null>(null);
|
||||||
|
const pageNodeMapRef = useRef<Map<number, HTMLElement>>(new Map());
|
||||||
|
const intersectionObserverRef = useRef<IntersectionObserver | null>(null);
|
||||||
const focusTargetRef = useRef<{
|
const focusTargetRef = useRef<{
|
||||||
ratioX: number;
|
ratioX: number;
|
||||||
ratioY: number;
|
ratioY: number;
|
||||||
@@ -152,7 +153,7 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
|||||||
if (viewportWidth <= 0) {
|
if (viewportWidth <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const desiredWidth = Math.min(MAX_RENDER_WIDTH, Math.max(viewportWidth, MIN_RENDER_WIDTH));
|
const desiredWidth = viewportWidth;
|
||||||
setRenderWidth((current) => {
|
setRenderWidth((current) => {
|
||||||
if (current === 0 || desiredWidth > current + RERENDER_DELTA) {
|
if (current === 0 || desiredWidth > current + RERENDER_DELTA) {
|
||||||
return desiredWidth;
|
return desiredWidth;
|
||||||
@@ -214,8 +215,6 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
|||||||
setPages([]);
|
setPages([]);
|
||||||
setStatus('loading');
|
setStatus('loading');
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
console.log('[PdfViewer] rendering pages', { renderWidth, mode: viewMode, src });
|
|
||||||
|
|
||||||
const renderDocument = async () => {
|
const renderDocument = async () => {
|
||||||
try {
|
try {
|
||||||
loadingTask = getDocument({
|
loadingTask = getDocument({
|
||||||
@@ -322,6 +321,100 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
|||||||
toggleViewMode();
|
toggleViewMode();
|
||||||
}, [toggleViewMode, viewportRef]);
|
}, [toggleViewMode, viewportRef]);
|
||||||
|
|
||||||
|
const handlePageVisibilityChange = useCallback((pageNumber: number, isVisible: boolean) => {
|
||||||
|
setVisiblePages((prev) => {
|
||||||
|
const alreadyVisible = prev.has(pageNumber);
|
||||||
|
if (alreadyVisible === isVisible) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (isVisible) {
|
||||||
|
next.add(pageNumber);
|
||||||
|
} else {
|
||||||
|
next.delete(pageNumber);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const shouldRenderPage = useCallback((pageNumber: number) => {
|
||||||
|
if (visiblePages.size === 0) {
|
||||||
|
return pageNumber === 1;
|
||||||
|
}
|
||||||
|
for (const visiblePage of visiblePages) {
|
||||||
|
if (Math.abs(visiblePage - pageNumber) <= 2) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, [visiblePages]);
|
||||||
|
|
||||||
|
const intersectionRoot = viewportRef?.current || null;
|
||||||
|
|
||||||
|
const registerPageNode = useCallback((pageNumber: number, node: HTMLElement | null) => {
|
||||||
|
const map = pageNodeMapRef.current;
|
||||||
|
const existing = map.get(pageNumber);
|
||||||
|
if (existing === node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (existing) {
|
||||||
|
intersectionObserverRef.current?.unobserve(existing);
|
||||||
|
map.delete(pageNumber);
|
||||||
|
}
|
||||||
|
if (node) {
|
||||||
|
node.dataset.pageNumber = String(pageNumber);
|
||||||
|
map.set(pageNumber, node);
|
||||||
|
intersectionObserverRef.current?.observe(node);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let frame: number | null = null;
|
||||||
|
let observer: IntersectionObserver | null = null;
|
||||||
|
|
||||||
|
const attachObserver = () => {
|
||||||
|
const stackElement = containerRef.current;
|
||||||
|
if (!stackElement) {
|
||||||
|
frame = requestAnimationFrame(attachObserver);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const root = intersectionRoot
|
||||||
|
|| stackElement.closest('.document-viewer__viewport')
|
||||||
|
|| undefined;
|
||||||
|
observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
const attr = entry.target.getAttribute('data-page-number');
|
||||||
|
if (!attr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pageNumber = Number(attr);
|
||||||
|
if (!Number.isFinite(pageNumber) || pageNumber <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handlePageVisibilityChange(pageNumber, entry.isIntersecting);
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
root: root as Element | undefined,
|
||||||
|
rootMargin: '200px 0px',
|
||||||
|
threshold: 0.1,
|
||||||
|
});
|
||||||
|
intersectionObserverRef.current = observer;
|
||||||
|
pageNodeMapRef.current.forEach((node) => observer?.observe(node));
|
||||||
|
};
|
||||||
|
|
||||||
|
attachObserver();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (frame !== null) {
|
||||||
|
cancelAnimationFrame(frame);
|
||||||
|
}
|
||||||
|
observer?.disconnect();
|
||||||
|
if (intersectionObserverRef.current === observer) {
|
||||||
|
intersectionObserverRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [handlePageVisibilityChange, intersectionRoot]);
|
||||||
|
|
||||||
const pageElements = useMemo(() => (
|
const pageElements = useMemo(() => (
|
||||||
pages.map((page) => (
|
pages.map((page) => (
|
||||||
<Fragment key={`${page.number}-${Math.round(page.scale * 100)}-${Math.round(pixelRatio * 100)}`}>
|
<Fragment key={`${page.number}-${Math.round(page.scale * 100)}-${Math.round(pixelRatio * 100)}`}>
|
||||||
@@ -329,13 +422,17 @@ const PdfViewer = ({ src, title, className, viewportRef }: PdfViewerProps): JSX.
|
|||||||
descriptor={page}
|
descriptor={page}
|
||||||
pdfRef={pdfRef}
|
pdfRef={pdfRef}
|
||||||
pixelRatio={pixelRatio}
|
pixelRatio={pixelRatio}
|
||||||
rootRef={containerRef}
|
|
||||||
viewportRef={viewportRef}
|
|
||||||
onPageClick={handlePageClick}
|
onPageClick={handlePageClick}
|
||||||
|
viewportWidth={viewportWidth}
|
||||||
|
viewportHeight={viewportHeight}
|
||||||
|
viewMode={viewMode}
|
||||||
|
shouldRender={shouldRenderPage(page.number)}
|
||||||
|
registerPageNode={registerPageNode}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))
|
))
|
||||||
), [handlePageClick, pages, pixelRatio, viewportRef]);
|
), [handlePageClick, pages, pixelRatio, registerPageNode, shouldRenderPage,
|
||||||
|
viewportHeight, viewportWidth, viewMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={viewerClasses}>
|
<div className={viewerClasses}>
|
||||||
@@ -363,23 +460,34 @@ interface PdfPageCanvasProps {
|
|||||||
descriptor: PageDescriptor;
|
descriptor: PageDescriptor;
|
||||||
pdfRef: MutableRefObject<PDFDocumentProxy | null>;
|
pdfRef: MutableRefObject<PDFDocumentProxy | null>;
|
||||||
pixelRatio: number;
|
pixelRatio: number;
|
||||||
rootRef: RefObject<HTMLDivElement>;
|
|
||||||
viewportRef?: RefObject<HTMLElement | null>;
|
|
||||||
onPageClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
onPageClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
|
viewportWidth: number;
|
||||||
|
viewportHeight: number;
|
||||||
|
viewMode: ViewMode;
|
||||||
|
shouldRender: boolean;
|
||||||
|
registerPageNode: (pageNumber: number, node: HTMLElement | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PdfPageCanvas({
|
function PdfPageCanvas({
|
||||||
descriptor,
|
descriptor,
|
||||||
pdfRef,
|
pdfRef,
|
||||||
pixelRatio,
|
pixelRatio,
|
||||||
rootRef,
|
|
||||||
viewportRef,
|
|
||||||
onPageClick,
|
onPageClick,
|
||||||
|
viewportWidth,
|
||||||
|
viewportHeight,
|
||||||
|
viewMode,
|
||||||
|
shouldRender,
|
||||||
|
registerPageNode,
|
||||||
}: PdfPageCanvasProps): JSX.Element {
|
}: PdfPageCanvasProps): JSX.Element {
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const renderTaskRef = useRef<RenderTask | null>(null);
|
const renderTaskRef = useRef<RenderTask | null>(null);
|
||||||
const hasRenderedRef = useRef(false);
|
const hasRenderedRef = useRef(false);
|
||||||
|
const setWrapperNode = useCallback((node: HTMLDivElement | null) => {
|
||||||
|
wrapperRef.current = node;
|
||||||
|
registerPageNode(descriptor.number, node);
|
||||||
|
}, [descriptor.number, registerPageNode]);
|
||||||
|
const [canvasMounted, setCanvasMounted] = useState(false);
|
||||||
|
|
||||||
const renderPage = useCallback(async () => {
|
const renderPage = useCallback(async () => {
|
||||||
if (renderTaskRef.current || hasRenderedRef.current) {
|
if (renderTaskRef.current || hasRenderedRef.current) {
|
||||||
@@ -393,23 +501,56 @@ function PdfPageCanvas({
|
|||||||
try {
|
try {
|
||||||
const page = await pdf.getPage(descriptor.number);
|
const page = await pdf.getPage(descriptor.number);
|
||||||
const viewport = page.getViewport({ scale: descriptor.scale });
|
const viewport = page.getViewport({ scale: descriptor.scale });
|
||||||
const context = canvas.getContext('2d');
|
const qualityScale = 1;
|
||||||
if (!context) {
|
const supportsOffscreen = typeof OffscreenCanvas !== 'undefined';
|
||||||
return;
|
let renderContext: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null = null;
|
||||||
|
let offscreenCanvas: OffscreenCanvas | null = null;
|
||||||
|
|
||||||
|
if (supportsOffscreen) {
|
||||||
|
offscreenCanvas = new OffscreenCanvas(
|
||||||
|
viewport.width * pixelRatio * qualityScale,
|
||||||
|
viewport.height * pixelRatio * qualityScale,
|
||||||
|
);
|
||||||
|
renderContext = offscreenCanvas.getContext('2d');
|
||||||
|
} else {
|
||||||
|
renderContext = canvas.getContext('2d');
|
||||||
|
if (!renderContext) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
canvas.width = viewport.width * pixelRatio;
|
||||||
|
canvas.height = viewport.height * pixelRatio;
|
||||||
}
|
}
|
||||||
canvas.width = viewport.width * pixelRatio;
|
|
||||||
canvas.height = viewport.height * pixelRatio;
|
renderContext?.setTransform(pixelRatio * qualityScale, 0, 0, pixelRatio * qualityScale, 0, 0);
|
||||||
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
||||||
const renderTask = page.render({ canvasContext: context, viewport, canvas });
|
const renderTask = page.render({
|
||||||
|
canvasContext: renderContext as CanvasRenderingContext2D,
|
||||||
|
viewport,
|
||||||
|
canvas: supportsOffscreen ? undefined : canvas,
|
||||||
|
});
|
||||||
renderTaskRef.current = renderTask;
|
renderTaskRef.current = renderTask;
|
||||||
await renderTask.promise;
|
await renderTask.promise;
|
||||||
hasRenderedRef.current = true;
|
hasRenderedRef.current = true;
|
||||||
|
|
||||||
|
if (offscreenCanvas) {
|
||||||
|
const bitmap = offscreenCanvas.transferToImageBitmap();
|
||||||
|
canvas.width = bitmap.width;
|
||||||
|
canvas.height = bitmap.height;
|
||||||
|
const destContext = canvas.getContext('2d');
|
||||||
|
destContext?.drawImage(bitmap, 0, 0);
|
||||||
|
bitmap.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
page.cleanup();
|
||||||
|
|
||||||
const wrapper = wrapperRef.current;
|
const wrapper = wrapperRef.current;
|
||||||
if (wrapper) {
|
if (wrapper) {
|
||||||
wrapper.querySelector('.textLayer')?.remove();
|
wrapper.querySelector('.textLayer')?.remove();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.name === 'RenderingCancelledException') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error('Failed to render PDF page', descriptor.number, error);
|
console.error('Failed to render PDF page', descriptor.number, error);
|
||||||
} finally {
|
} finally {
|
||||||
renderTaskRef.current = null;
|
renderTaskRef.current = null;
|
||||||
@@ -417,6 +558,9 @@ function PdfPageCanvas({
|
|||||||
}, [descriptor.number, descriptor.scale, pdfRef, pixelRatio]);
|
}, [descriptor.number, descriptor.scale, pdfRef, pixelRatio]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!canvasMounted) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -429,45 +573,87 @@ function PdfPageCanvas({
|
|||||||
renderTaskRef.current?.cancel();
|
renderTaskRef.current?.cancel();
|
||||||
wrapperNode?.querySelector('.textLayer')?.remove();
|
wrapperNode?.querySelector('.textLayer')?.remove();
|
||||||
};
|
};
|
||||||
}, [descriptor.height, descriptor.width]);
|
}, [canvasMounted, descriptor.height, descriptor.width]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const target = wrapperRef.current;
|
setCanvasMounted(false);
|
||||||
if (!target) {
|
hasRenderedRef.current = false;
|
||||||
return undefined;
|
renderTaskRef.current?.cancel();
|
||||||
|
}, [descriptor.number]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (shouldRender) {
|
||||||
|
setCanvasMounted((prev) => (prev ? prev : true));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const root = viewportRef?.current
|
setCanvasMounted((prev) => {
|
||||||
|| rootRef.current?.closest?.('.document-viewer__viewport') as HTMLElement | null
|
if (!prev) {
|
||||||
|| undefined;
|
return prev;
|
||||||
const observer = new IntersectionObserver((entries) => {
|
}
|
||||||
entries.forEach((entry) => {
|
hasRenderedRef.current = false;
|
||||||
if (!entry.isIntersecting || hasRenderedRef.current) {
|
renderTaskRef.current?.cancel();
|
||||||
return;
|
wrapperRef.current?.querySelector('.textLayer')?.remove();
|
||||||
}
|
return false;
|
||||||
void renderPage();
|
|
||||||
});
|
|
||||||
}, {
|
|
||||||
root,
|
|
||||||
rootMargin: '200px 0px',
|
|
||||||
threshold: 0.1,
|
|
||||||
});
|
});
|
||||||
observer.observe(target);
|
}, [shouldRender]);
|
||||||
return () => observer.disconnect();
|
|
||||||
}, [renderPage, rootRef, viewportRef]);
|
useEffect(() => {
|
||||||
|
if (!canvasMounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void renderPage();
|
||||||
|
}, [canvasMounted, renderPage]);
|
||||||
|
|
||||||
|
const limitAxis: 'width' | 'height' = useMemo(() => {
|
||||||
|
if (viewMode === 'fit-width') {
|
||||||
|
return 'width';
|
||||||
|
}
|
||||||
|
const safeViewportWidth = Math.max(1, viewportWidth);
|
||||||
|
const safeViewportHeight = Math.max(1, viewportHeight);
|
||||||
|
const safePageWidth = Math.max(1, descriptor.width);
|
||||||
|
const safePageHeight = Math.max(1, descriptor.height);
|
||||||
|
const widthScale = safeViewportWidth / safePageWidth;
|
||||||
|
const heightScale = safeViewportHeight / safePageHeight;
|
||||||
|
if (!Number.isFinite(widthScale) && Number.isFinite(heightScale)) {
|
||||||
|
return 'height';
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(heightScale) && Number.isFinite(widthScale)) {
|
||||||
|
return 'width';
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(widthScale) && !Number.isFinite(heightScale)) {
|
||||||
|
return 'width';
|
||||||
|
}
|
||||||
|
return widthScale <= heightScale ? 'width' : 'height';
|
||||||
|
}, [descriptor.height, descriptor.width, viewMode, viewportHeight, viewportWidth]);
|
||||||
|
|
||||||
|
const wrapperClassName = useMemo(() => (
|
||||||
|
['pdf-viewer__page-wrapper',
|
||||||
|
limitAxis === 'width' ? 'pdf-viewer__page-wrapper--limit-width' : 'pdf-viewer__page-wrapper--limit-height',
|
||||||
|
].filter(Boolean).join(' ')
|
||||||
|
), [limitAxis]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={wrapperRef}
|
ref={setWrapperNode}
|
||||||
className="pdf-viewer__page-wrapper"
|
className={wrapperClassName}
|
||||||
|
data-page-number={descriptor.number}
|
||||||
|
style={{
|
||||||
|
aspectRatio: `${descriptor.width} / ${descriptor.height}`,
|
||||||
|
'--pdf-viewer-page-width': `${descriptor.width}px`,
|
||||||
|
'--pdf-viewer-page-height': `${descriptor.height}px`,
|
||||||
|
'--pdf-viewer-page-aspect': `${descriptor.width / descriptor.height}`,
|
||||||
|
}}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
onPageClick(event);
|
onPageClick(event);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<canvas
|
{canvasMounted ? (
|
||||||
ref={canvasRef}
|
<canvas
|
||||||
className="pdf-viewer__page"
|
ref={canvasRef}
|
||||||
aria-label={`Page ${descriptor.number}`}
|
className="pdf-viewer__page"
|
||||||
/>
|
aria-label={`Page ${descriptor.number}`}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -445,19 +445,46 @@
|
|||||||
|
|
||||||
.pdf-viewer__page-wrapper {
|
.pdf-viewer__page-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-viewer__canvas-stack--contain .pdf-viewer__page {
|
.pdf-viewer__page-wrapper:not(:has(> canvas)) {
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
background-image: url("data:image/svg+xml,\
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' width='400' height='400' fill='white' fill-opacity='0.05'>\
|
||||||
|
<rect x='200' width='200' height='200' />\
|
||||||
|
<rect y='200' width='200' height='200' />\
|
||||||
|
</svg>");
|
||||||
|
|
||||||
|
background-size: 30px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer__canvas-stack--fit-width .pdf-viewer__page-wrapper--limit-width {
|
||||||
|
width: var(--pdf-viewer-page-width, 100%);
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: calc(var(--pdf-viewer-page-width) / var(--pdf-viewer-page-height));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer__canvas-stack--contain .pdf-viewer__page-wrapper--limit-height {
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: min(100%, var(--pdf-viewer-viewport-width, 100vw));
|
height: min(var(--pdf-viewer-page-height, 100%), var(--pdf-viewer-viewport-height, 100vh));
|
||||||
|
max-width: min(var(--pdf-viewer-page-width, 100%), var(--pdf-viewer-viewport-width, 100vw));
|
||||||
|
aspect-ratio: calc(var(--pdf-viewer-page-width) / var(--pdf-viewer-page-height));
|
||||||
|
}
|
||||||
|
|
||||||
|
.pdf-viewer__canvas-stack--contain .pdf-viewer__page-wrapper--limit-width {
|
||||||
|
width: var(--pdf-viewer-page-width, 100%);
|
||||||
|
height: auto;
|
||||||
|
max-width: min(var(--pdf-viewer-page-width, 100%), var(--pdf-viewer-viewport-width, 100vw));
|
||||||
max-height: var(--pdf-viewer-viewport-height, 100vh);
|
max-height: var(--pdf-viewer-viewport-height, 100vh);
|
||||||
|
aspect-ratio: calc(var(--pdf-viewer-page-width) / var(--pdf-viewer-page-height));
|
||||||
}
|
}
|
||||||
|
|
||||||
.pdf-viewer__page {
|
.pdf-viewer__page {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: 100%;
|
||||||
box-shadow: 0 1.25rem 2.5rem -1.25rem var(--shadow-medium);
|
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user