mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-11-11 02:29:54 +05:30
feat: unlock pdf init
This commit is contained in:
parent
d25f7ca557
commit
94c1acd7ce
8 changed files with 436 additions and 70 deletions
146
.idea/workspace.xml
generated
146
.idea/workspace.xml
generated
|
|
@ -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">{
|
||||
"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": "main",
|
||||
"ignore.virus.scanning.warn.message": "true",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src/pages/tools/json",
|
||||
"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": "refactai_advanced_settings",
|
||||
"ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
<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 />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
];
|
||||
|
|
|
|||
109
src/pages/tools/pdf/unlock-pdf/index.tsx
Normal file
109
src/pages/tools/pdf/unlock-pdf/index.tsx
Normal 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)}
|
||||
/>
|
||||
))
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
24
src/pages/tools/pdf/unlock-pdf/meta.ts
Normal file
24
src/pages/tools/pdf/unlock-pdf/meta.ts
Normal 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'))
|
||||
});
|
||||
23
src/pages/tools/pdf/unlock-pdf/service.ts
Normal file
23
src/pages/tools/pdf/unlock-pdf/service.ts
Normal 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')
|
||||
);
|
||||
}
|
||||
5
src/pages/tools/pdf/unlock-pdf/types.ts
Normal file
5
src/pages/tools/pdf/unlock-pdf/types.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export type Speed = 'fast' | 'normal' | 'slow';
|
||||
|
||||
export type InitialValuesType = {
|
||||
speed: Speed;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue