Merge pull request #88 from iib0011/compression

feat: pdf compression
This commit is contained in:
Ibrahima G. Coulibaly
2025-04-03 02:04:12 +00:00
committed by GitHub
13 changed files with 6569 additions and 121 deletions

232
.idea/workspace.xml generated
View File

@@ -4,23 +4,11 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="feat: svg change colors">
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="fix: path">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" 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/pages/tools/image/generic/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/index.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/change-opacity/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/change-opacity/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/change-opacity/meta.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/change-opacity/meta.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/change-opacity/service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/change-opacity/service.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/create-transparent/create-transparent.e2e.spec.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/create-transparent/create-transparent.e2e.spec.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/create-transparent/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/create-transparent/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/create-transparent/meta.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/create-transparent/meta.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/create-transparent/test.png" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/create-transparent/test.png" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/crop/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/crop/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/crop/meta.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/crop/meta.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/png/index.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/remove-background/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/remove-background/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/image/png/remove-background/meta.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/image/generic/remove-background/meta.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/tools/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/tools/index.ts" 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/pages/tools/pdf/compress-pdf/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/pdf/compress-pdf/index.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/pages/tools/pdf/compress-pdf/service.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/pdf/compress-pdf/service.ts" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -37,7 +25,7 @@
<option name="PUSH_AUTO_UPDATE" value="true" />
<option name="RECENT_BRANCH_BY_REPOSITORY">
<map>
<entry key="$PROJECT_DIR$" value="image-resize" />
<entry key="$PROJECT_DIR$" value="chesterkxng" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@@ -134,6 +122,13 @@
&quot;number&quot;: 76
},
&quot;lastSeen&quot;: 1743352150953
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6Q0JBe&quot;,
&quot;number&quot;: 82
},
&quot;lastSeen&quot;: 1743470267269
}
]
}</component>
@@ -165,56 +160,56 @@
<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/@types&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": "compression",
"ignore.virus.scanning.warn.message": "true",
"kotlin-language-version-configured": "true",
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src",
"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>
}]]></component>
<component name="ReactDesignerToolWindowState">
<option name="myId2Visible">
<map>
@@ -226,14 +221,14 @@
</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<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" />
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\.husky" />
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src\assets" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src\pages\tools\image\generic" />
<recent name="C:\Users\Ibrahima\IdeaProjects\omni-tools\src\lib\ghostscript" />
<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\pages\tools" />
@@ -411,19 +406,8 @@
<workItem from="1743047367993" duration="986000" />
<workItem from="1743103182313" duration="4264000" />
<workItem from="1743348610793" duration="21855000" />
<workItem from="1743556259185" duration="7150000" />
<workItem from="1743569964813" duration="352000" />
<workItem from="1743570403937" duration="109000" />
<workItem from="1743607343305" duration="20000" />
<workItem from="1743619618671" duration="7027000" />
</task>
<task id="LOCAL-00136" summary="chore: img">
<option name="closed" value="true" />
<created>1740680778110</created>
<option name="number" value="00136" />
<option name="presentableId" value="LOCAL-00136" />
<option name="project" value="LOCAL" />
<updated>1740680778110</updated>
<workItem from="1743397561176" duration="25000" />
<workItem from="1743458576265" duration="13083000" />
</task>
<task id="LOCAL-00137" summary="docs: readme">
<option name="closed" value="true" />
@@ -737,79 +721,87 @@
<option name="project" value="LOCAL" />
<updated>1743355166426</updated>
</task>
<task id="LOCAL-00176" summary="chore: tool description">
<task id="LOCAL-00176" summary="fix: gif speed">
<option name="closed" value="true" />
<created>1743560690570</created>
<created>1743385388051</created>
<option name="number" value="00176" />
<option name="presentableId" value="LOCAL-00176" />
<option name="project" value="LOCAL" />
<updated>1743560690571</updated>
<updated>1743385388051</updated>
</task>
<task id="LOCAL-00177" summary="feat: image resize init">
<task id="LOCAL-00177" summary="fix: tsc">
<option name="closed" value="true" />
<created>1743565606951</created>
<created>1743385467178</created>
<option name="number" value="00177" />
<option name="presentableId" value="LOCAL-00177" />
<option name="project" value="LOCAL" />
<updated>1743565606951</updated>
<updated>1743385467178</updated>
</task>
<task id="LOCAL-00178" summary="feat: svg resize">
<task id="LOCAL-00178" summary="fix: background color">
<option name="closed" value="true" />
<created>1743566704552</created>
<created>1743385898871</created>
<option name="number" value="00178" />
<option name="presentableId" value="LOCAL-00178" />
<option name="project" value="LOCAL" />
<updated>1743566704552</updated>
<updated>1743385898871</updated>
</task>
<task id="LOCAL-00179" summary="feat: gif resize">
<task id="LOCAL-00179" summary="docs: github trendings">
<option name="closed" value="true" />
<created>1743567906528</created>
<created>1743459110471</created>
<option name="number" value="00179" />
<option name="presentableId" value="LOCAL-00179" />
<option name="project" value="LOCAL" />
<updated>1743567906528</updated>
<updated>1743459110471</updated>
</task>
<task id="LOCAL-00180" summary="chore: add color">
<task id="LOCAL-00180" summary="docs: optimize">
<option name="closed" value="true" />
<created>1743569026879</created>
<created>1743459205311</created>
<option name="number" value="00180" />
<option name="presentableId" value="LOCAL-00180" />
<option name="project" value="LOCAL" />
<updated>1743569026879</updated>
<updated>1743459205311</updated>
</task>
<task id="LOCAL-00181" summary="docs: readme">
<task id="LOCAL-00181" summary="fix: stars button width for 1k+ 😊">
<option name="closed" value="true" />
<created>1743569711302</created>
<created>1743470832619</created>
<option name="number" value="00181" />
<option name="presentableId" value="LOCAL-00181" />
<option name="project" value="LOCAL" />
<updated>1743569711302</updated>
<updated>1743470832619</updated>
</task>
<task id="LOCAL-00182" summary="feat: compress image">
<task id="LOCAL-00182" summary="feat: compress pdf">
<option name="closed" value="true" />
<created>1743621506634</created>
<created>1743644598841</created>
<option name="number" value="00182" />
<option name="presentableId" value="LOCAL-00182" />
<option name="project" value="LOCAL" />
<updated>1743621506634</updated>
<updated>1743644598841</updated>
</task>
<task id="LOCAL-00183" summary="chore: remove labels">
<task id="LOCAL-00183" summary="refactor: compress pdf">
<option name="closed" value="true" />
<created>1743621602590</created>
<created>1743644703041</created>
<option name="number" value="00183" />
<option name="presentableId" value="LOCAL-00183" />
<option name="project" value="LOCAL" />
<updated>1743621602590</updated>
<updated>1743644703042</updated>
</task>
<task id="LOCAL-00184" summary="feat: svg change colors">
<task id="LOCAL-00184" summary="refactor: lib">
<option name="closed" value="true" />
<created>1743623617781</created>
<created>1743644942488</created>
<option name="number" value="00184" />
<option name="presentableId" value="LOCAL-00184" />
<option name="project" value="LOCAL" />
<updated>1743623617781</updated>
<updated>1743644942488</updated>
</task>
<option name="localTasksCounter" value="185" />
<task id="LOCAL-00185" summary="fix: path">
<option name="closed" value="true" />
<created>1743645074051</created>
<option name="number" value="00185" />
<option name="presentableId" value="LOCAL-00185" />
<option name="project" value="LOCAL" />
<updated>1743645074051</updated>
</task>
<option name="localTasksCounter" value="186" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -856,7 +848,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="refactor: sum" />
<MESSAGE value="fix: tools by category scroll" />
<MESSAGE value="fix: missing meta" />
<MESSAGE value="feat: trim video" />
@@ -872,16 +863,17 @@
<MESSAGE value="fix: typos" />
<MESSAGE value="feat: compress video" />
<MESSAGE value="chore: compress video icon" />
<MESSAGE value="chore: tool description" />
<MESSAGE value="feat: image resize init" />
<MESSAGE value="feat: svg resize" />
<MESSAGE value="feat: gif resize" />
<MESSAGE value="chore: add color" />
<MESSAGE value="docs: readme" />
<MESSAGE value="feat: compress image" />
<MESSAGE value="chore: remove labels" />
<MESSAGE value="feat: svg change colors" />
<option name="LAST_COMMIT_MESSAGE" value="feat: svg change colors" />
<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+ " />
<MESSAGE value="feat: compress pdf" />
<MESSAGE value="refactor: compress pdf" />
<MESSAGE value="refactor: lib" />
<MESSAGE value="fix: path" />
<option name="LAST_COMMIT_MESSAGE" value="fix: path" />
</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",

