mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-02 18:45:21 +00:00
html to pdf
This commit is contained in:
parent
8c91113125
commit
8da72af478
@ -42,6 +42,7 @@
|
|||||||
"test:watch": "vitest --watch",
|
"test:watch": "vitest --watch",
|
||||||
"test:coverage": "vitest --coverage",
|
"test:coverage": "vitest --coverage",
|
||||||
"test:e2e": "playwright test",
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
"test:e2e:install": "playwright install"
|
"test:e2e:install": "playwright install"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
|
@ -224,49 +224,46 @@ const FileEditor = ({
|
|||||||
// Handle PDF files normally
|
// Handle PDF files normally
|
||||||
allExtractedFiles.push(file);
|
allExtractedFiles.push(file);
|
||||||
} else if (file.type === 'application/zip' || file.type === 'application/x-zip-compressed' || file.name.toLowerCase().endsWith('.zip')) {
|
} else if (file.type === 'application/zip' || file.type === 'application/x-zip-compressed' || file.name.toLowerCase().endsWith('.zip')) {
|
||||||
// Handle ZIP files
|
// Handle ZIP files - only expand if they contain PDFs
|
||||||
try {
|
try {
|
||||||
// Validate ZIP file first
|
// Validate ZIP file first
|
||||||
const validation = await zipFileService.validateZipFile(file);
|
const validation = await zipFileService.validateZipFile(file);
|
||||||
if (!validation.isValid) {
|
|
||||||
errors.push(`ZIP file "${file.name}": ${validation.errors.join(', ')}`);
|
if (validation.isValid && validation.containsPDFs) {
|
||||||
continue;
|
// ZIP contains PDFs - extract them
|
||||||
}
|
|
||||||
|
|
||||||
// Extract PDF files from ZIP
|
|
||||||
setZipExtractionProgress({
|
|
||||||
isExtracting: true,
|
|
||||||
currentFile: file.name,
|
|
||||||
progress: 0,
|
|
||||||
extractedCount: 0,
|
|
||||||
totalFiles: validation.fileCount
|
|
||||||
});
|
|
||||||
|
|
||||||
const extractionResult = await zipFileService.extractPdfFiles(file, (progress) => {
|
|
||||||
setZipExtractionProgress({
|
setZipExtractionProgress({
|
||||||
isExtracting: true,
|
isExtracting: true,
|
||||||
currentFile: progress.currentFile,
|
currentFile: file.name,
|
||||||
progress: progress.progress,
|
progress: 0,
|
||||||
extractedCount: progress.extractedCount,
|
extractedCount: 0,
|
||||||
totalFiles: progress.totalFiles
|
totalFiles: validation.fileCount
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Reset extraction progress
|
const extractionResult = await zipFileService.extractPdfFiles(file, (progress) => {
|
||||||
setZipExtractionProgress({
|
setZipExtractionProgress({
|
||||||
isExtracting: false,
|
isExtracting: true,
|
||||||
currentFile: '',
|
currentFile: progress.currentFile,
|
||||||
progress: 0,
|
progress: progress.progress,
|
||||||
extractedCount: 0,
|
extractedCount: progress.extractedCount,
|
||||||
totalFiles: 0
|
totalFiles: progress.totalFiles
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if (extractionResult.success) {
|
// Reset extraction progress
|
||||||
allExtractedFiles.push(...extractionResult.extractedFiles);
|
setZipExtractionProgress({
|
||||||
|
isExtracting: false,
|
||||||
// Record ZIP extraction operation
|
currentFile: '',
|
||||||
const operationId = `zip-extract-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
progress: 0,
|
||||||
const operation: FileOperation = {
|
extractedCount: 0,
|
||||||
|
totalFiles: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
if (extractionResult.success) {
|
||||||
|
allExtractedFiles.push(...extractionResult.extractedFiles);
|
||||||
|
|
||||||
|
// Record ZIP extraction operation
|
||||||
|
const operationId = `zip-extract-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
const operation: FileOperation = {
|
||||||
id: operationId,
|
id: operationId,
|
||||||
type: 'convert',
|
type: 'convert',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@ -290,8 +287,13 @@ const FileEditor = ({
|
|||||||
if (extractionResult.errors.length > 0) {
|
if (extractionResult.errors.length > 0) {
|
||||||
errors.push(...extractionResult.errors);
|
errors.push(...extractionResult.errors);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
errors.push(`Failed to extract ZIP file "${file.name}": ${extractionResult.errors.join(', ')}`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
errors.push(`Failed to extract ZIP file "${file.name}": ${extractionResult.errors.join(', ')}`);
|
// ZIP doesn't contain PDFs or is invalid - treat as regular file
|
||||||
|
console.log(`Adding ZIP file as regular file: ${file.name} (no PDFs found)`);
|
||||||
|
allExtractedFiles.push(file);
|
||||||
}
|
}
|
||||||
} catch (zipError) {
|
} catch (zipError) {
|
||||||
errors.push(`Failed to process ZIP file "${file.name}": ${zipError instanceof Error ? zipError.message : 'Unknown error'}`);
|
errors.push(`Failed to process ZIP file "${file.name}": ${zipError instanceof Error ? zipError.message : 'Unknown error'}`);
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { Stack, Text, Group, Divider, UnstyledButton, useMantineTheme, useMantineColorScheme } from "@mantine/core";
|
import { Stack, Text, Group, Divider, UnstyledButton, useMantineTheme, useMantineColorScheme, NumberInput, Slider } from "@mantine/core";
|
||||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMultipleEndpointsEnabled } from "../../../hooks/useEndpointConfig";
|
import { useMultipleEndpointsEnabled } from "../../../hooks/useEndpointConfig";
|
||||||
import { isImageFormat } from "../../../utils/convertUtils";
|
import { isImageFormat, isWebFormat } from "../../../utils/convertUtils";
|
||||||
import { useFileSelectionActions } from "../../../contexts/FileSelectionContext";
|
import { useFileSelectionActions } from "../../../contexts/FileSelectionContext";
|
||||||
import { useFileContext } from "../../../contexts/FileContext";
|
import { useFileContext } from "../../../contexts/FileContext";
|
||||||
|
import { detectFileExtension } from "../../../utils/fileUtils";
|
||||||
import GroupedFormatDropdown from "./GroupedFormatDropdown";
|
import GroupedFormatDropdown from "./GroupedFormatDropdown";
|
||||||
import ConvertToImageSettings from "./ConvertToImageSettings";
|
import ConvertToImageSettings from "./ConvertToImageSettings";
|
||||||
import ConvertFromImageSettings from "./ConvertFromImageSettings";
|
import ConvertFromImageSettings from "./ConvertFromImageSettings";
|
||||||
@ -234,6 +235,40 @@ const ConvertSettings = ({
|
|||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{/* HTML to PDF specific options */}
|
||||||
|
{((isWebFormat(parameters.fromExtension) && parameters.toExtension === 'pdf') ||
|
||||||
|
(parameters.isSmartDetection && parameters.smartDetectionType === 'web')) && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<Stack gap="sm" data-testid="html-options-section">
|
||||||
|
<Text size="sm" fw={500} data-testid="html-options-title">{t("convert.htmlOptions", "HTML Options")}:</Text>
|
||||||
|
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text size="xs" fw={500}>{t("convert.zoomLevel", "Zoom Level")}:</Text>
|
||||||
|
<NumberInput
|
||||||
|
value={parameters.htmlOptions.zoomLevel}
|
||||||
|
onChange={(value) => onParameterChange('htmlOptions', { ...parameters.htmlOptions, zoomLevel: Number(value) || 1.0 })}
|
||||||
|
min={0.1}
|
||||||
|
max={3.0}
|
||||||
|
step={0.1}
|
||||||
|
precision={1}
|
||||||
|
disabled={disabled}
|
||||||
|
data-testid="zoom-level-input"
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
value={parameters.htmlOptions.zoomLevel}
|
||||||
|
onChange={(value) => onParameterChange('htmlOptions', { ...parameters.htmlOptions, zoomLevel: value })}
|
||||||
|
min={0.1}
|
||||||
|
max={3.0}
|
||||||
|
step={0.1}
|
||||||
|
disabled={disabled}
|
||||||
|
data-testid="zoom-level-slider"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* EML specific options */}
|
{/* EML specific options */}
|
||||||
{parameters.fromExtension === 'eml' && parameters.toExtension === 'pdf' && (
|
{parameters.fromExtension === 'eml' && parameters.toExtension === 'pdf' && (
|
||||||
<>
|
<>
|
||||||
|
@ -75,7 +75,7 @@ export const FROM_FORMAT_OPTIONS = [
|
|||||||
{ value: 'webp', label: 'WEBP', group: 'Image' },
|
{ value: 'webp', label: 'WEBP', group: 'Image' },
|
||||||
{ value: 'svg', label: 'SVG', group: 'Image' },
|
{ value: 'svg', label: 'SVG', group: 'Image' },
|
||||||
{ value: 'html', label: 'HTML', group: 'Web' },
|
{ value: 'html', label: 'HTML', group: 'Web' },
|
||||||
{ value: 'htm', label: 'HTM', group: 'Web' },
|
{ value: 'zip', label: 'ZIP', group: 'Web' },
|
||||||
{ value: 'md', label: 'MD', group: 'Text' },
|
{ value: 'md', label: 'MD', group: 'Text' },
|
||||||
{ value: 'txt', label: 'TXT', group: 'Text' },
|
{ value: 'txt', label: 'TXT', group: 'Text' },
|
||||||
{ value: 'rtf', label: 'RTF', group: 'Text' },
|
{ value: 'rtf', label: 'RTF', group: 'Text' },
|
||||||
@ -112,7 +112,8 @@ export const CONVERSION_MATRIX: Record<string, string[]> = {
|
|||||||
'xlsx': ['pdf'], 'xls': ['pdf'], 'ods': ['pdf'],
|
'xlsx': ['pdf'], 'xls': ['pdf'], 'ods': ['pdf'],
|
||||||
'pptx': ['pdf'], 'ppt': ['pdf'], 'odp': ['pdf'],
|
'pptx': ['pdf'], 'ppt': ['pdf'], 'odp': ['pdf'],
|
||||||
'jpg': ['pdf'], 'jpeg': ['pdf'], 'png': ['pdf'], 'gif': ['pdf'], 'bmp': ['pdf'], 'tiff': ['pdf'], 'webp': ['pdf'], 'svg': ['pdf'],
|
'jpg': ['pdf'], 'jpeg': ['pdf'], 'png': ['pdf'], 'gif': ['pdf'], 'bmp': ['pdf'], 'tiff': ['pdf'], 'webp': ['pdf'], 'svg': ['pdf'],
|
||||||
'html': ['pdf'], 'htm': ['pdf'],
|
'html': ['pdf'],
|
||||||
|
'zip': ['pdf'],
|
||||||
'md': ['pdf'],
|
'md': ['pdf'],
|
||||||
'txt': ['pdf'], 'rtf': ['pdf'],
|
'txt': ['pdf'], 'rtf': ['pdf'],
|
||||||
'eml': ['pdf']
|
'eml': ['pdf']
|
||||||
@ -136,7 +137,8 @@ export const EXTENSION_TO_ENDPOINT: Record<string, Record<string, string>> = {
|
|||||||
'pptx': { 'pdf': 'file-to-pdf' }, 'ppt': { 'pdf': 'file-to-pdf' }, 'odp': { 'pdf': 'file-to-pdf' },
|
'pptx': { 'pdf': 'file-to-pdf' }, 'ppt': { 'pdf': 'file-to-pdf' }, 'odp': { 'pdf': 'file-to-pdf' },
|
||||||
'jpg': { 'pdf': 'img-to-pdf' }, 'jpeg': { 'pdf': 'img-to-pdf' }, 'png': { 'pdf': 'img-to-pdf' },
|
'jpg': { 'pdf': 'img-to-pdf' }, 'jpeg': { 'pdf': 'img-to-pdf' }, 'png': { 'pdf': 'img-to-pdf' },
|
||||||
'gif': { 'pdf': 'img-to-pdf' }, 'bmp': { 'pdf': 'img-to-pdf' }, 'tiff': { 'pdf': 'img-to-pdf' }, 'webp': { 'pdf': 'img-to-pdf' }, 'svg': { 'pdf': 'img-to-pdf' },
|
'gif': { 'pdf': 'img-to-pdf' }, 'bmp': { 'pdf': 'img-to-pdf' }, 'tiff': { 'pdf': 'img-to-pdf' }, 'webp': { 'pdf': 'img-to-pdf' }, 'svg': { 'pdf': 'img-to-pdf' },
|
||||||
'html': { 'pdf': 'html-to-pdf' }, 'htm': { 'pdf': 'html-to-pdf' },
|
'html': { 'pdf': 'html-to-pdf' },
|
||||||
|
'zip': { 'pdf': 'html-to-pdf' },
|
||||||
'md': { 'pdf': 'markdown-to-pdf' },
|
'md': { 'pdf': 'markdown-to-pdf' },
|
||||||
'txt': { 'pdf': 'file-to-pdf' }, 'rtf': { 'pdf': 'file-to-pdf' },
|
'txt': { 'pdf': 'file-to-pdf' }, 'rtf': { 'pdf': 'file-to-pdf' },
|
||||||
'eml': { 'pdf': 'eml-to-pdf' }
|
'eml': { 'pdf': 'eml-to-pdf' }
|
||||||
|
@ -5,8 +5,9 @@ import { useFileContext } from '../../../contexts/FileContext';
|
|||||||
import { FileOperation } from '../../../types/fileContext';
|
import { FileOperation } from '../../../types/fileContext';
|
||||||
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
|
import { generateThumbnailForFile } from '../../../utils/thumbnailUtils';
|
||||||
import { ConvertParameters } from './useConvertParameters';
|
import { ConvertParameters } from './useConvertParameters';
|
||||||
|
import { detectFileExtension } from '../../../utils/fileUtils';
|
||||||
|
|
||||||
import { getEndpointUrl, isImageFormat } from '../../../utils/convertUtils';
|
import { getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils';
|
||||||
|
|
||||||
export interface ConvertOperationHook {
|
export interface ConvertOperationHook {
|
||||||
executeOperation: (
|
executeOperation: (
|
||||||
@ -29,6 +30,99 @@ export interface ConvertOperationHook {
|
|||||||
clearError: () => void;
|
clearError: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Utility functions for better maintainability
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if multiple files should be processed separately
|
||||||
|
*/
|
||||||
|
const shouldProcessFilesSeparately = (
|
||||||
|
selectedFiles: File[],
|
||||||
|
parameters: ConvertParameters
|
||||||
|
): boolean => {
|
||||||
|
return selectedFiles.length > 1 && (
|
||||||
|
// Image to PDF with combineImages = false
|
||||||
|
((isImageFormat(parameters.fromExtension) || parameters.fromExtension === 'image') &&
|
||||||
|
parameters.toExtension === 'pdf' && !parameters.imageOptions.combineImages) ||
|
||||||
|
// PDF to image conversions (each PDF should generate its own image file)
|
||||||
|
(parameters.fromExtension === 'pdf' && isImageFormat(parameters.toExtension)) ||
|
||||||
|
// Web files to PDF conversions (each web file should generate its own PDF)
|
||||||
|
((isWebFormat(parameters.fromExtension) || parameters.fromExtension === 'web') &&
|
||||||
|
parameters.toExtension === 'pdf') ||
|
||||||
|
// Web files smart detection
|
||||||
|
(parameters.isSmartDetection && parameters.smartDetectionType === 'web') ||
|
||||||
|
// Mixed file types (smart detection)
|
||||||
|
(parameters.isSmartDetection && parameters.smartDetectionType === 'mixed')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a file from API response with appropriate naming
|
||||||
|
*/
|
||||||
|
const createFileFromResponse = (
|
||||||
|
responseData: any,
|
||||||
|
headers: any,
|
||||||
|
originalFileName: string,
|
||||||
|
targetExtension: string
|
||||||
|
): File => {
|
||||||
|
const contentType = headers?.['content-type'] || 'application/octet-stream';
|
||||||
|
const blob = new Blob([responseData], { type: contentType });
|
||||||
|
|
||||||
|
const originalName = originalFileName.split('.')[0];
|
||||||
|
let filename: string;
|
||||||
|
|
||||||
|
// Check if response is a ZIP
|
||||||
|
if (contentType.includes('zip') || headers?.['content-disposition']?.includes('.zip')) {
|
||||||
|
filename = `${originalName}_converted.zip`;
|
||||||
|
} else {
|
||||||
|
filename = `${originalName}_converted.${targetExtension}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new File([blob], filename, { type: contentType });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates thumbnails for multiple files
|
||||||
|
*/
|
||||||
|
const generateThumbnailsForFiles = async (files: File[]): Promise<string[]> => {
|
||||||
|
const thumbnails: string[] = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
const thumbnail = await generateThumbnailForFile(file);
|
||||||
|
thumbnails.push(thumbnail);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to generate thumbnail for ${file.name}:`, error);
|
||||||
|
thumbnails.push('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return thumbnails;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates download URL and filename for single or multiple files
|
||||||
|
*/
|
||||||
|
const createDownloadInfo = async (files: File[]): Promise<{ url: string; filename: string }> => {
|
||||||
|
if (files.length === 1) {
|
||||||
|
// Single file - direct download
|
||||||
|
const url = window.URL.createObjectURL(files[0]);
|
||||||
|
return { url, filename: files[0].name };
|
||||||
|
} else {
|
||||||
|
// Multiple files - create ZIP for convenient download
|
||||||
|
const JSZip = (await import('jszip')).default;
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
zip.file(file.name, file);
|
||||||
|
});
|
||||||
|
|
||||||
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||||
|
const zipUrl = window.URL.createObjectURL(zipBlob);
|
||||||
|
|
||||||
|
return { url: zipUrl, filename: 'converted_files.zip' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const useConvertOperation = (): ConvertOperationHook => {
|
export const useConvertOperation = (): ConvertOperationHook => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
@ -58,7 +152,7 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
formData.append("fileInput", file);
|
formData.append("fileInput", file);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { fromExtension, toExtension, imageOptions } = parameters;
|
const { fromExtension, toExtension, imageOptions, htmlOptions } = parameters;
|
||||||
|
|
||||||
// Add conversion-specific parameters
|
// Add conversion-specific parameters
|
||||||
if (isImageFormat(toExtension)) {
|
if (isImageFormat(toExtension)) {
|
||||||
@ -76,6 +170,9 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
formData.append("fitOption", imageOptions.fitOption);
|
formData.append("fitOption", imageOptions.fitOption);
|
||||||
formData.append("colorType", imageOptions.colorType);
|
formData.append("colorType", imageOptions.colorType);
|
||||||
formData.append("autoRotate", imageOptions.autoRotate.toString());
|
formData.append("autoRotate", imageOptions.autoRotate.toString());
|
||||||
|
} else if ((fromExtension === 'html' || fromExtension === 'zip') && toExtension === 'pdf') {
|
||||||
|
// HTML to PDF conversion with zoom level (includes ZIP files with HTML)
|
||||||
|
formData.append("zoom", htmlOptions.zoomLevel.toString());
|
||||||
} else if (fromExtension === 'pdf' && toExtension === 'csv') {
|
} else if (fromExtension === 'pdf' && toExtension === 'csv') {
|
||||||
// CSV extraction - always process all pages for simplified workflow
|
// CSV extraction - always process all pages for simplified workflow
|
||||||
formData.append("pageNumbers", "all");
|
formData.append("pageNumbers", "all");
|
||||||
@ -103,6 +200,7 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
fromExtension: parameters.fromExtension,
|
fromExtension: parameters.fromExtension,
|
||||||
toExtension: parameters.toExtension,
|
toExtension: parameters.toExtension,
|
||||||
imageOptions: parameters.imageOptions,
|
imageOptions: parameters.imageOptions,
|
||||||
|
htmlOptions: parameters.htmlOptions,
|
||||||
},
|
},
|
||||||
fileSize: selectedFiles[0].size
|
fileSize: selectedFiles[0].size
|
||||||
}
|
}
|
||||||
@ -148,16 +246,8 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this should be processed as separate files
|
// Use utility function to determine processing strategy
|
||||||
const shouldProcessSeparately = selectedFiles.length > 1 && (
|
if (shouldProcessFilesSeparately(selectedFiles, parameters)) {
|
||||||
// Image to PDF with combineImages = false
|
|
||||||
((isImageFormat(parameters.fromExtension) || parameters.fromExtension === 'image') &&
|
|
||||||
parameters.toExtension === 'pdf' && !parameters.imageOptions.combineImages) ||
|
|
||||||
// Mixed file types (smart detection)
|
|
||||||
(parameters.isSmartDetection && parameters.smartDetectionType === 'mixed')
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldProcessSeparately) {
|
|
||||||
// Process each file separately with appropriate endpoint
|
// Process each file separately with appropriate endpoint
|
||||||
await executeMultipleSeparateFiles(parameters, selectedFiles);
|
await executeMultipleSeparateFiles(parameters, selectedFiles);
|
||||||
} else {
|
} else {
|
||||||
@ -175,7 +265,6 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
|
|
||||||
const results: File[] = [];
|
const results: File[] = [];
|
||||||
const thumbnails: string[] = [];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Process each file separately
|
// Process each file separately
|
||||||
@ -183,8 +272,8 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
const file = selectedFiles[i];
|
const file = selectedFiles[i];
|
||||||
setStatus(t("convert.processingFile", `Processing file ${i + 1} of ${selectedFiles.length}...`));
|
setStatus(t("convert.processingFile", `Processing file ${i + 1} of ${selectedFiles.length}...`));
|
||||||
|
|
||||||
// Detect the specific file type for this file
|
// Detect the specific file type for this file using the shared utility
|
||||||
const fileExtension = file.name.split('.').pop()?.toLowerCase() || '';
|
const fileExtension = detectFileExtension(file.name);
|
||||||
|
|
||||||
// Determine the best endpoint for this specific file type
|
// Determine the best endpoint for this specific file type
|
||||||
let endpoint = getEndpointUrl(fileExtension, parameters.toExtension);
|
let endpoint = getEndpointUrl(fileExtension, parameters.toExtension);
|
||||||
@ -209,23 +298,15 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
||||||
const blob = new Blob([response.data]);
|
|
||||||
|
|
||||||
// Generate filename for this specific file
|
|
||||||
const originalName = file.name.split('.')[0];
|
|
||||||
const filename = `${originalName}_converted.${parameters.toExtension}`;
|
|
||||||
const convertedFile = new File([blob], filename, { type: blob.type });
|
|
||||||
|
|
||||||
|
// Use utility function to create file from response
|
||||||
|
const convertedFile = createFileFromResponse(
|
||||||
|
response.data,
|
||||||
|
response.headers,
|
||||||
|
file.name,
|
||||||
|
parameters.toExtension
|
||||||
|
);
|
||||||
results.push(convertedFile);
|
results.push(convertedFile);
|
||||||
|
|
||||||
// Generate thumbnail
|
|
||||||
try {
|
|
||||||
const thumbnail = await generateThumbnailForFile(convertedFile);
|
|
||||||
thumbnails.push(thumbnail);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Failed to generate thumbnail for ${filename}:`, error);
|
|
||||||
thumbnails.push('');
|
|
||||||
}
|
|
||||||
|
|
||||||
markOperationApplied(fileId, operationId);
|
markOperationApplied(fileId, operationId);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -236,19 +317,31 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
|
console.log(`Multi-file conversion completed: ${results.length} files processed from ${selectedFiles.length} input files`);
|
||||||
|
console.log('Result files:', results.map(f => f.name));
|
||||||
|
|
||||||
|
// Use utility function to generate thumbnails
|
||||||
|
const generatedThumbnails = await generateThumbnailsForFiles(results);
|
||||||
|
|
||||||
// Set results for multiple files
|
// Set results for multiple files
|
||||||
setFiles(results);
|
setFiles(results);
|
||||||
setThumbnails(thumbnails);
|
setThumbnails(generatedThumbnails);
|
||||||
|
|
||||||
// Add all converted files to FileContext
|
// Add all converted files to FileContext
|
||||||
await addFiles(results);
|
await addFiles(results);
|
||||||
|
|
||||||
// For multiple separate files, use the first file for download
|
// Use utility function to create download info
|
||||||
const firstFileBlob = new Blob([results[0]]);
|
try {
|
||||||
const firstFileUrl = window.URL.createObjectURL(firstFileBlob);
|
const { url, filename } = await createDownloadInfo(results);
|
||||||
|
setDownloadUrl(url);
|
||||||
setDownloadUrl(firstFileUrl);
|
setDownloadFilename(filename);
|
||||||
setDownloadFilename(results[0].name);
|
} catch (error) {
|
||||||
|
console.error('Failed to create download info:', error);
|
||||||
|
// Fallback to first file only
|
||||||
|
const url = window.URL.createObjectURL(results[0]);
|
||||||
|
setDownloadUrl(url);
|
||||||
|
setDownloadFilename(results[0].name);
|
||||||
|
}
|
||||||
setStatus(t("convert.multipleFilesComplete", `Converted ${results.length} files successfully`));
|
setStatus(t("convert.multipleFilesComplete", `Converted ${results.length} files successfully`));
|
||||||
} else {
|
} else {
|
||||||
setErrorMessage(t("convert.errorAllFilesFailed", "All files failed to convert"));
|
setErrorMessage(t("convert.errorAllFilesFailed", "All files failed to convert"));
|
||||||
@ -283,20 +376,25 @@ export const useConvertOperation = (): ConvertOperationHook => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
||||||
const blob = new Blob([response.data]);
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
// Generate filename based on conversion
|
// Use utility function to create file from response
|
||||||
const originalName = selectedFiles.length === 1
|
const originalFileName = selectedFiles.length === 1
|
||||||
? selectedFiles[0].name.split('.')[0]
|
? selectedFiles[0].name
|
||||||
: 'combined_images';
|
: 'combined_files.pdf'; // Default extension for combined files
|
||||||
const filename = `${originalName}_converted.${parameters.toExtension}`;
|
|
||||||
|
|
||||||
|
const convertedFile = createFileFromResponse(
|
||||||
|
response.data,
|
||||||
|
response.headers,
|
||||||
|
originalFileName,
|
||||||
|
parameters.toExtension
|
||||||
|
);
|
||||||
|
|
||||||
|
const url = window.URL.createObjectURL(convertedFile);
|
||||||
setDownloadUrl(url);
|
setDownloadUrl(url);
|
||||||
setDownloadFilename(filename);
|
setDownloadFilename(convertedFile.name);
|
||||||
setStatus(t("downloadComplete"));
|
setStatus(t("downloadComplete"));
|
||||||
|
|
||||||
await processResults(blob, filename);
|
await processResults(new Blob([convertedFile]), convertedFile.name);
|
||||||
markOperationApplied(fileId, operationId);
|
markOperationApplied(fileId, operationId);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
@ -207,31 +207,4 @@ describe('useConvertParameters', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('File Extension Detection', () => {
|
|
||||||
|
|
||||||
test('should detect file extensions correctly', () => {
|
|
||||||
const { result } = renderHook(() => useConvertParameters());
|
|
||||||
|
|
||||||
expect(result.current.detectFileExtension('document.pdf')).toBe('pdf');
|
|
||||||
expect(result.current.detectFileExtension('image.PNG')).toBe('png'); // Should lowercase
|
|
||||||
expect(result.current.detectFileExtension('file.docx')).toBe('docx');
|
|
||||||
expect(result.current.detectFileExtension('archive.tar.gz')).toBe('gz'); // Last extension
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle files without extensions', () => {
|
|
||||||
const { result } = renderHook(() => useConvertParameters());
|
|
||||||
|
|
||||||
// Files without extensions should return empty string
|
|
||||||
expect(result.current.detectFileExtension('noextension')).toBe('');
|
|
||||||
expect(result.current.detectFileExtension('')).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle edge cases', () => {
|
|
||||||
const { result } = renderHook(() => useConvertParameters());
|
|
||||||
|
|
||||||
expect(result.current.detectFileExtension('file.')).toBe('');
|
|
||||||
expect(result.current.detectFileExtension('.hidden')).toBe('hidden');
|
|
||||||
expect(result.current.detectFileExtension('file.UPPER')).toBe('upper');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
@ -9,7 +9,8 @@ import {
|
|||||||
type OutputOption,
|
type OutputOption,
|
||||||
type FitOption
|
type FitOption
|
||||||
} from '../../../constants/convertConstants';
|
} from '../../../constants/convertConstants';
|
||||||
import { getEndpointName as getEndpointNameUtil, getEndpointUrl, isImageFormat } from '../../../utils/convertUtils';
|
import { getEndpointName as getEndpointNameUtil, getEndpointUrl, isImageFormat, isWebFormat } from '../../../utils/convertUtils';
|
||||||
|
import { detectFileExtension as detectFileExtensionUtil } from '../../../utils/fileUtils';
|
||||||
|
|
||||||
export interface ConvertParameters {
|
export interface ConvertParameters {
|
||||||
fromExtension: string;
|
fromExtension: string;
|
||||||
@ -22,8 +23,11 @@ export interface ConvertParameters {
|
|||||||
autoRotate: boolean;
|
autoRotate: boolean;
|
||||||
combineImages: boolean;
|
combineImages: boolean;
|
||||||
};
|
};
|
||||||
|
htmlOptions: {
|
||||||
|
zoomLevel: number;
|
||||||
|
};
|
||||||
isSmartDetection: boolean;
|
isSmartDetection: boolean;
|
||||||
smartDetectionType: 'mixed' | 'images' | 'none';
|
smartDetectionType: 'mixed' | 'images' | 'web' | 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConvertParametersHook {
|
export interface ConvertParametersHook {
|
||||||
@ -34,7 +38,6 @@ export interface ConvertParametersHook {
|
|||||||
getEndpointName: () => string;
|
getEndpointName: () => string;
|
||||||
getEndpoint: () => string;
|
getEndpoint: () => string;
|
||||||
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
getAvailableToExtensions: (fromExtension: string) => Array<{value: string, label: string, group: string}>;
|
||||||
detectFileExtension: (filename: string) => string;
|
|
||||||
analyzeFileTypes: (files: Array<{name: string}>) => void;
|
analyzeFileTypes: (files: Array<{name: string}>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,6 +52,9 @@ const initialParameters: ConvertParameters = {
|
|||||||
autoRotate: true,
|
autoRotate: true,
|
||||||
combineImages: true,
|
combineImages: true,
|
||||||
},
|
},
|
||||||
|
htmlOptions: {
|
||||||
|
zoomLevel: 1.0,
|
||||||
|
},
|
||||||
isSmartDetection: false,
|
isSmartDetection: false,
|
||||||
smartDetectionType: 'none',
|
smartDetectionType: 'none',
|
||||||
};
|
};
|
||||||
@ -93,6 +99,9 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
|||||||
} else if (smartDetectionType === 'images') {
|
} else if (smartDetectionType === 'images') {
|
||||||
// All images -> PDF using img-to-pdf endpoint
|
// All images -> PDF using img-to-pdf endpoint
|
||||||
return 'img-to-pdf';
|
return 'img-to-pdf';
|
||||||
|
} else if (smartDetectionType === 'web') {
|
||||||
|
// All web files -> PDF using html-to-pdf endpoint
|
||||||
|
return 'html-to-pdf';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,6 +118,9 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
|||||||
} else if (smartDetectionType === 'images') {
|
} else if (smartDetectionType === 'images') {
|
||||||
// All images -> PDF using img-to-pdf endpoint
|
// All images -> PDF using img-to-pdf endpoint
|
||||||
return '/api/v1/convert/img/pdf';
|
return '/api/v1/convert/img/pdf';
|
||||||
|
} else if (smartDetectionType === 'web') {
|
||||||
|
// All web files -> PDF using html-to-pdf endpoint
|
||||||
|
return '/api/v1/convert/html/pdf';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,26 +143,23 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
const analyzeFileTypes = (files: Array<{name: string}>) => {
|
const analyzeFileTypes = (files: Array<{name: string}>) => {
|
||||||
if (files.length <= 1) {
|
if (files.length === 0) {
|
||||||
// Single file or no files - use regular detection with auto-target selection
|
// No files - reset to empty state
|
||||||
const detectedExt = files.length === 1 ? detectFileExtension(files[0].name) : '';
|
setParameters(prev => ({
|
||||||
|
...prev,
|
||||||
|
isSmartDetection: false,
|
||||||
|
smartDetectionType: 'none',
|
||||||
|
fromExtension: '',
|
||||||
|
toExtension: ''
|
||||||
|
}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (files.length === 1) {
|
||||||
|
// Single file - use regular detection with auto-target selection
|
||||||
|
const detectedExt = detectFileExtensionUtil(files[0].name);
|
||||||
let fromExt = detectedExt;
|
let fromExt = detectedExt;
|
||||||
let availableTargets = detectedExt ? CONVERSION_MATRIX[detectedExt] || [] : [];
|
let availableTargets = detectedExt ? CONVERSION_MATRIX[detectedExt] || [] : [];
|
||||||
|
|
||||||
@ -174,7 +183,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Multiple files - analyze file types
|
// Multiple files - analyze file types
|
||||||
const extensions = files.map(file => detectFileExtension(file.name));
|
const extensions = files.map(file => detectFileExtensionUtil(file.name));
|
||||||
const uniqueExtensions = [...new Set(extensions)];
|
const uniqueExtensions = [...new Set(extensions)];
|
||||||
|
|
||||||
if (uniqueExtensions.length === 1) {
|
if (uniqueExtensions.length === 1) {
|
||||||
@ -201,6 +210,7 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
|||||||
} else {
|
} else {
|
||||||
// Mixed file types
|
// Mixed file types
|
||||||
const allImages = uniqueExtensions.every(ext => isImageFormat(ext));
|
const allImages = uniqueExtensions.every(ext => isImageFormat(ext));
|
||||||
|
const allWeb = uniqueExtensions.every(ext => isWebFormat(ext));
|
||||||
|
|
||||||
if (allImages) {
|
if (allImages) {
|
||||||
// All files are images - use image-to-pdf conversion
|
// All files are images - use image-to-pdf conversion
|
||||||
@ -211,6 +221,15 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
|||||||
fromExtension: 'image',
|
fromExtension: 'image',
|
||||||
toExtension: 'pdf'
|
toExtension: 'pdf'
|
||||||
}));
|
}));
|
||||||
|
} else if (allWeb) {
|
||||||
|
// All files are web files - use html-to-pdf conversion
|
||||||
|
setParameters(prev => ({
|
||||||
|
...prev,
|
||||||
|
isSmartDetection: true,
|
||||||
|
smartDetectionType: 'web',
|
||||||
|
fromExtension: 'html',
|
||||||
|
toExtension: 'pdf'
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
// Mixed non-image types - use file-to-pdf conversion
|
// Mixed non-image types - use file-to-pdf conversion
|
||||||
setParameters(prev => ({
|
setParameters(prev => ({
|
||||||
@ -232,7 +251,6 @@ export const useConvertParameters = (): ConvertParametersHook => {
|
|||||||
getEndpointName,
|
getEndpointName,
|
||||||
getEndpoint,
|
getEndpoint,
|
||||||
getAvailableToExtensions,
|
getAvailableToExtensions,
|
||||||
detectFileExtension,
|
|
||||||
analyzeFileTypes,
|
analyzeFileTypes,
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -426,71 +426,6 @@ test.describe('Convert Tool E2E Tests', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Error Handling', () => {
|
|
||||||
|
|
||||||
test('should handle corrupted file gracefully', async ({ page }) => {
|
|
||||||
// Create a corrupted file
|
|
||||||
const corruptedPath = resolveTestFixturePath('corrupted.pdf');
|
|
||||||
fs.writeFileSync(corruptedPath, 'This is not a valid PDF file');
|
|
||||||
|
|
||||||
await page.setInputFiles('input[type="file"]', corruptedPath);
|
|
||||||
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
|
||||||
|
|
||||||
// Click the Convert tool button
|
|
||||||
await page.click('[data-testid="tool-convert"]');
|
|
||||||
|
|
||||||
// Wait for convert mode and select file
|
|
||||||
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
|
|
||||||
await page.click('[data-testid="file-thumbnail-checkbox"]');
|
|
||||||
|
|
||||||
await page.click('[data-testid="convert-from-dropdown"]');
|
|
||||||
await page.click('[data-testid="format-option-pdf"]');
|
|
||||||
|
|
||||||
await page.click('[data-testid="convert-to-dropdown"]');
|
|
||||||
await page.click('[data-testid="format-option-png"]');
|
|
||||||
|
|
||||||
await page.click('[data-testid="convert-button"]');
|
|
||||||
|
|
||||||
// Should show error message
|
|
||||||
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
|
|
||||||
// Error text content will vary based on translation, so just check error message exists
|
|
||||||
await expect(page.locator('[data-testid="error-message"]')).not.toBeEmpty();
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
fs.unlinkSync(corruptedPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle backend unavailability', async ({ page }) => {
|
|
||||||
// This test would require mocking the backend or testing during downtime
|
|
||||||
// For now, we'll simulate by checking error handling UI
|
|
||||||
|
|
||||||
await page.setInputFiles('input[type="file"]', TEST_FILES.pdf);
|
|
||||||
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
|
||||||
|
|
||||||
// Click the Convert tool button
|
|
||||||
await page.click('[data-testid="tool-convert"]');
|
|
||||||
|
|
||||||
// Wait for convert mode and select file
|
|
||||||
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
|
|
||||||
await page.click('[data-testid="file-thumbnail-checkbox"]');
|
|
||||||
|
|
||||||
// Mock network failure
|
|
||||||
await page.route('**/api/v1/convert/**', route => {
|
|
||||||
route.abort('failed');
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.click('[data-testid="convert-from-dropdown"]');
|
|
||||||
await page.click('[data-testid="format-option-pdf"]');
|
|
||||||
|
|
||||||
await page.click('[data-testid="convert-to-dropdown"]');
|
|
||||||
await page.click('[data-testid="format-option-png"]');
|
|
||||||
|
|
||||||
await page.click('[data-testid="convert-button"]');
|
|
||||||
|
|
||||||
// Should show network error
|
|
||||||
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('UI/UX Flow', () => {
|
test.describe('UI/UX Flow', () => {
|
||||||
|
|
||||||
@ -551,44 +486,9 @@ test.describe('Convert Tool E2E Tests', () => {
|
|||||||
await expect(page.locator('[data-testid="image-options-section"]')).not.toBeVisible();
|
await expect(page.locator('[data-testid="image-options-section"]')).not.toBeVisible();
|
||||||
await expect(page.locator('[data-testid="dpi-input"]')).not.toBeVisible();
|
await expect(page.locator('[data-testid="dpi-input"]')).not.toBeVisible();
|
||||||
|
|
||||||
// Should show CSV options
|
|
||||||
await expect(page.locator('[data-testid="csv-options-section"]')).toBeVisible();
|
|
||||||
await expect(page.locator('[data-testid="page-numbers-input"]')).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should handle CSV page number input correctly', async ({ page }) => {
|
|
||||||
await page.setInputFiles('input[type="file"]', TEST_FILES.pdf);
|
|
||||||
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
|
||||||
|
|
||||||
// Click the Convert tool button
|
|
||||||
await page.click('[data-testid="tool-convert"]');
|
|
||||||
|
|
||||||
// Wait for convert mode and select file
|
|
||||||
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 5000 });
|
|
||||||
await page.click('[data-testid="file-thumbnail-checkbox"]');
|
|
||||||
|
|
||||||
await page.click('[data-testid="convert-from-dropdown"]');
|
|
||||||
await page.click('[data-testid="format-option-pdf"]');
|
|
||||||
|
|
||||||
await page.click('[data-testid="convert-to-dropdown"]');
|
|
||||||
await page.click('[data-testid="format-option-csv"]');
|
|
||||||
|
|
||||||
// Should show CSV options with default value
|
|
||||||
await expect(page.locator('[data-testid="page-numbers-input"]')).toBeVisible();
|
|
||||||
await expect(page.locator('[data-testid="page-numbers-input"]')).toHaveValue('all');
|
|
||||||
|
|
||||||
// Should be able to change page numbers
|
|
||||||
await page.fill('[data-testid="page-numbers-input"]', '1,3,5-7');
|
|
||||||
await expect(page.locator('[data-testid="page-numbers-input"]')).toHaveValue('1,3,5-7');
|
|
||||||
|
|
||||||
// Should show help text
|
|
||||||
await expect(page.locator('[data-testid="page-numbers-input"]')).toHaveAttribute('placeholder', /e\.g\.,.*all/);
|
|
||||||
|
|
||||||
// Mathematical function should work too
|
|
||||||
await page.fill('[data-testid="page-numbers-input"]', '2n+1');
|
|
||||||
await expect(page.locator('[data-testid="page-numbers-input"]')).toHaveValue('2n+1');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('should show progress indicators during conversion', async ({ page }) => {
|
test('should show progress indicators during conversion', async ({ page }) => {
|
||||||
await page.setInputFiles('input[type="file"]', TEST_FILES.pdf);
|
await page.setInputFiles('input[type="file"]', TEST_FILES.pdf);
|
||||||
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
await page.waitForSelector('[data-testid="file-thumbnail"]', { timeout: 10000 });
|
||||||
|
@ -12,6 +12,7 @@ import { FileContextProvider } from '../../contexts/FileContext';
|
|||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import i18n from '../../i18n/config';
|
import i18n from '../../i18n/config';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { detectFileExtension } from '../../utils/fileUtils';
|
||||||
|
|
||||||
// Mock axios
|
// Mock axios
|
||||||
vi.mock('axios');
|
vi.mock('axios');
|
||||||
@ -349,7 +350,7 @@ describe('Convert Tool - Smart Detection Integration Tests', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
testCases.forEach(({ filename, expected }) => {
|
testCases.forEach(({ filename, expected }) => {
|
||||||
const detected = result.current.detectFileExtension(filename);
|
const detected = detectFileExtension(filename);
|
||||||
expect(detected).toBe(expected);
|
expect(detected).toBe(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
125
frontend/src/tests/test-fixtures/sample.htm
Normal file
125
frontend/src/tests/test-fixtures/sample.htm
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Test HTML Document</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 40px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
border-bottom: 2px solid #3498db;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #34495e;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.highlight {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
padding: 10px;
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Test HTML Document for Convert Tool</h1>
|
||||||
|
|
||||||
|
<p>This is a <strong>test HTML file</strong> for testing the HTML to PDF conversion functionality. It contains various HTML elements to ensure proper conversion.</p>
|
||||||
|
|
||||||
|
<h2>Text Formatting</h2>
|
||||||
|
<p>This paragraph contains <strong>bold text</strong>, <em>italic text</em>, and <code>inline code</code>.</p>
|
||||||
|
|
||||||
|
<div class="highlight">
|
||||||
|
<p><strong>Important:</strong> This is a highlighted section that should be preserved in the PDF output.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Lists</h2>
|
||||||
|
<h3>Unordered List</h3>
|
||||||
|
<ul>
|
||||||
|
<li>First item</li>
|
||||||
|
<li>Second item with <a href="https://example.com">a link</a></li>
|
||||||
|
<li>Third item</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Ordered List</h3>
|
||||||
|
<ol>
|
||||||
|
<li>Primary point</li>
|
||||||
|
<li>Secondary point</li>
|
||||||
|
<li>Tertiary point</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h2>Table</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Column 1</th>
|
||||||
|
<th>Column 2</th>
|
||||||
|
<th>Column 3</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Data A</td>
|
||||||
|
<td>Data B</td>
|
||||||
|
<td>Data C</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Test 1</td>
|
||||||
|
<td>Test 2</td>
|
||||||
|
<td>Test 3</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Sample X</td>
|
||||||
|
<td>Sample Y</td>
|
||||||
|
<td>Sample Z</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Code Block</h2>
|
||||||
|
<pre><code>function testFunction() {
|
||||||
|
console.log("This is a test function");
|
||||||
|
return "Hello from HTML to PDF conversion";
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<h2>Final Notes</h2>
|
||||||
|
<p>This HTML document should convert to a well-formatted PDF that preserves:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Text formatting (bold, italic)</li>
|
||||||
|
<li>Headings and hierarchy</li>
|
||||||
|
<li>Tables with proper borders</li>
|
||||||
|
<li>Lists (ordered and unordered)</li>
|
||||||
|
<li>Code formatting</li>
|
||||||
|
<li>Basic CSS styling</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p><small>Generated for Stirling PDF Convert Tool testing purposes.</small></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -49,4 +49,11 @@ export const isConversionSupported = (fromExtension: string, toExtension: string
|
|||||||
*/
|
*/
|
||||||
export const isImageFormat = (extension: string): boolean => {
|
export const isImageFormat = (extension: string): boolean => {
|
||||||
return ['png', 'jpg', 'jpeg', 'gif', 'tiff', 'bmp', 'webp', 'svg'].includes(extension.toLowerCase());
|
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());
|
||||||
};
|
};
|
@ -125,4 +125,51 @@ export function cleanupFileUrls(files: FileWithUrl[]): void {
|
|||||||
export function shouldUseDirectIndexedDBAccess(file: FileWithUrl): boolean {
|
export function shouldUseDirectIndexedDBAccess(file: FileWithUrl): boolean {
|
||||||
const FILE_SIZE_LIMIT = 100 * 1024 * 1024; // 100MB
|
const FILE_SIZE_LIMIT = 100 * 1024 * 1024; // 100MB
|
||||||
return file.size > FILE_SIZE_LIMIT;
|
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}`;
|
||||||
}
|
}
|
@ -24,6 +24,11 @@ export async function generateThumbnailForFile(file: File): Promise<string | und
|
|||||||
console.log('Skipping thumbnail generation for large file:', file.name);
|
console.log('Skipping thumbnail generation for large file:', file.name);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!file.type.startsWith('application/pdf')) {
|
||||||
|
console.warn('File is not a PDF, skipping thumbnail generation:', file.name);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Generating thumbnail for', file.name);
|
console.log('Generating thumbnail for', file.name);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user