feat: unlock pdf init

This commit is contained in:
Ibrahima G. Coulibaly 2025-06-13 03:35:45 +01:00
commit 94c1acd7ce
8 changed files with 436 additions and 70 deletions

146
.idea/workspace.xml generated
View file

@ -4,8 +4,15 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="feat: qr code generation init">
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="feat: unlock pdf init">
<change afterPath="$PROJECT_DIR$/src/pages/tools/pdf/unlock-pdf/index.tsx" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/pages/tools/pdf/unlock-pdf/meta.ts" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/pages/tools/pdf/unlock-pdf/service.ts" afterDir="false" />
<change afterPath="$PROJECT_DIR$/src/pages/tools/pdf/unlock-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/lib/ghostscript/background-worker.js" beforeDir="false" afterPath="$PROJECT_DIR$/src/lib/ghostscript/background-worker.js" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/lib/ghostscript/worker-init.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/lib/ghostscript/worker-init.ts" 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" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -22,7 +29,7 @@
<option name="PUSH_AUTO_UPDATE" value="true" />
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="76d615ec7c369b7342e0f276392a4cba9c531aef" />
<entry key="$PROJECT_DIR$" value="main" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@ -199,56 +206,57 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ASKED_ADD_EXTERNAL_FILES&quot;: &quot;true&quot;,
&quot;ASKED_SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
&quot;Docker.Dockerfile build.executor&quot;: &quot;Run&quot;,
&quot;Docker.Dockerfile.executor&quot;: &quot;Run&quot;,
&quot;Playwright.Create transparent PNG.should make png color transparent.executor&quot;: &quot;Run&quot;,
&quot;Playwright.JoinText Component.executor&quot;: &quot;Run&quot;,
&quot;Playwright.JoinText Component.should merge text pieces with specified join character.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.OpenProjectViewOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;Vitest.compute function (1).executor&quot;: &quot;Run&quot;,
&quot;Vitest.compute function.executor&quot;: &quot;Run&quot;,
&quot;Vitest.mergeText.executor&quot;: &quot;Run&quot;,
&quot;Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor&quot;: &quot;Run&quot;,
&quot;Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor&quot;: &quot;Run&quot;,
&quot;Vitest.parsePageRanges.executor&quot;: &quot;Run&quot;,
&quot;Vitest.removeDuplicateLines function.executor&quot;: &quot;Run&quot;,
&quot;Vitest.removeDuplicateLines function.newlines option.executor&quot;: &quot;Run&quot;,
&quot;Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor&quot;: &quot;Run&quot;,
&quot;Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp.executor&quot;: &quot;Run&quot;,
&quot;Vitest.replaceText function.executor&quot;: &quot;Run&quot;,
&quot;Vitest.timeBetweenDates.executor&quot;: &quot;Run&quot;,
&quot;git-widget-placeholder&quot;: &quot;main&quot;,
&quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;,
&quot;kotlin-language-version-configured&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/Ibrahima/IdeaProjects/omni-tools/src/pages/tools/json&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;npm.build.executor&quot;: &quot;Run&quot;,
&quot;npm.dev.executor&quot;: &quot;Run&quot;,
&quot;npm.lint.executor&quot;: &quot;Run&quot;,
&quot;npm.prebuild.executor&quot;: &quot;Run&quot;,
&quot;npm.script:create:tool.executor&quot;: &quot;Run&quot;,
&quot;npm.test.executor&quot;: &quot;Run&quot;,
&quot;npm.test:e2e.executor&quot;: &quot;Run&quot;,
&quot;npm.test:e2e:run.executor&quot;: &quot;Run&quot;,
&quot;prettierjs.PrettierConfiguration.Package&quot;: &quot;C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier&quot;,
&quot;project.structure.last.edited&quot;: &quot;Problems&quot;,
&quot;project.structure.proportion&quot;: &quot;0.0&quot;,
&quot;project.structure.side.proportion&quot;: &quot;0.2&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;refactai_advanced_settings&quot;,
&quot;ts.external.directory.path&quot;: &quot;C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ASKED_ADD_EXTERNAL_FILES": "true",
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
"Docker.Dockerfile build.executor": "Run",
"Docker.Dockerfile.executor": "Run",
"Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
"Playwright.JoinText Component.executor": "Run",
"Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.git.unshallow": "true",
"Vitest.compute function (1).executor": "Run",
"Vitest.compute function.executor": "Run",
"Vitest.mergeText.executor": "Run",
"Vitest.mergeText.should merge lines and preserve blank lines when deleteBlankLines is false.executor": "Run",
"Vitest.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
"Vitest.parsePageRanges.executor": "Run",
"Vitest.removeDuplicateLines function.executor": "Run",
"Vitest.removeDuplicateLines function.newlines option.executor": "Run",
"Vitest.removeDuplicateLines function.newlines option.should filter newlines when newlines is set to filter.executor": "Run",
"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": "unlock-pdf",
"ignore.virus.scanning.warn.message": "true",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src/pages/tools/pdf",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"npm.build.executor": "Run",
"npm.dev.executor": "Run",
"npm.lint.executor": "Run",
"npm.prebuild.executor": "Run",
"npm.script:create:tool.executor": "Run",
"npm.test.executor": "Run",
"npm.test:e2e.executor": "Run",
"npm.test:e2e:run.executor": "Run",
"prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
"project.structure.last.edited": "Problems",
"project.structure.proportion": "0.0",
"project.structure.side.proportion": "0.2",
"settings.editor.selected.configurable": "preferences.pluginManager",
"ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
"ts.rename.search.for.js.occurrences": "false",
"vue.rearranger.settings.migration": "true"
}
}</component>
}]]></component>
<component name="ReactDesignerToolWindowState">
<option name="myId2Visible">
<map>
@ -260,11 +268,11 @@
</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src\pages\tools\pdf" />
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src\pages\tools\json" />
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src" />
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\@types" />
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\public\assets" />
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src\components\input" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src\lib\ghostscript" />
@ -353,11 +361,11 @@
</list>
<recent_temporary>
<list>
<item itemvalue="npm.dev" />
<item itemvalue="Vitest.replaceText function (regexp mode).should return the original text when passed an invalid regexp" />
<item itemvalue="Vitest.parsePageRanges" />
<item itemvalue="Vitest.timeBetweenDates" />
<item itemvalue="Vitest.calculateTimeBetweenDates" />
<item itemvalue="npm.dev" />
</list>
</recent_temporary>
</component>
@ -462,14 +470,12 @@
<workItem from="1748026506667" duration="2536000" />
<workItem from="1748282636141" duration="478000" />
<workItem from="1749047510481" duration="879000" />
</task>
<task id="LOCAL-00152" summary="feat: crop png">
<option name="closed" value="true" />
<created>1741492688761</created>
<option name="number" value="00152" />
<option name="presentableId" value="LOCAL-00152" />
<option name="project" value="LOCAL" />
<updated>1741492688761</updated>
<workItem from="1749214774130" duration="1093000" />
<workItem from="1749348736119" duration="1186000" />
<workItem from="1749471084697" duration="7000" />
<workItem from="1749568940505" duration="21000" />
<workItem from="1749774497094" duration="1313000" />
<workItem from="1749775850632" duration="6240000" />
</task>
<task id="LOCAL-00153" summary="chore: remove unnecessary files">
<option name="closed" value="true" />
@ -855,7 +861,15 @@
<option name="project" value="LOCAL" />
<updated>1749147227565</updated>
</task>
<option name="localTasksCounter" value="201" />
<task id="LOCAL-00201" summary="docs: readme">
<option name="closed" value="true" />
<created>1749348977357</created>
<option name="number" value="00201" />
<option name="presentableId" value="LOCAL-00201" />
<option name="project" value="LOCAL" />
<updated>1749348977357</updated>
</task>
<option name="localTasksCounter" value="202" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@ -902,8 +916,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="fix: gif speed" />
<MESSAGE value="fix: tsc" />
<MESSAGE value="fix: background color" />
<MESSAGE value="docs: github trendings" />
<MESSAGE value="docs: optimize" />
@ -927,7 +939,9 @@
<MESSAGE value="chore: remove unnecessary prop" />
<MESSAGE value="fix: compute flow" />
<MESSAGE value="feat: qr code generation init" />
<option name="LAST_COMMIT_MESSAGE" value="feat: qr code generation init" />
<MESSAGE value="docs: readme" />
<MESSAGE value="feat: unlock pdf init" />
<option name="LAST_COMMIT_MESSAGE" value="feat: unlock pdf init" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />

