mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-12-29 16:16:02 +00:00
chore: pdf compression init
This commit is contained in:
235
src/pages/tools/pdf/compress-pdf/index.tsx
Normal file
235
src/pages/tools/pdf/compress-pdf/index.tsx
Normal 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}`
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
22
src/pages/tools/pdf/compress-pdf/meta.ts
Normal file
22
src/pages/tools/pdf/compress-pdf/meta.ts
Normal 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'))
|
||||
});
|
||||
107
src/pages/tools/pdf/compress-pdf/service.test.ts
Normal file
107
src/pages/tools/pdf/compress-pdf/service.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
59
src/pages/tools/pdf/compress-pdf/service.ts
Normal file
59
src/pages/tools/pdf/compress-pdf/service.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
5
src/pages/tools/pdf/compress-pdf/types.ts
Normal file
5
src/pages/tools/pdf/compress-pdf/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type CompressionLevel = 'low' | 'medium' | 'high';
|
||||
|
||||
export type InitialValuesType = {
|
||||
compressionLevel: CompressionLevel;
|
||||
};
|
||||
@@ -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
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user