Add PDF manipulation functionality with options to duplicate pages and combine pages 2-up

This commit is contained in:
2025-03-13 16:26:12 -03:00
commit f9dec6d1e0
5 changed files with 1430 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

26
package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "pdf-pages-reducer",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "ts-node src/index.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/inquirer-fuzzy-path": "^2.3.9",
"@types/node": "^22.13.10",
"ts-node": "^10.9.2",
"typescript": "^5.8.2"
},
"dependencies": {
"@types/inquirer": "^9.0.7",
"inquirer": "^12.4.3",
"inquirer-fuzzy-path": "^2.3.0",
"pdf-lib": "^1.17.1"
}
}

939
pnpm-lock.yaml generated Normal file
View File

@@ -0,0 +1,939 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@types/inquirer':
specifier: ^9.0.7
version: 9.0.7
inquirer:
specifier: ^12.4.3
version: 12.4.3(@types/node@22.13.10)
inquirer-fuzzy-path:
specifier: ^2.3.0
version: 2.3.0
pdf-lib:
specifier: ^1.17.1
version: 1.17.1
devDependencies:
'@types/inquirer-fuzzy-path':
specifier: ^2.3.9
version: 2.3.9
'@types/node':
specifier: ^22.13.10
version: 22.13.10
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@22.13.10)(typescript@5.8.2)
typescript:
specifier: ^5.8.2
version: 5.8.2
packages:
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
'@inquirer/checkbox@4.1.3':
resolution: {integrity: sha512-KU1MGwf24iABJjGESxhyj+/rlQYSRoCfcuHDEHXfZ1DENmbuSRfyrUb+LLjHoee5TNOFKwaFxDXc5/zRwJUPMQ==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/confirm@5.1.7':
resolution: {integrity: sha512-Xrfbrw9eSiHb+GsesO8TQIeHSMTP0xyvTCeeYevgZ4sKW+iz9w/47bgfG9b0niQm+xaLY2EWPBINUPldLwvYiw==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/core@10.1.8':
resolution: {integrity: sha512-HpAqR8y715zPpM9e/9Q+N88bnGwqqL8ePgZ0SMv/s3673JLMv3bIkoivGmjPqXlEgisUksSXibweQccUwEx4qQ==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/editor@4.2.8':
resolution: {integrity: sha512-UkGKbMFlQw5k4ZLjDwEi5z8NIVlP/3DAlLHta0o0pSsdpPThNmPtUL8mvGCHUaQtR+QrxR9yRYNWgKMsHkfIUA==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/expand@4.0.10':
resolution: {integrity: sha512-leyBouGJ77ggv51Jb/OJmLGGnU2HYc13MZ2iiPNLwe2VgFgZPVqsrRWSa1RAHKyazjOyvSNKLD1B2K7A/iWi1g==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/figures@1.0.11':
resolution: {integrity: sha512-eOg92lvrn/aRUqbxRyvpEWnrvRuTYRifixHkYVpJiygTgVSBIHDqLh0SrMQXkafvULg3ck11V7xvR+zcgvpHFw==}
engines: {node: '>=18'}
'@inquirer/input@4.1.7':
resolution: {integrity: sha512-rCQAipJNA14UTH84df/z4jDJ9LZ54H6zzuCAi7WZ0qVqx3CSqLjfXAMd5cpISIxbiHVJCPRB81gZksq6CZsqDg==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/number@3.0.10':
resolution: {integrity: sha512-GLsdnxzNefjCJUmWyjaAuNklHgDpCTL4RMllAVhVvAzBwRW9g38eZ5tWgzo1lirtSDTpsh593hqXVhxvdrjfwA==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/password@4.0.10':
resolution: {integrity: sha512-JC538ujqeYKkFqLoWZ0ILBteIUO2yajBMVEUZSxjl9x6fiEQtM+I5Rca7M2D8edMDbyHLnXifGH1hJZdh8V5rA==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/prompts@7.3.3':
resolution: {integrity: sha512-QS1AQgJ113iE/nmym03yKZKHvGjVWwkGZT3B1yKrrMG0bJKQg1jUkntFP8aPd2FUQzu/nga7QU2eDpzIP5it0Q==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/rawlist@4.0.10':
resolution: {integrity: sha512-vOQbQkmhaCsF2bUmjoyRSZJBz77UnIF/F3ZS2LMgwbgyaG2WgwKHh0WKNj0APDB72WDbZijhW5nObQbk+TnbcA==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/search@3.0.10':
resolution: {integrity: sha512-EAVKAz6P1LajZOdoL+R+XC3HJYSU261fbJzO4fCkJJ7UPFcm+nP+gzC+DDZWsb2WK9PQvKsnaKiNKsY8B6dBWQ==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/select@4.0.10':
resolution: {integrity: sha512-Tg8S9nESnCfISu5tCZSuXpXq0wHuDVimj7xyHstABgR34zcJnLdq/VbjB2mdZvNAMAehYBnNzSjxB06UE8LLAA==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/type@3.0.5':
resolution: {integrity: sha512-ZJpeIYYueOz/i/ONzrfof8g89kNdO2hjGuvULROo3O8rlB2CRtSseE5KeirnyE4t/thAn/EwvS/vuQeJCn+NZg==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.0':
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@pdf-lib/standard-fonts@1.0.0':
resolution: {integrity: sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==}
'@pdf-lib/upng@1.0.1':
resolution: {integrity: sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==}
'@tsconfig/node10@1.0.11':
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
'@tsconfig/node12@1.0.11':
resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
'@tsconfig/node14@1.0.3':
resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
'@tsconfig/node16@1.0.4':
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
'@types/inquirer-autocomplete-prompt@2.0.2':
resolution: {integrity: sha512-Y7RM1dY3KVg11JnFkaQkTT+2Cgmn9K8De/VtrTT2a5grGIoMfkQuYM5Sss+65oiuqg1h1cTsKHG8pkoPsASdbQ==}
'@types/inquirer-fuzzy-path@2.3.9':
resolution: {integrity: sha512-hOzDXaPDENzAioMYM520CCe09GnKQQ417v8wU95YNft0hiaL1sKJVtoyWqOODqD6CTq54b5ddSewuK3LJAvg0g==}
'@types/inquirer@8.2.10':
resolution: {integrity: sha512-IdD5NmHyVjWM8SHWo/kPBgtzXatwPkfwzyP3fN1jF2g9BWt5WO+8hL2F4o2GKIYsU40PpqeevuUWvkS/roXJkA==}
'@types/inquirer@9.0.7':
resolution: {integrity: sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==}
'@types/node@22.13.10':
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
'@types/through@0.0.33':
resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==}
acorn-walk@8.3.4:
resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
engines: {node: '>=0.4.0'}
acorn@8.14.1:
resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
engines: {node: '>=0.4.0'}
hasBin: true
ansi-escapes@3.2.0:
resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==}
engines: {node: '>=4'}
ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
ansi-regex@3.0.1:
resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==}
engines: {node: '>=4'}
ansi-regex@4.1.1:
resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==}
engines: {node: '>=6'}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
chardet@0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
cli-cursor@2.1.0:
resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==}
engines: {node: '>=4'}
cli-width@2.2.1:
resolution: {integrity: sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==}
cli-width@4.1.0:
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
engines: {node: '>= 12'}
color-convert@1.9.3:
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
color-name@1.1.3:
resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==}
color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
create-require@1.1.1:
resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
diff@4.0.2:
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
engines: {node: '>=0.3.1'}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
external-editor@3.1.0:
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
engines: {node: '>=4'}
figures@2.0.0:
resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==}
engines: {node: '>=4'}
figures@3.2.0:
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
engines: {node: '>=8'}
fuzzy@0.1.3:
resolution: {integrity: sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==}
engines: {node: '>= 0.6.0'}
has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
inquirer-autocomplete-prompt@1.4.0:
resolution: {integrity: sha512-qHgHyJmbULt4hI+kCmwX92MnSxDs/Yhdt4wPA30qnoa01OF6uTXV8yvH4hKXgdaTNmkZ9D01MHjqKYEuJN+ONw==}
engines: {node: '>=10'}
peerDependencies:
inquirer: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0
inquirer-fuzzy-path@2.3.0:
resolution: {integrity: sha512-zfHC/97GSkxKKM7IctZM22x1sVi+FYBh9oaHTmI7Er/GKFpNykUgtviTmqqpiFQs5yJoSowxbT0PHy6N+H+QRg==}
engines: {node: '>=8.0'}
inquirer@12.4.3:
resolution: {integrity: sha512-p9+jcDKhFHKTunvpffCk7I9eKt8+NPNWO8hMSSoLPv5vahP5Vhr78qWzDtA+6FBWQtFTuLFUWmxTyhC6G2Xz/Q==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
inquirer@6.5.2:
resolution: {integrity: sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==}
engines: {node: '>=6.0.0'}
is-fullwidth-code-point@2.0.0:
resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==}
engines: {node: '>=4'}
is-fullwidth-code-point@3.0.0:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
mimic-fn@1.2.0:
resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==}
engines: {node: '>=4'}
mute-stream@0.0.7:
resolution: {integrity: sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==}
mute-stream@2.0.0:
resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==}
engines: {node: ^18.17.0 || >=20.5.0}
onetime@2.0.1:
resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==}
engines: {node: '>=4'}
os-tmpdir@1.0.2:
resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==}
engines: {node: '>=0.10.0'}
pako@1.0.11:
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
pdf-lib@1.17.1:
resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==}
restore-cursor@2.0.0:
resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==}
engines: {node: '>=4'}
run-async@2.4.1:
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
engines: {node: '>=0.12.0'}
run-async@3.0.0:
resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==}
engines: {node: '>=0.12.0'}
rxjs@6.6.7:
resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==}
engines: {npm: '>=2.0.0'}
rxjs@7.8.2:
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
signal-exit@3.0.7:
resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
signal-exit@4.1.0:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'}
string-width@2.1.1:
resolution: {integrity: sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==}
engines: {node: '>=4'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
strip-ansi@4.0.0:
resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==}
engines: {node: '>=4'}
strip-ansi@5.2.0:
resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==}
engines: {node: '>=6'}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
tmp@0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'}
ts-node@10.9.2:
resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
hasBin: true
peerDependencies:
'@swc/core': '>=1.2.50'
'@swc/wasm': '>=1.2.50'
'@types/node': '*'
typescript: '>=2.7'
peerDependenciesMeta:
'@swc/core':
optional: true
'@swc/wasm':
optional: true
tslib@1.14.1:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
type-fest@0.21.3:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
typescript@5.8.2:
resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.20.0:
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
yn@3.1.1:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
engines: {node: '>=6'}
yoctocolors-cjs@2.1.2:
resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==}
engines: {node: '>=18'}
snapshots:
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
'@inquirer/checkbox@4.1.3(@types/node@22.13.10)':
dependencies:
'@inquirer/core': 10.1.8(@types/node@22.13.10)
'@inquirer/figures': 1.0.11
'@inquirer/type': 3.0.5(@types/node@22.13.10)
ansi-escapes: 4.3.2
yoctocolors-cjs: 2.1.2
optionalDependencies:
'@types/node': 22.13.10
'@inquirer/confirm@5.1.7(@types/node@22.13.10)':
dependencies:
'@inquirer/core': 10.1.8(@types/node@22.13.10)
'@inquirer/type': 3.0.5(@types/node@22.13.10)
optionalDependencies:
'@types/node': 22.13.10
'@inquirer/core@10.1.8(@types/node@22.13.10)':
dependencies:
'@inquirer/figures': 1.0.11
'@inquirer/type': 3.0.5(@types/node@22.13.10)
ansi-escapes: 4.3.2
cli-width: 4.1.0
mute-stream: 2.0.0
signal-exit: 4.1.0
wrap-ansi: 6.2.0
yoctocolors-cjs: 2.1.2
optionalDependencies:
'@types/node': 22.13.10
'@inquirer/editor@4.2.8(@types/node@22.13.10)':
dependencies:
'@inquirer/core': 10.1.8(@types/node@22.13.10)
'@inquirer/type': 3.0.5(@types/node@22.13.10)
external-editor: 3.1.0
optionalDependencies:
'@types/node': 22.13.10
'@inquirer/expand@4.0.10(@types/node@22.13.10)':
dependencies:
'@inquirer/core': 10.1.8(@types/node@22.13.10)
'@inquirer/type': 3.0.5(@types/node@22.13.10)
yoctocolors-cjs: 2.1.2
optionalDependencies:
'@types/node': 22.13.10
'@inquirer/figures@1.0.11': {}
'@inquirer/input@4.1.7(@types/node@22.13.10)':
dependencies:
'@inquirer/core': 10.1.8(@types/node@22.13.10)
'@inquirer/type': 3.0.5(@types/node@22.13.10)
optionalDependencies:
'@types/node': 22.13.10
'@inquirer/number@3.0.10(@types/node@22.13.10)':
dependencies:
'@inquirer/core': 10.1.8(@types/node@22.13.10)
'@inquirer/type': 3.0.5(@types/node@22.13.10)
optionalDependencies:
'@types/node': 22.13.10
'@inquirer/password@4.0.10(@types/node@22.13.10)':
dependencies:
'@inquirer/core': 10.1.8(@types/node@22.13.10)
'@inquirer/type': 3.0.5(@types/node@22.13.10)
ansi-escapes: 4.3.2
optionalDependencies:
'@types/node': 22.13.10
'@inquirer/prompts@7.3.3(@types/node@22.13.10)':
dependencies:
'@inquirer/checkbox': 4.1.3(@types/node@22.13.10)
'@inquirer/confirm': 5.1.7(@types/node@22.13.10)
'@inquirer/editor': 4.2.8(@types/node@22.13.10)
'@inquirer/expand': 4.0.10(@types/node@22.13.10)
'@inquirer/input': 4.1.7(@types/node@22.13.10)
'@inquirer/number': 3.0.10(@types/node@22.13.10)
'@inquirer/password': 4.0.10(@types/node@22.13.10)
'@inquirer/rawlist': 4.0.10(@types/node@22.13.10)
'@inquirer/search': 3.0.10(@types/node@22.13.10)
'@inquirer/select': 4.0.10(@types/node@22.13.10)
optionalDependencies:
'@types/node': 22.13.10
'@inquirer/rawlist@4.0.10(@types/node@22.13.10)':
dependencies:
'@inquirer/core': 10.1.8(@types/node@22.13.10)
'@inquirer/type': 3.0.5(@types/node@22.13.10)
yoctocolors-cjs: 2.1.2
optionalDependencies:
'@types/node': 22.13.10
'@inquirer/search@3.0.10(@types/node@22.13.10)':
dependencies:
'@inquirer/core': 10.1.8(@types/node@22.13.10)
'@inquirer/figures': 1.0.11
'@inquirer/type': 3.0.5(@types/node@22.13.10)
yoctocolors-cjs: 2.1.2
optionalDependencies:
'@types/node': 22.13.10
'@inquirer/select@4.0.10(@types/node@22.13.10)':
dependencies:
'@inquirer/core': 10.1.8(@types/node@22.13.10)
'@inquirer/figures': 1.0.11
'@inquirer/type': 3.0.5(@types/node@22.13.10)
ansi-escapes: 4.3.2
yoctocolors-cjs: 2.1.2
optionalDependencies:
'@types/node': 22.13.10
'@inquirer/type@3.0.5(@types/node@22.13.10)':
optionalDependencies:
'@types/node': 22.13.10
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.0': {}
'@jridgewell/trace-mapping@0.3.9':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@pdf-lib/standard-fonts@1.0.0':
dependencies:
pako: 1.0.11
'@pdf-lib/upng@1.0.1':
dependencies:
pako: 1.0.11
'@tsconfig/node10@1.0.11': {}
'@tsconfig/node12@1.0.11': {}
'@tsconfig/node14@1.0.3': {}
'@tsconfig/node16@1.0.4': {}
'@types/inquirer-autocomplete-prompt@2.0.2':
dependencies:
'@types/inquirer': 8.2.10
'@types/inquirer-fuzzy-path@2.3.9':
dependencies:
'@types/inquirer': 8.2.10
'@types/inquirer-autocomplete-prompt': 2.0.2
'@types/inquirer@8.2.10':
dependencies:
'@types/through': 0.0.33
rxjs: 7.8.2
'@types/inquirer@9.0.7':
dependencies:
'@types/through': 0.0.33
rxjs: 7.8.2
'@types/node@22.13.10':
dependencies:
undici-types: 6.20.0
'@types/through@0.0.33':
dependencies:
'@types/node': 22.13.10
acorn-walk@8.3.4:
dependencies:
acorn: 8.14.1
acorn@8.14.1: {}
ansi-escapes@3.2.0: {}
ansi-escapes@4.3.2:
dependencies:
type-fest: 0.21.3
ansi-regex@3.0.1: {}
ansi-regex@4.1.1: {}
ansi-regex@5.0.1: {}
ansi-styles@3.2.1:
dependencies:
color-convert: 1.9.3
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
arg@4.1.3: {}
chalk@2.4.2:
dependencies:
ansi-styles: 3.2.1
escape-string-regexp: 1.0.5
supports-color: 5.5.0
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
supports-color: 7.2.0
chardet@0.7.0: {}
cli-cursor@2.1.0:
dependencies:
restore-cursor: 2.0.0
cli-width@2.2.1: {}
cli-width@4.1.0: {}
color-convert@1.9.3:
dependencies:
color-name: 1.1.3
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
color-name@1.1.3: {}
color-name@1.1.4: {}
create-require@1.1.1: {}
diff@4.0.2: {}
emoji-regex@8.0.0: {}
escape-string-regexp@1.0.5: {}
external-editor@3.1.0:
dependencies:
chardet: 0.7.0
iconv-lite: 0.4.24
tmp: 0.0.33
figures@2.0.0:
dependencies:
escape-string-regexp: 1.0.5
figures@3.2.0:
dependencies:
escape-string-regexp: 1.0.5
fuzzy@0.1.3: {}
has-flag@3.0.0: {}
has-flag@4.0.0: {}
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
inquirer-autocomplete-prompt@1.4.0(inquirer@6.5.2):
dependencies:
ansi-escapes: 4.3.2
chalk: 4.1.2
figures: 3.2.0
inquirer: 6.5.2
run-async: 2.4.1
rxjs: 6.6.7
inquirer-fuzzy-path@2.3.0:
dependencies:
ansi-styles: 3.2.1
fuzzy: 0.1.3
inquirer: 6.5.2
inquirer-autocomplete-prompt: 1.4.0(inquirer@6.5.2)
strip-ansi: 4.0.0
inquirer@12.4.3(@types/node@22.13.10):
dependencies:
'@inquirer/core': 10.1.8(@types/node@22.13.10)
'@inquirer/prompts': 7.3.3(@types/node@22.13.10)
'@inquirer/type': 3.0.5(@types/node@22.13.10)
ansi-escapes: 4.3.2
mute-stream: 2.0.0
run-async: 3.0.0
rxjs: 7.8.2
optionalDependencies:
'@types/node': 22.13.10
inquirer@6.5.2:
dependencies:
ansi-escapes: 3.2.0
chalk: 2.4.2
cli-cursor: 2.1.0
cli-width: 2.2.1
external-editor: 3.1.0
figures: 2.0.0
lodash: 4.17.21
mute-stream: 0.0.7
run-async: 2.4.1
rxjs: 6.6.7
string-width: 2.1.1
strip-ansi: 5.2.0
through: 2.3.8
is-fullwidth-code-point@2.0.0: {}
is-fullwidth-code-point@3.0.0: {}
lodash@4.17.21: {}
make-error@1.3.6: {}
mimic-fn@1.2.0: {}
mute-stream@0.0.7: {}
mute-stream@2.0.0: {}
onetime@2.0.1:
dependencies:
mimic-fn: 1.2.0
os-tmpdir@1.0.2: {}
pako@1.0.11: {}
pdf-lib@1.17.1:
dependencies:
'@pdf-lib/standard-fonts': 1.0.0
'@pdf-lib/upng': 1.0.1
pako: 1.0.11
tslib: 1.14.1
restore-cursor@2.0.0:
dependencies:
onetime: 2.0.1
signal-exit: 3.0.7
run-async@2.4.1: {}
run-async@3.0.0: {}
rxjs@6.6.7:
dependencies:
tslib: 1.14.1
rxjs@7.8.2:
dependencies:
tslib: 2.8.1
safer-buffer@2.1.2: {}
signal-exit@3.0.7: {}
signal-exit@4.1.0: {}
string-width@2.1.1:
dependencies:
is-fullwidth-code-point: 2.0.0
strip-ansi: 4.0.0
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
strip-ansi@4.0.0:
dependencies:
ansi-regex: 3.0.1
strip-ansi@5.2.0:
dependencies:
ansi-regex: 4.1.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
supports-color@5.5.0:
dependencies:
has-flag: 3.0.0
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
through@2.3.8: {}
tmp@0.0.33:
dependencies:
os-tmpdir: 1.0.2
ts-node@10.9.2(@types/node@22.13.10)(typescript@5.8.2):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 22.13.10
acorn: 8.14.1
acorn-walk: 8.3.4
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 5.8.2
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
tslib@1.14.1: {}
tslib@2.8.1: {}
type-fest@0.21.3: {}
typescript@5.8.2: {}
undici-types@6.20.0: {}
v8-compile-cache-lib@3.0.1: {}
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
string-width: 4.2.3
strip-ansi: 6.0.1
yn@3.1.1: {}
yoctocolors-cjs@2.1.2: {}