View file

@ -1,4 +1,4 @@
import { COMPRESS_ACTION, PROTECT_ACTION } from './worker-init';
import { COMPRESS_ACTION, PROTECT_ACTION, UNLOCK_ACTION } from './worker-init';
function loadScript() {
import('./gs-worker.js');
@ -87,7 +87,7 @@ function protectPdf(dataStruct, responseCallback) {
// Validate password
if (!password) {
responseCallback({
console.error({
error: 'Password is required for encryption',
url: dataStruct.url
});
@ -153,6 +153,178 @@ function protectPdf(dataStruct, responseCallback) {
xhr.send();
}
function unlockPdf(dataStruct, responseCallback) {
const speed = dataStruct.speed || 'normal';
let passwordListUrl;
console.log('unlockPdf', dataStruct);
// Determine which password list to download based on speed
if (speed === 'fast') {
passwordListUrl =
'https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Passwords/Common-Credentials/xato-net-10-million-passwords-1000.txt';
} else if (speed === 'normal') {
passwordListUrl =
'https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Passwords/Common-Credentials/xato-net-10-million-passwords-100000.txt';
} else {
// Default or handle other speeds
passwordListUrl =
'https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Passwords/Common-Credentials/xato-net-10-million-passwords-100000.txt';
}
// --- Step 1: Download the password list ---
var xhrPasswordList = new XMLHttpRequest();
xhrPasswordList.open('GET', passwordListUrl);
xhrPasswordList.onload = function () {
if (xhrPasswordList.status === 200) {
const passwordList = xhrPasswordList.responseText
.split('\n')
.map((p) => p.trim());
// --- Step 2: Proceed with PDF processing and dictionary attack ---
processPdfWithDictionary(dataStruct, responseCallback, passwordList);
} else {
console.error(
'Failed to download password list:',
xhrPasswordList.statusText
);
console.error('Failed to download password list');
}
};
xhrPasswordList.onerror = function () {
console.error('Network error while downloading password list.');
console.error('Network error while downloading password list');
};
xhrPasswordList.send();
}
function processPdfWithDictionary(dataStruct, responseCallback, passwordList) {
var xhrPdf = new XMLHttpRequest();
xhrPdf.open('GET', dataStruct.psDataURL);
xhrPdf.responseType = 'arraybuffer';
xhrPdf.onload = function () {
console.log('PDF onload');
self.URL.revokeObjectURL(dataStruct.psDataURL);
let currentPasswordIndex = 0;
const tryNextPassword = () => {
if (currentPasswordIndex >= passwordList.length) {
console.error('All passwords tried. PDF could not be unlocked.');
return;
}
const password = passwordList[currentPasswordIndex];
console.log(
`Attempting with password: "${password}" (${currentPasswordIndex + 1}/${
passwordList.length
})`
);
Module = {
preRun: [
function () {
self.Module.FS.writeFile(
'input.pdf',
new Uint8Array(xhrPdf.response)
);
}
],
postRun: [
function () {
try {
var uarray = self.Module.FS.readFile('output.pdf', {
encoding: 'binary'
});
// If readFile succeeds, it means the password was correct
var blob = new Blob([uarray], {
type: 'application/octet-stream'
});
var pdfDataURL = self.URL.createObjectURL(blob);
console.log(
`PDF unlocked successfully with password: "${password}"`
);
responseCallback({
pdfDataURL: pdfDataURL,
url: dataStruct.url,
type: PROTECT_ACTION
});
} catch (error) {
console.error('Error:', error);
}
}
],
arguments: [
'-sDEVICE=pdfwrite',
'-dCompatibilityLevel=1.4',
`-sPDFPassword=${password}`, // Use the current password from the list
'-DNOPAUSE',
'-dQUIET',
'-dBATCH',
'-sOutputFile=output.pdf',
'input.pdf'
],
print: function (text) {
if (text.includes('Error: Password did not work')) {
try {
if (self.Module && typeof self.Module.exit === 'function') {
self.Module.exit(1);
}
// Clean up any files that might have been created
if (self.Module && self.Module.FS) {
try {
if (self.Module.FS.readdir('/').includes('input.pdf')) {
self.Module.FS.unlink('input.pdf');
}
if (self.Module.FS.readdir('/').includes('output.pdf')) {
self.Module.FS.unlink('output.pdf');
}
} catch (fsError) {
console.log('Filesystem cleanup error:', fsError);
}
}
} catch (exitError) {
console.log('Error during module exit:', exitError);
}
currentPasswordIndex++;
setTimeout(tryNextPassword, 10); // Small delay to prevent tight loop
}
},
printErr: function (text) {
// Ghostscript might print errors here if the password is wrong
// We need to parse these to determine if it's a password error or another issue
console.error('Ghostscript error:', text);
},
totalDependencies: 0,
noExitRuntime: 1
};
if (!self.Module) {
self.Module = Module;
loadScript(); // Assuming loadScript() loads the Emscripten-compiled Ghostscript
} else {
self.Module['calledRun'] = false;
self.Module['postRun'] = Module.postRun;
self.Module['preRun'] = Module.preRun;
self.Module['arguments'] = Module.arguments;
self.Module.callMain();
}
};
// Start the dictionary attack
tryNextPassword();
};
xhrPdf.onerror = function () {
console.error('Network error while downloading PDF.');
};
xhrPdf.send();
}
// Assuming PROTECT_ACTION and loadScript() are defined elsewhere in your worker/global scope.
// For example:
// const PROTECT_ACTION = 'pdf_protection_status';
// function loadScript() {
// importScripts('ghostscript_module.js'); // Or whatever your Emscripten output file is named
// }
self.addEventListener('message', function ({ data: e }) {
console.log('message', e);
// e.data contains the message sent to the worker.
@ -160,13 +332,16 @@ self.addEventListener('message', function ({ data: e }) {
return;
}
console.log('Message received from main script', e.data);
const responseCallback = ({ pdfDataURL, type }) => {
const responseCallback = ({ pdfDataURL, type, log }) => {
self.postMessage(pdfDataURL);
console.log(log);
};
if (e.data.type === COMPRESS_ACTION) {
compressPdf(e.data, responseCallback);
} else if (e.data.type === PROTECT_ACTION) {
protectPdf(e.data, responseCallback);
} else if (e.data.type === UNLOCK_ACTION) {
unlockPdf(e.data, responseCallback);
}
});

View file

@ -1,5 +1,6 @@
export const COMPRESS_ACTION = 'compress-pdf';
export const PROTECT_ACTION = 'protect-pdf';
export const UNLOCK_ACTION = 'unlock-pdf';
export async function compressWithGhostScript(dataStruct: {
psDataURL: string;
@ -14,6 +15,7 @@ export async function compressWithGhostScript(dataStruct: {
export async function protectWithGhostScript(dataStruct: {
psDataURL: string;
password: string;
}): Promise<string> {
const worker = getWorker();
worker.postMessage({
@ -23,6 +25,18 @@ export async function protectWithGhostScript(dataStruct: {
return getListener(worker);
}
export async function unlockWithGhostScript(dataStruct: {
psDataURL: string;
speed: 'slow' | 'normal' | 'fast';
}): Promise<string> {
const worker = getWorker();
worker.postMessage({
data: { ...dataStruct, type: UNLOCK_ACTION },
target: 'wasm'
});
return getListener(worker);
}
const getListener = (worker: Worker): Promise<string> => {
return new Promise((resolve, reject) => {
const listener = (e: MessageEvent) => {

View file

@ -5,6 +5,7 @@ import { DefinedTool } from '@tools/defineTool';
import { tool as compressPdfTool } from './compress-pdf/meta';
import { tool as protectPdfTool } from './protect-pdf/meta';
import { meta as pdfToEpub } from './pdf-to-epub/meta';
import { meta as unlockPdfTool } from './unlock-pdf/meta';
export const pdfTools: DefinedTool[] = [
splitPdfMeta,
@ -12,5 +13,6 @@ export const pdfTools: DefinedTool[] = [
compressPdfTool,
protectPdfTool,
mergePdf,
pdfToEpub
pdfToEpub,
unlockPdfTool
];

View file

@ -0,0 +1,109 @@
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, Speed } from './types';
import { unlockPdf } from './service';
import { CustomSnackBarContext } from '../../../../contexts/CustomSnackBarContext';
import RadioWithTextField from '@components/options/RadioWithTextField';
import SimpleRadio from '@components/options/SimpleRadio';
const initialValues: InitialValuesType = {
speed: 'normal'
};
const speeds: {
label: string;
description: string;
value: Speed;
}[] = [
{
label: 'Slow',
value: 'slow',
description: 'More probable to unlock the PDF, but may take longer.'
},
{
label: 'Normal',
value: 'normal',
description: 'Balanced between speed and success rate.'
},
{
label: 'Fast',
value: 'fast',
description:
'Faster unlocking, but less thorough; may fail on complex passwords.'
}
];
export default function UnlockPdf({
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 {
setIsProcessing(true);
const protectedPdf = await unlockPdf(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={'Unlocked PDF'}
value={result}
extension={'pdf'}
loading={isProcessing}
loadingText={'Unlocking PDF'}
/>
}
getGroups={({ values, updateField }) => [
{
title: 'Speed',
component: speeds.map(({ label, description, value }) => (
<SimpleRadio
key={value}
checked={value === values.speed}
title={label}
description={description}
onClick={() => updateField('speed', value)}
/>
))
}
]}
/>
);
}

View file

@ -0,0 +1,24 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const meta = defineTool('pdf', {
name: 'Unlock PDF',
path: 'unlock-pdf',
icon: 'material-symbols:lock_open',
description:
'Remove password protection from your PDF files securely in your browser',
shortDescription: 'Unlock PDF files securely',
keywords: [
'pdf',
'unlock',
'remove password',
'decrypt',
'unprotect',
'security',
'browser',
'decryption'
],
longDescription:
'Remove password protection from your PDF files securely in your browser. Your files never leave your device, ensuring complete privacy while unlocking your documents for easier access and sharing.',
component: lazy(() => import('./index'))
});

View file

@ -0,0 +1,23 @@
import { InitialValuesType } from './types';
import { loadPDFData } from '../utils';
import { unlockWithGhostScript } from '../../../../lib/ghostscript/worker-init';
export async function unlockPdf(
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),
speed: options.speed
};
const protectedFileUrl: string = await unlockWithGhostScript(dataObject);
return await loadPDFData(
protectedFileUrl,
pdfFile.name.replace('.pdf', '-unlocked.pdf')
);
}

View file

@ -0,0 +1,5 @@
export type Speed = 'fast' | 'normal' | 'slow';
export type InitialValuesType = {
speed: Speed;
};