+Subject: Test Email for Convert Tool
+Content-Type: multipart/alternative;
+ boundary="------------boundary123456789"
+
+This is a multi-part message in MIME format.
+--------------boundary123456789
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+
+Test Email for Convert Tool
+===========================
+
+This is a test email for testing the EML to PDF conversion functionality.
+
+Email Details:
+- From: test@example.com
+- To: recipient@example.com
+- Subject: Test Email for Convert Tool
+- Date: January 1, 2024
+
+Content Features:
+- Plain text content
+- HTML content (in alternative part)
+- Headers and metadata
+- MIME structure
+
+This email should convert to a PDF that includes:
+1. Email headers (From, To, Subject, Date)
+2. Email body content
+3. Proper formatting
+
+Important Notes:
+- This is a test email only
+- Generated for Stirling PDF testing
+- Contains no sensitive information
+- Should preserve email formatting in PDF
+
+Best regards,
+Test Email System
+
+--------------boundary123456789
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+
+
+
+
+
+ Test Email
+
+
+ Test Email for Convert Tool
+
+ This is a test email for testing the EML to PDF conversion functionality.
+
+ Email Details:
+
+ - From: test@example.com
+ - To: recipient@example.com
+ - Subject: Test Email for Convert Tool
+ - Date: January 1, 2024
+
+
+ Content Features:
+
+ - Plain text content
+ - HTML content (this part)
+ - Headers and metadata
+ - MIME structure
+
+
+
+
This email should convert to a PDF that includes:
+
+ - Email headers (From, To, Subject, Date)
+ - Email body content
+ - Proper formatting
+
+
+
+ Important Notes:
+
+ - This is a test email only
+ - Generated for Stirling PDF testing
+ - Contains no sensitive information
+ - Should preserve email formatting in PDF
+
+
+ Best regards,
+ Test Email System
+
+
+
+--------------boundary123456789--
\ No newline at end of file
diff --git a/frontend/src/tests/test-fixtures/sample.htm b/frontend/src/tests/test-fixtures/sample.htm
new file mode 100644
index 000000000..83a5260a7
--- /dev/null
+++ b/frontend/src/tests/test-fixtures/sample.htm
@@ -0,0 +1,125 @@
+
+
+
+
+
+ Test HTML Document
+
+
+
+ Test HTML Document for Convert Tool
+
+ This is a test HTML file for testing the HTML to PDF conversion functionality. It contains various HTML elements to ensure proper conversion.
+
+ Text Formatting
+ This paragraph contains bold text, italic text, and inline code
.
+
+
+
Important: This is a highlighted section that should be preserved in the PDF output.
+
+
+ Lists
+ Unordered List
+
+ - First item
+ - Second item with a link
+ - Third item
+
+
+ Ordered List
+
+ - Primary point
+ - Secondary point
+ - Tertiary point
+
+
+ Table
+
+
+
+ Column 1 |
+ Column 2 |
+ Column 3 |
+
+
+
+
+ Data A |
+ Data B |
+ Data C |
+
+
+ Test 1 |
+ Test 2 |
+ Test 3 |
+
+
+ Sample X |
+ Sample Y |
+ Sample Z |
+
+
+
+
+ Code Block
+ function testFunction() {
+ console.log("This is a test function");
+ return "Hello from HTML to PDF conversion";
+}
+
+ Final Notes
+ This HTML document should convert to a well-formatted PDF that preserves:
+
+ - Text formatting (bold, italic)
+ - Headings and hierarchy
+ - Tables with proper borders
+ - Lists (ordered and unordered)
+ - Code formatting
+ - Basic CSS styling
+
+
+ Generated for Stirling PDF Convert Tool testing purposes.
+
+
\ No newline at end of file
diff --git a/frontend/src/tests/test-fixtures/sample.html b/frontend/src/tests/test-fixtures/sample.html
new file mode 100644
index 000000000..83a5260a7
--- /dev/null
+++ b/frontend/src/tests/test-fixtures/sample.html
@@ -0,0 +1,125 @@
+
+
+
+
+
+ Test HTML Document
+
+
+
+ Test HTML Document for Convert Tool
+
+ This is a test HTML file for testing the HTML to PDF conversion functionality. It contains various HTML elements to ensure proper conversion.
+
+ Text Formatting
+ This paragraph contains bold text, italic text, and inline code
.
+
+
+
Important: This is a highlighted section that should be preserved in the PDF output.
+
+
+ Lists
+ Unordered List
+
+ - First item
+ - Second item with a link
+ - Third item
+
+
+ Ordered List
+
+ - Primary point
+ - Secondary point
+ - Tertiary point
+
+
+ Table
+
+
+
+ Column 1 |
+ Column 2 |
+ Column 3 |
+
+
+
+
+ Data A |
+ Data B |
+ Data C |
+
+
+ Test 1 |
+ Test 2 |
+ Test 3 |
+
+
+ Sample X |
+ Sample Y |
+ Sample Z |
+
+
+
+
+ Code Block
+ function testFunction() {
+ console.log("This is a test function");
+ return "Hello from HTML to PDF conversion";
+}
+
+ Final Notes
+ This HTML document should convert to a well-formatted PDF that preserves:
+
+ - Text formatting (bold, italic)
+ - Headings and hierarchy
+ - Tables with proper borders
+ - Lists (ordered and unordered)
+ - Code formatting
+ - Basic CSS styling
+
+
+ Generated for Stirling PDF Convert Tool testing purposes.
+
+
\ No newline at end of file
diff --git a/frontend/src/tests/test-fixtures/sample.jpg b/frontend/src/tests/test-fixtures/sample.jpg
new file mode 100644
index 000000000..a2dc48c27
Binary files /dev/null and b/frontend/src/tests/test-fixtures/sample.jpg differ
diff --git a/frontend/src/tests/test-fixtures/sample.md b/frontend/src/tests/test-fixtures/sample.md
new file mode 100644
index 000000000..fba73ad74
--- /dev/null
+++ b/frontend/src/tests/test-fixtures/sample.md
@@ -0,0 +1,49 @@
+# Test Document for Convert Tool
+
+This is a **test** markdown file for testing the markdown to PDF conversion functionality.
+
+## Features Being Tested
+
+- **Bold text**
+- *Italic text*
+- [Links](https://example.com)
+- Lists and formatting
+
+### Code Block
+
+```javascript
+console.log('Hello, world!');
+function testFunction() {
+ return "This is a test";
+}
+```
+
+### Table
+
+| Column 1 | Column 2 | Column 3 |
+|----------|----------|----------|
+| Data 1 | Data 2 | Data 3 |
+| Test A | Test B | Test C |
+
+## Lists
+
+### Unordered List
+- Item 1
+- Item 2
+ - Nested item
+ - Another nested item
+- Item 3
+
+### Ordered List
+1. First item
+2. Second item
+3. Third item
+
+## Blockquote
+
+> This is a blockquote for testing purposes.
+> It should be properly formatted in the PDF output.
+
+## Conclusion
+
+This markdown file contains various elements to test the conversion functionality. The PDF output should preserve formatting, tables, code blocks, and other markdown elements.
\ No newline at end of file
diff --git a/frontend/src/tests/test-fixtures/sample.pdf b/frontend/src/tests/test-fixtures/sample.pdf
new file mode 100644
index 000000000..a7fb3ba0b
Binary files /dev/null and b/frontend/src/tests/test-fixtures/sample.pdf differ
diff --git a/frontend/src/tests/test-fixtures/sample.png b/frontend/src/tests/test-fixtures/sample.png
new file mode 100644
index 000000000..b6993935a
Binary files /dev/null and b/frontend/src/tests/test-fixtures/sample.png differ
diff --git a/frontend/src/tests/test-fixtures/sample.pptx b/frontend/src/tests/test-fixtures/sample.pptx
new file mode 100644
index 000000000..2067ee215
--- /dev/null
+++ b/frontend/src/tests/test-fixtures/sample.pptx
@@ -0,0 +1,12 @@
+# Test PPTX Presentation
+
+## Slide 1: Title
+This is a test PowerPoint presentation for conversion testing.
+
+## Slide 2: Content
+- Test bullet point 1
+- Test bullet point 2
+- Test bullet point 3
+
+## Slide 3: Conclusion
+This file should be sufficient for testing presentation conversions.
\ No newline at end of file
diff --git a/frontend/src/tests/test-fixtures/sample.svg b/frontend/src/tests/test-fixtures/sample.svg
new file mode 100644
index 000000000..c2056280a
--- /dev/null
+++ b/frontend/src/tests/test-fixtures/sample.svg
@@ -0,0 +1,32 @@
+
\ No newline at end of file
diff --git a/frontend/src/tests/test-fixtures/sample.txt b/frontend/src/tests/test-fixtures/sample.txt
new file mode 100644
index 000000000..903e18f09
--- /dev/null
+++ b/frontend/src/tests/test-fixtures/sample.txt
@@ -0,0 +1,8 @@
+This is a test text file for conversion testing.
+
+It contains multiple lines of text to test various conversion scenarios.
+Special characters: àáâãäåæçèéêëìíîïðñòóôõö÷øùúûüýþÿ
+Numbers: 1234567890
+Symbols: !@#$%^&*()_+-=[]{}|;':\",./<>?
+
+This file should be sufficient for testing text-based conversions.
\ No newline at end of file
diff --git a/frontend/src/tests/test-fixtures/sample.xlsx b/frontend/src/tests/test-fixtures/sample.xlsx
new file mode 100644
index 000000000..7eb45724b
--- /dev/null
+++ b/frontend/src/tests/test-fixtures/sample.xlsx
@@ -0,0 +1,6 @@
+Name,Age,City,Country,Department,Salary
+John Doe,30,New York,USA,Engineering,75000
+Jane Smith,25,London,UK,Marketing,65000
+Bob Johnson,35,Toronto,Canada,Sales,70000
+Alice Brown,28,Sydney,Australia,Design,68000
+Charlie Wilson,42,Berlin,Germany,Operations,72000
\ No newline at end of file
diff --git a/frontend/src/tests/test-fixtures/sample.xml b/frontend/src/tests/test-fixtures/sample.xml
new file mode 100644
index 000000000..f39b92f6f
--- /dev/null
+++ b/frontend/src/tests/test-fixtures/sample.xml
@@ -0,0 +1,18 @@
+
+
+ Test Document
+
+
+ Introduction
+ This is a test XML document for conversion testing.
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/tools/Convert.tsx b/frontend/src/tools/Convert.tsx
new file mode 100644
index 000000000..3512ca8eb
--- /dev/null
+++ b/frontend/src/tools/Convert.tsx
@@ -0,0 +1,207 @@
+import React, { useEffect, useMemo, useRef } from "react";
+import { Button, Stack, Text } from "@mantine/core";
+import { useTranslation } from "react-i18next";
+import DownloadIcon from "@mui/icons-material/Download";
+import { useEndpointEnabled } from "../hooks/useEndpointConfig";
+import { useFileContext } from "../contexts/FileContext";
+import { useToolFileSelection } from "../contexts/FileSelectionContext";
+
+import ToolStep, { ToolStepContainer } from "../components/tools/shared/ToolStep";
+import OperationButton from "../components/tools/shared/OperationButton";
+import ErrorNotification from "../components/tools/shared/ErrorNotification";
+import FileStatusIndicator from "../components/tools/shared/FileStatusIndicator";
+import ResultsPreview from "../components/tools/shared/ResultsPreview";
+
+import ConvertSettings from "../components/tools/convert/ConvertSettings";
+
+import { useConvertParameters } from "../hooks/tools/convert/useConvertParameters";
+import { useConvertOperation } from "../hooks/tools/convert/useConvertOperation";
+import { BaseToolProps } from "../types/tool";
+
+const Convert = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
+ const { t } = useTranslation();
+ const { setCurrentMode, activeFiles } = useFileContext();
+ const { selectedFiles } = useToolFileSelection();
+ const scrollContainerRef = useRef(null);
+
+ const convertParams = useConvertParameters();
+ const convertOperation = useConvertOperation();
+
+ const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled(
+ convertParams.getEndpointName()
+ );
+
+ const scrollToBottom = () => {
+ if (scrollContainerRef.current) {
+ scrollContainerRef.current.scrollTo({
+ top: scrollContainerRef.current.scrollHeight,
+ behavior: 'smooth'
+ });
+ }
+ };
+
+ const hasFiles = selectedFiles.length > 0;
+ const hasResults = convertOperation.downloadUrl !== null;
+ const filesCollapsed = hasFiles;
+ const settingsCollapsed = hasResults;
+
+ useEffect(() => {
+ if (selectedFiles.length > 0) {
+ convertParams.analyzeFileTypes(selectedFiles);
+ } else {
+ // Only reset when there are no active files at all
+ // If there are active files but no selected files, keep current format (user filtered by format)
+ if (activeFiles.length === 0) {
+ convertParams.resetParameters();
+ }
+ }
+ }, [selectedFiles, activeFiles]);
+
+ useEffect(() => {
+ // Only clear results if we're not currently processing and parameters changed
+ if (!convertOperation.isLoading) {
+ convertOperation.resetResults();
+ onPreviewFile?.(null);
+ }
+ }, [convertParams.parameters.fromExtension, convertParams.parameters.toExtension]);
+
+ useEffect(() => {
+ if (hasFiles) {
+ setTimeout(scrollToBottom, 100);
+ }
+ }, [hasFiles]);
+
+ useEffect(() => {
+ if (hasResults) {
+ setTimeout(scrollToBottom, 100);
+ }
+ }, [hasResults]);
+
+ const handleConvert = async () => {
+ try {
+ await convertOperation.executeOperation(
+ convertParams.parameters,
+ selectedFiles
+ );
+ if (convertOperation.files && onComplete) {
+ onComplete(convertOperation.files);
+ }
+ } catch (error) {
+ if (onError) {
+ onError(error instanceof Error ? error.message : 'Convert operation failed');
+ }
+ }
+ };
+
+ const handleThumbnailClick = (file: File) => {
+ onPreviewFile?.(file);
+ sessionStorage.setItem('previousMode', 'convert');
+ setCurrentMode('viewer');
+ };
+
+ const handleSettingsReset = () => {
+ convertOperation.resetResults();
+ onPreviewFile?.(null);
+ setCurrentMode('convert');
+ };
+
+ const previewResults = useMemo(() =>
+ convertOperation.files?.map((file, index) => ({
+ file,
+ thumbnail: convertOperation.thumbnails[index]
+ })) || [],
+ [convertOperation.files, convertOperation.thumbnails]
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {hasFiles && convertParams.parameters.fromExtension && convertParams.parameters.toExtension && (
+
+ )}
+
+
+
+
+
+ {convertOperation.status && (
+ {convertOperation.status}
+ )}
+
+
+
+ {convertOperation.downloadUrl && (
+ }
+ color="green"
+ fullWidth
+ mb="md"
+ data-testid="download-button"
+ >
+ {t("convert.downloadConverted", "Download Converted File")}
+
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default Convert;
diff --git a/frontend/src/utils/convertUtils.test.ts b/frontend/src/utils/convertUtils.test.ts
new file mode 100644
index 000000000..4f44f949b
--- /dev/null
+++ b/frontend/src/utils/convertUtils.test.ts
@@ -0,0 +1,334 @@
+/**
+ * Unit tests for convertUtils
+ */
+
+import { describe, test, expect } from 'vitest';
+import {
+ getEndpointName,
+ getEndpointUrl,
+ isConversionSupported,
+ isImageFormat
+} from './convertUtils';
+
+describe('convertUtils', () => {
+
+ describe('getEndpointName', () => {
+
+ test('should return correct endpoint names for all supported conversions', () => {
+ // PDF to Image formats
+ expect(getEndpointName('pdf', 'png')).toBe('pdf-to-img');
+ expect(getEndpointName('pdf', 'jpg')).toBe('pdf-to-img');
+ expect(getEndpointName('pdf', 'gif')).toBe('pdf-to-img');
+ expect(getEndpointName('pdf', 'tiff')).toBe('pdf-to-img');
+ expect(getEndpointName('pdf', 'bmp')).toBe('pdf-to-img');
+ expect(getEndpointName('pdf', 'webp')).toBe('pdf-to-img');
+
+ // PDF to Office formats
+ expect(getEndpointName('pdf', 'docx')).toBe('pdf-to-word');
+ expect(getEndpointName('pdf', 'odt')).toBe('pdf-to-word');
+ expect(getEndpointName('pdf', 'pptx')).toBe('pdf-to-presentation');
+ expect(getEndpointName('pdf', 'odp')).toBe('pdf-to-presentation');
+
+ // PDF to Data formats
+ expect(getEndpointName('pdf', 'csv')).toBe('pdf-to-csv');
+ expect(getEndpointName('pdf', 'txt')).toBe('pdf-to-text');
+ expect(getEndpointName('pdf', 'rtf')).toBe('pdf-to-text');
+ expect(getEndpointName('pdf', 'md')).toBe('pdf-to-markdown');
+
+ // PDF to Web formats
+ expect(getEndpointName('pdf', 'html')).toBe('pdf-to-html');
+ expect(getEndpointName('pdf', 'xml')).toBe('pdf-to-xml');
+
+ // PDF to PDF/A
+ expect(getEndpointName('pdf', 'pdfa')).toBe('pdf-to-pdfa');
+
+ // Office Documents to PDF
+ expect(getEndpointName('docx', 'pdf')).toBe('file-to-pdf');
+ expect(getEndpointName('doc', 'pdf')).toBe('file-to-pdf');
+ expect(getEndpointName('odt', 'pdf')).toBe('file-to-pdf');
+
+ // Spreadsheets to PDF
+ expect(getEndpointName('xlsx', 'pdf')).toBe('file-to-pdf');
+ expect(getEndpointName('xls', 'pdf')).toBe('file-to-pdf');
+ expect(getEndpointName('ods', 'pdf')).toBe('file-to-pdf');
+
+ // Presentations to PDF
+ expect(getEndpointName('pptx', 'pdf')).toBe('file-to-pdf');
+ expect(getEndpointName('ppt', 'pdf')).toBe('file-to-pdf');
+ expect(getEndpointName('odp', 'pdf')).toBe('file-to-pdf');
+
+ // Images to PDF
+ expect(getEndpointName('jpg', 'pdf')).toBe('img-to-pdf');
+ expect(getEndpointName('jpeg', 'pdf')).toBe('img-to-pdf');
+ expect(getEndpointName('png', 'pdf')).toBe('img-to-pdf');
+ expect(getEndpointName('gif', 'pdf')).toBe('img-to-pdf');
+ expect(getEndpointName('bmp', 'pdf')).toBe('img-to-pdf');
+ expect(getEndpointName('tiff', 'pdf')).toBe('img-to-pdf');
+ expect(getEndpointName('webp', 'pdf')).toBe('img-to-pdf');
+
+ // Web formats to PDF
+ expect(getEndpointName('html', 'pdf')).toBe('html-to-pdf');
+
+ // Markdown to PDF
+ expect(getEndpointName('md', 'pdf')).toBe('markdown-to-pdf');
+
+ // Text formats to PDF
+ expect(getEndpointName('txt', 'pdf')).toBe('file-to-pdf');
+ expect(getEndpointName('rtf', 'pdf')).toBe('file-to-pdf');
+
+ // Email to PDF
+ expect(getEndpointName('eml', 'pdf')).toBe('eml-to-pdf');
+ });
+
+ test('should return empty string for unsupported conversions', () => {
+ expect(getEndpointName('pdf', 'exe')).toBe('');
+ expect(getEndpointName('wav', 'pdf')).toBe('file-to-pdf'); // Try using file to pdf as fallback
+ expect(getEndpointName('png', 'docx')).toBe(''); // Images can't convert to Word docs
+ });
+
+ test('should handle empty or invalid inputs', () => {
+ expect(getEndpointName('', '')).toBe('');
+ expect(getEndpointName('pdf', '')).toBe('');
+ expect(getEndpointName('', 'pdf')).toBe('');
+ expect(getEndpointName('nonexistent', 'alsononexistent')).toBe('');
+ });
+ });
+
+ describe('getEndpointUrl', () => {
+
+ test('should return correct endpoint URLs for all supported conversions', () => {
+ // PDF to Image formats
+ expect(getEndpointUrl('pdf', 'png')).toBe('/api/v1/convert/pdf/img');
+ expect(getEndpointUrl('pdf', 'jpg')).toBe('/api/v1/convert/pdf/img');
+ expect(getEndpointUrl('pdf', 'gif')).toBe('/api/v1/convert/pdf/img');
+ expect(getEndpointUrl('pdf', 'tiff')).toBe('/api/v1/convert/pdf/img');
+ expect(getEndpointUrl('pdf', 'bmp')).toBe('/api/v1/convert/pdf/img');
+ expect(getEndpointUrl('pdf', 'webp')).toBe('/api/v1/convert/pdf/img');
+
+ // PDF to Office formats
+ expect(getEndpointUrl('pdf', 'docx')).toBe('/api/v1/convert/pdf/word');
+ expect(getEndpointUrl('pdf', 'odt')).toBe('/api/v1/convert/pdf/word');
+ expect(getEndpointUrl('pdf', 'pptx')).toBe('/api/v1/convert/pdf/presentation');
+ expect(getEndpointUrl('pdf', 'odp')).toBe('/api/v1/convert/pdf/presentation');
+
+ // PDF to Data formats
+ expect(getEndpointUrl('pdf', 'csv')).toBe('/api/v1/convert/pdf/csv');
+ expect(getEndpointUrl('pdf', 'txt')).toBe('/api/v1/convert/pdf/text');
+ expect(getEndpointUrl('pdf', 'rtf')).toBe('/api/v1/convert/pdf/text');
+ expect(getEndpointUrl('pdf', 'md')).toBe('/api/v1/convert/pdf/markdown');
+
+ // PDF to Web formats
+ expect(getEndpointUrl('pdf', 'html')).toBe('/api/v1/convert/pdf/html');
+ expect(getEndpointUrl('pdf', 'xml')).toBe('/api/v1/convert/pdf/xml');
+
+ // PDF to PDF/A
+ expect(getEndpointUrl('pdf', 'pdfa')).toBe('/api/v1/convert/pdf/pdfa');
+
+ // Office Documents to PDF
+ expect(getEndpointUrl('docx', 'pdf')).toBe('/api/v1/convert/file/pdf');
+ expect(getEndpointUrl('doc', 'pdf')).toBe('/api/v1/convert/file/pdf');
+ expect(getEndpointUrl('odt', 'pdf')).toBe('/api/v1/convert/file/pdf');
+
+ // Spreadsheets to PDF
+ expect(getEndpointUrl('xlsx', 'pdf')).toBe('/api/v1/convert/file/pdf');
+ expect(getEndpointUrl('xls', 'pdf')).toBe('/api/v1/convert/file/pdf');
+ expect(getEndpointUrl('ods', 'pdf')).toBe('/api/v1/convert/file/pdf');
+
+ // Presentations to PDF
+ expect(getEndpointUrl('pptx', 'pdf')).toBe('/api/v1/convert/file/pdf');
+ expect(getEndpointUrl('ppt', 'pdf')).toBe('/api/v1/convert/file/pdf');
+ expect(getEndpointUrl('odp', 'pdf')).toBe('/api/v1/convert/file/pdf');
+
+ // Images to PDF
+ expect(getEndpointUrl('jpg', 'pdf')).toBe('/api/v1/convert/img/pdf');
+ expect(getEndpointUrl('jpeg', 'pdf')).toBe('/api/v1/convert/img/pdf');
+ expect(getEndpointUrl('png', 'pdf')).toBe('/api/v1/convert/img/pdf');
+ expect(getEndpointUrl('gif', 'pdf')).toBe('/api/v1/convert/img/pdf');
+ expect(getEndpointUrl('bmp', 'pdf')).toBe('/api/v1/convert/img/pdf');
+ expect(getEndpointUrl('tiff', 'pdf')).toBe('/api/v1/convert/img/pdf');
+ expect(getEndpointUrl('webp', 'pdf')).toBe('/api/v1/convert/img/pdf');
+
+ // Web formats to PDF
+ expect(getEndpointUrl('html', 'pdf')).toBe('/api/v1/convert/html/pdf');
+
+ // Markdown to PDF
+ expect(getEndpointUrl('md', 'pdf')).toBe('/api/v1/convert/markdown/pdf');
+
+ // Text formats to PDF
+ expect(getEndpointUrl('txt', 'pdf')).toBe('/api/v1/convert/file/pdf');
+ expect(getEndpointUrl('rtf', 'pdf')).toBe('/api/v1/convert/file/pdf');
+
+ // Email to PDF
+ expect(getEndpointUrl('eml', 'pdf')).toBe('/api/v1/convert/eml/pdf');
+ });
+
+ test('should return empty string for unsupported conversions', () => {
+ expect(getEndpointUrl('pdf', 'exe')).toBe('');
+ expect(getEndpointUrl('wav', 'pdf')).toBe('/api/v1/convert/file/pdf'); // Try using file to pdf as fallback
+ expect(getEndpointUrl('invalid', 'invalid')).toBe('');
+ });
+
+ test('should handle empty inputs', () => {
+ expect(getEndpointUrl('', '')).toBe('');
+ expect(getEndpointUrl('pdf', '')).toBe('');
+ expect(getEndpointUrl('', 'pdf')).toBe('');
+ });
+ });
+
+ describe('isConversionSupported', () => {
+
+ test('should return true for all supported conversions', () => {
+ // PDF to Image formats
+ expect(isConversionSupported('pdf', 'png')).toBe(true);
+ expect(isConversionSupported('pdf', 'jpg')).toBe(true);
+ expect(isConversionSupported('pdf', 'gif')).toBe(true);
+ expect(isConversionSupported('pdf', 'tiff')).toBe(true);
+ expect(isConversionSupported('pdf', 'bmp')).toBe(true);
+ expect(isConversionSupported('pdf', 'webp')).toBe(true);
+
+ // PDF to Office formats
+ expect(isConversionSupported('pdf', 'docx')).toBe(true);
+ expect(isConversionSupported('pdf', 'odt')).toBe(true);
+ expect(isConversionSupported('pdf', 'pptx')).toBe(true);
+ expect(isConversionSupported('pdf', 'odp')).toBe(true);
+
+ // PDF to Data formats
+ expect(isConversionSupported('pdf', 'csv')).toBe(true);
+ expect(isConversionSupported('pdf', 'txt')).toBe(true);
+ expect(isConversionSupported('pdf', 'rtf')).toBe(true);
+ expect(isConversionSupported('pdf', 'md')).toBe(true);
+
+ // PDF to Web formats
+ expect(isConversionSupported('pdf', 'html')).toBe(true);
+ expect(isConversionSupported('pdf', 'xml')).toBe(true);
+
+ // PDF to PDF/A
+ expect(isConversionSupported('pdf', 'pdfa')).toBe(true);
+
+ // Office Documents to PDF
+ expect(isConversionSupported('docx', 'pdf')).toBe(true);
+ expect(isConversionSupported('doc', 'pdf')).toBe(true);
+ expect(isConversionSupported('odt', 'pdf')).toBe(true);
+
+ // Spreadsheets to PDF
+ expect(isConversionSupported('xlsx', 'pdf')).toBe(true);
+ expect(isConversionSupported('xls', 'pdf')).toBe(true);
+ expect(isConversionSupported('ods', 'pdf')).toBe(true);
+
+ // Presentations to PDF
+ expect(isConversionSupported('pptx', 'pdf')).toBe(true);
+ expect(isConversionSupported('ppt', 'pdf')).toBe(true);
+ expect(isConversionSupported('odp', 'pdf')).toBe(true);
+
+ // Images to PDF
+ expect(isConversionSupported('jpg', 'pdf')).toBe(true);
+ expect(isConversionSupported('jpeg', 'pdf')).toBe(true);
+ expect(isConversionSupported('png', 'pdf')).toBe(true);
+ expect(isConversionSupported('gif', 'pdf')).toBe(true);
+ expect(isConversionSupported('bmp', 'pdf')).toBe(true);
+ expect(isConversionSupported('tiff', 'pdf')).toBe(true);
+ expect(isConversionSupported('webp', 'pdf')).toBe(true);
+
+ // Web formats to PDF
+ expect(isConversionSupported('html', 'pdf')).toBe(true);
+ expect(isConversionSupported('htm', 'pdf')).toBe(true);
+
+ // Markdown to PDF
+ expect(isConversionSupported('md', 'pdf')).toBe(true);
+
+ // Text formats to PDF
+ expect(isConversionSupported('txt', 'pdf')).toBe(true);
+ expect(isConversionSupported('rtf', 'pdf')).toBe(true);
+
+ // Email to PDF
+ expect(isConversionSupported('eml', 'pdf')).toBe(true);
+ });
+
+ test('should return false for unsupported conversions', () => {
+ expect(isConversionSupported('pdf', 'exe')).toBe(false);
+ expect(isConversionSupported('wav', 'pdf')).toBe(true); // Fallback to file to pdf
+ expect(isConversionSupported('png', 'docx')).toBe(false);
+ expect(isConversionSupported('nonexistent', 'alsononexistent')).toBe(false);
+ });
+
+ test('should handle empty inputs', () => {
+ expect(isConversionSupported('', '')).toBe(false);
+ expect(isConversionSupported('pdf', '')).toBe(false);
+ expect(isConversionSupported('', 'pdf')).toBe(false);
+ });
+ });
+
+ describe('isImageFormat', () => {
+
+ test('should return true for image formats', () => {
+ expect(isImageFormat('png')).toBe(true);
+ expect(isImageFormat('jpg')).toBe(true);
+ expect(isImageFormat('jpeg')).toBe(true);
+ expect(isImageFormat('gif')).toBe(true);
+ expect(isImageFormat('tiff')).toBe(true);
+ expect(isImageFormat('bmp')).toBe(true);
+ expect(isImageFormat('webp')).toBe(true);
+ });
+
+ test('should return false for non-image formats', () => {
+ expect(isImageFormat('pdf')).toBe(false);
+ expect(isImageFormat('docx')).toBe(false);
+ expect(isImageFormat('txt')).toBe(false);
+ expect(isImageFormat('csv')).toBe(false);
+ expect(isImageFormat('html')).toBe(false);
+ expect(isImageFormat('xml')).toBe(false);
+ });
+
+ test('should handle case insensitivity', () => {
+ expect(isImageFormat('PNG')).toBe(true);
+ expect(isImageFormat('JPG')).toBe(true);
+ expect(isImageFormat('JPEG')).toBe(true);
+ expect(isImageFormat('Png')).toBe(true);
+ expect(isImageFormat('JpG')).toBe(true);
+ });
+
+ test('should handle empty and invalid inputs', () => {
+ expect(isImageFormat('')).toBe(false);
+ expect(isImageFormat('invalid')).toBe(false);
+ expect(isImageFormat('123')).toBe(false);
+ expect(isImageFormat('.')).toBe(false);
+ });
+
+ test('should handle mixed case and edge cases', () => {
+ expect(isImageFormat('webP')).toBe(true);
+ expect(isImageFormat('WEBP')).toBe(true);
+ expect(isImageFormat('tIFf')).toBe(true);
+ expect(isImageFormat('bMp')).toBe(true);
+ });
+ });
+
+ describe('Edge Cases and Error Handling', () => {
+
+ test('should handle null and undefined inputs gracefully', () => {
+ // Note: TypeScript prevents these, but test runtime behavior for robustness
+ // The current implementation handles these gracefully by returning falsy values
+ expect(getEndpointName(null as any, null as any)).toBe('');
+ expect(getEndpointUrl(undefined as any, undefined as any)).toBe('');
+ expect(isConversionSupported(null as any, null as any)).toBe(false);
+
+ // isImageFormat will throw because it calls toLowerCase() on null/undefined
+ expect(() => isImageFormat(null as any)).toThrow();
+ expect(() => isImageFormat(undefined as any)).toThrow();
+ });
+
+ test('should handle special characters in file extensions', () => {
+ expect(isImageFormat('png@')).toBe(false);
+ expect(isImageFormat('jpg#')).toBe(false);
+ expect(isImageFormat('png.')).toBe(false);
+ expect(getEndpointName('pdf@', 'png')).toBe('');
+ expect(getEndpointName('pdf', 'png#')).toBe('');
+ });
+
+ test('should handle very long extension names', () => {
+ const longExtension = 'a'.repeat(100);
+ expect(isImageFormat(longExtension)).toBe(false);
+ expect(getEndpointName('pdf', longExtension)).toBe('');
+ expect(getEndpointName(longExtension, 'pdf')).toBe('file-to-pdf'); // Fallback to file to pdf
+ });
+ });
+});
\ No newline at end of file
diff --git a/frontend/src/utils/convertUtils.ts b/frontend/src/utils/convertUtils.ts
new file mode 100644
index 000000000..9c058c5ce
--- /dev/null
+++ b/frontend/src/utils/convertUtils.ts
@@ -0,0 +1,59 @@
+import {
+ CONVERSION_ENDPOINTS,
+ ENDPOINT_NAMES,
+ EXTENSION_TO_ENDPOINT
+} from '../constants/convertConstants';
+
+/**
+ * Resolves the endpoint name for a given conversion
+ */
+export const getEndpointName = (fromExtension: string, toExtension: string): string => {
+ if (!fromExtension || !toExtension) return '';
+
+ let endpointKey = EXTENSION_TO_ENDPOINT[fromExtension]?.[toExtension];
+
+ // If no explicit mapping exists and we're converting to PDF,
+ // fall back to 'any' which uses file-to-pdf endpoint
+ if (!endpointKey && toExtension === 'pdf' && fromExtension !== 'any') {
+ endpointKey = EXTENSION_TO_ENDPOINT['any']?.[toExtension];
+ }
+
+ return endpointKey || '';
+};
+
+/**
+ * Resolves the full endpoint URL for a given conversion
+ */
+export const getEndpointUrl = (fromExtension: string, toExtension: string): string => {
+ const endpointName = getEndpointName(fromExtension, toExtension);
+ if (!endpointName) return '';
+
+ // Find the endpoint URL from CONVERSION_ENDPOINTS using the endpoint name
+ for (const [key, endpoint] of Object.entries(CONVERSION_ENDPOINTS)) {
+ if (ENDPOINT_NAMES[key as keyof typeof ENDPOINT_NAMES] === endpointName) {
+ return endpoint;
+ }
+ }
+ return '';
+};
+
+/**
+ * Checks if a conversion is supported
+ */
+export const isConversionSupported = (fromExtension: string, toExtension: string): boolean => {
+ return getEndpointName(fromExtension, toExtension) !== '';
+};
+
+/**
+ * Checks if the given extension is an image format
+ */
+export const isImageFormat = (extension: string): boolean => {
+ return ['png', 'jpg', 'jpeg', 'gif', 'tiff', 'bmp', 'webp', 'svg'].includes(extension.toLowerCase());
+};
+
+/**
+ * Checks if the given extension is a web format
+ */
+export const isWebFormat = (extension: string): boolean => {
+ return ['html', 'zip'].includes(extension.toLowerCase());
+};
\ No newline at end of file
diff --git a/frontend/src/utils/fileResponseUtils.test.ts b/frontend/src/utils/fileResponseUtils.test.ts
new file mode 100644
index 000000000..2f16a7c61
--- /dev/null
+++ b/frontend/src/utils/fileResponseUtils.test.ts
@@ -0,0 +1,147 @@
+/**
+ * Unit tests for file response utility functions
+ */
+
+import { describe, test, expect } from 'vitest';
+import { getFilenameFromHeaders, createFileFromApiResponse } from './fileResponseUtils';
+
+describe('fileResponseUtils', () => {
+
+ describe('getFilenameFromHeaders', () => {
+
+ test('should extract filename from content-disposition header', () => {
+ const contentDisposition = 'attachment; filename="document.pdf"';
+ const filename = getFilenameFromHeaders(contentDisposition);
+
+ expect(filename).toBe('document.pdf');
+ });
+
+ test('should extract filename without quotes', () => {
+ const contentDisposition = 'attachment; filename=document.pdf';
+ const filename = getFilenameFromHeaders(contentDisposition);
+
+ expect(filename).toBe('document.pdf');
+ });
+
+ test('should handle single quotes', () => {
+ const contentDisposition = "attachment; filename='document.pdf'";
+ const filename = getFilenameFromHeaders(contentDisposition);
+
+ expect(filename).toBe('document.pdf');
+ });
+
+ test('should return null for malformed header', () => {
+ const contentDisposition = 'attachment; invalid=format';
+ const filename = getFilenameFromHeaders(contentDisposition);
+
+ expect(filename).toBe(null);
+ });
+
+ test('should return null for empty header', () => {
+ const filename = getFilenameFromHeaders('');
+
+ expect(filename).toBe(null);
+ });
+
+ test('should return null for undefined header', () => {
+ const filename = getFilenameFromHeaders();
+
+ expect(filename).toBe(null);
+ });
+
+ test('should handle complex filenames with spaces and special chars', () => {
+ const contentDisposition = 'attachment; filename="My Document (1).pdf"';
+ const filename = getFilenameFromHeaders(contentDisposition);
+
+ expect(filename).toBe('My Document (1).pdf');
+ });
+
+ test('should handle filename with extension when downloadHtml is enabled', () => {
+ const contentDisposition = 'attachment; filename="email_content.html"';
+ const filename = getFilenameFromHeaders(contentDisposition);
+
+ expect(filename).toBe('email_content.html');
+ });
+ });
+
+ describe('createFileFromApiResponse', () => {
+
+ test('should create file using header filename when available', () => {
+ const responseData = new Uint8Array([1, 2, 3, 4]);
+ const headers = {
+ 'content-type': 'application/pdf',
+ 'content-disposition': 'attachment; filename="server_filename.pdf"'
+ };
+ const fallbackFilename = 'fallback.pdf';
+
+ const file = createFileFromApiResponse(responseData, headers, fallbackFilename);
+
+ expect(file.name).toBe('server_filename.pdf');
+ expect(file.type).toBe('application/pdf');
+ expect(file.size).toBe(4);
+ });
+
+ test('should use fallback filename when no header filename', () => {
+ const responseData = new Uint8Array([1, 2, 3, 4]);
+ const headers = {
+ 'content-type': 'application/pdf'
+ };
+ const fallbackFilename = 'converted_file.pdf';
+
+ const file = createFileFromApiResponse(responseData, headers, fallbackFilename);
+
+ expect(file.name).toBe('converted_file.pdf');
+ expect(file.type).toBe('application/pdf');
+ });
+
+ test('should handle HTML response when downloadHtml is enabled', () => {
+ const responseData = 'Test';
+ const headers = {
+ 'content-type': 'text/html',
+ 'content-disposition': 'attachment; filename="email_content.html"'
+ };
+ const fallbackFilename = 'fallback.pdf';
+
+ const file = createFileFromApiResponse(responseData, headers, fallbackFilename);
+
+ expect(file.name).toBe('email_content.html');
+ expect(file.type).toBe('text/html');
+ });
+
+ test('should handle ZIP response', () => {
+ const responseData = new Uint8Array([80, 75, 3, 4]); // ZIP file signature
+ const headers = {
+ 'content-type': 'application/zip',
+ 'content-disposition': 'attachment; filename="converted_files.zip"'
+ };
+ const fallbackFilename = 'fallback.pdf';
+
+ const file = createFileFromApiResponse(responseData, headers, fallbackFilename);
+
+ expect(file.name).toBe('converted_files.zip');
+ expect(file.type).toBe('application/zip');
+ });
+
+ test('should use default content-type when none provided', () => {
+ const responseData = new Uint8Array([1, 2, 3, 4]);
+ const headers = {};
+ const fallbackFilename = 'test.bin';
+
+ const file = createFileFromApiResponse(responseData, headers, fallbackFilename);
+
+ expect(file.name).toBe('test.bin');
+ expect(file.type).toBe('application/octet-stream');
+ });
+
+ test('should handle null/undefined headers gracefully', () => {
+ const responseData = new Uint8Array([1, 2, 3, 4]);
+ const headers = null;
+ const fallbackFilename = 'test.bin';
+
+ const file = createFileFromApiResponse(responseData, headers, fallbackFilename);
+
+ expect(file.name).toBe('test.bin');
+ expect(file.type).toBe('application/octet-stream');
+ });
+ });
+});
\ No newline at end of file
diff --git a/frontend/src/utils/fileResponseUtils.ts b/frontend/src/utils/fileResponseUtils.ts
new file mode 100644
index 000000000..6e4422099
--- /dev/null
+++ b/frontend/src/utils/fileResponseUtils.ts
@@ -0,0 +1,37 @@
+/**
+ * Generic utility functions for handling file responses from API endpoints
+ */
+
+/**
+ * Extracts filename from Content-Disposition header
+ * @param contentDisposition - Content-Disposition header value
+ * @returns Filename if found, null otherwise
+ */
+export const getFilenameFromHeaders = (contentDisposition: string = ''): string | null => {
+ const match = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
+ if (match && match[1]) {
+ return match[1].replace(/['"]/g, '');
+ }
+ return null;
+};
+
+/**
+ * Creates a File object from API response using the filename from headers
+ * @param responseData - The response data (blob/arraybuffer/string)
+ * @param headers - Response headers object
+ * @param fallbackFilename - Filename to use if none provided in headers
+ * @returns File object
+ */
+export const createFileFromApiResponse = (
+ responseData: any,
+ headers: any,
+ fallbackFilename: string
+): File => {
+ const contentType = headers?.['content-type'] || 'application/octet-stream';
+ const contentDisposition = headers?.['content-disposition'] || '';
+
+ const filename = getFilenameFromHeaders(contentDisposition) || fallbackFilename;
+ const blob = new Blob([responseData], { type: contentType });
+
+ return new File([blob], filename, { type: contentType });
+};
\ No newline at end of file
diff --git a/frontend/src/utils/fileUtils.ts b/frontend/src/utils/fileUtils.ts
index bff3f5b1c..b42d2f646 100644
--- a/frontend/src/utils/fileUtils.ts
+++ b/frontend/src/utils/fileUtils.ts
@@ -125,4 +125,51 @@ export function cleanupFileUrls(files: FileWithUrl[]): void {
export function shouldUseDirectIndexedDBAccess(file: FileWithUrl): boolean {
const FILE_SIZE_LIMIT = 100 * 1024 * 1024; // 100MB
return file.size > FILE_SIZE_LIMIT;
+}
+
+/**
+ * Detects and normalizes file extension from filename
+ * @param filename - The filename to extract extension from
+ * @returns Normalized file extension in lowercase, empty string if no extension
+ */
+export function detectFileExtension(filename: string): string {
+ if (!filename || typeof filename !== 'string') return '';
+
+ const parts = filename.split('.');
+ // If there's no extension (no dots or only one part), return empty string
+ if (parts.length <= 1) return '';
+
+ // Get the last part (extension) in lowercase
+ let extension = parts[parts.length - 1].toLowerCase();
+
+ // Normalize common extension variants
+ if (extension === 'jpeg') extension = 'jpg';
+
+ return extension;
+}
+
+/**
+ * Gets the filename without extension
+ * @param filename - The filename to process
+ * @returns Filename without extension
+ */
+export function getFilenameWithoutExtension(filename: string): string {
+ if (!filename || typeof filename !== 'string') return '';
+
+ const parts = filename.split('.');
+ if (parts.length <= 1) return filename;
+
+ // Return all parts except the last one (extension)
+ return parts.slice(0, -1).join('.');
+}
+
+/**
+ * Creates a new filename with a different extension
+ * @param filename - Original filename
+ * @param newExtension - New extension (without dot)
+ * @returns New filename with the specified extension
+ */
+export function changeFileExtension(filename: string, newExtension: string): string {
+ const nameWithoutExt = getFilenameWithoutExtension(filename);
+ return `${nameWithoutExt}.${newExtension}`;
}
\ No newline at end of file
diff --git a/frontend/src/utils/thumbnailUtils.ts b/frontend/src/utils/thumbnailUtils.ts
index f0c28631a..35444035a 100644
--- a/frontend/src/utils/thumbnailUtils.ts
+++ b/frontend/src/utils/thumbnailUtils.ts
@@ -24,6 +24,11 @@ export async function generateThumbnailForFile(file: File): Promise