351
src/index.ts Normal file
View File

@@ -0,0 +1,351 @@
import inquirer from "inquirer";
import { PDFDocument, degrees } from "pdf-lib";
import * as fs from "fs";
import * as path from "path";
// Extend the orientation type to include "stacked"
type OrientationType = "horizontal" | "vertical" | "stacked";
interface Answers {
operation: string;
pdfPath: string;
copies: number;
orientation: OrientationType;
specifyPaperSize: boolean;
selectedSize: [number, number];
}
// -------------- Existing duplicate function --------------
async function duplicatePages(
inputPath: string,
copies: number
): Promise<Uint8Array> {
const pdfBytes = fs.readFileSync(inputPath);
const pdfDoc = await PDFDocument.load(pdfBytes);
const pageCount = pdfDoc.getPageCount();
for (let i = 0; i < pageCount; i++) {
// For each page, insert (copies-1) additional copies after it
for (let j = 0; j < copies - 1; j++) {
const [copiedPage] = await pdfDoc.copyPages(pdfDoc, [i]);
pdfDoc.insertPage(i + 1 + j, copiedPage);
}
}
return pdfDoc.save();
}
// -------------- Updated 2-up function --------------
async function combinePages2In1(
inputPath: string,
orientation: OrientationType,
paperSize?: [number, number]
): Promise<Uint8Array> {
// 1) Load the input PDF
const pdfBytes = fs.readFileSync(inputPath);
const originalPdf = await PDFDocument.load(pdfBytes);
// 2) Create a new PDFDocument for the 2-up output
const newPdf = await PDFDocument.create();
// 3) Decide final page size (portrait)
let finalWidth: number;
let finalHeight: number;
if (paperSize) {
[finalWidth, finalHeight] = paperSize; // e.g. [595.28, 841.89] for A4
} else {
// Default to A4 if none chosen
finalWidth = 595.28;
finalHeight = 841.89;
}
// 4) For each page in the original PDF, add a new 2-up page
const pageCount = originalPdf.getPageCount();
for (let i = 0; i < pageCount; i++) {
// Add a blank page in the new PDF
const newPage = newPdf.addPage([finalWidth, finalHeight]);
// Copy the same page twice
const [origPage1] = await newPdf.copyPages(originalPdf, [i]);
const [origPage2] = await newPdf.copyPages(originalPdf, [i]);
// Embed them
const embedded1 = await newPdf.embedPage(origPage1);
const embedded2 = await newPdf.embedPage(origPage2);
// Original size
const { width: w, height: h } = origPage1.getSize();
if (orientation === "vertical") {
// ---------------------------------------------------
// "VERTICAL" = side-by-side (left/right)
// ---------------------------------------------------
//
// ┌─────────┬─────────┐
// │ Page1 │ Page2 │
// └─────────┴─────────┘
//
// Each half is finalWidth/2 wide, finalHeight tall.
// Use "contain" scaling so each page fits entirely.
const slotWidth = finalWidth / 2;
const slotHeight = finalHeight;
const scale = Math.min(slotWidth / w, slotHeight / h);
const scaledW = w * scale;
const scaledH = h * scale;
// Left slot offsets
const offsetX1 = (slotWidth - scaledW) / 2;
const offsetY1 = (slotHeight - scaledH) / 2;
newPage.drawPage(embedded1, {
x: offsetX1,
y: offsetY1,
xScale: scale,
yScale: scale,
});
// Right slot offsets
const offsetX2 = slotWidth + (slotWidth - scaledW) / 2;
const offsetY2 = offsetY1;
newPage.drawPage(embedded2, {
x: offsetX2,
y: offsetY2,
xScale: scale,
yScale: scale,
});
} else if (orientation === "horizontal") {
// ---------------------------------------------------
// "HORIZONTAL" = top & bottom WITH rotation
// ---------------------------------------------------
//
// ┌─────────┐ (top half)
// │ Page1 │ rotated +90
// ├─────────┤
// │ Page2 │ rotated +90
// └─────────┘ (bottom half)
//
// Each half is finalWidth wide, finalHeight/2 tall.
// We rotate each original page so it appears landscape.
const slotWidth = finalWidth;
const slotHeight = finalHeight / 2;
// After a +90 rotation, the bounding box is effectively (h × w).
const scale = Math.min(slotWidth / h, slotHeight / w);
// We'll shift x by + h*scale so the rotated page remains in view
// and center it within the slot.
// Page1 in the TOP half
const offsetX1 = (slotWidth - h * scale) / 2;
const offsetY1 = slotHeight + (slotHeight - w * scale) / 2;
newPage.drawPage(embedded1, {
x: offsetX1 + h * scale,
y: offsetY1,
xScale: scale,
yScale: scale,
rotate: degrees(90),
});
// Page2 in the BOTTOM half
const offsetX2 = (slotWidth - h * scale) / 2;
const offsetY2 = (slotHeight - w * scale) / 2;
newPage.drawPage(embedded2, {
x: offsetX2 + h * scale,
y: offsetY2,
xScale: scale,
yScale: scale,
rotate: degrees(90),
});
} else {
// ---------------------------------------------------
// "STACKED" = top & bottom WITHOUT rotation
// ---------------------------------------------------
//
// ┌─────────┐ (top half)
// │ Page1 │ normal orientation
// ├─────────┤
// │ Page2 │ normal orientation
// └─────────┘ (bottom half)
//
// Each half is finalWidth wide, finalHeight/2 tall.
// We do not rotate. We simply scale to fit.
const slotWidth = finalWidth;
const slotHeight = finalHeight / 2;
const scale = Math.min(slotWidth / w, slotHeight / h);
const scaledW = w * scale;
const scaledH = h * scale;
// Place Page1 in the top half
// top half's y-range is [slotHeight, finalHeight]
const offsetX1 = (slotWidth - scaledW) / 2;
const offsetY1 = slotHeight + (slotHeight - scaledH) / 2;
newPage.drawPage(embedded1, {
x: offsetX1,
y: offsetY1,
xScale: scale,
yScale: scale,
});
// Place Page2 in the bottom half
const offsetX2 = (slotWidth - scaledW) / 2;
const offsetY2 = (slotHeight - scaledH) / 2;
newPage.drawPage(embedded2, {
x: offsetX2,
y: offsetY2,
xScale: scale,
yScale: scale,
});
}
}
// 5) Save and return
return newPdf.save();
}
// -------------- Updated main() --------------
async function main() {
try {
// 1) Check if PDF path is provided as a command-line argument
let pdfPath: string | undefined = process.argv[2];
// 2) If not provided, prompt for PDF path
if (!pdfPath) {
const answer = await inquirer.prompt<Pick<Answers, "pdfPath">>({
type: "input",
name: "pdfPath",
message: "Enter the path to your PDF file:",
validate: async (input: string) => {
try {
fs.existsSync(input);
return (
path.extname(input).toLowerCase() === ".pdf" ||
"Please provide a PDF file"
);
} catch {
return "File does not exist";
}
},
});
pdfPath = answer.pdfPath;
} else {
// Validate the provided PDF path
if (
!fs.existsSync(pdfPath) ||
path.extname(pdfPath).toLowerCase() !== ".pdf"
) {
console.error("Invalid PDF path provided as argument.");
return;
}
}
// 3) Ask user what operation to perform
const { operation } = await inquirer.prompt<Pick<Answers, "operation">>({
type: "list",
name: "operation",
message: "What operation would you like to perform?",
choices: ["Duplicate PDF pages", "Combine pages 2 in 1"],
});
// 4) Perform the chosen operation
if (operation === "Duplicate PDF pages") {
// Ask how many copies
const { copies } = await inquirer.prompt<Pick<Answers, "copies">>({
type: "number",
name: "copies",
message: "How many copies of each page do you want?",
validate: (input?: number) =>
(input && input > 0) || "Please enter a number greater than 0",
});
// Output path for duplicated pages
const outputPath = path.join(
path.dirname(pdfPath),
`${path.basename(pdfPath, ".pdf")}_duplicated.pdf`
);
// Duplicate
const pdfBytes = await duplicatePages(pdfPath, copies);
fs.writeFileSync(outputPath, pdfBytes);
console.log(`Success! Output saved to: ${outputPath}`);
} else {
// "Combine pages 2 in 1"
// 4.1) Ask orientation
// - "vertical" => side by side
// - "horizontal" => top/bottom with rotation
// - "stacked" => top/bottom no rotation
const { orientation } = await inquirer.prompt<
Pick<Answers, "orientation">
>({
type: "list",
name: "orientation",
message: "Select arrangement (affects content placement):",
choices: [
{ name: "Vertical (side-by-side)", value: "vertical" },
{ name: "Horizontal (top/bottom, rotated)", value: "horizontal" },
{ name: "Stacked (top/bottom, no rotation)", value: "stacked" },
],
});
// 4.2) Ask if user wants to specify a paper size
const { specifyPaperSize } = await inquirer.prompt<
Pick<Answers, "specifyPaperSize">
>({
type: "confirm",
name: "specifyPaperSize",
message: "Would you like to choose a common paper size for the output?",
default: false,
});
let chosenPaperSize: [number, number] | undefined = undefined;
if (specifyPaperSize) {
const { selectedSize } = await inquirer.prompt<
Pick<Answers, "selectedSize">
>({
type: "list",
name: "selectedSize",
message: "Select a paper size (output will be portrait):",
choices: [
{ name: "A4 (210 × 297 mm)", value: [595.28, 841.89] },
{ name: "Letter (8.5 × 11 in)", value: [612, 792] },
{ name: "Legal (8.5 × 14 in)", value: [612, 1008] },
{ name: "A3 (297 × 420 mm)", value: [841.89, 1190.55] },
{ name: "Tabloid (11 × 17 in)", value: [792, 1224] },
],
});
chosenPaperSize = selectedSize;
}
// 4.3) Output path
const outputPath = path.join(
path.dirname(pdfPath),
`${path.basename(pdfPath, ".pdf")}_2up.pdf`
);
// 4.4) Generate the 2-up PDF
const pdfBytes = await combinePages2In1(
pdfPath,
orientation,
chosenPaperSize
);
fs.writeFileSync(outputPath, pdfBytes);
console.log(`Success! 2-up PDF saved to: ${outputPath}`);
}
} catch (error) {
console.error("An error occurred:", error);
}
}
main();

113
tsconfig.json Normal file
View File

@@ -0,0 +1,113 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "libReplacement": true, /* Enable lib replacement. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}