Merge pull request #150 from omenmn/pdftopng

feat: pdf to png
This commit is contained in:
Ibrahima G. Coulibaly
2025-07-07 02:31:45 +01:00
committed by GitHub
8 changed files with 506 additions and 185 deletions

393
.idea/workspace.xml generated
View File

@@ -4,17 +4,18 @@
<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="fix: tsc">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/package-lock.json" beforeDir="false" afterPath="$PROJECT_DIR$/package-lock.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/package.json" beforeDir="false" afterPath="$PROJECT_DIR$/package.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/result/ResultFooter.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/result/ResultFooter.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/components/result/ToolMultiFileResult.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/result/ToolMultiFileResult.tsx" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FormatOnSaveOptions">
<option name="myRunOnSave" value="true" />
</component>
<component name="Git.Merge.Settings">
<option name="BRANCH" value="origin/main" />
</component>
@@ -22,7 +23,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="fork/C043/main" />
</map>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@@ -41,142 +42,173 @@
&quot;state&quot;: &quot;OPEN&quot;
}
}</component>
<component name="GitHubPullRequestState">{
&quot;prStates&quot;: [
<component name="GitHubPullRequestState"><![CDATA[{
"prStates": [
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts51PkS9&quot;,
&quot;number&quot;: 22
"id": {
"id": "PR_kwDOMJIfts51PkS9",
"number": 22
},
&quot;lastSeen&quot;: 1741207144695
"lastSeen": 1741207144695
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6NiNYl&quot;,
&quot;number&quot;: 32
"id": {
"id": "PR_kwDOMJIfts6NiNYl",
"number": 32
},
&quot;lastSeen&quot;: 1741209723869
"lastSeen": 1741209723869
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6Nheyd&quot;,
&quot;number&quot;: 31
"id": {
"id": "PR_kwDOMJIfts6Nheyd",
"number": 31
},
&quot;lastSeen&quot;: 1741213371410
"lastSeen": 1741213371410
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6NmRBs&quot;,
&quot;number&quot;: 33
"id": {
"id": "PR_kwDOMJIfts6NmRBs",
"number": 33
},
&quot;lastSeen&quot;: 1741282429036
"lastSeen": 1741282429036
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts5zyFTs&quot;,
&quot;number&quot;: 15
"id": {
"id": "PR_kwDOMJIfts5zyFTs",
"number": 15
},
&quot;lastSeen&quot;: 1741535540953
"lastSeen": 1741535540953
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6QQB3c&quot;,
&quot;number&quot;: 59
"id": {
"id": "PR_kwDOMJIfts6QQB3c",
"number": 59
},
&quot;lastSeen&quot;: 1743018960900
"lastSeen": 1743018960900
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6QMPEg&quot;,
&quot;number&quot;: 58
"id": {
"id": "PR_kwDOMJIfts6QMPEg",
"number": 58
},
&quot;lastSeen&quot;: 1743019452983
"lastSeen": 1743019452983
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6QZvRI&quot;,
&quot;number&quot;: 61
"id": {
"id": "PR_kwDOMJIfts6QZvRI",
"number": 61
},
&quot;lastSeen&quot;: 1743103196866
"lastSeen": 1743103196866
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6QqPrQ&quot;,
&quot;number&quot;: 73
"id": {
"id": "PR_kwDOMJIfts6QqPrQ",
"number": 73
},
&quot;lastSeen&quot;: 1743265865001
"lastSeen": 1743265865001
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6Qp5nI&quot;,
&quot;number&quot;: 72
"id": {
"id": "PR_kwDOMJIfts6Qp5nI",
"number": 72
},
&quot;lastSeen&quot;: 1743338472110
"lastSeen": 1743338472110
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6QsjlS&quot;,
&quot;number&quot;: 76
"id": {
"id": "PR_kwDOMJIfts6QsjlS",
"number": 76
},
&quot;lastSeen&quot;: 1743352150953
"lastSeen": 1743352150953
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6Q0JBe&quot;,
&quot;number&quot;: 82
"id": {
"id": "PR_kwDOMJIfts6Q0JBe",
"number": 82
},
&quot;lastSeen&quot;: 1743470267269
"lastSeen": 1743470267269
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6UE9-x&quot;,
&quot;number&quot;: 102
"id": {
"id": "PR_kwDOMJIfts6UE9-x",
"number": 102
},
&quot;lastSeen&quot;: 1747171977348
"lastSeen": 1747171977348
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6XPua_&quot;,
&quot;number&quot;: 117
"id": {
"id": "PR_kwDOMJIfts6XPua_",
"number": 117
},
&quot;lastSeen&quot;: 1747929835864
"lastSeen": 1747929835864
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6XY-mZ&quot;,
&quot;number&quot;: 119
"id": {
"id": "PR_kwDOMJIfts6XY-mZ",
"number": 119
},
&quot;lastSeen&quot;: 1748028108508
"lastSeen": 1748028108508
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6Xdz4n&quot;,
&quot;number&quot;: 120
"id": {
"id": "PR_kwDOMJIfts6Xdz4n",
"number": 120
},
&quot;lastSeen&quot;: 1748282672214
"lastSeen": 1748282672214
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6X_zxl&quot;,
&quot;number&quot;: 131
"id": {
"id": "PR_kwDOMJIfts6X_zxl",
"number": 131
},
&quot;lastSeen&quot;: 1748881279494
"lastSeen": 1748881279494
},
{
&quot;id&quot;: {
&quot;id&quot;: &quot;PR_kwDOMJIfts6XsHfL&quot;,
&quot;number&quot;: 128
"id": {
"id": "PR_kwDOMJIfts6bhieT",
"number": 152
},
&quot;lastSeen&quot;: 1748881318360
"lastSeen": 1751848489082
},
{
"id": {
"id": "PR_kwDOMJIfts6dOyRk",
"number": 154
},
"lastSeen": 1751849436454
},
{
"id": {
"id": "PR_kwDOMJIfts6cHjNi",
"number": 153
},
"lastSeen": 1751849501498
},
{
"id": {
"id": "PR_kwDOMJIfts6Zs1FN",
"number": 145
},
"lastSeen": 1751849770308
},
{
"id": {
"id": "PR_kwDOMJIfts6bgKi9",
"number": 150
},
"lastSeen": 1751850367300
}
]
}</component>
}]]></component>
<component name="GithubPullRequestsUISettings">{
&quot;selectedUrlAndAccountId&quot;: {
&quot;url&quot;: &quot;https://github.com/iib0011/omni-tools.git&quot;,
&quot;accountId&quot;: &quot;45f8cd51-000f-4ba4-a4c6-c4d96ac9b1e5&quot;
}
}</component>
<component name="GoLibraries">
<option name="indexEntireGoPath" value="true" />
</component>
<component name="HighlightingSettingsPerFile">
<setting file="file://$PROJECT_DIR$/node_modules/react-image-crop/dist/index.d.ts" root0="SKIP_INSPECTION" />
</component>
@@ -199,56 +231,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/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": "#150 on fork/omenmn/pdftopng",
"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>
}]]></component>
<component name="ReactDesignerToolWindowState">
<option name="myId2Visible">
<map>
@@ -353,11 +385,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,46 +494,7 @@
<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>
</task>
<task id="LOCAL-00153" summary="chore: remove unnecessary files">
<option name="closed" value="true" />
<created>1741492943849</created>
<option name="number" value="00153" />
<option name="presentableId" value="LOCAL-00153" />
<option name="project" value="LOCAL" />
<updated>1741492943849</updated>
</task>
<task id="LOCAL-00154" summary="refactor: validateJson">
<option name="closed" value="true" />
<created>1741535390090</created>
<option name="number" value="00154" />
<option name="presentableId" value="LOCAL-00154" />
<option name="project" value="LOCAL" />
<updated>1741535390090</updated>
</task>
<task id="LOCAL-00155" summary="refactor: use ToolContent">
<option name="closed" value="true" />
<created>1741540939154</created>
<option name="number" value="00155" />
<option name="presentableId" value="LOCAL-00155" />
<option name="project" value="LOCAL" />
<updated>1741540939154</updated>
</task>
<task id="LOCAL-00156" summary="feat: missing tools">
<option name="closed" value="true" />
<created>1741542318259</created>
<option name="number" value="00156" />
<option name="presentableId" value="LOCAL-00156" />
<option name="project" value="LOCAL" />
<updated>1741542318259</updated>
<workItem from="1751846528195" duration="4358000" />
</task>
<task id="LOCAL-00157" summary="refactor: use ToolContent">
<option name="closed" value="true" />
@@ -855,7 +848,47 @@
<option name="project" value="LOCAL" />
<updated>1749147227565</updated>
</task>
<option name="localTasksCounter" value="201" />
<task id="LOCAL-00201" summary="chore: rename from Omni Tools to OmniTools">
<option name="closed" value="true" />
<created>1751630993003</created>
<option name="number" value="00201" />
<option name="presentableId" value="LOCAL-00201" />
<option name="project" value="LOCAL" />
<updated>1751630993003</updated>
</task>
<task id="LOCAL-00202" summary="fix: tools by category page title">
<option name="closed" value="true" />
<created>1751846877842</created>
<option name="number" value="00202" />
<option name="presentableId" value="LOCAL-00202" />
<option name="project" value="LOCAL" />
<updated>1751846877842</updated>
</task>
<task id="LOCAL-00203" summary="chore: use scrollY">
<option name="closed" value="true" />
<created>1751848478091</created>
<option name="number" value="00203" />
<option name="presentableId" value="LOCAL-00203" />
<option name="project" value="LOCAL" />
<updated>1751848478091</updated>
</task>
<task id="LOCAL-00204" summary="chore: remove flip x and y">
<option name="closed" value="true" />
<created>1751849423899</created>
<option name="number" value="00204" />
<option name="presentableId" value="LOCAL-00204" />
<option name="project" value="LOCAL" />
<updated>1751849423899</updated>
</task>
<task id="LOCAL-00205" summary="fix: tsc">
<option name="closed" value="true" />
<created>1751850152784</created>
<option name="number" value="00205" />
<option name="presentableId" value="LOCAL-00205" />
<option name="project" value="LOCAL" />
<updated>1751850152784</updated>
</task>
<option name="localTasksCounter" value="206" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -902,11 +935,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" />
<MESSAGE value="fix: stars button width for 1k+ " />
<MESSAGE value="feat: compress pdf" />
<MESSAGE value="refactor: compress pdf" />
@@ -927,7 +955,16 @@
<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="chore: rename from Omni Tools to OmniTools" />
<MESSAGE value="fix: tools by category page title" />
<MESSAGE value="chore: use scrollY" />
<MESSAGE value="chore: remove flip x and y" />
<MESSAGE value="fix: tsc" />
<option name="LAST_COMMIT_MESSAGE" value="fix: tsc" />
</component>
<component name="VgoProject">
<integration-enabled>false</integration-enabled>
<settings-migrated>true</settings-migrated>
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />

