noupload 1.0.0
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/.github/workflows/release.yml +73 -0
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/biome.json +34 -0
- package/bunfig.toml +7 -0
- package/dist/index.js +192 -0
- package/install.sh +68 -0
- package/package.json +47 -0
- package/scripts/inspect-help.ts +15 -0
- package/site/index.html +112 -0
- package/src/cli.ts +24 -0
- package/src/commands/audio/convert.ts +107 -0
- package/src/commands/audio/extract.ts +84 -0
- package/src/commands/audio/fade.ts +128 -0
- package/src/commands/audio/index.ts +35 -0
- package/src/commands/audio/merge.ts +109 -0
- package/src/commands/audio/normalize.ts +110 -0
- package/src/commands/audio/reverse.ts +64 -0
- package/src/commands/audio/speed.ts +101 -0
- package/src/commands/audio/trim.ts +98 -0
- package/src/commands/audio/volume.ts +91 -0
- package/src/commands/audio/waveform.ts +117 -0
- package/src/commands/doctor.ts +125 -0
- package/src/commands/image/adjust.ts +129 -0
- package/src/commands/image/border.ts +94 -0
- package/src/commands/image/bulk-compress.ts +111 -0
- package/src/commands/image/bulk-convert.ts +114 -0
- package/src/commands/image/bulk-resize.ts +112 -0
- package/src/commands/image/compress.ts +95 -0
- package/src/commands/image/convert.ts +116 -0
- package/src/commands/image/crop.ts +96 -0
- package/src/commands/image/favicon.ts +89 -0
- package/src/commands/image/filters.ts +108 -0
- package/src/commands/image/index.ts +49 -0
- package/src/commands/image/resize.ts +110 -0
- package/src/commands/image/rotate.ts +90 -0
- package/src/commands/image/strip-metadata.ts +60 -0
- package/src/commands/image/to-base64.ts +72 -0
- package/src/commands/image/watermark.ts +141 -0
- package/src/commands/pdf/compress.ts +157 -0
- package/src/commands/pdf/decrypt.ts +102 -0
- package/src/commands/pdf/delete-pages.ts +112 -0
- package/src/commands/pdf/duplicate.ts +119 -0
- package/src/commands/pdf/encrypt.ts +161 -0
- package/src/commands/pdf/from-images.ts +104 -0
- package/src/commands/pdf/index.ts +55 -0
- package/src/commands/pdf/merge.ts +84 -0
- package/src/commands/pdf/ocr.ts +270 -0
- package/src/commands/pdf/organize.ts +88 -0
- package/src/commands/pdf/page-numbers.ts +152 -0
- package/src/commands/pdf/reverse.ts +71 -0
- package/src/commands/pdf/rotate.ts +116 -0
- package/src/commands/pdf/sanitize.ts +77 -0
- package/src/commands/pdf/sign.ts +156 -0
- package/src/commands/pdf/split.ts +148 -0
- package/src/commands/pdf/to-images.ts +84 -0
- package/src/commands/pdf/to-text.ts +51 -0
- package/src/commands/pdf/watermark.ts +179 -0
- package/src/commands/qr/bulk-generate.ts +136 -0
- package/src/commands/qr/generate.ts +128 -0
- package/src/commands/qr/index.ts +16 -0
- package/src/commands/qr/scan.ts +114 -0
- package/src/commands/setup.ts +156 -0
- package/src/index.ts +42 -0
- package/src/lib/audio/ffmpeg.ts +93 -0
- package/src/utils/colors.ts +41 -0
- package/src/utils/detect.ts +222 -0
- package/src/utils/errors.ts +89 -0
- package/src/utils/files.ts +148 -0
- package/src/utils/logger.ts +90 -0
- package/src/utils/pdf-tools.ts +220 -0
- package/src/utils/progress.ts +142 -0
- package/src/utils/style.ts +38 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { defineCommand } from 'citty';
|
|
3
|
+
import { PDFDocument } from 'pdf-lib';
|
|
4
|
+
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
5
|
+
import { ensureDir, generateOutputPath, getFileSize, resolvePath } from '../../utils/files';
|
|
6
|
+
import { fileResult, success } from '../../utils/logger';
|
|
7
|
+
import { withSpinner } from '../../utils/progress';
|
|
8
|
+
|
|
9
|
+
export const sanitize = defineCommand({
|
|
10
|
+
meta: {
|
|
11
|
+
name: 'sanitize',
|
|
12
|
+
description: 'Remove metadata and hidden data from a PDF',
|
|
13
|
+
},
|
|
14
|
+
args: {
|
|
15
|
+
input: {
|
|
16
|
+
type: 'positional',
|
|
17
|
+
description: 'Input PDF file',
|
|
18
|
+
required: true,
|
|
19
|
+
},
|
|
20
|
+
output: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
alias: 'o',
|
|
23
|
+
description: 'Output file path',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
async run({ args }) {
|
|
27
|
+
try {
|
|
28
|
+
const input = requireArg(args.input, 'input');
|
|
29
|
+
|
|
30
|
+
const inputPath = resolvePath(input as string);
|
|
31
|
+
if (!existsSync(inputPath)) {
|
|
32
|
+
throw new FileNotFoundError(input as string);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const outputPath = generateOutputPath(inputPath, args.output, '-sanitized');
|
|
36
|
+
ensureDir(outputPath);
|
|
37
|
+
|
|
38
|
+
const inputSize = getFileSize(inputPath);
|
|
39
|
+
|
|
40
|
+
await withSpinner(
|
|
41
|
+
'Sanitizing PDF...',
|
|
42
|
+
async () => {
|
|
43
|
+
const pdfBytes = await Bun.file(inputPath).arrayBuffer();
|
|
44
|
+
const pdf = await PDFDocument.load(pdfBytes);
|
|
45
|
+
|
|
46
|
+
// Remove all metadata
|
|
47
|
+
pdf.setTitle('');
|
|
48
|
+
pdf.setAuthor('');
|
|
49
|
+
pdf.setSubject('');
|
|
50
|
+
pdf.setKeywords([]);
|
|
51
|
+
pdf.setProducer('');
|
|
52
|
+
pdf.setCreator('');
|
|
53
|
+
pdf.setCreationDate(new Date(0));
|
|
54
|
+
pdf.setModificationDate(new Date(0));
|
|
55
|
+
|
|
56
|
+
// Save the sanitized PDF
|
|
57
|
+
const sanitizedBytes = await pdf.save({
|
|
58
|
+
useObjectStreams: true,
|
|
59
|
+
addDefaultPage: false,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await Bun.write(outputPath, sanitizedBytes);
|
|
63
|
+
},
|
|
64
|
+
'PDF sanitized successfully'
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
fileResult(inputPath, outputPath, {
|
|
68
|
+
before: inputSize,
|
|
69
|
+
after: getFileSize(outputPath),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
success('Removed metadata and hidden data from PDF');
|
|
73
|
+
} catch (err) {
|
|
74
|
+
handleError(err);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { defineCommand } from 'citty';
|
|
3
|
+
import { PDFDocument } from 'pdf-lib';
|
|
4
|
+
import sharp from 'sharp';
|
|
5
|
+
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
6
|
+
import { ensureDir, generateOutputPath, getFileSize, resolvePath } from '../../utils/files';
|
|
7
|
+
import { fileResult, success } from '../../utils/logger';
|
|
8
|
+
import { withSpinner } from '../../utils/progress';
|
|
9
|
+
|
|
10
|
+
type SignPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'center';
|
|
11
|
+
|
|
12
|
+
export const sign = defineCommand({
|
|
13
|
+
meta: {
|
|
14
|
+
name: 'sign',
|
|
15
|
+
description: 'Add a signature image to a PDF',
|
|
16
|
+
},
|
|
17
|
+
args: {
|
|
18
|
+
input: {
|
|
19
|
+
type: 'positional',
|
|
20
|
+
description: 'Input PDF file',
|
|
21
|
+
required: true,
|
|
22
|
+
},
|
|
23
|
+
output: {
|
|
24
|
+
type: 'string',
|
|
25
|
+
alias: 'o',
|
|
26
|
+
description: 'Output file path',
|
|
27
|
+
},
|
|
28
|
+
image: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
alias: 'i',
|
|
31
|
+
description: 'Signature image file (PNG, JPG)',
|
|
32
|
+
required: true,
|
|
33
|
+
},
|
|
34
|
+
page: {
|
|
35
|
+
type: 'string',
|
|
36
|
+
alias: 'p',
|
|
37
|
+
description: 'Page to add signature to (default: last page)',
|
|
38
|
+
},
|
|
39
|
+
position: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'Position: bottom-right, bottom-left, top-right, top-left, center',
|
|
42
|
+
default: 'bottom-right',
|
|
43
|
+
},
|
|
44
|
+
width: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
alias: 'w',
|
|
47
|
+
description: 'Signature width in pixels',
|
|
48
|
+
default: '150',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
async run({ args }) {
|
|
52
|
+
try {
|
|
53
|
+
const input = requireArg(args.input, 'input');
|
|
54
|
+
const imagePath = requireArg(args.image, 'image');
|
|
55
|
+
|
|
56
|
+
const inputPath = resolvePath(input as string);
|
|
57
|
+
const signaturePath = resolvePath(imagePath);
|
|
58
|
+
|
|
59
|
+
if (!existsSync(inputPath)) {
|
|
60
|
+
throw new FileNotFoundError(input as string);
|
|
61
|
+
}
|
|
62
|
+
if (!existsSync(signaturePath)) {
|
|
63
|
+
throw new FileNotFoundError(imagePath);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const outputPath = generateOutputPath(inputPath, args.output, '-signed');
|
|
67
|
+
ensureDir(outputPath);
|
|
68
|
+
|
|
69
|
+
const inputSize = getFileSize(inputPath);
|
|
70
|
+
const position = (args.position || 'bottom-right') as SignPosition;
|
|
71
|
+
const signatureWidth = Number.parseInt(args.width || '150', 10);
|
|
72
|
+
|
|
73
|
+
await withSpinner(
|
|
74
|
+
'Adding signature...',
|
|
75
|
+
async () => {
|
|
76
|
+
const pdfBytes = await Bun.file(inputPath).arrayBuffer();
|
|
77
|
+
const pdf = await PDFDocument.load(pdfBytes);
|
|
78
|
+
|
|
79
|
+
// Process signature image
|
|
80
|
+
const signatureBuffer = await Bun.file(signaturePath).arrayBuffer();
|
|
81
|
+
const processedSignature = await sharp(Buffer.from(signatureBuffer))
|
|
82
|
+
.resize(signatureWidth)
|
|
83
|
+
.png()
|
|
84
|
+
.toBuffer();
|
|
85
|
+
|
|
86
|
+
// Embed image
|
|
87
|
+
const signatureImage = await pdf.embedPng(processedSignature);
|
|
88
|
+
const signatureDims = signatureImage.scale(1);
|
|
89
|
+
|
|
90
|
+
// Determine which page to sign
|
|
91
|
+
const totalPages = pdf.getPageCount();
|
|
92
|
+
const pageNum = args.page ? Number.parseInt(args.page, 10) - 1 : totalPages - 1;
|
|
93
|
+
const page = pdf.getPages()[pageNum];
|
|
94
|
+
|
|
95
|
+
if (!page) {
|
|
96
|
+
throw new Error(`Page ${pageNum + 1} does not exist`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const { width, height } = page.getSize();
|
|
100
|
+
|
|
101
|
+
// Calculate position
|
|
102
|
+
let x: number;
|
|
103
|
+
let y: number;
|
|
104
|
+
const margin = 50;
|
|
105
|
+
|
|
106
|
+
switch (position) {
|
|
107
|
+
case 'bottom-left':
|
|
108
|
+
x = margin;
|
|
109
|
+
y = margin;
|
|
110
|
+
break;
|
|
111
|
+
case 'bottom-right':
|
|
112
|
+
x = width - signatureDims.width - margin;
|
|
113
|
+
y = margin;
|
|
114
|
+
break;
|
|
115
|
+
case 'top-left':
|
|
116
|
+
x = margin;
|
|
117
|
+
y = height - signatureDims.height - margin;
|
|
118
|
+
break;
|
|
119
|
+
case 'top-right':
|
|
120
|
+
x = width - signatureDims.width - margin;
|
|
121
|
+
y = height - signatureDims.height - margin;
|
|
122
|
+
break;
|
|
123
|
+
case 'center':
|
|
124
|
+
x = (width - signatureDims.width) / 2;
|
|
125
|
+
y = (height - signatureDims.height) / 2;
|
|
126
|
+
break;
|
|
127
|
+
default:
|
|
128
|
+
x = width - signatureDims.width - margin;
|
|
129
|
+
y = margin;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Draw signature
|
|
133
|
+
page.drawImage(signatureImage, {
|
|
134
|
+
x,
|
|
135
|
+
y,
|
|
136
|
+
width: signatureDims.width,
|
|
137
|
+
height: signatureDims.height,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const signedBytes = await pdf.save();
|
|
141
|
+
await Bun.write(outputPath, signedBytes);
|
|
142
|
+
},
|
|
143
|
+
'Signature added successfully'
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
fileResult(inputPath, outputPath, {
|
|
147
|
+
before: inputSize,
|
|
148
|
+
after: getFileSize(outputPath),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
success('PDF signed');
|
|
152
|
+
} catch (err) {
|
|
153
|
+
handleError(err);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { defineCommand } from 'citty';
|
|
4
|
+
import { PDFDocument } from 'pdf-lib';
|
|
5
|
+
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
6
|
+
import { ensureOutputDir, getBasename, getFileSize, resolvePath } from '../../utils/files';
|
|
7
|
+
import { fileResult, info, success } from '../../utils/logger';
|
|
8
|
+
import { withSpinner } from '../../utils/progress';
|
|
9
|
+
|
|
10
|
+
// Parse page range string like "1-5,7,10-15" into array of page indices (0-based)
|
|
11
|
+
function parsePageRanges(rangeStr: string, totalPages: number): number[] {
|
|
12
|
+
const pages: Set<number> = new Set();
|
|
13
|
+
const parts = rangeStr.split(',').map((s) => s.trim());
|
|
14
|
+
|
|
15
|
+
for (const part of parts) {
|
|
16
|
+
if (part.includes('-')) {
|
|
17
|
+
const [start, end] = part.split('-').map((s) => Number.parseInt(s.trim(), 10));
|
|
18
|
+
const startPage = Math.max(1, start);
|
|
19
|
+
const endPage = Math.min(totalPages, end);
|
|
20
|
+
for (let i = startPage; i <= endPage; i++) {
|
|
21
|
+
pages.add(i - 1); // Convert to 0-based index
|
|
22
|
+
}
|
|
23
|
+
} else {
|
|
24
|
+
const page = Number.parseInt(part, 10);
|
|
25
|
+
if (page >= 1 && page <= totalPages) {
|
|
26
|
+
pages.add(page - 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return Array.from(pages).sort((a, b) => a - b);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const split = defineCommand({
|
|
35
|
+
meta: {
|
|
36
|
+
name: 'split',
|
|
37
|
+
description: 'Split a PDF into multiple files',
|
|
38
|
+
},
|
|
39
|
+
args: {
|
|
40
|
+
input: {
|
|
41
|
+
type: 'positional',
|
|
42
|
+
description: 'Input PDF file',
|
|
43
|
+
required: true,
|
|
44
|
+
},
|
|
45
|
+
output: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
alias: 'o',
|
|
48
|
+
description: 'Output directory',
|
|
49
|
+
required: true,
|
|
50
|
+
},
|
|
51
|
+
pages: {
|
|
52
|
+
type: 'string',
|
|
53
|
+
alias: 'p',
|
|
54
|
+
description: 'Page ranges to extract (e.g., "1-5,10-15")',
|
|
55
|
+
},
|
|
56
|
+
every: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
alias: 'e',
|
|
59
|
+
description: 'Split every N pages',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
async run({ args }) {
|
|
63
|
+
try {
|
|
64
|
+
const input = requireArg(args.input, 'input');
|
|
65
|
+
const output = requireArg(args.output, 'output');
|
|
66
|
+
|
|
67
|
+
const inputPath = resolvePath(input as string);
|
|
68
|
+
if (!existsSync(inputPath)) {
|
|
69
|
+
throw new FileNotFoundError(input as string);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const outputDir = resolvePath(output);
|
|
73
|
+
ensureOutputDir(`${outputDir}/`);
|
|
74
|
+
|
|
75
|
+
const inputSize = getFileSize(inputPath);
|
|
76
|
+
const basename = getBasename(inputPath);
|
|
77
|
+
|
|
78
|
+
await withSpinner(
|
|
79
|
+
'Splitting PDF...',
|
|
80
|
+
async () => {
|
|
81
|
+
const pdfBytes = await Bun.file(inputPath).arrayBuffer();
|
|
82
|
+
const pdf = await PDFDocument.load(pdfBytes);
|
|
83
|
+
const totalPages = pdf.getPageCount();
|
|
84
|
+
|
|
85
|
+
if (args.every) {
|
|
86
|
+
// Split every N pages
|
|
87
|
+
const chunkSize = Number.parseInt(args.every, 10);
|
|
88
|
+
let chunkIndex = 1;
|
|
89
|
+
|
|
90
|
+
for (let start = 0; start < totalPages; start += chunkSize) {
|
|
91
|
+
const end = Math.min(start + chunkSize, totalPages);
|
|
92
|
+
const newPdf = await PDFDocument.create();
|
|
93
|
+
const pages = await newPdf.copyPages(
|
|
94
|
+
pdf,
|
|
95
|
+
Array.from({ length: end - start }, (_, i) => start + i)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
for (const page of pages) {
|
|
99
|
+
newPdf.addPage(page);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const outputPath = join(outputDir, `${basename}_part${chunkIndex}.pdf`);
|
|
103
|
+
const bytes = await newPdf.save();
|
|
104
|
+
await Bun.write(outputPath, bytes);
|
|
105
|
+
chunkIndex++;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
info(`Created ${chunkIndex - 1} PDF files`);
|
|
109
|
+
} else if (args.pages) {
|
|
110
|
+
// Extract specific pages
|
|
111
|
+
const pageIndices = parsePageRanges(args.pages, totalPages);
|
|
112
|
+
const newPdf = await PDFDocument.create();
|
|
113
|
+
const pages = await newPdf.copyPages(pdf, pageIndices);
|
|
114
|
+
|
|
115
|
+
for (const page of pages) {
|
|
116
|
+
newPdf.addPage(page);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const outputPath = join(outputDir, `${basename}_pages.pdf`);
|
|
120
|
+
const bytes = await newPdf.save();
|
|
121
|
+
await Bun.write(outputPath, bytes);
|
|
122
|
+
|
|
123
|
+
info(`Extracted ${pageIndices.length} pages`);
|
|
124
|
+
} else {
|
|
125
|
+
// Split into individual pages
|
|
126
|
+
for (let i = 0; i < totalPages; i++) {
|
|
127
|
+
const newPdf = await PDFDocument.create();
|
|
128
|
+
const [page] = await newPdf.copyPages(pdf, [i]);
|
|
129
|
+
newPdf.addPage(page);
|
|
130
|
+
|
|
131
|
+
const outputPath = join(outputDir, `${basename}_page${i + 1}.pdf`);
|
|
132
|
+
const bytes = await newPdf.save();
|
|
133
|
+
await Bun.write(outputPath, bytes);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
info(`Created ${totalPages} individual page files`);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
'PDF split successfully'
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
fileResult(inputPath, outputDir, { before: inputSize, after: 0 });
|
|
143
|
+
success(`PDF split into ${outputDir}`);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
handleError(err);
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { defineCommand } from 'citty';
|
|
3
|
+
import { PDFDocument } from 'pdf-lib';
|
|
4
|
+
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
5
|
+
import { ensureOutputDir, getBasename, resolvePath } from '../../utils/files';
|
|
6
|
+
import { info, success } from '../../utils/logger';
|
|
7
|
+
import { convertPdfToImages } from '../../utils/pdf-tools';
|
|
8
|
+
import { withSpinner } from '../../utils/progress';
|
|
9
|
+
|
|
10
|
+
export const toImages = defineCommand({
|
|
11
|
+
meta: {
|
|
12
|
+
name: 'to-images',
|
|
13
|
+
description: 'Convert PDF pages to images',
|
|
14
|
+
},
|
|
15
|
+
args: {
|
|
16
|
+
input: {
|
|
17
|
+
type: 'positional',
|
|
18
|
+
description: 'Input PDF file',
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
output: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
alias: 'o',
|
|
24
|
+
description: 'Output directory',
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
format: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
alias: 'f',
|
|
30
|
+
description: 'Image format: png, jpg',
|
|
31
|
+
default: 'png',
|
|
32
|
+
},
|
|
33
|
+
dpi: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
alias: 'd',
|
|
36
|
+
description: 'Resolution in DPI',
|
|
37
|
+
default: '150',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
async run({ args }) {
|
|
41
|
+
try {
|
|
42
|
+
const input = requireArg(args.input, 'input');
|
|
43
|
+
const output = requireArg(args.output, 'output');
|
|
44
|
+
|
|
45
|
+
const inputPath = resolvePath(input as string);
|
|
46
|
+
if (!existsSync(inputPath)) {
|
|
47
|
+
throw new FileNotFoundError(input as string);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const outputDir = resolvePath(output);
|
|
51
|
+
ensureOutputDir(`${outputDir}/`);
|
|
52
|
+
|
|
53
|
+
const format = (args.format || 'png').toLowerCase() as 'png' | 'jpg';
|
|
54
|
+
const dpi = Number.parseInt(args.dpi || '150', 10);
|
|
55
|
+
const basename = getBasename(inputPath);
|
|
56
|
+
|
|
57
|
+
// Get page count
|
|
58
|
+
const pdfBytes = await Bun.file(inputPath).arrayBuffer();
|
|
59
|
+
const pdf = await PDFDocument.load(pdfBytes);
|
|
60
|
+
const pageCount = pdf.getPageCount();
|
|
61
|
+
|
|
62
|
+
info(`PDF has ${pageCount} pages. Converting...`);
|
|
63
|
+
|
|
64
|
+
const files = await withSpinner(
|
|
65
|
+
'Converting pages to images...',
|
|
66
|
+
async () => {
|
|
67
|
+
return await convertPdfToImages({
|
|
68
|
+
pdfPath: inputPath,
|
|
69
|
+
outputDir,
|
|
70
|
+
dpi,
|
|
71
|
+
format,
|
|
72
|
+
basename,
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
`Converted ${pageCount} pages`
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
success(`Images saved to: ${outputDir}`);
|
|
79
|
+
info(`Created ${files.length} files`);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
handleError(err);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { defineCommand } from 'citty';
|
|
3
|
+
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
4
|
+
import { ensureDir, generateOutputPath, resolvePath } from '../../utils/files';
|
|
5
|
+
import { success } from '../../utils/logger';
|
|
6
|
+
import { extractPdfText } from '../../utils/pdf-tools';
|
|
7
|
+
import { withSpinner } from '../../utils/progress';
|
|
8
|
+
|
|
9
|
+
export const toText = defineCommand({
|
|
10
|
+
meta: {
|
|
11
|
+
name: 'to-text',
|
|
12
|
+
description: 'Extract text content from a PDF',
|
|
13
|
+
},
|
|
14
|
+
args: {
|
|
15
|
+
input: {
|
|
16
|
+
type: 'positional',
|
|
17
|
+
description: 'Input PDF file',
|
|
18
|
+
required: true,
|
|
19
|
+
},
|
|
20
|
+
output: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
alias: 'o',
|
|
23
|
+
description: 'Output text file',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
async run({ args }) {
|
|
27
|
+
try {
|
|
28
|
+
const input = requireArg(args.input, 'input');
|
|
29
|
+
|
|
30
|
+
const inputPath = resolvePath(input as string);
|
|
31
|
+
if (!existsSync(inputPath)) {
|
|
32
|
+
throw new FileNotFoundError(input as string);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const outputPath = generateOutputPath(inputPath, args.output, '', 'txt');
|
|
36
|
+
ensureDir(outputPath);
|
|
37
|
+
|
|
38
|
+
await withSpinner(
|
|
39
|
+
'Extracting text...',
|
|
40
|
+
async () => {
|
|
41
|
+
await extractPdfText(inputPath, outputPath);
|
|
42
|
+
},
|
|
43
|
+
'Text extraction complete'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
success(`Text saved to: ${outputPath}`);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
handleError(err);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { defineCommand } from 'citty';
|
|
3
|
+
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
4
|
+
import { ensureDir, generateOutputPath, getFileSize, resolvePath } from '../../utils/files';
|
|
5
|
+
import { fileResult, success } from '../../utils/logger';
|
|
6
|
+
import { withSpinner } from '../../utils/progress';
|
|
7
|
+
|
|
8
|
+
type Position = 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'diagonal';
|
|
9
|
+
|
|
10
|
+
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
|
11
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
12
|
+
if (!result) {
|
|
13
|
+
return { r: 0.5, g: 0.5, b: 0.5 };
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
r: Number.parseInt(result[1], 16) / 255,
|
|
17
|
+
g: Number.parseInt(result[2], 16) / 255,
|
|
18
|
+
b: Number.parseInt(result[3], 16) / 255,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const watermark = defineCommand({
|
|
23
|
+
meta: {
|
|
24
|
+
name: 'watermark',
|
|
25
|
+
description: 'Add a text watermark to a PDF',
|
|
26
|
+
},
|
|
27
|
+
args: {
|
|
28
|
+
input: {
|
|
29
|
+
type: 'positional',
|
|
30
|
+
description: 'Input PDF file',
|
|
31
|
+
required: true,
|
|
32
|
+
},
|
|
33
|
+
output: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
alias: 'o',
|
|
36
|
+
description: 'Output file path',
|
|
37
|
+
},
|
|
38
|
+
text: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
alias: 't',
|
|
41
|
+
description: 'Watermark text',
|
|
42
|
+
required: true,
|
|
43
|
+
},
|
|
44
|
+
opacity: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: 'Opacity (0.0 - 1.0)',
|
|
47
|
+
default: '0.3',
|
|
48
|
+
},
|
|
49
|
+
size: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
alias: 's',
|
|
52
|
+
description: 'Font size',
|
|
53
|
+
default: '50',
|
|
54
|
+
},
|
|
55
|
+
color: {
|
|
56
|
+
type: 'string',
|
|
57
|
+
alias: 'c',
|
|
58
|
+
description: 'Text color (hex)',
|
|
59
|
+
default: '#888888',
|
|
60
|
+
},
|
|
61
|
+
position: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
alias: 'p',
|
|
64
|
+
description: 'Position: center, diagonal, top-left, top-right, bottom-left, bottom-right',
|
|
65
|
+
default: 'diagonal',
|
|
66
|
+
},
|
|
67
|
+
x: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
description: 'Custom X coordinate (overrides position)',
|
|
70
|
+
},
|
|
71
|
+
y: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'Custom Y coordinate (overrides position)',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
async run({ args }) {
|
|
77
|
+
try {
|
|
78
|
+
const input = requireArg(args.input, 'input');
|
|
79
|
+
const text = requireArg(args.text, 'text');
|
|
80
|
+
|
|
81
|
+
const inputPath = resolvePath(input as string);
|
|
82
|
+
if (!existsSync(inputPath)) {
|
|
83
|
+
throw new FileNotFoundError(input as string);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const outputPath = generateOutputPath(inputPath, args.output, '-watermarked');
|
|
87
|
+
ensureDir(outputPath);
|
|
88
|
+
|
|
89
|
+
const inputSize = getFileSize(inputPath);
|
|
90
|
+
const opacity = Number.parseFloat(args.opacity || '0.3');
|
|
91
|
+
const fontSize = Number.parseInt(args.size || '50', 10);
|
|
92
|
+
const color = hexToRgb(args.color || '#888888');
|
|
93
|
+
const position = (args.position || 'diagonal') as Position;
|
|
94
|
+
const customX = args.x ? Number.parseInt(args.x, 10) : undefined;
|
|
95
|
+
const customY = args.y ? Number.parseInt(args.y, 10) : undefined;
|
|
96
|
+
|
|
97
|
+
const { PDFDocument, StandardFonts, rgb } = await import('pdf-lib');
|
|
98
|
+
|
|
99
|
+
await withSpinner(
|
|
100
|
+
'Adding watermark...',
|
|
101
|
+
async () => {
|
|
102
|
+
const pdfBytes = await Bun.file(inputPath).arrayBuffer();
|
|
103
|
+
const pdf = await PDFDocument.load(pdfBytes);
|
|
104
|
+
const font = await pdf.embedFont(StandardFonts.Helvetica);
|
|
105
|
+
const pages = pdf.getPages();
|
|
106
|
+
|
|
107
|
+
for (const page of pages) {
|
|
108
|
+
const { width, height } = page.getSize();
|
|
109
|
+
const textWidth = font.widthOfTextAtSize(text, fontSize);
|
|
110
|
+
|
|
111
|
+
let x: number;
|
|
112
|
+
let y: number;
|
|
113
|
+
let rotate = 0;
|
|
114
|
+
|
|
115
|
+
if (customX !== undefined && customY !== undefined) {
|
|
116
|
+
x = customX;
|
|
117
|
+
y = customY;
|
|
118
|
+
} else {
|
|
119
|
+
switch (position) {
|
|
120
|
+
case 'center':
|
|
121
|
+
x = (width - textWidth) / 2;
|
|
122
|
+
y = height / 2;
|
|
123
|
+
break;
|
|
124
|
+
case 'diagonal':
|
|
125
|
+
x = width / 4;
|
|
126
|
+
y = height / 3;
|
|
127
|
+
rotate = 45;
|
|
128
|
+
break;
|
|
129
|
+
case 'top-left':
|
|
130
|
+
x = 50;
|
|
131
|
+
y = height - 50 - fontSize;
|
|
132
|
+
break;
|
|
133
|
+
case 'top-right':
|
|
134
|
+
x = width - textWidth - 50;
|
|
135
|
+
y = height - 50 - fontSize;
|
|
136
|
+
break;
|
|
137
|
+
case 'bottom-left':
|
|
138
|
+
x = 50;
|
|
139
|
+
y = 50;
|
|
140
|
+
break;
|
|
141
|
+
case 'bottom-right':
|
|
142
|
+
x = width - textWidth - 50;
|
|
143
|
+
y = 50;
|
|
144
|
+
break;
|
|
145
|
+
default:
|
|
146
|
+
x = (width - textWidth) / 2;
|
|
147
|
+
y = height / 2;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
page.drawText(text, {
|
|
152
|
+
x,
|
|
153
|
+
y,
|
|
154
|
+
size: fontSize,
|
|
155
|
+
font,
|
|
156
|
+
color: rgb(color.r, color.g, color.b),
|
|
157
|
+
opacity,
|
|
158
|
+
// biome-ignore lint/suspicious/noExplicitAny: internal rotation object structure
|
|
159
|
+
rotate: rotate ? ({ type: 'degrees', angle: rotate } as any) : undefined,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const watermarkedBytes = await pdf.save();
|
|
164
|
+
await Bun.write(outputPath, watermarkedBytes);
|
|
165
|
+
},
|
|
166
|
+
'Watermark added successfully'
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
fileResult(inputPath, outputPath, {
|
|
170
|
+
before: inputSize,
|
|
171
|
+
after: getFileSize(outputPath),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
success(`Added watermark "${text}" to all pages`);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
handleError(err);
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
});
|