From 8016d271aa0319d89aeb3ddfee078f7a821d6b52 Mon Sep 17 00:00:00 2001 From: Reece Browne <74901996+reecebrowne@users.noreply.github.com> Date: Tue, 25 Nov 2025 21:27:47 +0000 Subject: [PATCH] Handle non-pdf gracefully in viewer (#5004) # Description of Changes --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### Translations (if applicable) - [ ] I ran [`scripts/counter_translation.py`](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/docs/counter_translation.md) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --- .../public/locales/en-GB/translation.json | 11 ++- .../core/components/viewer/LocalEmbedPDF.tsx | 26 ++++++ frontend/src/core/utils/fileUtils.test.ts | 87 +++++++++++++++++++ frontend/src/core/utils/fileUtils.ts | 23 +++++ 4 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 frontend/src/core/utils/fileUtils.test.ts diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index e4329b857..32aae3748 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -3890,14 +3890,17 @@ "actualSize": "Actual Size" }, "viewer": { + "cannotPreviewFile": "Cannot Preview File", + "dualPageView": "Dual Page View", "firstPage": "First Page", "lastPage": "Last Page", - "previousPage": "Previous Page", "nextPage": "Next Page", - "zoomIn": "Zoom In", - "zoomOut": "Zoom Out", + "onlyPdfSupported": "The viewer only supports PDF files. This file appears to be a different format.", + "previousPage": "Previous Page", "singlePageView": "Single Page View", - "dualPageView": "Dual Page View" + "unknownFile": "Unknown file", + "zoomIn": "Zoom In", + "zoomOut": "Zoom Out" }, "rightRail": { "closeSelected": "Close Selected Files", diff --git a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx index c08eabcbb..2de2a2e58 100644 --- a/frontend/src/core/components/viewer/LocalEmbedPDF.tsx +++ b/frontend/src/core/components/viewer/LocalEmbedPDF.tsx @@ -41,6 +41,8 @@ import { HistoryAPIBridge } from '@app/components/viewer/HistoryAPIBridge'; import type { SignatureAPI, HistoryAPI } from '@app/components/viewer/viewerTypes'; import { ExportAPIBridge } from '@app/components/viewer/ExportAPIBridge'; import { BookmarkAPIBridge } from '@app/components/viewer/BookmarkAPIBridge'; +import { isPdfFile } from '@app/utils/fileUtils'; +import { useTranslation } from 'react-i18next'; interface LocalEmbedPDFProps { file?: File | Blob; @@ -52,6 +54,7 @@ interface LocalEmbedPDFProps { } export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatureAdded, signatureApiRef, historyApiRef }: LocalEmbedPDFProps) { + const { t } = useTranslation(); const [pdfUrl, setPdfUrl] = useState(null); const [, setAnnotations] = useState>([]); @@ -171,6 +174,29 @@ export function LocalEmbedPDF({ file, url, enableAnnotations = false, onSignatur ); } + // Check if the file is actually a PDF + if (file && !isPdfFile(file)) { + const fileName = 'name' in file ? file.name : t('viewer.unknownFile'); + return ( +
+ +
📄
+ + {t('viewer.cannotPreviewFile')} + + + {t('viewer.onlyPdfSupported')} + + + + {fileName} + + +
+
+ ); + } + if (isLoading || !engine || !pdfUrl) { return ; } diff --git a/frontend/src/core/utils/fileUtils.test.ts b/frontend/src/core/utils/fileUtils.test.ts new file mode 100644 index 000000000..499a49bab --- /dev/null +++ b/frontend/src/core/utils/fileUtils.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect } from 'vitest'; +import { isPdfFile, detectFileExtension, formatFileSize } from '@app/utils/fileUtils'; + +describe('fileUtils', () => { + describe('isPdfFile', () => { + it('should return true for PDF files with correct MIME type', () => { + const pdfFile = new File(['content'], 'document.pdf', { type: 'application/pdf' }); + expect(isPdfFile(pdfFile)).toBe(true); + }); + + it('should return true for PDF files with .pdf extension even without MIME type', () => { + const pdfFile = new File(['content'], 'document.pdf', { type: '' }); + expect(isPdfFile(pdfFile)).toBe(true); + }); + + it('should return false for non-PDF files', () => { + const txtFile = new File(['content'], 'document.txt', { type: 'text/plain' }); + expect(isPdfFile(txtFile)).toBe(false); + }); + + it('should return false for image files', () => { + const imageFile = new File(['content'], 'image.png', { type: 'image/png' }); + expect(isPdfFile(imageFile)).toBe(false); + }); + + it('should return false for null', () => { + expect(isPdfFile(null)).toBe(false); + }); + + it('should return false for undefined', () => { + expect(isPdfFile(undefined)).toBe(false); + }); + + it('should handle file-like objects with name and type', () => { + const fileLike = { name: 'test.pdf', type: 'application/pdf' }; + expect(isPdfFile(fileLike)).toBe(true); + }); + + it('should handle file-like objects with PDF extension but no type', () => { + const fileLike = { name: 'test.pdf', type: '' }; + expect(isPdfFile(fileLike)).toBe(true); + }); + }); + + describe('detectFileExtension', () => { + it('should detect PDF extension', () => { + expect(detectFileExtension('document.pdf')).toBe('pdf'); + }); + + it('should detect extension in uppercase', () => { + expect(detectFileExtension('document.PDF')).toBe('pdf'); + }); + + it('should return empty string for files without extension', () => { + expect(detectFileExtension('document')).toBe(''); + }); + + it('should handle multiple dots in filename', () => { + expect(detectFileExtension('my.document.pdf')).toBe('pdf'); + }); + + it('should normalize jpeg to jpg', () => { + expect(detectFileExtension('image.jpeg')).toBe('jpg'); + }); + }); + + describe('formatFileSize', () => { + it('should format bytes', () => { + expect(formatFileSize(0)).toBe('0 B'); + expect(formatFileSize(500)).toBe('500 B'); + }); + + it('should format kilobytes', () => { + expect(formatFileSize(1024)).toBe('1 KB'); + expect(formatFileSize(2048)).toBe('2 KB'); + }); + + it('should format megabytes', () => { + expect(formatFileSize(1024 * 1024)).toBe('1 MB'); + expect(formatFileSize(5 * 1024 * 1024)).toBe('5 MB'); + }); + + it('should format gigabytes', () => { + expect(formatFileSize(1024 * 1024 * 1024)).toBe('1 GB'); + }); + }); +}); diff --git a/frontend/src/core/utils/fileUtils.ts b/frontend/src/core/utils/fileUtils.ts index b7e3a429c..0f1471401 100644 --- a/frontend/src/core/utils/fileUtils.ts +++ b/frontend/src/core/utils/fileUtils.ts @@ -51,3 +51,26 @@ export function detectFileExtension(filename: string): string { return extension; } + +/** + * Checks if a file is a PDF based on extension and MIME type + * @param file - File or file-like object with name and type properties + * @returns true if the file appears to be a PDF + */ +export function isPdfFile(file: { name?: string; type?: string } | File | Blob | null | undefined): boolean { + if (!file) return false; + + const name = 'name' in file ? file.name : undefined; + const type = file.type; + + // Check MIME type first (most reliable) + if (type === 'application/pdf') return true; + + // Check file extension as fallback + if (name) { + const ext = detectFileExtension(name); + if (ext === 'pdf') return true; + } + + return false; +}