6
package-lock.json generated
View File

@@ -8680,9 +8680,9 @@
"license": "0BSD"
},
"node_modules/pdfjs-dist": {
"version": "5.2.133",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.2.133.tgz",
"integrity": "sha512-abE6ZWDxztt+gGFzfm4bX2ggfxUk9wsDEoFzIJm9LozaY3JdXR7jyLK4Bjs+XLXplCduuWS1wGhPC4tgTn/kzg==",
"version": "5.3.31",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.3.31.tgz",
"integrity": "sha512-EhPdIjNX0fcdwYQO+e3BAAJPXt+XI29TZWC7COhIXs/K0JHcUt1Gdz1ITpebTwVMFiLsukdUZ3u0oTO7jij+VA==",
"license": "Apache-2.0",
"engines": {
"node": ">=20.16.0 || >=22.3.0"

View File

@@ -8,12 +8,14 @@ export default function ResultFooter({
handleDownload,
handleCopy,
disabled,
hideCopy
hideCopy,
downloadLabel = 'Download'
}: {
handleDownload: () => void;
handleCopy: () => void;
handleCopy?: () => void;
disabled?: boolean;
hideCopy?: boolean;
downloadLabel?: string;
}) {
return (
<Stack mt={1} direction={'row'} spacing={2}>
@@ -22,7 +24,7 @@ export default function ResultFooter({
onClick={handleDownload}
startIcon={<DownloadIcon />}
>
Save as
{downloadLabel}
</Button>
{!hideCopy && (
<Button

View File

@@ -0,0 +1,145 @@
import {
Box,
CircularProgress,
Typography,
useTheme,
Button
} from '@mui/material';
import InputHeader from '../InputHeader';
import greyPattern from '@assets/grey-pattern.png';
import { globalInputHeight } from '../../config/uiConfig';
import ResultFooter from './ResultFooter';
export default function ToolFileResult({
title = 'Result',
value,
zipFile,
loading,
loadingText
}: {
title?: string;
value: File[];
zipFile?: File | null;
loading?: boolean;
loadingText?: string;
}) {
const theme = useTheme();
const getFileType = (
file: File
): 'image' | 'video' | 'audio' | 'pdf' | 'unknown' => {
if (file.type.startsWith('image/')) return 'image';
if (file.type.startsWith('video/')) return 'video';
if (file.type.startsWith('audio/')) return 'audio';
if (file.type.startsWith('application/pdf')) return 'pdf';
return 'unknown';
};
const handleDownload = (file: File) => {
const url = URL.createObjectURL(file);
const a = document.createElement('a');
a.href = url;
a.download = file.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
return (
<Box>
<InputHeader title={title} />
<Box
sx={{
width: '100%',
height: globalInputHeight,
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: 2,
border: 1,
borderRadius: 2,
boxShadow: '5',
bgcolor: 'background.paper',
alignItems: 'center',
p: 2
}}
>
{loading ? (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
height: globalInputHeight
}}
>
<CircularProgress />
<Typography variant="body2" sx={{ mt: 2 }}>
{loadingText}... This may take a moment.
</Typography>
</Box>
) : (
value.length > 0 &&
value.map((file, idx) => {
const preview = URL.createObjectURL(file);
const fileType = getFileType(file);
return (
<Box
key={idx}
sx={{
backgroundImage:
fileType === 'image' && theme.palette.mode !== 'dark'
? `url(${greyPattern})`
: 'none',
p: 1,
border: '1px solid #ddd',
borderRadius: 2,
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}}
>
{fileType === 'image' && (
<img
src={preview}
alt={`Preview ${idx}`}
style={{ maxWidth: '100%', maxHeight: 300 }}
/>
)}
{fileType === 'video' && (
<video src={preview} controls style={{ maxWidth: '100%' }} />
)}
{fileType === 'audio' && (
<audio src={preview} controls style={{ width: '100%' }} />
)}
{fileType === 'pdf' && (
<iframe src={preview} width="100%" height="400px" />
)}
{fileType === 'unknown' && (
<Typography>File ready. Click below to download.</Typography>
)}
<Button
onClick={() => handleDownload(file)}
size="small"
sx={{ mt: 1 }}
variant="contained"
>
Download {file.name}
</Button>
</Box>
);
})
)}
</Box>
<ResultFooter
downloadLabel={'Download All as ZIP'}
hideCopy
disabled={!zipFile}
handleDownload={() => zipFile && handleDownload(zipFile)}
/>
</Box>
);
}

View File

@@ -1,3 +1,4 @@
import { tool as pdfPdfToPng } from './pdf-to-png/meta';
import { tool as pdfRotatePdf } from './rotate-pdf/meta';
import { meta as splitPdfMeta } from './split-pdf/meta';
import { meta as mergePdf } from './merge-pdf/meta';
@@ -12,5 +13,6 @@ export const pdfTools: DefinedTool[] = [
compressPdfTool,
protectPdfTool,
mergePdf,
pdfToEpub
pdfToEpub,
pdfPdfToPng
];

View File

@@ -0,0 +1,70 @@
import { useState } from 'react';
import ToolContent from '@components/ToolContent';
import ToolPdfInput from '@components/input/ToolPdfInput';
import { ToolComponentProps } from '@tools/defineTool';
import { convertPdfToPngImages } from './service';
import ToolMultiFileResult from '@components/result/ToolMultiFileResult';
type ImagePreview = {
blob: Blob;
url: string;
filename: string;
};
export default function PdfToPng({ title }: ToolComponentProps) {
const [input, setInput] = useState<File | null>(null);
const [images, setImages] = useState<ImagePreview[]>([]);
const [zipBlob, setZipBlob] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
const compute = async (_: {}, file: File | null) => {
if (!file) return;
setLoading(true);
setImages([]);
setZipBlob(null);
try {
const { images, zipFile } = await convertPdfToPngImages(file);
setImages(images);
setZipBlob(zipFile);
} catch (err) {
console.error('Conversion failed:', err);
} finally {
setLoading(false);
}
};
return (
<ToolContent
title={title}
input={input}
setInput={setInput}
initialValues={{}}
compute={compute}
inputComponent={
<ToolPdfInput
value={input}
onChange={setInput}
accept={['application/pdf']}
title="Upload a PDF"
/>
}
resultComponent={
<ToolMultiFileResult
title="Converted PNG Pages"
value={images.map((img) => {
return new File([img.blob], img.filename, { type: 'image/png' });
})}
zipFile={zipBlob}
loading={loading}
loadingText="Converting PDF pages"
/>
}
getGroups={null}
toolInfo={{
title: 'Convert PDF pages into PNG images',
description:
'Upload your PDF and get each page rendered as a high-quality PNG. You can preview, download individually, or get all images in a ZIP.'
}}
/>
);
}

View File

@@ -0,0 +1,14 @@
import { defineTool } from '@tools/defineTool';
import { lazy } from 'react';
export const tool = defineTool('pdf', {
name: 'PDF to PNG',
path: 'pdf-to-png',
icon: 'mdi:image-multiple', // Iconify icon ID
description: 'Transform PDF documents into PNG panels.',
shortDescription: 'Convert PDF into PNG images',
keywords: ['pdf', 'png', 'convert', 'image', 'extract', 'pages'],
longDescription:
'Upload a PDF and convert each page into a high-quality PNG image directly in your browser. This tool is ideal for extracting visual content or sharing individual pages. No data is uploaded — everything runs locally.',
component: lazy(() => import('./index'))
});

View File

@@ -0,0 +1,51 @@
import * as pdfjsLib from 'pdfjs-dist';
import pdfjsWorker from 'pdfjs-dist/build/pdf.worker.min?url';
import JSZip from 'jszip';
pdfjsLib.GlobalWorkerOptions.workerSrc = pdfjsWorker;
type ImagePreview = {
blob: Blob;
url: string;
filename: string;
};
export async function convertPdfToPngImages(pdfFile: File): Promise<{
images: ImagePreview[];
zipFile: File;
}> {
const arrayBuffer = await pdfFile.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
const zip = new JSZip();
const images: ImagePreview[] = [];
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const viewport = page.getViewport({ scale: 2 });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d')!;
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: context, viewport }).promise;
const blob = await new Promise<Blob>((resolve) =>
canvas.toBlob((b) => b && resolve(b), 'image/png')
);
const filename = `page-${i}.png`;
const url = URL.createObjectURL(blob);
images.push({ blob, url, filename });
zip.file(filename, blob);
}
const zipBuffer = await zip.generateAsync({ type: 'arraybuffer' });
const zipFile = new File(
[zipBuffer],
pdfFile.name.replace(/\.pdf$/i, '-pages.zip'),
{ type: 'application/zip' }
);
return { images, zipFile };
}