chore: pdf compression init

This commit is contained in:
Ibrahima G. Coulibaly
2025-04-01 10:59:23 +00:00
parent deb869a619
commit 34955c7ace
9 changed files with 519 additions and 85 deletions

View File

@@ -0,0 +1,235 @@
import { Box, Typography } from '@mui/material';
import React, { useContext, useEffect, useState } from 'react';
import ToolContent from '@components/ToolContent';
import { ToolComponentProps } from '@tools/defineTool';
import ToolPdfInput from '@components/input/ToolPdfInput';
import ToolFileResult from '@components/result/ToolFileResult';
import { CardExampleType } from '@components/examples/ToolExamples';
import { PDFDocument } from 'pdf-lib';
import { CompressionLevel, InitialValuesType } from './types';
import { compressPdf } from './service';
import SimpleRadio from '@components/options/SimpleRadio';
import { CustomSnackBarContext } from '../../../../contexts/CustomSnackBarContext';
const initialValues: InitialValuesType = {
compressionLevel: 'medium'
};
const exampleCards: CardExampleType<InitialValuesType>[] = [
{
title: 'Low Compression',
description: 'Slightly reduce file size with minimal quality loss',
sampleText: '',
sampleResult: '',
sampleOptions: {
compressionLevel: 'low'
}
},
{
title: 'Medium Compression',
description: 'Balance between file size and quality',
sampleText: '',
sampleResult: '',
sampleOptions: {
compressionLevel: 'medium'
}
},
{
title: 'High Compression',
description: 'Maximum file size reduction with some quality loss',
sampleText: '',
sampleResult: '',
sampleOptions: {
compressionLevel: 'high'
}
}
];
export default function CompressPdf({
title,
longDescription
}: ToolComponentProps) {
const [input, setInput] = useState<File | null>(null);
const [result, setResult] = useState<File | null>(null);
const [resultSize, setResultSize] = useState<string>('');
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [fileInfo, setFileInfo] = useState<{
size: string;
pages: number;
} | null>(null);
const { showSnackBar } = useContext(CustomSnackBarContext);
// Get the PDF info when a file is uploaded
useEffect(() => {
const getPdfInfo = async () => {
if (!input) {
setFileInfo(null);
return;
}
try {
const arrayBuffer = await input.arrayBuffer();
const pdf = await PDFDocument.load(arrayBuffer);
const pages = pdf.getPageCount();
const size = formatFileSize(input.size);
setFileInfo({ size, pages });
} catch (error) {
console.error('Error getting PDF info:', error);
setFileInfo(null);
showSnackBar(
'Error reading PDF file. Please make sure it is a valid PDF.',
'error'
);
}
};
getPdfInfo();
}, [input]);
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
const compute = async (values: InitialValuesType, input: File | null) => {
if (!input) return;
try {
setIsProcessing(true);
const compressedPdf = await compressPdf(input, values);
setResult(compressedPdf);
// Log compression results
const compressionRatio = (compressedPdf.size / input.size) * 100;
console.log(`Compression Ratio: ${compressionRatio.toFixed(2)}%`);
setResultSize(formatFileSize(compressedPdf.size));
} catch (error) {
console.error('Error compressing PDF:', error);
showSnackBar(
`Failed to compress PDF: ${
error instanceof Error ? error.message : String(error)
}`,
'error'
);
setResult(null);
} finally {
setIsProcessing(false);
}
};
const compressionOptions: {
value: CompressionLevel;
label: string;
description: string;
}[] = [
{
value: 'low',
label: 'Low Compression',
description: 'Slightly reduce file size with minimal quality loss'
},
{
value: 'medium',
label: 'Medium Compression',
description: 'Balance between file size and quality'
},
{
value: 'high',
label: 'High Compression',
description: 'Maximum file size reduction with some quality loss'
}
];
return (
<ToolContent
title={title}
input={input}
setInput={setInput}
initialValues={initialValues}
compute={compute}
exampleCards={exampleCards}
inputComponent={
<ToolPdfInput
value={input}
onChange={setInput}
accept={['application/pdf']}
title={'Input PDF'}
/>
}
resultComponent={
<ToolFileResult
title={'Compressed PDF'}
value={result}
extension={'pdf'}
loading={isProcessing}
loadingText={'Compressing PDF'}
/>
}
getGroups={({ values, updateField }) => [
{
title: 'Compression Settings',
component: (
<Box>
<Typography variant="subtitle2" sx={{ mb: 1 }}>
Compression Level
</Typography>
{compressionOptions.map((option) => (
<SimpleRadio
key={option.value}
title={option.label}
description={option.description}
checked={values.compressionLevel === option.value}
onClick={() => {
updateField('compressionLevel', option.value);
}}
/>
))}
{fileInfo && (
<Box
sx={{
mt: 2,
p: 2,
bgcolor: 'background.paper',
borderRadius: 1
}}
>
<Typography variant="body2">
File size: <strong>{fileInfo.size}</strong>
</Typography>
<Typography variant="body2">
Pages: <strong>{fileInfo.pages}</strong>
</Typography>
{resultSize && (
<Typography variant="body2">
Compressed file size: <strong>{resultSize}</strong>
</Typography>
)}
</Box>
)}
</Box>
)
}
]}
toolInfo={{
title: 'How to Use the Compress PDF Tool',
description: `This tool allows you to compress PDF files to reduce their size while maintaining reasonable quality.
Choose a compression level:
- Low Compression: Slightly reduces file size with minimal quality loss
- Medium Compression: Balances between file size and quality
- High Compression: Maximum file size reduction with some quality loss
Note: The compression results may vary depending on the content of your PDF. Documents with many images will typically see greater size reduction than text-only documents.
${longDescription}`
}}
/>
);
}

View File

