diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 27386d7..2a5c410 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,33 +4,17 @@
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
@@ -47,7 +31,7 @@
@@ -144,6 +128,13 @@
"number": 76
},
"lastSeen": 1743352150953
+ },
+ {
+ "id": {
+ "id": "PR_kwDOMJIfts6Q0JBe",
+ "number": 82
+ },
+ "lastSeen": 1743470267269
}
]
}]]>
@@ -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 @@
-
-
-
- 1740619610168
-
-
-
- 1740619610169
-
-
-
- 1740620866551
-
-
-
- 1740620866551
-
-
-
- 1740661424202
-
-
-
- 1740661424202
-
-
-
- 1740661540908
-
-
-
- 1740661540908
-
-
-
- 1740661744828
-
-
-
- 1740661744828
-
-
-
- 1740661864615
-
-
-
- 1740661864615
+
+
@@ -814,7 +759,55 @@
1743355166426
-
+
+
+ 1743385388051
+
+
+
+ 1743385388051
+
+
+
+ 1743385467178
+
+
+
+ 1743385467178
+
+
+
+ 1743385898871
+
+
+
+ 1743385898871
+
+
+
+ 1743459110471
+
+
+
+ 1743459110471
+
+
+
+ 1743459205311
+
+
+
+ 1743459205311
+
+
+
+ 1743470832619
+
+
+
+ 1743470832619
+
+
@@ -861,12 +854,6 @@
-
-
-
-
-
-
@@ -886,7 +873,13 @@
-
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index b2074f9..b0fc540 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 68dfa2d..75794d8 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/pages/tools/pdf/compress-pdf/index.tsx b/src/pages/tools/pdf/compress-pdf/index.tsx
new file mode 100644
index 0000000..fc4dbc3
--- /dev/null
+++ b/src/pages/tools/pdf/compress-pdf/index.tsx
@@ -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[] = [
+ {
+ 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(null);
+ const [result, setResult] = useState(null);
+ const [resultSize, setResultSize] = useState('');
+ const [isProcessing, setIsProcessing] = useState(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 (
+
+ }
+ resultComponent={
+
+ }
+ getGroups={({ values, updateField }) => [
+ {
+ title: 'Compression Settings',
+ component: (
+
+
+ Compression Level
+
+
+ {compressionOptions.map((option) => (
+ {
+ updateField('compressionLevel', option.value);
+ }}
+ />
+ ))}
+
+ {fileInfo && (
+
+
+ File size: {fileInfo.size}
+
+
+ Pages: {fileInfo.pages}
+
+ {resultSize && (
+
+ Compressed file size: {resultSize}
+
+ )}
+
+ )}
+
+ )
+ }
+ ]}
+ 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}`
+ }}
+ />
+ );
+}
diff --git a/src/pages/tools/pdf/compress-pdf/meta.ts b/src/pages/tools/pdf/compress-pdf/meta.ts
new file mode 100644
index 0000000..c3da8b4
--- /dev/null
+++ b/src/pages/tools/pdf/compress-pdf/meta.ts
@@ -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'))
+});
diff --git a/src/pages/tools/pdf/compress-pdf/service.test.ts b/src/pages/tools/pdf/compress-pdf/service.test.ts
new file mode 100644
index 0000000..6c64c78
--- /dev/null
+++ b/src/pages/tools/pdf/compress-pdf/service.test.ts
@@ -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');
+ });
+});
diff --git a/src/pages/tools/pdf/compress-pdf/service.ts b/src/pages/tools/pdf/compress-pdf/service.ts
new file mode 100644
index 0000000..e0e5f7c
--- /dev/null
+++ b/src/pages/tools/pdf/compress-pdf/service.ts
@@ -0,0 +1,59 @@
+import { CompressionLevel, InitialValuesType } from './types';
+import { PDFDocument } from 'pdf-lib';
+
+export async function compressPdf(
+ pdfFile: File,
+ options: InitialValuesType
+): Promise {
+ // 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
+ };
+ }
+}
diff --git a/src/pages/tools/pdf/compress-pdf/types.ts b/src/pages/tools/pdf/compress-pdf/types.ts
new file mode 100644
index 0000000..8b6f155
--- /dev/null
+++ b/src/pages/tools/pdf/compress-pdf/types.ts
@@ -0,0 +1,5 @@
+export type CompressionLevel = 'low' | 'medium' | 'high';
+
+export type InitialValuesType = {
+ compressionLevel: CompressionLevel;
+};
diff --git a/src/pages/tools/pdf/index.ts b/src/pages/tools/pdf/index.ts
index 380e593..a1e8475 100644
--- a/src/pages/tools/pdf/index.ts
+++ b/src/pages/tools/pdf/index.ts
@@ -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
+];