mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-05 12:05:22 +00:00
Multi file batching for compress
This commit is contained in:
parent
f821a364cb
commit
95e5f23d25
@ -20,7 +20,7 @@ export interface CompressOperationHook {
|
|||||||
parameters: CompressParameters,
|
parameters: CompressParameters,
|
||||||
selectedFiles: File[]
|
selectedFiles: File[]
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
// Flattened result properties for cleaner access
|
// Flattened result properties for cleaner access
|
||||||
files: File[];
|
files: File[];
|
||||||
thumbnails: string[];
|
thumbnails: string[];
|
||||||
@ -30,7 +30,7 @@ export interface CompressOperationHook {
|
|||||||
status: string;
|
status: string;
|
||||||
errorMessage: string | null;
|
errorMessage: string | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
|
||||||
// Result management functions
|
// Result management functions
|
||||||
resetResults: () => void;
|
resetResults: () => void;
|
||||||
clearError: () => void;
|
clearError: () => void;
|
||||||
@ -38,13 +38,13 @@ export interface CompressOperationHook {
|
|||||||
|
|
||||||
export const useCompressOperation = (): CompressOperationHook => {
|
export const useCompressOperation = (): CompressOperationHook => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
recordOperation,
|
recordOperation,
|
||||||
markOperationApplied,
|
markOperationApplied,
|
||||||
markOperationFailed,
|
markOperationFailed,
|
||||||
addFiles
|
addFiles
|
||||||
} = useFileContext();
|
} = useFileContext();
|
||||||
|
|
||||||
// Internal state management
|
// Internal state management
|
||||||
const [files, setFiles] = useState<File[]>([]);
|
const [files, setFiles] = useState<File[]>([]);
|
||||||
const [thumbnails, setThumbnails] = useState<string[]>([]);
|
const [thumbnails, setThumbnails] = useState<string[]>([]);
|
||||||
@ -55,15 +55,27 @@ export const useCompressOperation = (): CompressOperationHook => {
|
|||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// Track blob URLs for cleanup
|
||||||
|
const [blobUrls, setBlobUrls] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const cleanupBlobUrls = useCallback(() => {
|
||||||
|
blobUrls.forEach(url => {
|
||||||
|
try {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to revoke blob URL:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setBlobUrls([]);
|
||||||
|
}, [blobUrls]);
|
||||||
|
|
||||||
const buildFormData = useCallback((
|
const buildFormData = useCallback((
|
||||||
parameters: CompressParameters,
|
parameters: CompressParameters,
|
||||||
selectedFiles: File[]
|
file: File
|
||||||
) => {
|
) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|
||||||
selectedFiles.forEach(file => {
|
formData.append("fileInput", file);
|
||||||
formData.append("fileInput", file);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (parameters.compressionMethod === 'quality') {
|
if (parameters.compressionMethod === 'quality') {
|
||||||
formData.append("optimizeLevel", parameters.compressionLevel.toString());
|
formData.append("optimizeLevel", parameters.compressionLevel.toString());
|
||||||
@ -74,7 +86,7 @@ export const useCompressOperation = (): CompressOperationHook => {
|
|||||||
formData.append("expectedOutputSize", fileSize);
|
formData.append("expectedOutputSize", fileSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
formData.append("grayscale", parameters.grayscale.toString());
|
formData.append("grayscale", parameters.grayscale.toString());
|
||||||
|
|
||||||
const endpoint = "/api/v1/misc/compress-pdf";
|
const endpoint = "/api/v1/misc/compress-pdf";
|
||||||
@ -87,7 +99,7 @@ export const useCompressOperation = (): CompressOperationHook => {
|
|||||||
selectedFiles: File[]
|
selectedFiles: File[]
|
||||||
): { operation: FileOperation; operationId: string; fileId: string } => {
|
): { operation: FileOperation; operationId: string; fileId: string } => {
|
||||||
const operationId = `compress-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
const operationId = `compress-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||||
const fileId = selectedFiles[0].name;
|
const fileId = selectedFiles.map(f => f.name).join(',');
|
||||||
|
|
||||||
const operation: FileOperation = {
|
const operation: FileOperation = {
|
||||||
id: operationId,
|
id: operationId,
|
||||||
@ -96,74 +108,20 @@ export const useCompressOperation = (): CompressOperationHook => {
|
|||||||
fileIds: selectedFiles.map(f => f.name),
|
fileIds: selectedFiles.map(f => f.name),
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
metadata: {
|
metadata: {
|
||||||
originalFileName: selectedFiles[0].name,
|
originalFileNames: selectedFiles.map(f => f.name),
|
||||||
parameters: {
|
parameters: {
|
||||||
compressionLevel: parameters.compressionLevel,
|
compressionLevel: parameters.compressionLevel,
|
||||||
grayscale: parameters.grayscale,
|
grayscale: parameters.grayscale,
|
||||||
expectedSize: parameters.expectedSize,
|
expectedSize: parameters.expectedSize,
|
||||||
},
|
},
|
||||||
fileSize: selectedFiles[0].size
|
totalFileSize: selectedFiles.reduce((sum, f) => sum + f.size, 0),
|
||||||
|
fileCount: selectedFiles.length
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return { operation, operationId, fileId };
|
return { operation, operationId, fileId };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const processResults = useCallback(async (blob: Blob, selectedFiles: File[]) => {
|
|
||||||
try {
|
|
||||||
// Check if the response is a PDF file directly or a ZIP file
|
|
||||||
const contentType = blob.type;
|
|
||||||
console.log('Response content type:', contentType);
|
|
||||||
|
|
||||||
if (contentType === 'application/pdf') {
|
|
||||||
// Direct PDF response
|
|
||||||
const originalFileName = selectedFiles[0].name;
|
|
||||||
const pdfFile = new File([blob], `compressed_${originalFileName}`, { type: "application/pdf" });
|
|
||||||
setFiles([pdfFile]);
|
|
||||||
setThumbnails([]);
|
|
||||||
setIsGeneratingThumbnails(true);
|
|
||||||
|
|
||||||
// Add file to FileContext
|
|
||||||
await addFiles([pdfFile]);
|
|
||||||
|
|
||||||
// Generate thumbnail
|
|
||||||
const thumbnail = await generateThumbnailForFile(pdfFile);
|
|
||||||
setThumbnails([thumbnail || '']);
|
|
||||||
setIsGeneratingThumbnails(false);
|
|
||||||
} else {
|
|
||||||
// ZIP file response (like split operation)
|
|
||||||
const zipFile = new File([blob], "compress_result.zip", { type: "application/zip" });
|
|
||||||
const extractionResult = await zipFileService.extractPdfFiles(zipFile);
|
|
||||||
|
|
||||||
if (extractionResult.success && extractionResult.extractedFiles.length > 0) {
|
|
||||||
// Set local state for preview
|
|
||||||
setFiles(extractionResult.extractedFiles);
|
|
||||||
setThumbnails([]);
|
|
||||||
setIsGeneratingThumbnails(true);
|
|
||||||
|
|
||||||
// Add extracted files to FileContext for future use
|
|
||||||
await addFiles(extractionResult.extractedFiles);
|
|
||||||
|
|
||||||
const thumbnails = await Promise.all(
|
|
||||||
extractionResult.extractedFiles.map(async (file) => {
|
|
||||||
try {
|
|
||||||
const thumbnail = await generateThumbnailForFile(file);
|
|
||||||
return thumbnail || '';
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Failed to generate thumbnail for ${file.name}:`, error);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setThumbnails(thumbnails);
|
|
||||||
setIsGeneratingThumbnails(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (extractError) {
|
|
||||||
console.warn('Failed to process results:', extractError);
|
|
||||||
}
|
|
||||||
}, [addFiles]);
|
|
||||||
|
|
||||||
const executeOperation = useCallback(async (
|
const executeOperation = useCallback(async (
|
||||||
parameters: CompressParameters,
|
parameters: CompressParameters,
|
||||||
@ -173,32 +131,93 @@ export const useCompressOperation = (): CompressOperationHook => {
|
|||||||
setStatus(t("noFileSelected"));
|
setStatus(t("noFileSelected"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const validFiles = selectedFiles.filter(file => file.size > 0);
|
||||||
|
if (validFiles.length === 0) {
|
||||||
|
setErrorMessage('No valid files to compress. All selected files are empty.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validFiles.length < selectedFiles.length) {
|
||||||
|
console.warn(`Skipping ${selectedFiles.length - validFiles.length} empty files`);
|
||||||
|
}
|
||||||
|
|
||||||
const { operation, operationId, fileId } = createOperation(parameters, selectedFiles);
|
const { operation, operationId, fileId } = createOperation(parameters, selectedFiles);
|
||||||
const { formData, endpoint } = buildFormData(parameters, selectedFiles);
|
|
||||||
|
|
||||||
recordOperation(fileId, operation);
|
recordOperation(fileId, operation);
|
||||||
|
|
||||||
setStatus(t("loading"));
|
setStatus(t("loading"));
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
|
setFiles([]);
|
||||||
|
setThumbnails([]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
const compressedFiles: File[] = [];
|
||||||
|
|
||||||
// Determine the correct content type from the response
|
|
||||||
const contentType = response.headers['content-type'] || 'application/zip';
|
|
||||||
const blob = new Blob([response.data], { type: contentType });
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
// Generate dynamic filename based on original file and content type
|
|
||||||
const originalFileName = selectedFiles[0].name;
|
|
||||||
const filename = `compressed_${originalFileName}`;
|
|
||||||
setDownloadFilename(filename);
|
|
||||||
setDownloadUrl(url);
|
|
||||||
setStatus(t("downloadComplete"));
|
|
||||||
|
|
||||||
await processResults(blob, selectedFiles);
|
const failedFiles: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < validFiles.length; i++) {
|
||||||
|
const file = validFiles[i];
|
||||||
|
setStatus(`Compressing ${file.name} (${i + 1}/${validFiles.length})`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { formData, endpoint } = buildFormData(parameters, file);
|
||||||
|
const response = await axios.post(endpoint, formData, { responseType: "blob" });
|
||||||
|
|
||||||
|
const contentType = response.headers['content-type'] || 'application/pdf';
|
||||||
|
const blob = new Blob([response.data], { type: contentType });
|
||||||
|
const compressedFile = new File([blob], `compressed_${file.name}`, { type: contentType });
|
||||||
|
|
||||||
|
compressedFiles.push(compressedFile);
|
||||||
|
} catch (fileError) {
|
||||||
|
console.error(`Failed to compress ${file.name}:`, fileError);
|
||||||
|
failedFiles.push(file.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedFiles.length > 0 && compressedFiles.length === 0) {
|
||||||
|
throw new Error(`Failed to compress all files: ${failedFiles.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failedFiles.length > 0) {
|
||||||
|
setStatus(`Compressed ${compressedFiles.length}/${validFiles.length} files. Failed: ${failedFiles.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFiles(compressedFiles);
|
||||||
|
setIsGeneratingThumbnails(true);
|
||||||
|
|
||||||
|
await addFiles(compressedFiles);
|
||||||
|
|
||||||
|
cleanupBlobUrls();
|
||||||
|
|
||||||
|
if (compressedFiles.length === 1) {
|
||||||
|
const url = window.URL.createObjectURL(compressedFiles[0]);
|
||||||
|
setDownloadUrl(url);
|
||||||
|
setBlobUrls([url]);
|
||||||
|
setDownloadFilename(`compressed_${selectedFiles[0].name}`);
|
||||||
|
} else {
|
||||||
|
const { zipFile } = await zipFileService.createZipFromFiles(compressedFiles, 'compressed_files.zip');
|
||||||
|
const url = window.URL.createObjectURL(zipFile);
|
||||||
|
setDownloadUrl(url);
|
||||||
|
setBlobUrls([url]);
|
||||||
|
setDownloadFilename(`compressed_${validFiles.length}_files.zip`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbnails = await Promise.all(
|
||||||
|
compressedFiles.map(async (file) => {
|
||||||
|
try {
|
||||||
|
const thumbnail = await generateThumbnailForFile(file);
|
||||||
|
return thumbnail || '';
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to generate thumbnail for ${file.name}:`, error);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setThumbnails(thumbnails);
|
||||||
|
setIsGeneratingThumbnails(false);
|
||||||
|
setStatus(t("downloadComplete"));
|
||||||
markOperationApplied(fileId, operationId);
|
markOperationApplied(fileId, operationId);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -214,9 +233,10 @@ export const useCompressOperation = (): CompressOperationHook => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [t, createOperation, buildFormData, recordOperation, markOperationApplied, markOperationFailed, processResults]);
|
}, [t, createOperation, buildFormData, recordOperation, markOperationApplied, markOperationFailed, addFiles]);
|
||||||
|
|
||||||
const resetResults = useCallback(() => {
|
const resetResults = useCallback(() => {
|
||||||
|
cleanupBlobUrls();
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
setThumbnails([]);
|
setThumbnails([]);
|
||||||
setIsGeneratingThumbnails(false);
|
setIsGeneratingThumbnails(false);
|
||||||
@ -224,7 +244,7 @@ export const useCompressOperation = (): CompressOperationHook => {
|
|||||||
setStatus('');
|
setStatus('');
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}, []);
|
}, [cleanupBlobUrls]);
|
||||||
|
|
||||||
const clearError = useCallback(() => {
|
const clearError = useCallback(() => {
|
||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
@ -232,8 +252,6 @@ export const useCompressOperation = (): CompressOperationHook => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
executeOperation,
|
executeOperation,
|
||||||
|
|
||||||
// Flattened result properties for cleaner access
|
|
||||||
files,
|
files,
|
||||||
thumbnails,
|
thumbnails,
|
||||||
isGeneratingThumbnails,
|
isGeneratingThumbnails,
|
||||||
@ -242,9 +260,9 @@ export const useCompressOperation = (): CompressOperationHook => {
|
|||||||
status,
|
status,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|
||||||
// Result management functions
|
// Result management functions
|
||||||
resetResults,
|
resetResults,
|
||||||
clearError,
|
clearError,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -103,6 +103,37 @@ export class ZipFileService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a ZIP file from an array of files
|
||||||
|
*/
|
||||||
|
async createZipFromFiles(files: File[], zipFilename: string): Promise<{ zipFile: File; size: number }> {
|
||||||
|
try {
|
||||||
|
const zip = new JSZip();
|
||||||
|
|
||||||
|
// Add each file to the ZIP
|
||||||
|
for (const file of files) {
|
||||||
|
const content = await file.arrayBuffer();
|
||||||
|
zip.file(file.name, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate ZIP blob
|
||||||
|
const zipBlob = await zip.generateAsync({
|
||||||
|
type: 'blob',
|
||||||
|
compression: 'DEFLATE',
|
||||||
|
compressionOptions: { level: 6 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const zipFile = new File([zipBlob], zipFilename, {
|
||||||
|
type: 'application/zip',
|
||||||
|
lastModified: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
return { zipFile, size: zipFile.size };
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to create ZIP file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract PDF files from a ZIP archive
|
* Extract PDF files from a ZIP archive
|
||||||
*/
|
*/
|
||||||
|
@ -63,7 +63,7 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hasFiles = selectedFiles.length > 0;
|
const hasFiles = selectedFiles.length > 0;
|
||||||
const hasResults = compressOperation.downloadUrl !== null;
|
const hasResults = compressOperation.files.length > 0 || compressOperation.downloadUrl !== null;
|
||||||
const filesCollapsed = hasFiles;
|
const filesCollapsed = hasFiles;
|
||||||
const settingsCollapsed = hasResults;
|
const settingsCollapsed = hasResults;
|
||||||
|
|
||||||
@ -84,7 +84,11 @@ const Compress = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => {
|
|||||||
isVisible={true}
|
isVisible={true}
|
||||||
isCollapsed={filesCollapsed}
|
isCollapsed={filesCollapsed}
|
||||||
isCompleted={filesCollapsed}
|
isCompleted={filesCollapsed}
|
||||||
completedMessage={hasFiles ? `Selected: ${selectedFiles[0]?.name}` : undefined}
|
completedMessage={hasFiles ?
|
||||||
|
selectedFiles.length === 1
|
||||||
|
? `Selected: ${selectedFiles[0].name}`
|
||||||
|
: `Selected: ${selectedFiles.length} files`
|
||||||
|
: undefined}
|
||||||
>
|
>
|
||||||
<FileStatusIndicator
|
<FileStatusIndicator
|
||||||
selectedFiles={selectedFiles}
|
selectedFiles={selectedFiles}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user