From d3e4f598857543e0c8f991267d0c8ed997829ed3 Mon Sep 17 00:00:00 2001 From: Alf Eaton Date: Fri, 3 Nov 2023 11:11:33 +0000 Subject: [PATCH] Add PDF file preview (#15503) GitOrigin-RevId: d5d84da65d328222556bfa9f5b585f7e47ba14ad --- .../file-view/components/file-view-pdf.tsx | 70 +++++++++++++++++++ .../file-view/components/file-view.jsx | 12 +++- .../stylesheets/app/editor/file-view.less | 13 +++- 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 services/web/frontend/js/features/file-view/components/file-view-pdf.tsx diff --git a/services/web/frontend/js/features/file-view/components/file-view-pdf.tsx b/services/web/frontend/js/features/file-view/components/file-view-pdf.tsx new file mode 100644 index 0000000000..2a613d2762 --- /dev/null +++ b/services/web/frontend/js/features/file-view/components/file-view-pdf.tsx @@ -0,0 +1,70 @@ +import { FC, useCallback } from 'react' +import useIsMounted from '@/shared/hooks/use-is-mounted' +import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path' +import { useIdeContext } from '@/shared/context/ide-context' +import { debugConsole } from '@/utils/debugging' + +const FileViewPdf: FC<{ + fileId: string + onLoad: () => void + onError: () => void +}> = ({ fileId, onLoad, onError }) => { + const mountedRef = useIsMounted() + + const { fileTreeManager } = useIdeContext() + const { previewByPath } = useFileTreePathContext() + + const handleContainer = useCallback( + async (element: HTMLDivElement | null) => { + if (element) { + const { PDFJS } = await import( + '../../pdf-preview/util/pdf-js-versions' + ).then(m => m.default as any) + + // bail out if loading PDF.js took too long + if (!mountedRef.current) { + onError() + return + } + + const entity = fileTreeManager.findEntityById(fileId) + const path = fileTreeManager.getEntityPath(entity) + const preview = previewByPath(path) + + if (!preview) { + onError() + return + } + + const pdf = await PDFJS.getDocument(preview.url).promise + + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i) + const viewport = page.getViewport({ scale: 1 }) + + const canvas = document.createElement('canvas') + canvas.classList.add('pdf-page') + canvas.width = viewport.width + canvas.height = viewport.height + element.append(canvas) + page.render({ + canvasContext: canvas.getContext('2d'), + viewport, + }) + } + + onLoad() + + return () => { + pdf.cleanup().catch(debugConsole.error) + pdf.destroy() + } + } + }, + [fileTreeManager, mountedRef, previewByPath, fileId, onLoad, onError] + ) + + return
+} + +export default FileViewPdf diff --git a/services/web/frontend/js/features/file-view/components/file-view.jsx b/services/web/frontend/js/features/file-view/components/file-view.jsx index 4a17ac3128..7ae1970b1b 100644 --- a/services/web/frontend/js/features/file-view/components/file-view.jsx +++ b/services/web/frontend/js/features/file-view/components/file-view.jsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next' import FileViewHeader from './file-view-header' import FileViewImage from './file-view-image' +import FileViewPdf from './file-view-pdf' import FileViewText from './file-view-text' import Icon from '../../../shared/components/icon' @@ -24,8 +25,8 @@ export default function FileView({ file }) { editableFilenames.includes(file.name.toLowerCase()) const isImageFile = imageExtensions.includes(extension) - - const isUnpreviewableFile = !isEditableTextFile && !isImageFile + const isPdfFile = extension === 'pdf' + const isUnpreviewableFile = !isEditableTextFile && !isImageFile && !isPdfFile const handleLoad = useCallback(() => { setContentLoading(false) @@ -52,6 +53,13 @@ export default function FileView({ file }) { {isEditableTextFile && ( )} + {isPdfFile && ( + + )} ) diff --git a/services/web/frontend/stylesheets/app/editor/file-view.less b/services/web/frontend/stylesheets/app/editor/file-view.less index d0d454c3be..573d86da41 100644 --- a/services/web/frontend/stylesheets/app/editor/file-view.less +++ b/services/web/frontend/stylesheets/app/editor/file-view.less @@ -4,7 +4,8 @@ background-color: @gray-lightest; text-align: center; overflow: auto; - img { + img, + .file-view-pdf { max-width: 100%; max-height: 90%; display: block; @@ -14,6 +15,16 @@ .box-shadow(0 2px 3px @gray;); background-color: white; } + .file-view-pdf { + overflow: auto; + width: max-content; + display: flex; + flex-direction: column; + align-items: center; + .pdf-page:not(:last-of-type) { + border-bottom: 1px solid @gray; + } + } p.no-preview { margin-top: @line-height-computed / 2; font-size: 24px;