39
public/gs.js Normal file
View File

@@ -0,0 +1,39 @@
// This is a placeholder file for the actual Ghostscript WASM implementation
// In a real implementation, this would be the compiled Ghostscript WASM module
// You would need to download the actual Ghostscript WASM files from:
// https://github.com/ochachacha/ps2pdf-wasm or compile it yourself
// This simulates the Module loading process that would occur with the real WASM file
(function () {
// Simulate WASM loading
console.log('Loading Ghostscript WASM module...');
// Expose a simulated Module to the window
window.Module = window.Module || {};
// Simulate filesystem
window.FS = {
writeFile: function (name, data) {
console.log(`[Simulated] Writing file: ${name}`);
return true;
},
readFile: function (name, options) {
console.log(`[Simulated] Reading file: ${name}`);
// Return a sample Uint8Array that would represent a PDF
return new Uint8Array(10);
}
};
// Mark module as initialized after a delay to simulate loading
setTimeout(function () {
window.Module.calledRun = true;
console.log('Ghostscript WASM module loaded');
// Add callMain method for direct calling
window.Module.callMain = function (args) {
console.log('[Simulated] Running Ghostscript with args:', args);
// In a real implementation, this would execute the WASM module with the given arguments
};
}, 1000);
})();

View File

@@ -0,0 +1,89 @@
function loadScript() {
import('./gs-worker.js');
}
var Module;
function _GSPS2PDF(dataStruct, responseCallback) {
const compressionLevel = dataStruct.compressionLevel || 'medium';
// Set PDF settings based on compression level
let pdfSettings;
switch (compressionLevel) {
case 'low':
pdfSettings = '/printer'; // Higher quality, less compression
break;
case 'medium':
pdfSettings = '/ebook'; // Medium quality and compression
break;
case 'high':
pdfSettings = '/screen'; // Lower quality, higher compression
break;
default:
pdfSettings = '/ebook'; // Default to medium
}
// first download the ps data
var xhr = new XMLHttpRequest();
xhr.open('GET', dataStruct.psDataURL);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
console.log('onload');
// release the URL
self.URL.revokeObjectURL(dataStruct.psDataURL);
//set up EMScripten environment
Module = {
preRun: [
function () {
self.Module.FS.writeFile('input.pdf', new Uint8Array(xhr.response));
}
],
postRun: [
function () {
var uarray = self.Module.FS.readFile('output.pdf', {
encoding: 'binary'
});
var blob = new Blob([uarray], { type: 'application/octet-stream' });
var pdfDataURL = self.URL.createObjectURL(blob);
responseCallback({ pdfDataURL: pdfDataURL, url: dataStruct.url });
}
],
arguments: [
'-sDEVICE=pdfwrite',
'-dCompatibilityLevel=1.4',
`-dPDFSETTINGS=${pdfSettings}`,
'-DNOPAUSE',
'-dQUIET',
'-dBATCH',
'-sOutputFile=output.pdf',
'input.pdf'
],
print: function (text) {},
printErr: function (text) {},
totalDependencies: 0,
noExitRuntime: 1
};
// Module.setStatus("Loading Ghostscript...");
if (!self.Module) {
self.Module = Module;
loadScript();
} else {
self.Module['calledRun'] = false;
self.Module['postRun'] = Module.postRun;
self.Module['preRun'] = Module.preRun;
self.Module.callMain();
}
};
xhr.send();
}
self.addEventListener('message', function ({ data: e }) {
console.log('message', e);
// e.data contains the message sent to the worker.
if (e.target !== 'wasm') {
return;
}
console.log('Message received from main script', e.data);
_GSPS2PDF(e.data, ({ pdfDataURL }) => self.postMessage(pdfDataURL));
});
console.log('Worker ready');

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
export async function compressWithGhostScript(dataStruct) {
const worker = new Worker(
new URL('./background-worker.js', import.meta.url),
{ type: 'module' }
);
worker.postMessage({ data: dataStruct, target: 'wasm' });
return new Promise((resolve, reject) => {
const listener = (e) => {
resolve(e.data);
worker.removeEventListener('message', listener);
setTimeout(() => worker.terminate(), 0);
};
worker.addEventListener('message', listener);
});
}

View 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>
)
}
]}
/>
);
}

View 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'))
});

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,44 @@
import { InitialValuesType } from './types';
import { compressWithGhostScript } from '../../../../lib/ghostscript/worker-init';
/**
* 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);
}
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();
});
}

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