@@ -0,0 +1,22 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('pdf', {
name: 'Compress PDF',
path: 'compress-pdf',
icon: 'material-symbols:compress',
description: 'Reduce PDF file size while maintaining quality',
shortDescription: 'Compress PDF files to reduce size',
keywords: [
'pdf',
'compress',
'reduce',
'size',
'optimize',
'shrink',
'file size'
],
longDescription:
'Compress PDF files to reduce their size while maintaining reasonable quality. Useful for sharing documents via email, uploading to websites, or saving storage space.',
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,107 @@
import { describe, it, expect, vi } from 'vitest';
import { compressPdf } from './service';
import { CompressionLevel } from './types';
// Mock the mupdf module
vi.mock('mupdf', () => {
return {
Document: {
openDocument: vi.fn(() => ({
countPages: vi.fn(() => 2),
loadPage: vi.fn(() => ({}))
}))
},
PDFWriter: vi.fn(() => ({
addPage: vi.fn(),
asBuffer: vi.fn(() => Buffer.from('test'))
}))
};
});
// Mock the pdf-lib module
vi.mock('pdf-lib', () => {
return {
PDFDocument: {
load: vi.fn(() => ({
getPageCount: vi.fn(() => 2)
}))
}
};
});
describe('compressPdf', () => {
it('should compress a PDF file with low compression', async () => {
// Create a mock File
const mockFile = new File(['test'], 'test.pdf', {
type: 'application/pdf'
});
// Mock arrayBuffer method
mockFile.arrayBuffer = vi.fn().mockResolvedValue(new ArrayBuffer(4));
// Call the function with low compression
const result = await compressPdf(mockFile, {
compressionLevel: 'low' as CompressionLevel
});
// Check the result
expect(result).toBeInstanceOf(File);
expect(result.name).toBe('test-compressed.pdf');
expect(result.type).toBe('application/pdf');
});
it('should compress a PDF file with medium compression', async () => {
// Create a mock File
const mockFile = new File(['test'], 'test.pdf', {
type: 'application/pdf'
});
// Mock arrayBuffer method
mockFile.arrayBuffer = vi.fn().mockResolvedValue(new ArrayBuffer(4));
// Call the function with medium compression
const result = await compressPdf(mockFile, {
compressionLevel: 'medium' as CompressionLevel
});
// Check the result
expect(result).toBeInstanceOf(File);
expect(result.name).toBe('test-compressed.pdf');
expect(result.type).toBe('application/pdf');
});
it('should compress a PDF file with high compression', async () => {
// Create a mock File
const mockFile = new File(['test'], 'test.pdf', {
type: 'application/pdf'
});
// Mock arrayBuffer method
mockFile.arrayBuffer = vi.fn().mockResolvedValue(new ArrayBuffer(4));
// Call the function with high compression
const result = await compressPdf(mockFile, {
compressionLevel: 'high' as CompressionLevel
});
// Check the result
expect(result).toBeInstanceOf(File);
expect(result.name).toBe('test-compressed.pdf');
expect(result.type).toBe('application/pdf');
});
it('should handle errors during compression', async () => {
// Create a mock File
const mockFile = new File(['test'], 'test.pdf', {
type: 'application/pdf'
});
// Mock arrayBuffer method to throw an error
mockFile.arrayBuffer = vi.fn().mockRejectedValue(new Error('Test error'));
// Check that the function throws an error
await expect(
compressPdf(mockFile, { compressionLevel: 'medium' as CompressionLevel })
).rejects.toThrow('Failed to compress PDF: Test error');
});
});

View File

@@ -0,0 +1,59 @@
import { CompressionLevel, InitialValuesType } from './types';
import { PDFDocument } from 'pdf-lib';
export async function compressPdf(
pdfFile: File,
options: InitialValuesType
): Promise<File> {
// Check if file is a PDF
if (pdfFile.type !== 'application/pdf') {
throw new Error('The provided file is not a PDF');
}
// Read the file as an ArrayBuffer
const arrayBuffer = await pdfFile.arrayBuffer();
// Load PDF document using pdf-lib
const pdfDoc = await PDFDocument.load(arrayBuffer);
// Apply compression based on the selected level
const compressionOptions = getCompressionOptions(options.compressionLevel);
// pdf-lib has different compression approach than mupdf
// Compression is applied during the save operation
const compressedPdfBytes = await pdfDoc.save({
useObjectStreams: true, // More efficient storage
...compressionOptions
});
// Create a new File object with the compressed PDF
return new File([compressedPdfBytes], `compressed_${pdfFile.name}`, {
type: 'application/pdf'
});
}
/**
* Helper function to get compression options based on level
* @param level - Compression level (low, medium, or high)
* @returns Object with appropriate compression settings for pdf-lib
*/
function getCompressionOptions(level: CompressionLevel) {
switch (level) {
case 'low':
return {
addDefaultPage: false,
compress: true
};
case 'medium':
return {
addDefaultPage: false,
compress: true
};
case 'high':
return {
addDefaultPage: false,
compress: true,
objectsPerTick: 100 // Process more objects at once for higher compression
};
}
}

View File

@@ -0,0 +1,5 @@
export type CompressionLevel = 'low' | 'medium' | 'high';
export type InitialValuesType = {
compressionLevel: CompressionLevel;
};

View File

@@ -1,5 +1,10 @@
import { tool as pdfRotatePdf } from './rotate-pdf/meta';
import { meta as splitPdfMeta } from './split-pdf/meta';
import { tool as compressPdfTool } from './compress-pdf/meta';
import { DefinedTool } from '@tools/defineTool';
export const pdfTools: DefinedTool[] = [splitPdfMeta, pdfRotatePdf];
export const pdfTools: DefinedTool[] = [
splitPdfMeta,
pdfRotatePdf,
compressPdfTool
];