mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-09-26 09:29:30 +02:00
Merge pull request #102 from rohit267/feat/pdf-merge
[Feature] Merge PDFs
This commit is contained in:
282
.idea/workspace.xml
generated
282
.idea/workspace.xml
generated
@@ -4,13 +4,9 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="ci: on tag push, push docker images">
|
<list default="true" id="b30e2810-c4c1-4aad-b134-794e52cc1c7d" name="Changes" comment="fix: misc">
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/components/input/ToolVideoInput.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/components/input/ToolVideoInput.tsx" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/src/pages/tools/pdf/split-pdf/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/pdf/split-pdf/index.tsx" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/compress/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/compress/index.tsx" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/loop/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/loop/index.tsx" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/rotate/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/rotate/index.tsx" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/src/pages/tools/video/trim/index.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/pages/tools/video/trim/index.tsx" afterDir="false" />
|
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -27,7 +23,7 @@
|
|||||||
<option name="PUSH_AUTO_UPDATE" value="true" />
|
<option name="PUSH_AUTO_UPDATE" value="true" />
|
||||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||||
<map>
|
<map>
|
||||||
<entry key="$PROJECT_DIR$" value="chesterkxng" />
|
<entry key="$PROJECT_DIR$" value="main" />
|
||||||
</map>
|
</map>
|
||||||
</option>
|
</option>
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
@@ -46,94 +42,101 @@
|
|||||||
"state": "OPEN"
|
"state": "OPEN"
|
||||||
}
|
}
|
||||||
}</component>
|
}</component>
|
||||||
<component name="GitHubPullRequestState">{
|
<component name="GitHubPullRequestState"><![CDATA[{
|
||||||
"prStates": [
|
"prStates": [
|
||||||
{
|
{
|
||||||
"id": {
|
"id": {
|
||||||
"id": "PR_kwDOMJIfts51PkS9",
|
"id": "PR_kwDOMJIfts51PkS9",
|
||||||
"number": 22
|
"number": 22
|
||||||
},
|
},
|
||||||
"lastSeen": 1741207144695
|
"lastSeen": 1741207144695
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": {
|
"id": {
|
||||||
"id": "PR_kwDOMJIfts6NiNYl",
|
"id": "PR_kwDOMJIfts6NiNYl",
|
||||||
"number": 32
|
"number": 32
|
||||||
},
|
},
|
||||||
"lastSeen": 1741209723869
|
"lastSeen": 1741209723869
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": {
|
"id": {
|
||||||
"id": "PR_kwDOMJIfts6Nheyd",
|
"id": "PR_kwDOMJIfts6Nheyd",
|
||||||
"number": 31
|
"number": 31
|
||||||
},
|
},
|
||||||
"lastSeen": 1741213371410
|
"lastSeen": 1741213371410
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": {
|
"id": {
|
||||||
"id": "PR_kwDOMJIfts6NmRBs",
|
"id": "PR_kwDOMJIfts6NmRBs",
|
||||||
"number": 33
|
"number": 33
|
||||||
},
|
},
|
||||||
"lastSeen": 1741282429036
|
"lastSeen": 1741282429036
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": {
|
"id": {
|
||||||
"id": "PR_kwDOMJIfts5zyFTs",
|
"id": "PR_kwDOMJIfts5zyFTs",
|
||||||
"number": 15
|
"number": 15
|
||||||
},
|
},
|
||||||
"lastSeen": 1741535540953
|
"lastSeen": 1741535540953
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": {
|
"id": {
|
||||||
"id": "PR_kwDOMJIfts6QQB3c",
|
"id": "PR_kwDOMJIfts6QQB3c",
|
||||||
"number": 59
|
"number": 59
|
||||||
},
|
},
|
||||||
"lastSeen": 1743018960900
|
"lastSeen": 1743018960900
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": {
|
"id": {
|
||||||
"id": "PR_kwDOMJIfts6QMPEg",
|
"id": "PR_kwDOMJIfts6QMPEg",
|
||||||
"number": 58
|
"number": 58
|
||||||
},
|
},
|
||||||
"lastSeen": 1743019452983
|
"lastSeen": 1743019452983
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": {
|
"id": {
|
||||||
"id": "PR_kwDOMJIfts6QZvRI",
|
"id": "PR_kwDOMJIfts6QZvRI",
|
||||||
"number": 61
|
"number": 61
|
||||||
},
|
},
|
||||||
"lastSeen": 1743103196866
|
"lastSeen": 1743103196866
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": {
|
"id": {
|
||||||
"id": "PR_kwDOMJIfts6QqPrQ",
|
"id": "PR_kwDOMJIfts6QqPrQ",
|
||||||
"number": 73
|
"number": 73
|
||||||
},
|
},
|
||||||
"lastSeen": 1743265865001
|
"lastSeen": 1743265865001
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": {
|
"id": {
|
||||||
"id": "PR_kwDOMJIfts6Qp5nI",
|
"id": "PR_kwDOMJIfts6Qp5nI",
|
||||||
"number": 72
|
"number": 72
|
||||||
},
|
},
|
||||||
"lastSeen": 1743338472110
|
"lastSeen": 1743338472110
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": {
|
"id": {
|
||||||
"id": "PR_kwDOMJIfts6QsjlS",
|
"id": "PR_kwDOMJIfts6QsjlS",
|
||||||
"number": 76
|
"number": 76
|
||||||
},
|
},
|
||||||
"lastSeen": 1743352150953
|
"lastSeen": 1743352150953
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": {
|
"id": {
|
||||||
"id": "PR_kwDOMJIfts6Q0JBe",
|
"id": "PR_kwDOMJIfts6Q0JBe",
|
||||||
"number": 82
|
"number": 82
|
||||||
},
|
},
|
||||||
"lastSeen": 1743470267269
|
"lastSeen": 1743470267269
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": {
|
||||||
|
"id": "PR_kwDOMJIfts6UE9-x",
|
||||||
|
"number": 102
|
||||||
|
},
|
||||||
|
"lastSeen": 1747171977348
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}</component>
|
}]]></component>
|
||||||
<component name="GithubPullRequestsUISettings">{
|
<component name="GithubPullRequestsUISettings">{
|
||||||
"selectedUrlAndAccountId": {
|
"selectedUrlAndAccountId": {
|
||||||
"url": "https://github.com/iib0011/omni-tools.git",
|
"url": "https://github.com/iib0011/omni-tools.git",
|
||||||
@@ -162,56 +165,56 @@
|
|||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent">{
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"ASKED_ADD_EXTERNAL_FILES": "true",
|
"ASKED_ADD_EXTERNAL_FILES": "true",
|
||||||
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
"Docker.Dockerfile build.executor": "Run",
|
"Docker.Dockerfile build.executor": "Run",
|
||||||
"Docker.Dockerfile.executor": "Run",
|
"Docker.Dockerfile.executor": "Run",
|
||||||
"Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
|
"Playwright.Create transparent PNG.should make png color transparent.executor": "Run",
|
||||||
"Playwright.JoinText Component.executor": "Run",
|
"Playwright.JoinText Component.executor": "Run",
|
||||||
"Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
|
"Playwright.JoinText Component.should merge text pieces with specified join character.executor": "Run",
|
||||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"RunOnceActivity.git.unshallow": "true",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"Vitest.compute function (1).executor": "Run",
|
"Vitest.compute function (1).executor": "Run",
|
||||||
"Vitest.compute function.executor": "Run",
|
"Vitest.compute function.executor": "Run",
|
||||||
"Vitest.mergeText.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 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.mergeText.should merge lines, preserve blank lines and trailing spaces when both deleteBlankLines and deleteTrailingSpaces are false.executor": "Run",
|
||||||
"Vitest.parsePageRanges.executor": "Run",
|
"Vitest.parsePageRanges.executor": "Run",
|
||||||
"Vitest.removeDuplicateLines function.executor": "Run",
|
"Vitest.removeDuplicateLines function.executor": "Run",
|
||||||
"Vitest.removeDuplicateLines function.newlines option.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.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 (regexp mode).should return the original text when passed an invalid regexp.executor": "Run",
|
||||||
"Vitest.replaceText function.executor": "Run",
|
"Vitest.replaceText function.executor": "Run",
|
||||||
"Vitest.timeBetweenDates.executor": "Run",
|
"Vitest.timeBetweenDates.executor": "Run",
|
||||||
"git-widget-placeholder": "main",
|
"git-widget-placeholder": "#102 on fork/rohit267/feat/pdf-merge",
|
||||||
"ignore.virus.scanning.warn.message": "true",
|
"ignore.virus.scanning.warn.message": "true",
|
||||||
"kotlin-language-version-configured": "true",
|
"kotlin-language-version-configured": "true",
|
||||||
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src",
|
"last_opened_file_path": "C:/Users/Ibrahima/IdeaProjects/omni-tools/src",
|
||||||
"node.js.detected.package.eslint": "true",
|
"node.js.detected.package.eslint": "true",
|
||||||
"node.js.detected.package.tslint": "true",
|
"node.js.detected.package.tslint": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"npm.build.executor": "Run",
|
"npm.build.executor": "Run",
|
||||||
"npm.dev.executor": "Run",
|
"npm.dev.executor": "Run",
|
||||||
"npm.lint.executor": "Run",
|
"npm.lint.executor": "Run",
|
||||||
"npm.prebuild.executor": "Run",
|
"npm.prebuild.executor": "Run",
|
||||||
"npm.script:create:tool.executor": "Run",
|
"npm.script:create:tool.executor": "Run",
|
||||||
"npm.test.executor": "Run",
|
"npm.test.executor": "Run",
|
||||||
"npm.test:e2e.executor": "Run",
|
"npm.test:e2e.executor": "Run",
|
||||||
"npm.test:e2e:run.executor": "Run",
|
"npm.test:e2e:run.executor": "Run",
|
||||||
"prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
|
"prettierjs.PrettierConfiguration.Package": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\prettier",
|
||||||
"project.structure.last.edited": "Problems",
|
"project.structure.last.edited": "Problems",
|
||||||
"project.structure.proportion": "0.0",
|
"project.structure.proportion": "0.0",
|
||||||
"project.structure.side.proportion": "0.2",
|
"project.structure.side.proportion": "0.2",
|
||||||
"settings.editor.selected.configurable": "refactai_advanced_settings",
|
"settings.editor.selected.configurable": "refactai_advanced_settings",
|
||||||
"ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
|
"ts.external.directory.path": "C:\\Users\\Ibrahima\\IdeaProjects\\omni-tools\\node_modules\\typescript\\lib",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
}
|
}
|
||||||
}</component>
|
}]]></component>
|
||||||
<component name="ReactDesignerToolWindowState">
|
<component name="ReactDesignerToolWindowState">
|
||||||
<option name="myId2Visible">
|
<option name="myId2Visible">
|
||||||
<map>
|
<map>
|
||||||
@@ -416,31 +419,10 @@
|
|||||||
<workItem from="1743782726563" duration="2444000" />
|
<workItem from="1743782726563" duration="2444000" />
|
||||||
<workItem from="1743811558991" duration="1279000" />
|
<workItem from="1743811558991" duration="1279000" />
|
||||||
<workItem from="1745523972292" duration="3000" />
|
<workItem from="1745523972292" duration="3000" />
|
||||||
<workItem from="1745687713234" duration="591000" />
|
<workItem from="1745687713234" duration="1747000" />
|
||||||
</task>
|
<workItem from="1745775228478" duration="1221000" />
|
||||||
<task id="LOCAL-00144" summary="feat: stringify json">
|
<workItem from="1745835676024" duration="68000" />
|
||||||
<option name="closed" value="true" />
|
<workItem from="1747171958176" duration="1105000" />
|
||||||
<created>1741417920442</created>
|
|
||||||
<option name="number" value="00144" />
|
|
||||||
<option name="presentableId" value="LOCAL-00144" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1741417920442</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00145" summary="feat: arithmetic sequence">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1741419142510</created>
|
|
||||||
<option name="number" value="00145" />
|
|
||||||
<option name="presentableId" value="LOCAL-00145" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1741419142510</updated>
|
|
||||||
</task>
|
|
||||||
<task id="LOCAL-00146" summary="style: tools height">
|
|
||||||
<option name="closed" value="true" />
|
|
||||||
<created>1741419188990</created>
|
|
||||||
<option name="number" value="00146" />
|
|
||||||
<option name="presentableId" value="LOCAL-00146" />
|
|
||||||
<option name="project" value="LOCAL" />
|
|
||||||
<updated>1741419188990</updated>
|
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00147" summary="chore: update meta">
|
<task id="LOCAL-00147" summary="chore: update meta">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -810,7 +792,31 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1743811980098</updated>
|
<updated>1743811980098</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="193" />
|
<task id="LOCAL-00193" summary="fix: add mkv to supported videos">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1745688369521</created>
|
||||||
|
<option name="number" value="00193" />
|
||||||
|
<option name="presentableId" value="LOCAL-00193" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1745688369521</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00194" summary="feat: drag and drop">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1745688866294</created>
|
||||||
|
<option name="number" value="00194" />
|
||||||
|
<option name="presentableId" value="LOCAL-00194" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1745688866294</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00195" summary="fix: misc">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1747172914927</created>
|
||||||
|
<option name="number" value="00195" />
|
||||||
|
<option name="presentableId" value="LOCAL-00195" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1747172914927</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="196" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -857,10 +863,6 @@
|
|||||||
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
|
<option name="CHECK_CODE_SMELLS_BEFORE_PROJECT_COMMIT" value="false" />
|
||||||
<option name="CHECK_NEW_TODO" value="false" />
|
<option name="CHECK_NEW_TODO" value="false" />
|
||||||
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
|
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
|
||||||
<MESSAGE value="chore: result file name" />
|
|
||||||
<MESSAGE value="chore: text result extensions" />
|
|
||||||
<MESSAGE value="chore: show new tools in landing" />
|
|
||||||
<MESSAGE value="chore: zoom on hover" />
|
|
||||||
<MESSAGE value="refactor: time between dates" />
|
<MESSAGE value="refactor: time between dates" />
|
||||||
<MESSAGE value="fix: typos" />
|
<MESSAGE value="fix: typos" />
|
||||||
<MESSAGE value="feat: compress video" />
|
<MESSAGE value="feat: compress video" />
|
||||||
@@ -882,7 +884,11 @@
|
|||||||
<MESSAGE value="feat: image to text" />
|
<MESSAGE value="feat: image to text" />
|
||||||
<MESSAGE value="chore: hideCopy if video or audio" />
|
<MESSAGE value="chore: hideCopy if video or audio" />
|
||||||
<MESSAGE value="chore: readme img and fix broken link" />
|
<MESSAGE value="chore: readme img and fix broken link" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="chore: readme img and fix broken link" />
|
<MESSAGE value="fix: add mkv to supported videos" />
|
||||||
|
<MESSAGE value="feat: drag and drop" />
|
||||||
|
<MESSAGE value="Merge branch 'feat/pdf-merge' of git-rohit:rohit267/omni-tools into feat/pdf-merge" />
|
||||||
|
<MESSAGE value="fix: misc" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="fix: misc" />
|
||||||
</component>
|
</component>
|
||||||
<component name="XSLT-Support.FileAssociations.UIState">
|
<component name="XSLT-Support.FileAssociations.UIState">
|
||||||
<expand />
|
<expand />
|
||||||
|
6313
pnpm-lock.yaml
generated
6313
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@ import {
|
|||||||
import { globalInputHeight } from '../../config/uiConfig';
|
import { globalInputHeight } from '../../config/uiConfig';
|
||||||
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
|
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
|
||||||
import greyPattern from '@assets/grey-pattern.png';
|
import greyPattern from '@assets/grey-pattern.png';
|
||||||
|
import { isArray } from 'lodash';
|
||||||
|
|
||||||
interface BaseFileInputComponentProps extends BaseFileInputProps {
|
interface BaseFileInputComponentProps extends BaseFileInputProps {
|
||||||
children: (props: { preview: string | undefined }) => ReactNode;
|
children: (props: { preview: string | undefined }) => ReactNode;
|
||||||
@@ -32,14 +33,18 @@ export default function BaseFileInput({
|
|||||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (value) {
|
try {
|
||||||
const objectUrl = createObjectURL(value);
|
if (isArray(value)) {
|
||||||
|
const objectUrl = createObjectURL(value[0]);
|
||||||
setPreview(objectUrl);
|
setPreview(objectUrl);
|
||||||
|
|
||||||
return () => revokeObjectURL(objectUrl);
|
return () => revokeObjectURL(objectUrl);
|
||||||
} else {
|
} else {
|
||||||
setPreview(null);
|
setPreview(null);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error previewing file:', error);
|
||||||
|
}
|
||||||
}, [value]);
|
}, [value]);
|
||||||
|
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -52,9 +57,9 @@ export default function BaseFileInput({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
if (value) {
|
if (isArray(value)) {
|
||||||
const blob = new Blob([value], { type: value.type });
|
const blob = new Blob([value[0]], { type: value[0].type });
|
||||||
const clipboardItem = new ClipboardItem({ [value.type]: blob });
|
const clipboardItem = new ClipboardItem({ [value[0].type]: blob });
|
||||||
|
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
.write([clipboardItem])
|
.write([clipboardItem])
|
||||||
@@ -65,6 +70,11 @@ export default function BaseFileInput({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function handleClear() {
|
||||||
|
// @ts-ignore
|
||||||
|
onChange(null);
|
||||||
|
}
|
||||||
|
|
||||||
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@@ -206,13 +216,18 @@ export default function BaseFileInput({
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
|
<InputFooter
|
||||||
|
handleCopy={handleCopy}
|
||||||
|
handleImport={handleImportClick}
|
||||||
|
handleClear={handleClear}
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
ref={fileInputRef}
|
ref={fileInputRef}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
type="file"
|
type="file"
|
||||||
accept={accept.join(',')}
|
accept={accept.join(',')}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
|
multiple={false}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@@ -2,23 +2,32 @@ import { Stack } from '@mui/material';
|
|||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import PublishIcon from '@mui/icons-material/Publish';
|
import PublishIcon from '@mui/icons-material/Publish';
|
||||||
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
|
||||||
import React from 'react';
|
import ClearIcon from '@mui/icons-material/Clear';
|
||||||
|
|
||||||
export default function InputFooter({
|
export default function InputFooter({
|
||||||
handleImport,
|
handleImport,
|
||||||
handleCopy
|
handleCopy,
|
||||||
|
handleClear
|
||||||
}: {
|
}: {
|
||||||
handleImport: () => void;
|
handleImport: () => void;
|
||||||
handleCopy: () => void;
|
handleCopy?: () => void;
|
||||||
|
handleClear?: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Stack mt={1} direction={'row'} spacing={2}>
|
<Stack mt={1} direction={'row'} spacing={2}>
|
||||||
<Button onClick={handleImport} startIcon={<PublishIcon />}>
|
<Button onClick={handleImport} startIcon={<PublishIcon />}>
|
||||||
Import from file
|
Import from file
|
||||||
</Button>
|
</Button>
|
||||||
|
{handleCopy && (
|
||||||
<Button onClick={handleCopy} startIcon={<ContentPasteIcon />}>
|
<Button onClick={handleCopy} startIcon={<ContentPasteIcon />}>
|
||||||
Copy to clipboard
|
Copy to clipboard
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
{handleClear && (
|
||||||
|
<Button onClick={handleClear} startIcon={<ClearIcon />}>
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
176
src/components/input/ToolMultiplePdfInput.tsx
Normal file
176
src/components/input/ToolMultiplePdfInput.tsx
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { ReactNode, useContext, useEffect, useRef, useState } from 'react';
|
||||||
|
import { Box, useTheme } from '@mui/material';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import InputHeader from '../InputHeader';
|
||||||
|
import InputFooter from './InputFooter';
|
||||||
|
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
|
||||||
|
import { isArray } from 'lodash';
|
||||||
|
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||||
|
|
||||||
|
interface MultiPdfInputComponentProps {
|
||||||
|
accept: string[];
|
||||||
|
title?: string;
|
||||||
|
type: 'pdf';
|
||||||
|
value: MultiPdfInput[];
|
||||||
|
onChange: (file: MultiPdfInput[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MultiPdfInput {
|
||||||
|
file: File;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ToolMultiFileInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
accept,
|
||||||
|
title,
|
||||||
|
type
|
||||||
|
}: MultiPdfInputComponentProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||||
|
|
||||||
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = event.target.files;
|
||||||
|
if (files)
|
||||||
|
onChange([
|
||||||
|
...value,
|
||||||
|
...Array.from(files).map((file) => ({ file, order: value.length }))
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImportClick = () => {
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleClear() {
|
||||||
|
onChange([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileNameTruncate(fileName: string) {
|
||||||
|
const maxLength = 10;
|
||||||
|
if (fileName.length > maxLength) {
|
||||||
|
return fileName.slice(0, maxLength) + '...';
|
||||||
|
}
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortList = () => {
|
||||||
|
const list = [...value];
|
||||||
|
list.sort((a, b) => a.order - b.order);
|
||||||
|
onChange(list);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reorderList = (sourceIndex: number, destinationIndex: number) => {
|
||||||
|
console.log(sourceIndex, destinationIndex);
|
||||||
|
if (destinationIndex === sourceIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = [...value];
|
||||||
|
|
||||||
|
if (destinationIndex === 0) {
|
||||||
|
list[sourceIndex].order = list[0].order - 1;
|
||||||
|
sortList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destinationIndex === list.length - 1) {
|
||||||
|
list[sourceIndex].order = list[list.length - 1].order + 1;
|
||||||
|
sortList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destinationIndex < sourceIndex) {
|
||||||
|
list[sourceIndex].order =
|
||||||
|
(list[destinationIndex].order + list[destinationIndex - 1].order) / 2;
|
||||||
|
sortList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list[sourceIndex].order =
|
||||||
|
(list[destinationIndex].order + list[destinationIndex + 1].order) / 2;
|
||||||
|
sortList();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<InputHeader
|
||||||
|
title={title || 'Input ' + type.charAt(0).toUpperCase() + type.slice(1)}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
height: '300px',
|
||||||
|
|
||||||
|
border: value?.length ? 0 : 1,
|
||||||
|
borderRadius: 2,
|
||||||
|
boxShadow: '5',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
sx={{
|
||||||
|
overflow: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value?.length ? (
|
||||||
|
value.map((file, index) => (
|
||||||
|
<Box
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
margin: 1,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '200px',
|
||||||
|
border: 1,
|
||||||
|
borderRadius: 1,
|
||||||
|
padding: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<PictureAsPdfIcon />
|
||||||
|
<Typography sx={{ marginLeft: 1 }}>
|
||||||
|
{fileNameTruncate(file.file.name)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => {
|
||||||
|
const updatedFiles = value.filter((_, i) => i !== index);
|
||||||
|
onChange(updatedFiles);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✖
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
No files selected
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<InputFooter handleImport={handleImportClick} handleClear={handleClear} />
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
type="file"
|
||||||
|
accept={accept.join(',')}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
multiple={true}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@@ -173,7 +173,9 @@ export default function ToolFileResult({
|
|||||||
disabled={!value}
|
disabled={!value}
|
||||||
handleCopy={handleCopy}
|
handleCopy={handleCopy}
|
||||||
handleDownload={handleDownload}
|
handleDownload={handleDownload}
|
||||||
hideCopy={fileType === 'video' || fileType === 'audio'}
|
hideCopy={
|
||||||
|
fileType === 'video' || fileType === 'audio' || fileType === 'pdf'
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@@ -1,12 +1,14 @@
|
|||||||
import { tool as pdfRotatePdf } from './rotate-pdf/meta';
|
import { tool as pdfRotatePdf } from './rotate-pdf/meta';
|
||||||
import { meta as splitPdfMeta } from './split-pdf/meta';
|
import { meta as splitPdfMeta } from './split-pdf/meta';
|
||||||
|
import { meta as mergePdf } from './merge-pdf/meta';
|
||||||
|
import { DefinedTool } from '@tools/defineTool';
|
||||||
import { tool as compressPdfTool } from './compress-pdf/meta';
|
import { tool as compressPdfTool } from './compress-pdf/meta';
|
||||||
import { tool as protectPdfTool } from './protect-pdf/meta';
|
import { tool as protectPdfTool } from './protect-pdf/meta';
|
||||||
import { DefinedTool } from '@tools/defineTool';
|
|
||||||
|
|
||||||
export const pdfTools: DefinedTool[] = [
|
export const pdfTools: DefinedTool[] = [
|
||||||
splitPdfMeta,
|
splitPdfMeta,
|
||||||
pdfRotatePdf,
|
pdfRotatePdf,
|
||||||
compressPdfTool,
|
compressPdfTool,
|
||||||
protectPdfTool
|
protectPdfTool,
|
||||||
|
mergePdf
|
||||||
];
|
];
|
||||||
|
66
src/pages/tools/pdf/merge-pdf/index.tsx
Normal file
66
src/pages/tools/pdf/merge-pdf/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import ToolFileResult from '@components/result/ToolFileResult';
|
||||||
|
import ToolContent from '@components/ToolContent';
|
||||||
|
import { ToolComponentProps } from '@tools/defineTool';
|
||||||
|
import { mergePdf } from './service';
|
||||||
|
import ToolMultiPdfInput, {
|
||||||
|
MultiPdfInput
|
||||||
|
} from '@components/input/ToolMultiplePdfInput';
|
||||||
|
|
||||||
|
export default function MergePdf({ title }: ToolComponentProps) {
|
||||||
|
const [input, setInput] = useState<MultiPdfInput[]>([]);
|
||||||
|
const [result, setResult] = useState<File | null>(null);
|
||||||
|
const [isProcessing, setIsProcessing] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const compute = async (values: File[], input: MultiPdfInput[]) => {
|
||||||
|
if (input.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsProcessing(true);
|
||||||
|
const mergeResult = await mergePdf(input.map((i) => i.file));
|
||||||
|
setResult(mergeResult);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Error merging PDF:' + error);
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolContent
|
||||||
|
title={title}
|
||||||
|
input={input}
|
||||||
|
setInput={setInput}
|
||||||
|
initialValues={input.map((i) => i.file)}
|
||||||
|
compute={compute}
|
||||||
|
inputComponent={
|
||||||
|
<ToolMultiPdfInput
|
||||||
|
value={input}
|
||||||
|
onChange={(pdfInputs) => {
|
||||||
|
setInput(pdfInputs);
|
||||||
|
}}
|
||||||
|
accept={['application/pdf']}
|
||||||
|
title={'Input PDF'}
|
||||||
|
type="pdf"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
getGroups={null}
|
||||||
|
resultComponent={
|
||||||
|
<ToolFileResult
|
||||||
|
title={'Output merged PDF'}
|
||||||
|
value={result}
|
||||||
|
extension={'pdf'}
|
||||||
|
loading={isProcessing}
|
||||||
|
loadingText={'Extracting pages'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
toolInfo={{
|
||||||
|
title: 'How to Use the Merge PDF Tool?',
|
||||||
|
description: `This tool allows you to merge multiple PDF files into a single document.
|
||||||
|
To use the tool, simply upload the PDF files you want to merge. The tool will then combine all pages from the input files into a single PDF document.`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
12
src/pages/tools/pdf/merge-pdf/meta.ts
Normal file
12
src/pages/tools/pdf/merge-pdf/meta.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineTool } from '@tools/defineTool';
|
||||||
|
import { lazy } from 'react';
|
||||||
|
|
||||||
|
export const meta = defineTool('pdf', {
|
||||||
|
name: 'Merge PDF',
|
||||||
|
shortDescription: 'Merge multiple PDF files into a single document',
|
||||||
|
description: 'Combine multiple PDF files into a single document.',
|
||||||
|
icon: 'material-symbols-light:merge',
|
||||||
|
component: lazy(() => import('./index')),
|
||||||
|
keywords: ['pdf', 'merge', 'extract', 'pages', 'combine', 'document'],
|
||||||
|
path: 'merge-pdf'
|
||||||
|
});
|
43
src/pages/tools/pdf/merge-pdf/service.test.ts
Normal file
43
src/pages/tools/pdf/merge-pdf/service.test.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { parsePageRanges } from './service';
|
||||||
|
|
||||||
|
describe('parsePageRanges', () => {
|
||||||
|
test('should return all pages when input is empty', () => {
|
||||||
|
expect(parsePageRanges('', 5)).toEqual([1, 2, 3, 4, 5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse single page numbers', () => {
|
||||||
|
expect(parsePageRanges('1,3,5', 5)).toEqual([1, 3, 5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse page ranges', () => {
|
||||||
|
expect(parsePageRanges('2-4', 5)).toEqual([2, 3, 4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should parse mixed page numbers and ranges', () => {
|
||||||
|
expect(parsePageRanges('1,3-5', 5)).toEqual([1, 3, 4, 5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle whitespace', () => {
|
||||||
|
expect(parsePageRanges(' 1, 3 - 5 ', 5)).toEqual([1, 3, 4, 5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should ignore invalid page numbers', () => {
|
||||||
|
expect(parsePageRanges('1,a,3', 5)).toEqual([1, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should ignore out-of-range page numbers', () => {
|
||||||
|
expect(parsePageRanges('1,6,3', 5)).toEqual([1, 3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should limit ranges to valid pages', () => {
|
||||||
|
expect(parsePageRanges('0-6', 5)).toEqual([1, 2, 3, 4, 5]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should handle reversed ranges', () => {
|
||||||
|
expect(parsePageRanges('4-2', 5)).toEqual([2, 3, 4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove duplicates', () => {
|
||||||
|
expect(parsePageRanges('1,1,2,2-4,3', 5)).toEqual([1, 2, 3, 4]);
|
||||||
|
});
|
||||||
|
});
|
95
src/pages/tools/pdf/merge-pdf/service.ts
Normal file
95
src/pages/tools/pdf/merge-pdf/service.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { PDFDocument } from 'pdf-lib';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a page range string and returns an array of page numbers
|
||||||
|
* @param pageRangeStr String like "1,3-5,7" to extract pages 1, 3, 4, 5, and 7
|
||||||
|
* @param totalPages Total number of pages in the PDF
|
||||||
|
* @returns Array of page numbers to extract
|
||||||
|
*/
|
||||||
|
export function parsePageRanges(
|
||||||
|
pageRangeStr: string,
|
||||||
|
totalPages: number
|
||||||
|
): number[] {
|
||||||
|
if (!pageRangeStr.trim()) {
|
||||||
|
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageNumbers = new Set<number>();
|
||||||
|
const ranges = pageRangeStr.split(',');
|
||||||
|
|
||||||
|
for (const range of ranges) {
|
||||||
|
const trimmedRange = range.trim();
|
||||||
|
|
||||||
|
if (trimmedRange.includes('-')) {
|
||||||
|
const [start, end] = trimmedRange.split('-').map(Number);
|
||||||
|
if (!isNaN(start) && !isNaN(end)) {
|
||||||
|
// Handle both forward and reversed ranges
|
||||||
|
const normalizedStart = Math.min(start, end);
|
||||||
|
const normalizedEnd = Math.max(start, end);
|
||||||
|
|
||||||
|
for (
|
||||||
|
let i = Math.max(1, normalizedStart);
|
||||||
|
i <= Math.min(totalPages, normalizedEnd);
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
pageNumbers.add(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pageNum = parseInt(trimmedRange, 10);
|
||||||
|
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= totalPages) {
|
||||||
|
pageNumbers.add(pageNum);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...pageNumbers].sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a PDF file based on specified page ranges
|
||||||
|
* @param pdfFile The input PDF file
|
||||||
|
* @param pageRanges String specifying which pages to extract (e.g., "1,3-5,7")
|
||||||
|
* @returns Promise resolving to a new PDF file with only the selected pages
|
||||||
|
*/
|
||||||
|
export async function splitPdf(
|
||||||
|
pdfFile: File,
|
||||||
|
pageRanges: string
|
||||||
|
): Promise<File> {
|
||||||
|
const arrayBuffer = await pdfFile.arrayBuffer();
|
||||||
|
const sourcePdf = await PDFDocument.load(arrayBuffer);
|
||||||
|
const totalPages = sourcePdf.getPageCount();
|
||||||
|
const pagesToExtract = parsePageRanges(pageRanges, totalPages);
|
||||||
|
|
||||||
|
const newPdf = await PDFDocument.create();
|
||||||
|
const copiedPages = await newPdf.copyPages(
|
||||||
|
sourcePdf,
|
||||||
|
pagesToExtract.map((pageNum) => pageNum - 1)
|
||||||
|
);
|
||||||
|
copiedPages.forEach((page) => newPdf.addPage(page));
|
||||||
|
|
||||||
|
const newPdfBytes = await newPdf.save();
|
||||||
|
const newFileName = pdfFile.name.replace('.pdf', '-extracted.pdf');
|
||||||
|
return new File([newPdfBytes], newFileName, { type: 'application/pdf' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges multiple PDF files into a single document
|
||||||
|
* @param pdfFiles Array of PDF files to merge
|
||||||
|
* @returns Promise resolving to a new PDF file with all pages combined
|
||||||
|
*/
|
||||||
|
export async function mergePdf(pdfFiles: File[]): Promise<File> {
|
||||||
|
const mergedPdf = await PDFDocument.create();
|
||||||
|
for (const pdfFile of pdfFiles) {
|
||||||
|
const arrayBuffer = await pdfFile.arrayBuffer();
|
||||||
|
const pdf = await PDFDocument.load(arrayBuffer);
|
||||||
|
const copiedPages = await mergedPdf.copyPages(pdf, pdf.getPageIndices());
|
||||||
|
copiedPages.forEach((page) => mergedPdf.addPage(page));
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedPdfBytes = await mergedPdf.save();
|
||||||
|
const mergedFileName = 'merged.pdf';
|
||||||
|
return new File([mergedPdfBytes], mergedFileName, {
|
||||||
|
type: 'application/pdf'
|
||||||
|
});
|
||||||
|
}
|
@@ -10,6 +10,7 @@ import { InitialValuesType, RotationAngle } from './types';
|
|||||||
import { parsePageRanges, rotatePdf } from './service';
|
import { parsePageRanges, rotatePdf } from './service';
|
||||||
import SimpleRadio from '@components/options/SimpleRadio';
|
import SimpleRadio from '@components/options/SimpleRadio';
|
||||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||||
|
import { isArray } from 'lodash';
|
||||||
|
|
||||||
const initialValues: InitialValuesType = {
|
const initialValues: InitialValuesType = {
|
||||||
rotationAngle: 90,
|
rotationAngle: 90,
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Box, Typography } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
import React, { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import ToolFileResult from '@components/result/ToolFileResult';
|
import ToolFileResult from '@components/result/ToolFileResult';
|
||||||
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
import TextFieldWithDesc from '@components/options/TextFieldWithDesc';
|
||||||
import ToolContent from '@components/ToolContent';
|
import ToolContent from '@components/ToolContent';
|
||||||
|
Reference in New Issue
Block a user