mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-12-29 16:16:02 +00:00
Merge branch 'main' into feat/pdf-merge
This commit is contained in:
222
src/pages/tools/pdf/compress-pdf/index.tsx
Normal file
222
src/pages/tools/pdf/compress-pdf/index.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
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}
|
||||
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>
|
||||
<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);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
28
src/pages/tools/pdf/compress-pdf/meta.ts
Normal file
28
src/pages/tools/pdf/compress-pdf/meta.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
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 using Ghostscript',
|
||||
shortDescription: 'Compress PDF files securely in your browser',
|
||||
keywords: [
|
||||
'pdf',
|
||||
'compress',
|
||||
'reduce',
|
||||
'size',
|
||||
'optimize',
|
||||
'shrink',
|
||||
'file size',
|
||||
'ghostscript',
|
||||
'secure',
|
||||
'private',
|
||||
'browser',
|
||||
'webassembly'
|
||||
],
|
||||
longDescription:
|
||||
'Compress PDF files securely in your browser using Ghostscript. Your files never leave your device, ensuring complete privacy while reducing file sizes for email sharing, uploading to websites, or saving storage space. Powered by WebAssembly technology.',
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
28
src/pages/tools/pdf/compress-pdf/service.ts
Normal file
28
src/pages/tools/pdf/compress-pdf/service.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { InitialValuesType } from './types';
|
||||
import { compressWithGhostScript } from '../../../../lib/ghostscript/worker-init';
|
||||
import { loadPDFData } from '../utils';
|
||||
|
||||
/**
|
||||
* Compresses a PDF file using either Ghostscript WASM (preferred)
|
||||
* or falls back to pdf-lib if WASM fails
|
||||
*
|
||||
* @param pdfFile - The PDF file to compress
|
||||
* @param options - Compression options including compression level
|
||||
* @returns A Promise that resolves to a compressed PDF File
|
||||
*/
|
||||
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');
|
||||
}
|
||||
|
||||
const dataObject = {
|
||||
psDataURL: URL.createObjectURL(pdfFile),
|
||||
compressionLevel: options.compressionLevel
|
||||
};
|
||||
const compressedFileUrl: string = await compressWithGhostScript(dataObject);
|
||||
return await loadPDFData(compressedFileUrl, pdfFile.name);
|
||||
}
|
||||
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;
|
||||
};
|
||||
@@ -4,3 +4,13 @@ import { meta as mergePdf } from './merge-pdf/meta';
|
||||
import { DefinedTool } from '@tools/defineTool';
|
||||
|
||||
export const pdfTools: DefinedTool[] = [splitPdfMeta, pdfRotatePdf, mergePdf];
|
||||
import { tool as compressPdfTool } from './compress-pdf/meta';
|
||||
import { tool as protectPdfTool } from './protect-pdf/meta';
|
||||
import { DefinedTool } from '@tools/defineTool';
|
||||
|
||||
export const pdfTools: DefinedTool[] = [
|
||||
splitPdfMeta,
|
||||
pdfRotatePdf,
|
||||
compressPdfTool,
|
||||
protectPdfTool
|
||||
];
|
||||
|
||||
110
src/pages/tools/pdf/protect-pdf/index.tsx
Normal file
110
src/pages/tools/pdf/protect-pdf/index.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useContext, 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 { InitialValuesType } from './types';
|
||||
import { protectPdf } from './service';
|
||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||
import { CustomSnackBarContext } from '../../../../contexts/CustomSnackBarContext';
|
||||
|
||||
const initialValues: InitialValuesType = {
|
||||
password: '',
|
||||
confirmPassword: ''
|
||||
};
|
||||
|
||||
export default function ProtectPdf({
|
||||
title,
|
||||
longDescription
|
||||
}: ToolComponentProps) {
|
||||
const [input, setInput] = useState<File | null>(null);
|
||||
const [result, setResult] = useState<File | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
|
||||
const compute = async (values: InitialValuesType, input: File | null) => {
|
||||
if (!input) return;
|
||||
|
||||
try {
|
||||
// Validate passwords match
|
||||
if (values.password !== values.confirmPassword) {
|
||||
showSnackBar('Passwords do not match', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate password is not empty
|
||||
if (!values.password) {
|
||||
showSnackBar('Password cannot be empty', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProcessing(true);
|
||||
const protectedPdf = await protectPdf(input, values);
|
||||
setResult(protectedPdf);
|
||||
} catch (error) {
|
||||
console.error('Error protecting PDF:', error);
|
||||
showSnackBar(
|
||||
`Failed to protect PDF: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
'error'
|
||||
);
|
||||
setResult(null);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
initialValues={initialValues}
|
||||
compute={compute}
|
||||
inputComponent={
|
||||
<ToolPdfInput
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
accept={['application/pdf']}
|
||||
title={'Input PDF'}
|
||||
/>
|
||||
}
|
||||
resultComponent={
|
||||
<ToolFileResult
|
||||
title={'Protected PDF'}
|
||||
value={result}
|
||||
extension={'pdf'}
|
||||
loading={isProcessing}
|
||||
loadingText={'Protecting PDF'}
|
||||
/>
|
||||
}
|
||||
getGroups={({ values, updateField }) => [
|
||||
{
|
||||
title: 'Password Settings',
|
||||
component: (
|
||||
<Box>
|
||||
<TextFieldWithDesc
|
||||
title="Password"
|
||||
description="Enter a password to protect your PDF"
|
||||
placeholder="Enter password"
|
||||
type="password"
|
||||
value={values.password}
|
||||
onOwnChange={(value) => updateField('password', value)}
|
||||
/>
|
||||
<TextFieldWithDesc
|
||||
title="Confirm Password"
|
||||
description="Re-enter your password to confirm"
|
||||
placeholder="Confirm password"
|
||||
type="password"
|
||||
value={values.confirmPassword}
|
||||
onOwnChange={(value) => updateField('confirmPassword', value)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
27
src/pages/tools/pdf/protect-pdf/meta.ts
Normal file
27
src/pages/tools/pdf/protect-pdf/meta.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('pdf', {
|
||||
name: 'Protect PDF',
|
||||
path: 'protect-pdf',
|
||||
icon: 'material-symbols:lock',
|
||||
description:
|
||||
'Add password protection to your PDF files securely in your browser',
|
||||
shortDescription: 'Password protect PDF files securely',
|
||||
keywords: [
|
||||
'pdf',
|
||||
'protect',
|
||||
'password',
|
||||
'secure',
|
||||
'encrypt',
|
||||
'lock',
|
||||
'private',
|
||||
'confidential',
|
||||
'security',
|
||||
'browser',
|
||||
'encryption'
|
||||
],
|
||||
longDescription:
|
||||
'Add password protection to your PDF files securely in your browser. Your files never leave your device, ensuring complete privacy while securing your documents with password encryption. Perfect for protecting sensitive information, confidential documents, or personal data.',
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
45
src/pages/tools/pdf/protect-pdf/service.ts
Normal file
45
src/pages/tools/pdf/protect-pdf/service.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import { InitialValuesType } from './types';
|
||||
import {
|
||||
compressWithGhostScript,
|
||||
protectWithGhostScript
|
||||
} from '../../../../lib/ghostscript/worker-init';
|
||||
import { loadPDFData } from '../utils';
|
||||
|
||||
/**
|
||||
* Protects a PDF file with a password
|
||||
*
|
||||
* @param pdfFile - The PDF file to protect
|
||||
* @param options - Protection options including password and protection type
|
||||
* @returns A Promise that resolves to a password-protected PDF File
|
||||
*/
|
||||
export async function protectPdf(
|
||||
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');
|
||||
}
|
||||
|
||||
// Check if passwords match
|
||||
if (options.password !== options.confirmPassword) {
|
||||
throw new Error('Passwords do not match');
|
||||
}
|
||||
|
||||
// Check if password is empty
|
||||
if (!options.password) {
|
||||
throw new Error('Password cannot be empty');
|
||||
}
|
||||
|
||||
const dataObject = {
|
||||
psDataURL: URL.createObjectURL(pdfFile),
|
||||
password: options.password
|
||||
};
|
||||
const protectedFileUrl: string = await protectWithGhostScript(dataObject);
|
||||
console.log('protected', protectedFileUrl);
|
||||
return await loadPDFData(
|
||||
protectedFileUrl,
|
||||
pdfFile.name.replace('.pdf', '-protected.pdf')
|
||||
);
|
||||
}
|
||||
6
src/pages/tools/pdf/protect-pdf/types.ts
Normal file
6
src/pages/tools/pdf/protect-pdf/types.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type ProtectionType = 'owner' | 'user';
|
||||
|
||||
export type InitialValuesType = {
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
};
|
||||
16
src/pages/tools/pdf/utils.ts
Normal file
16
src/pages/tools/pdf/utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export function loadPDFData(url: string, filename: string): Promise<File> {
|
||||
return new Promise((resolve) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', url);
|
||||
xhr.responseType = 'arraybuffer';
|
||||
xhr.onload = function () {
|
||||
window.URL.revokeObjectURL(url);
|
||||
const blob = new Blob([xhr.response], { type: 'application/pdf' });
|
||||
const newFile = new File([blob], filename, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
resolve(newFile);
|
||||
};
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user