pinv-cli 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -0
- package/bun.lock +49 -0
- package/demo.gif +0 -0
- package/index.ts +178 -0
- package/package.json +31 -0
- package/tsconfig.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# pinv
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Invert PDF colors from the command line.
|
|
6
|
+
|
|
7
|
+
## Usage (Binary)
|
|
8
|
+
|
|
9
|
+
- Invert all PDFs in the current folder:
|
|
10
|
+
```
|
|
11
|
+
pinv
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- Invert all PDFs in a folder:
|
|
15
|
+
```
|
|
16
|
+
pinv path/to/folder
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- Invert a specific PDF:
|
|
20
|
+
```
|
|
21
|
+
pinv path/to/file.pdf
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## Add binary to PATH on macOS
|
|
26
|
+
|
|
27
|
+
After building, copy the binary to a directory in your PATH (e.g. `/usr/local/bin`):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
sudo cp /Users/<user>/pinv /usr/local/bin/pinv
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Build manually
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
bun build ./index.ts --compile --outfile pinv
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Creates `*_inverted.pdf` for each processed file.
|
package/bun.lock
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 0,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "pinv",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"chalk": "^5.3.0",
|
|
9
|
+
"commander": "^11.1.0",
|
|
10
|
+
"pdf-lib": "^1.17.1",
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@types/bun": "latest",
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"typescript": "^5",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
"packages": {
|
|
21
|
+
"@pdf-lib/standard-fonts": ["@pdf-lib/standard-fonts@1.0.0", "", { "dependencies": { "pako": "^1.0.6" } }, "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA=="],
|
|
22
|
+
|
|
23
|
+
"@pdf-lib/upng": ["@pdf-lib/upng@1.0.1", "", { "dependencies": { "pako": "^1.0.10" } }, "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ=="],
|
|
24
|
+
|
|
25
|
+
"@types/bun": ["@types/bun@1.2.23", "", { "dependencies": { "bun-types": "1.2.23" } }, "sha512-le8ueOY5b6VKYf19xT3McVbXqLqmxzPXHsQT/q9JHgikJ2X22wyTW3g3ohz2ZMnp7dod6aduIiq8A14Xyimm0A=="],
|
|
26
|
+
|
|
27
|
+
"@types/node": ["@types/node@24.6.0", "", { "dependencies": { "undici-types": "~7.13.0" } }, "sha512-F1CBxgqwOMc4GKJ7eY22hWhBVQuMYTtqI8L0FcszYcpYX0fzfDGpez22Xau8Mgm7O9fI+zA/TYIdq3tGWfweBA=="],
|
|
28
|
+
|
|
29
|
+
"@types/react": ["@types/react@19.1.16", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-WBM/nDbEZmDUORKnh5i1bTnAz6vTohUf9b8esSMu+b24+srbaxa04UbJgWx78CVfNXA20sNu0odEIluZDFdCog=="],
|
|
30
|
+
|
|
31
|
+
"bun-types": ["bun-types@1.2.23", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-R9f0hKAZXgFU3mlrA0YpE/fiDvwV0FT9rORApt2aQVWSuJDzZOyB5QLc0N/4HF57CS8IXJ6+L5E4W1bW6NS2Aw=="],
|
|
32
|
+
|
|
33
|
+
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
|
|
34
|
+
|
|
35
|
+
"commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="],
|
|
36
|
+
|
|
37
|
+
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
|
38
|
+
|
|
39
|
+
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
|
|
40
|
+
|
|
41
|
+
"pdf-lib": ["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.11.1" } }, "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw=="],
|
|
42
|
+
|
|
43
|
+
"tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="],
|
|
44
|
+
|
|
45
|
+
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
|
46
|
+
|
|
47
|
+
"undici-types": ["undici-types@7.13.0", "", {}, "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ=="],
|
|
48
|
+
}
|
|
49
|
+
}
|
package/demo.gif
ADDED
|
Binary file
|
package/index.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { program } from 'commander';
|
|
4
|
+
import { PDFDocument, PDFName, PDFArray, PDFDict, PDFRef } from 'pdf-lib';
|
|
5
|
+
import { readdir, readFile, writeFile, stat } from 'fs/promises';
|
|
6
|
+
import { dirname, basename, join } from 'path';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
const textEncoder = new TextEncoder();
|
|
10
|
+
const EXT_G_STATE_KEY = 'GSDiff';
|
|
11
|
+
|
|
12
|
+
const formatNumber = (value: number): string =>
|
|
13
|
+
Number(value.toFixed(4)).toString();
|
|
14
|
+
|
|
15
|
+
function invertDocumentColors(pdfDoc: PDFDocument): void {
|
|
16
|
+
const context = pdfDoc.context;
|
|
17
|
+
const extGStateName = PDFName.of(EXT_G_STATE_KEY);
|
|
18
|
+
const diffExtGStateRef = context.register(
|
|
19
|
+
context.obj({
|
|
20
|
+
Type: 'ExtGState',
|
|
21
|
+
BM: PDFName.of('Difference'),
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
for (const page of pdfDoc.getPages()) {
|
|
26
|
+
const { width, height } = page.getSize();
|
|
27
|
+
let resources =
|
|
28
|
+
page.node.get(PDFName.of('Resources')) as PDFDict | PDFRef | undefined;
|
|
29
|
+
|
|
30
|
+
if (!resources) {
|
|
31
|
+
resources = context.obj({});
|
|
32
|
+
page.node.set(PDFName.of('Resources'), resources);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const resourcesDict =
|
|
36
|
+
resources instanceof PDFRef ? context.lookup(resources, PDFDict) : resources;
|
|
37
|
+
|
|
38
|
+
let extGStates =
|
|
39
|
+
resourcesDict.get(PDFName.of('ExtGState')) as PDFDict | PDFRef | undefined;
|
|
40
|
+
|
|
41
|
+
if (!extGStates) {
|
|
42
|
+
extGStates = context.obj({});
|
|
43
|
+
resourcesDict.set(PDFName.of('ExtGState'), extGStates);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const extGStatesDict =
|
|
47
|
+
extGStates instanceof PDFRef ? context.lookup(extGStates, PDFDict) : extGStates;
|
|
48
|
+
|
|
49
|
+
if (!extGStatesDict.has(extGStateName)) {
|
|
50
|
+
extGStatesDict.set(extGStateName, diffExtGStateRef);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const widthStr = formatNumber(width);
|
|
54
|
+
const heightStr = formatNumber(height);
|
|
55
|
+
|
|
56
|
+
const whiteBackgroundRef = context.register(
|
|
57
|
+
context.stream(
|
|
58
|
+
textEncoder.encode(
|
|
59
|
+
`q\n1 1 1 rg\n0 0 ${widthStr} ${heightStr} re\nf\nQ\n`,
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const invertOverlayRef = context.register(
|
|
65
|
+
context.stream(
|
|
66
|
+
textEncoder.encode(
|
|
67
|
+
`q\n/${EXT_G_STATE_KEY} gs\n1 1 1 rg\n0 0 ${widthStr} ${heightStr} re\nf\nQ\n`,
|
|
68
|
+
),
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const contents = page.node.get(PDFName.of('Contents'));
|
|
73
|
+
const newContentsArray = context.obj([]);
|
|
74
|
+
|
|
75
|
+
newContentsArray.push(whiteBackgroundRef);
|
|
76
|
+
|
|
77
|
+
if (contents instanceof PDFArray) {
|
|
78
|
+
const count = contents.size();
|
|
79
|
+
for (let i = 0; i < count; i++) {
|
|
80
|
+
newContentsArray.push(contents.get(i));
|
|
81
|
+
}
|
|
82
|
+
} else if (contents) {
|
|
83
|
+
newContentsArray.push(contents);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
newContentsArray.push(invertOverlayRef);
|
|
87
|
+
page.node.set(PDFName.of('Contents'), newContentsArray);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function invertPDF(inputPath: string): Promise<void> {
|
|
92
|
+
try {
|
|
93
|
+
console.log(chalk.blue(`Processing: ${inputPath}`));
|
|
94
|
+
|
|
95
|
+
const pdfBytes = await readFile(inputPath);
|
|
96
|
+
const pdfDoc = await PDFDocument.load(pdfBytes);
|
|
97
|
+
invertDocumentColors(pdfDoc);
|
|
98
|
+
|
|
99
|
+
const outputBytes = await pdfDoc.save();
|
|
100
|
+
|
|
101
|
+
const dir = dirname(inputPath);
|
|
102
|
+
const name = basename(inputPath, '.pdf');
|
|
103
|
+
const outputPath = join(dir, `${name}_inverted.pdf`);
|
|
104
|
+
|
|
105
|
+
await writeFile(outputPath, outputBytes);
|
|
106
|
+
console.log(chalk.green(`ā Created: ${outputPath}`));
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(chalk.red(`ā Error processing ${inputPath}:`), error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function invertAllPDFsInFolder(folderPath: string): Promise<void> {
|
|
114
|
+
try {
|
|
115
|
+
const files = await readdir(folderPath);
|
|
116
|
+
const pdfFiles = files.filter(file =>
|
|
117
|
+
file.toLowerCase().endsWith('.pdf') &&
|
|
118
|
+
!file.toLowerCase().includes('_inverted.pdf')
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (pdfFiles.length === 0) {
|
|
122
|
+
console.log(chalk.yellow('No PDF files found in the specified directory.'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log(chalk.blue(`Found ${pdfFiles.length} PDF file(s) to process...`));
|
|
127
|
+
|
|
128
|
+
for (const file of pdfFiles) {
|
|
129
|
+
const fullPath = join(folderPath, file);
|
|
130
|
+
await invertPDF(fullPath);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(chalk.green(`\nā Completed processing ${pdfFiles.length} PDF file(s)!`));
|
|
134
|
+
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.error(chalk.red('Error reading directory:'), error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function main() {
|
|
141
|
+
program
|
|
142
|
+
.name('pinv')
|
|
143
|
+
.description('A CLI tool to invert PDF colors')
|
|
144
|
+
.version('0.0.2')
|
|
145
|
+
.argument('[path]', 'Path to PDF file or directory (defaults to current directory)')
|
|
146
|
+
.action(async (inputPath?: string) => {
|
|
147
|
+
try {
|
|
148
|
+
const targetPath = inputPath || process.cwd();
|
|
149
|
+
const stats = await stat(targetPath);
|
|
150
|
+
|
|
151
|
+
if (stats.isDirectory()) {
|
|
152
|
+
await invertAllPDFsInFolder(targetPath);
|
|
153
|
+
} else if (stats.isFile() && targetPath.toLowerCase().endsWith('.pdf')) {
|
|
154
|
+
await invertPDF(targetPath);
|
|
155
|
+
} else {
|
|
156
|
+
console.error(chalk.red('Error: Please provide a valid PDF file or directory path.'));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
} catch (error: any) {
|
|
160
|
+
if (error.code === 'ENOENT') {
|
|
161
|
+
console.error(chalk.red(`Error: Path "${inputPath}" does not exist.`));
|
|
162
|
+
} else {
|
|
163
|
+
console.error(chalk.red('An unexpected error occurred:'), error);
|
|
164
|
+
}
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await program.parseAsync();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
if (import.meta.main) {
|
|
174
|
+
main().catch(error => {
|
|
175
|
+
console.error(chalk.red('Fatal error:'), error);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
});
|
|
178
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pinv-cli",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "A CLI tool to invert PDF colors",
|
|
5
|
+
"module": "index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"private": false,
|
|
8
|
+
"bin": {
|
|
9
|
+
"pinv": "./index.ts"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "bun run index.ts",
|
|
13
|
+
"dev": "bun --watch index.ts",
|
|
14
|
+
"build": "bun build index.ts --outdir ./dist --target bun",
|
|
15
|
+
"install-global": "bun link"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["pdf", "invert", "cli", "colors"],
|
|
18
|
+
"author": "Your Name",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"chalk": "^5.3.0",
|
|
22
|
+
"commander": "^11.1.0",
|
|
23
|
+
"pdf-lib": "^1.17.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/bun": "latest"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"typescript": "^5"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|