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

161
.idea/workspace.xml generated
View File

@@ -4,33 +4,17 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="chore: compress video icon">
<change afterPath="$PROJECT_DIR$/@types/theme.d.ts" afterDir="false" />
<change afterPath="$PROJECT_DIR$/public/assets/background-dark.png" afterDir="false" />
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="fix: stars button width for 1k+ 😊">
<change afterPath="$PROJECT_DIR$/src/pages/tools/pdf/compress-pdf/index.tsx" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/pages/tools/pdf/compress-pdf/meta.ts" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/pages/tools/pdf/compress-pdf/service.test.ts" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/pages/tools/pdf/compress-pdf/service.ts" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/pages/tools/pdf/compress-pdf/types.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/App.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/App.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/Hero.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/Hero.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/Navbar/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/Navbar/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/ToolHeader.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/ToolHeader.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/ToolLayout.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/ToolLayout.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/allTools/ToolCard.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/allTools/ToolCard.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/examples/ExampleCard.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/examples/ExampleCard.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/input/BaseFileInput.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/input/BaseFileInput.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/input/ToolFileInput.tsx" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/input/ToolTextInput.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/input/ToolTextInput.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/options/ColorSelector.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/options/ColorSelector.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/options/TextFieldWithDesc.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/options/TextFieldWithDesc.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/options/ToolOptions.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/options/ToolOptions.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/result/ToolFileResult.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/result/ToolFileResult.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/result/ToolTextResult.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/result/ToolTextResult.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/config/muiConfig.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/config/muiConfig.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/home/Categories.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/home/Categories.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/home/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/home/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools-by-category/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools-by-category/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/pdf/split-pdf/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/pdf/split-pdf/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/compress/service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/compress/service.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/gif/change-speed/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/gif/change-speed/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tsconfig.json" beforeDir="false" afterPath="$PROJECT_DIR$/tsconfig.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/package-lock.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/pdf/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/pdf/index.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/vite.config.ts" beforeDir="false" afterPath="$PROJECT_DIR$/vite.config.ts" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -47,7 +31,7 @@
<option name="PUSH_AUTO_UPDATE" value="true" />
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="main" />
<entry key="$PROJECT_DIR$" value="chesterkxng" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@@ -144,6 +128,13 @@
"number": 76
},
"lastSeen": 1743352150953
},
{
"id": {
"id": "PR_kwDOMJIfts6Q0JBe",
"number": 82
},
"lastSeen": 1743470267269
}
]
}]]></component>
@@ -199,7 +190,7 @@
"Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
"Vitest.replaceText function.executor": "Run",
"Vitest.timeBetweenDates.executor": "Run",
"git-widget-placeholder": "dark-mode",
"git-widget-placeholder": "main",
"ignore.virus.scanning.warn.message": "true",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/@types",
@@ -421,54 +412,8 @@
<workItem from="1743047367993" duration="986000" />
<workItem from="1743103182313" duration="4264000" />
<workItem from="1743348610793" duration="21855000" />
</task>
<task id="LOCAL-00127" summary="chore: show tooloptions in example">
<option name="closed" value="true" />
<created>1740619610168</created>
<option name="number" value="00127" />
<option name="presentableId" value="LOCAL-00127" />
<option name="project" value="LOCAL" />
<updated>1740619610169</updated>
</task>
<task id="LOCAL-00128" summary="refact: examples">
<option name="closed" value="true" />
<created>1740620866551</created>
<option name="number" value="00128" />
<option name="presentableId" value="LOCAL-00128" />
<option name="project" value="LOCAL" />
<updated>1740620866551</updated>
</task>
<task id="LOCAL-00129" summary="feat: json pretty">
<option name="closed" value="true" />
<created>1740661424202</created>
<option name="number" value="00129" />
<option name="presentableId" value="LOCAL-00129" />
<option name="project" value="LOCAL" />
<updated>1740661424202</updated>
</task>
<task id="LOCAL-00130" summary="feat: json pretty">
<option name="closed" value="true" />
<created>1740661540908</created>
<option name="number" value="00130" />
<option name="presentableId" value="LOCAL-00130" />
<option name="project" value="LOCAL" />
<updated>1740661540908</updated>
</task>
<task id="LOCAL-00131" summary="style: tool categories">
<option name="closed" value="true" />
<created>1740661744828</created>
<option name="number" value="00131" />
<option name="presentableId" value="LOCAL-00131" />
<option name="project" value="LOCAL" />
<updated>1740661744828</updated>
</task>
<task id="LOCAL-00132" summary="chore: compute only if value">
<option name="closed" value="true" />
<created>1740661864615</created>
<option name="number" value="00132" />
<option name="presentableId" value="LOCAL-00132" />
<option name="project" value="LOCAL" />
<updated>1740661864615</updated>
<workItem from="1743397561176" duration="25000" />
<workItem from="1743458576265" duration="13083000" />
</task>
<task id="LOCAL-00133" summary="chore: remove prettify test">
<option name="closed" value="true" />
@@ -814,7 +759,55 @@
<option name="project" value="LOCAL" />
<updated>1743355166426</updated>
</task>
<option name="localTasksCounter" value="176" />
<task id="LOCAL-00176" summary="fix: gif speed">
<option name="closed" value="true" />
<created>1743385388051</created>
<option name="number" value="00176" />
<option name="presentableId" value="LOCAL-00176" />
<option name="project" value="LOCAL" />
<updated>1743385388051</updated>
</task>
<task id="LOCAL-00177" summary="fix: tsc">
<option name="closed" value="true" />
<created>1743385467178</created>
<option name="number" value="00177" />
<option name="presentableId" value="LOCAL-00177" />
<option name="project" value="LOCAL" />
<updated>1743385467178</updated>
</task>
<task id="LOCAL-00178" summary="fix: background color">
<option name="closed" value="true" />
<created>1743385898871</created>
<option name="number" value="00178" />
<option name="presentableId" value="LOCAL-00178" />
<option name="project" value="LOCAL" />
<updated>1743385898871</updated>
</task>
<task id="LOCAL-00179" summary="docs: github trendings">
<option name="closed" value="true" />
<created>1743459110471</created>
<option name="number" value="00179" />
<option name="presentableId" value="LOCAL-00179" />
<option name="project" value="LOCAL" />
<updated>1743459110471</updated>
</task>
<task id="LOCAL-00180" summary="docs: optimize">
<option name="closed" value="true" />
<created>1743459205311</created>
<option name="number" value="00180" />
<option name="presentableId" value="LOCAL-00180" />
<option name="project" value="LOCAL" />
<updated>1743459205311</updated>
</task>
<task id="LOCAL-00181" summary="fix: stars button width for 1k+ 😊">
<option name="closed" value="true" />
<created>1743470832619</created>
<option name="number" value="00181" />
<option name="presentableId" value="LOCAL-00181" />
<option name="project" value="LOCAL" />
<updated>1743470832619</updated>
</task>
<option name="localTasksCounter" value="182" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -861,12 +854,6 @@
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
<option name="CHECK_NEW_TODO" value="false" />
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
<MESSAGE value="style: tools height" />
<MESSAGE value="chore: update meta" />
<MESSAGE value="feat: change pgn opacity" />
<MESSAGE value="feat: crop png" />
<MESSAGE value="chore: remove unnecessary files" />
<MESSAGE value="refactor: validateJson" />
<MESSAGE value="feat: missing tools" />
<MESSAGE value="refactor: use ToolContent" />
<MESSAGE value="fix: prettify json" />
@@ -886,7 +873,13 @@
<MESSAGE value="fix: typos" />
<MESSAGE value="feat: compress video" />
<MESSAGE value="chore: compress video icon" />
<option name="LAST_COMMIT_MESSAGE" value="chore: compress video icon" />
<MESSAGE value="fix: gif speed" />
<MESSAGE value="fix: tsc" />
<MESSAGE value="fix: background color" />
<MESSAGE value="docs: github trendings" />
<MESSAGE value="docs: optimize" />
<MESSAGE value="fix: stars button width for 1k+ " />
<option name="LAST_COMMIT_MESSAGE" value="fix: stars button width for 1k+ " />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />

7
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@ffmpeg/util": "^0.12.2",
"@imgly/background-removal": "^1.6.0",
"@jimp/types": "^1.6.0",
"@jspawn/ghostscript-wasm": "^0.0.2",
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@playwright/test": "^1.45.0",
@@ -2038,6 +2039,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@jspawn/ghostscript-wasm": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@jspawn/ghostscript-wasm/-/ghostscript-wasm-0.0.2.tgz",
"integrity": "sha512-IhGvfXNezc+V3jyJlmjz7oxrjWPqFPcz1gqRdo0Y7EkVyFuL1A+tCRnQXx/BHQZPRvBDA+Uf0EqkvXzfMzoDcw==",
"license": "AGPL-3.0"
},
"node_modules/@mui/base": {
"version": "5.0.0-beta.40",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz",

View File

@@ -32,6 +32,7 @@
"@ffmpeg/util": "^0.12.2",
"@imgly/background-removal": "^1.6.0",
"@jimp/types": "^1.6.0",
"@jspawn/ghostscript-wasm": "^0.0.2",
"@mui/icons-material": "^5.15.20",
"@mui/material": "^5.15.20",
"@playwright/test": "^1.45.0",

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
];