noupload 1.0.1 → 1.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/.github/workflows/release.yml +64 -24
- package/dist/index.cjs +205 -0
- package/dist/index.js +77795 -169
- package/package.json +24 -19
- package/src/commands/image/adjust.ts +13 -50
- package/src/commands/image/border.ts +22 -18
- package/src/commands/image/bulk-compress.ts +5 -23
- package/src/commands/image/bulk-convert.ts +8 -30
- package/src/commands/image/bulk-resize.ts +21 -12
- package/src/commands/image/compress.ts +4 -25
- package/src/commands/image/convert.ts +5 -32
- package/src/commands/image/crop.ts +7 -6
- package/src/commands/image/favicon.ts +17 -16
- package/src/commands/image/filters.ts +14 -21
- package/src/commands/image/resize.ts +20 -17
- package/src/commands/image/rotate.ts +7 -6
- package/src/commands/image/strip-metadata.ts +9 -5
- package/src/commands/image/to-base64.ts +8 -14
- package/src/commands/image/watermark.ts +62 -65
- package/src/commands/pdf/from-images.ts +10 -10
- package/src/commands/pdf/sign.ts +15 -10
- package/src/commands/qr/scan.ts +7 -9
- package/src/index.ts +1 -1
- package/src/utils/image.ts +46 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { defineCommand } from 'citty';
|
|
3
|
-
import
|
|
3
|
+
import { Jimp } from 'jimp';
|
|
4
4
|
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
5
5
|
import { ensureDir, generateOutputPath, getFileSize, resolvePath } from '../../utils/files';
|
|
6
6
|
import { fileResult, info, success } from '../../utils/logger';
|
|
7
7
|
import { withSpinner } from '../../utils/progress';
|
|
8
|
+
import { writeImage } from '../../utils/image';
|
|
8
9
|
|
|
9
|
-
const AVAILABLE_FILTERS = ['grayscale', 'sepia', 'blur', '
|
|
10
|
+
const AVAILABLE_FILTERS = ['grayscale', 'sepia', 'blur', 'invert', 'normalize'];
|
|
10
11
|
|
|
11
12
|
export const filters = defineCommand({
|
|
12
13
|
meta: {
|
|
@@ -33,8 +34,8 @@ export const filters = defineCommand({
|
|
|
33
34
|
intensity: {
|
|
34
35
|
type: 'string',
|
|
35
36
|
alias: 'i',
|
|
36
|
-
description: 'Filter intensity (for blur
|
|
37
|
-
default: '
|
|
37
|
+
description: 'Filter intensity (for blur)',
|
|
38
|
+
default: '5',
|
|
38
39
|
},
|
|
39
40
|
},
|
|
40
41
|
async run({ args }) {
|
|
@@ -55,40 +56,32 @@ export const filters = defineCommand({
|
|
|
55
56
|
ensureDir(outputPath);
|
|
56
57
|
|
|
57
58
|
const inputSize = getFileSize(inputPath);
|
|
58
|
-
const intensity = Number.
|
|
59
|
+
const intensity = Number.parseInt(args.intensity || '5', 10);
|
|
59
60
|
|
|
60
61
|
await withSpinner(
|
|
61
62
|
`Applying ${filter} filter...`,
|
|
62
63
|
async () => {
|
|
63
|
-
|
|
64
|
+
const image = await Jimp.read(inputPath);
|
|
64
65
|
|
|
65
66
|
switch (filter) {
|
|
66
67
|
case 'grayscale':
|
|
67
|
-
|
|
68
|
+
image.greyscale();
|
|
68
69
|
break;
|
|
69
70
|
case 'sepia':
|
|
70
|
-
|
|
71
|
-
pipeline = pipeline.recomb([
|
|
72
|
-
[0.393, 0.769, 0.189],
|
|
73
|
-
[0.349, 0.686, 0.168],
|
|
74
|
-
[0.272, 0.534, 0.131],
|
|
75
|
-
]);
|
|
71
|
+
image.sepia();
|
|
76
72
|
break;
|
|
77
73
|
case 'blur':
|
|
78
|
-
|
|
74
|
+
image.blur(intensity);
|
|
79
75
|
break;
|
|
80
|
-
case '
|
|
81
|
-
|
|
82
|
-
break;
|
|
83
|
-
case 'negate':
|
|
84
|
-
pipeline = pipeline.negate();
|
|
76
|
+
case 'invert':
|
|
77
|
+
image.invert();
|
|
85
78
|
break;
|
|
86
79
|
case 'normalize':
|
|
87
|
-
|
|
80
|
+
image.normalize();
|
|
88
81
|
break;
|
|
89
82
|
}
|
|
90
83
|
|
|
91
|
-
await
|
|
84
|
+
await writeImage(image, outputPath);
|
|
92
85
|
},
|
|
93
86
|
'Filter applied successfully'
|
|
94
87
|
);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { defineCommand } from 'citty';
|
|
3
|
+
import { Jimp } from 'jimp';
|
|
3
4
|
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
4
5
|
import { ensureDir, generateOutputPath, getFileSize, resolvePath } from '../../utils/files';
|
|
5
6
|
import { fileResult, info, success } from '../../utils/logger';
|
|
6
7
|
import { withSpinner } from '../../utils/progress';
|
|
7
|
-
|
|
8
|
-
type FitMode = 'cover' | 'contain' | 'fill' | 'inside' | 'outside';
|
|
8
|
+
import { getMimeType, writeImage } from '../../utils/image';
|
|
9
9
|
|
|
10
10
|
export const resize = defineCommand({
|
|
11
11
|
meta: {
|
|
@@ -36,7 +36,7 @@ export const resize = defineCommand({
|
|
|
36
36
|
fit: {
|
|
37
37
|
type: 'string',
|
|
38
38
|
alias: 'f',
|
|
39
|
-
description: 'Fit mode: cover, contain,
|
|
39
|
+
description: 'Fit mode: cover, contain, inside (default: inside)',
|
|
40
40
|
default: 'inside',
|
|
41
41
|
},
|
|
42
42
|
scale: {
|
|
@@ -58,15 +58,10 @@ export const resize = defineCommand({
|
|
|
58
58
|
ensureDir(outputPath);
|
|
59
59
|
|
|
60
60
|
const inputSize = getFileSize(inputPath);
|
|
61
|
-
const fit = (args.fit || 'inside') as FitMode;
|
|
62
|
-
|
|
63
|
-
// Lazy load sharp
|
|
64
|
-
const { default: sharp } = await import('sharp');
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const originalHeight = metadata.height || 0;
|
|
62
|
+
const image = await Jimp.read(inputPath);
|
|
63
|
+
const originalWidth = image.width;
|
|
64
|
+
const originalHeight = image.height;
|
|
70
65
|
|
|
71
66
|
let targetWidth: number | undefined;
|
|
72
67
|
let targetHeight: number | undefined;
|
|
@@ -87,15 +82,23 @@ export const resize = defineCommand({
|
|
|
87
82
|
await withSpinner(
|
|
88
83
|
'Resizing image...',
|
|
89
84
|
async () => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
// Calculate dimensions maintaining aspect ratio if only one dimension specified
|
|
86
|
+
if (targetWidth && !targetHeight) {
|
|
87
|
+
const ratio = targetWidth / originalWidth;
|
|
88
|
+
targetHeight = Math.round(originalHeight * ratio);
|
|
89
|
+
} else if (!targetWidth && targetHeight) {
|
|
90
|
+
const ratio = targetHeight / originalHeight;
|
|
91
|
+
targetWidth = Math.round(originalWidth * ratio);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
image.resize({ w: targetWidth as number, h: targetHeight as number });
|
|
95
|
+
await writeImage(image, outputPath);
|
|
93
96
|
},
|
|
94
97
|
'Image resized successfully'
|
|
95
98
|
);
|
|
96
99
|
|
|
97
|
-
const
|
|
98
|
-
info(`${originalWidth}x${originalHeight} → ${
|
|
100
|
+
const outputImage = await Jimp.read(outputPath);
|
|
101
|
+
info(`${originalWidth}x${originalHeight} → ${outputImage.width}x${outputImage.height}`);
|
|
99
102
|
|
|
100
103
|
fileResult(inputPath, outputPath, {
|
|
101
104
|
before: inputSize,
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { defineCommand } from 'citty';
|
|
3
|
-
import
|
|
3
|
+
import { Jimp } from 'jimp';
|
|
4
4
|
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
5
5
|
import { ensureDir, generateOutputPath, getFileSize, resolvePath } from '../../utils/files';
|
|
6
6
|
import { fileResult, info, success } from '../../utils/logger';
|
|
7
7
|
import { withSpinner } from '../../utils/progress';
|
|
8
|
+
import { writeImage } from '../../utils/image';
|
|
8
9
|
|
|
9
10
|
export const rotate = defineCommand({
|
|
10
11
|
meta: {
|
|
@@ -54,20 +55,20 @@ export const rotate = defineCommand({
|
|
|
54
55
|
await withSpinner(
|
|
55
56
|
'Rotating image...',
|
|
56
57
|
async () => {
|
|
57
|
-
|
|
58
|
+
const image = await Jimp.read(inputPath);
|
|
58
59
|
|
|
59
60
|
if (args.angle) {
|
|
60
61
|
const angle = Number.parseInt(args.angle, 10);
|
|
61
|
-
|
|
62
|
+
image.rotate({ deg: angle });
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
if (args.flip === 'horizontal') {
|
|
65
|
-
|
|
66
|
+
image.flip({ horizontal: true, vertical: false });
|
|
66
67
|
} else if (args.flip === 'vertical') {
|
|
67
|
-
|
|
68
|
+
image.flip({ horizontal: false, vertical: true });
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
await
|
|
71
|
+
await writeImage(image, outputPath);
|
|
71
72
|
},
|
|
72
73
|
'Image rotated successfully'
|
|
73
74
|
);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { defineCommand } from 'citty';
|
|
3
|
-
import
|
|
3
|
+
import { Jimp } from 'jimp';
|
|
4
4
|
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
5
5
|
import { ensureDir, generateOutputPath, getFileSize, resolvePath } from '../../utils/files';
|
|
6
|
-
import { fileResult, success } from '../../utils/logger';
|
|
6
|
+
import { fileResult, info, success } from '../../utils/logger';
|
|
7
7
|
import { withSpinner } from '../../utils/progress';
|
|
8
|
+
import { writeImage } from '../../utils/image';
|
|
8
9
|
|
|
9
10
|
export const stripMetadata = defineCommand({
|
|
10
11
|
meta: {
|
|
@@ -40,13 +41,16 @@ export const stripMetadata = defineCommand({
|
|
|
40
41
|
await withSpinner(
|
|
41
42
|
'Removing metadata...',
|
|
42
43
|
async () => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// Jimp automatically strips EXIF data when reading/writing
|
|
45
|
+
// as it only works with raw pixel data
|
|
46
|
+
const image = await Jimp.read(inputPath);
|
|
47
|
+
await writeImage(image, outputPath);
|
|
46
48
|
},
|
|
47
49
|
'Metadata removed successfully'
|
|
48
50
|
);
|
|
49
51
|
|
|
52
|
+
info('EXIF and metadata stripped from image');
|
|
53
|
+
|
|
50
54
|
fileResult(inputPath, outputPath, {
|
|
51
55
|
before: inputSize,
|
|
52
56
|
after: getFileSize(outputPath),
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
+
import { extname } from 'node:path';
|
|
2
3
|
import { defineCommand } from 'citty';
|
|
3
|
-
import
|
|
4
|
+
import { Jimp } from 'jimp';
|
|
4
5
|
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
5
6
|
import { resolvePath } from '../../utils/files';
|
|
6
7
|
import { info, success } from '../../utils/logger';
|
|
7
8
|
import { withSpinner } from '../../utils/progress';
|
|
9
|
+
import { getMimeType } from '../../utils/image';
|
|
8
10
|
|
|
9
11
|
export const toBase64 = defineCommand({
|
|
10
12
|
meta: {
|
|
@@ -39,20 +41,12 @@ export const toBase64 = defineCommand({
|
|
|
39
41
|
await withSpinner(
|
|
40
42
|
'Converting to Base64...',
|
|
41
43
|
async () => {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
base64String = buffer.toString('base64');
|
|
44
|
+
const image = await Jimp.read(inputPath);
|
|
45
|
+
mimeType = getMimeType(inputPath);
|
|
45
46
|
|
|
46
|
-
//
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
png: 'image/png',
|
|
50
|
-
webp: 'image/webp',
|
|
51
|
-
gif: 'image/gif',
|
|
52
|
-
svg: 'image/svg+xml',
|
|
53
|
-
avif: 'image/avif',
|
|
54
|
-
};
|
|
55
|
-
mimeType = formatMap[metadata.format || 'jpeg'] || 'image/jpeg';
|
|
47
|
+
// biome-ignore lint/suspicious/noExplicitAny: Jimp types are complex
|
|
48
|
+
const buffer = await (image as any).getBuffer(mimeType);
|
|
49
|
+
base64String = buffer.toString('base64');
|
|
56
50
|
},
|
|
57
51
|
'Conversion complete'
|
|
58
52
|
);
|
|
@@ -1,27 +1,19 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
2
3
|
import { defineCommand } from 'citty';
|
|
4
|
+
import { Jimp } from 'jimp';
|
|
3
5
|
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
4
6
|
import { ensureDir, generateOutputPath, getFileSize, resolvePath } from '../../utils/files';
|
|
5
|
-
import { fileResult, success } from '../../utils/logger';
|
|
7
|
+
import { fileResult, info, success, warn } from '../../utils/logger';
|
|
6
8
|
import { withSpinner } from '../../utils/progress';
|
|
9
|
+
import { writeImage } from '../../utils/image';
|
|
7
10
|
|
|
8
11
|
type Position = 'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
9
12
|
|
|
10
|
-
function getGravity(position: Position): string {
|
|
11
|
-
const map: Record<Position, string> = {
|
|
12
|
-
center: 'center',
|
|
13
|
-
'top-left': 'northwest',
|
|
14
|
-
'top-right': 'northeast',
|
|
15
|
-
'bottom-left': 'southwest',
|
|
16
|
-
'bottom-right': 'southeast',
|
|
17
|
-
};
|
|
18
|
-
return map[position] || 'southeast';
|
|
19
|
-
}
|
|
20
|
-
|
|
21
13
|
export const watermark = defineCommand({
|
|
22
14
|
meta: {
|
|
23
15
|
name: 'watermark',
|
|
24
|
-
description: 'Add a text watermark to an image',
|
|
16
|
+
description: 'Add a text watermark to an image (requires a watermark image)',
|
|
25
17
|
},
|
|
26
18
|
args: {
|
|
27
19
|
input: {
|
|
@@ -34,10 +26,10 @@ export const watermark = defineCommand({
|
|
|
34
26
|
alias: 'o',
|
|
35
27
|
description: 'Output file path',
|
|
36
28
|
},
|
|
37
|
-
|
|
29
|
+
image: {
|
|
38
30
|
type: 'string',
|
|
39
|
-
alias: '
|
|
40
|
-
description: 'Watermark
|
|
31
|
+
alias: 'i',
|
|
32
|
+
description: 'Watermark image file (PNG with transparency recommended)',
|
|
41
33
|
required: true,
|
|
42
34
|
},
|
|
43
35
|
position: {
|
|
@@ -51,28 +43,27 @@ export const watermark = defineCommand({
|
|
|
51
43
|
description: 'Opacity (0.0-1.0)',
|
|
52
44
|
default: '0.5',
|
|
53
45
|
},
|
|
54
|
-
|
|
46
|
+
scale: {
|
|
55
47
|
type: 'string',
|
|
56
48
|
alias: 's',
|
|
57
|
-
description: '
|
|
58
|
-
default: '
|
|
59
|
-
},
|
|
60
|
-
color: {
|
|
61
|
-
type: 'string',
|
|
62
|
-
alias: 'c',
|
|
63
|
-
description: 'Text color (hex)',
|
|
64
|
-
default: '#ffffff',
|
|
49
|
+
description: 'Scale of watermark relative to image (0.0-1.0)',
|
|
50
|
+
default: '0.2',
|
|
65
51
|
},
|
|
66
52
|
},
|
|
67
53
|
async run({ args }) {
|
|
68
54
|
try {
|
|
69
55
|
const input = requireArg(args.input, 'input');
|
|
70
|
-
const
|
|
56
|
+
const watermarkImage = requireArg(args.image, 'image');
|
|
71
57
|
|
|
72
58
|
const inputPath = resolvePath(input as string);
|
|
59
|
+
const watermarkPath = resolvePath(watermarkImage);
|
|
60
|
+
|
|
73
61
|
if (!existsSync(inputPath)) {
|
|
74
62
|
throw new FileNotFoundError(input as string);
|
|
75
63
|
}
|
|
64
|
+
if (!existsSync(watermarkPath)) {
|
|
65
|
+
throw new FileNotFoundError(watermarkImage);
|
|
66
|
+
}
|
|
76
67
|
|
|
77
68
|
const outputPath = generateOutputPath(inputPath, args.output, '-watermarked');
|
|
78
69
|
ensureDir(outputPath);
|
|
@@ -80,50 +71,56 @@ export const watermark = defineCommand({
|
|
|
80
71
|
const inputSize = getFileSize(inputPath);
|
|
81
72
|
const position = (args.position || 'bottom-right') as Position;
|
|
82
73
|
const opacity = Number.parseFloat(args.opacity || '0.5');
|
|
83
|
-
const
|
|
84
|
-
const color = args.color || '#ffffff';
|
|
85
|
-
|
|
86
|
-
// Lazy load sharp
|
|
87
|
-
const { default: sharp } = await import('sharp');
|
|
74
|
+
const scale = Number.parseFloat(args.scale || '0.2');
|
|
88
75
|
|
|
89
76
|
await withSpinner(
|
|
90
77
|
'Adding watermark...',
|
|
91
78
|
async () => {
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
79
|
+
const image = await Jimp.read(inputPath);
|
|
80
|
+
const watermark = await Jimp.read(watermarkPath);
|
|
81
|
+
|
|
82
|
+
// Scale watermark relative to image size
|
|
83
|
+
const watermarkWidth = Math.round(image.width * scale);
|
|
84
|
+
const ratio = watermarkWidth / watermark.width;
|
|
85
|
+
const watermarkHeight = Math.round(watermark.height * ratio);
|
|
86
|
+
|
|
87
|
+
watermark.resize({ w: watermarkWidth, h: watermarkHeight });
|
|
88
|
+
watermark.opacity(opacity);
|
|
89
|
+
|
|
90
|
+
// Calculate position
|
|
91
|
+
let x: number;
|
|
92
|
+
let y: number;
|
|
93
|
+
const margin = 20;
|
|
96
94
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
95
|
+
switch (position) {
|
|
96
|
+
case 'top-left':
|
|
97
|
+
x = margin;
|
|
98
|
+
y = margin;
|
|
99
|
+
break;
|
|
100
|
+
case 'top-right':
|
|
101
|
+
x = image.width - watermarkWidth - margin;
|
|
102
|
+
y = margin;
|
|
103
|
+
break;
|
|
104
|
+
case 'bottom-left':
|
|
105
|
+
x = margin;
|
|
106
|
+
y = image.height - watermarkHeight - margin;
|
|
107
|
+
break;
|
|
108
|
+
case 'bottom-right':
|
|
109
|
+
x = image.width - watermarkWidth - margin;
|
|
110
|
+
y = image.height - watermarkHeight - margin;
|
|
111
|
+
break;
|
|
112
|
+
case 'center':
|
|
113
|
+
x = Math.round((image.width - watermarkWidth) / 2);
|
|
114
|
+
y = Math.round((image.height - watermarkHeight) / 2);
|
|
115
|
+
break;
|
|
116
|
+
default:
|
|
117
|
+
x = image.width - watermarkWidth - margin;
|
|
118
|
+
y = image.height - watermarkHeight - margin;
|
|
119
|
+
}
|
|
117
120
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
input: Buffer.from(svg),
|
|
122
|
-
// biome-ignore lint/suspicious/noExplicitAny: sharp types mismatch with lazy load
|
|
123
|
-
gravity: getGravity(position) as any,
|
|
124
|
-
},
|
|
125
|
-
])
|
|
126
|
-
.toFile(outputPath);
|
|
121
|
+
// Composite watermark onto image
|
|
122
|
+
image.composite(watermark, x, y);
|
|
123
|
+
await writeImage(image, outputPath);
|
|
127
124
|
},
|
|
128
125
|
'Watermark added successfully'
|
|
129
126
|
);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { defineCommand } from 'citty';
|
|
2
|
-
import
|
|
2
|
+
import { Jimp } from 'jimp';
|
|
3
3
|
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
4
4
|
import { ensureDir, getFileSize, globFiles, resolvePath } from '../../utils/files';
|
|
5
5
|
import { fileResult, info, success } from '../../utils/logger';
|
|
6
6
|
import { withSpinner } from '../../utils/progress';
|
|
7
|
+
import { getMimeType } from '../../utils/image';
|
|
7
8
|
|
|
8
9
|
export const fromImages = defineCommand({
|
|
9
10
|
meta: {
|
|
@@ -59,21 +60,20 @@ export const fromImages = defineCommand({
|
|
|
59
60
|
const pdf = await PDFDocument.create();
|
|
60
61
|
|
|
61
62
|
for (const imagePath of imageFiles) {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
63
|
+
const image = await Jimp.read(imagePath);
|
|
64
|
+
const mime = getMimeType(imagePath);
|
|
64
65
|
|
|
65
|
-
// Determine image type and convert if necessary
|
|
66
|
-
const metadata = await sharp(Buffer.from(imageBytes)).metadata();
|
|
67
66
|
// biome-ignore lint/suspicious/noExplicitAny: complex PDFImage type from lazy load
|
|
68
67
|
let embeddedImage: any;
|
|
69
68
|
|
|
70
|
-
if (
|
|
71
|
-
|
|
69
|
+
if (mime === 'image/png') {
|
|
70
|
+
// biome-ignore lint/suspicious/noExplicitAny: Jimp types are complex
|
|
71
|
+
const pngBuffer = await (image as any).getBuffer('image/png');
|
|
72
|
+
embeddedImage = await pdf.embedPng(pngBuffer);
|
|
72
73
|
} else {
|
|
73
74
|
// Convert to JPEG for other formats
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
.toBuffer();
|
|
75
|
+
// biome-ignore lint/suspicious/noExplicitAny: Jimp types are complex
|
|
76
|
+
const jpegBuffer = await (image as any).getBuffer('image/jpeg', { quality: 90 });
|
|
77
77
|
embeddedImage = await pdf.embedJpg(jpegBuffer);
|
|
78
78
|
}
|
|
79
79
|
|
package/src/commands/pdf/sign.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { defineCommand } from 'citty';
|
|
3
3
|
import { PDFDocument } from 'pdf-lib';
|
|
4
|
-
import
|
|
4
|
+
import { Jimp } from 'jimp';
|
|
5
5
|
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
6
6
|
import { ensureDir, generateOutputPath, getFileSize, resolvePath } from '../../utils/files';
|
|
7
7
|
import { fileResult, success } from '../../utils/logger';
|
|
@@ -76,16 +76,21 @@ export const sign = defineCommand({
|
|
|
76
76
|
const pdfBytes = await Bun.file(inputPath).arrayBuffer();
|
|
77
77
|
const pdf = await PDFDocument.load(pdfBytes);
|
|
78
78
|
|
|
79
|
-
// Process signature image
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
// Process signature image with Jimp
|
|
80
|
+
const signatureImage = await Jimp.read(signaturePath);
|
|
81
|
+
|
|
82
|
+
// Calculate height to maintain aspect ratio
|
|
83
|
+
const ratio = signatureWidth / signatureImage.width;
|
|
84
|
+
const signatureHeight = Math.round(signatureImage.height * ratio);
|
|
85
|
+
|
|
86
|
+
signatureImage.resize({ w: signatureWidth, h: signatureHeight });
|
|
87
|
+
|
|
88
|
+
// biome-ignore lint/suspicious/noExplicitAny: Jimp types are complex
|
|
89
|
+
const processedSignature = await (signatureImage as any).getBuffer('image/png');
|
|
85
90
|
|
|
86
91
|
// Embed image
|
|
87
|
-
const
|
|
88
|
-
const signatureDims =
|
|
92
|
+
const embeddedSignature = await pdf.embedPng(processedSignature);
|
|
93
|
+
const signatureDims = embeddedSignature.scale(1);
|
|
89
94
|
|
|
90
95
|
// Determine which page to sign
|
|
91
96
|
const totalPages = pdf.getPageCount();
|
|
@@ -130,7 +135,7 @@ export const sign = defineCommand({
|
|
|
130
135
|
}
|
|
131
136
|
|
|
132
137
|
// Draw signature
|
|
133
|
-
page.drawImage(
|
|
138
|
+
page.drawImage(embeddedSignature, {
|
|
134
139
|
x,
|
|
135
140
|
y,
|
|
136
141
|
width: signatureDims.width,
|
package/src/commands/qr/scan.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { defineCommand } from 'citty';
|
|
3
3
|
import jsQR from 'jsqr';
|
|
4
|
-
import
|
|
4
|
+
import { Jimp } from 'jimp';
|
|
5
5
|
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
6
6
|
import { resolvePath } from '../../utils/files';
|
|
7
7
|
import { info, success, warn } from '../../utils/logger';
|
|
@@ -47,19 +47,17 @@ export const scan = defineCommand({
|
|
|
47
47
|
const result = await withSpinner(
|
|
48
48
|
'Scanning QR code...',
|
|
49
49
|
async (): Promise<ScanResult | null> => {
|
|
50
|
-
// Load image
|
|
51
|
-
const image =
|
|
52
|
-
const { width, height } =
|
|
50
|
+
// Load image with Jimp
|
|
51
|
+
const image = await Jimp.read(inputPath);
|
|
52
|
+
const { width, height } = image;
|
|
53
53
|
|
|
54
54
|
if (!width || !height) {
|
|
55
55
|
throw new Error('Could not read image dimensions');
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// Create Uint8ClampedArray for jsQR
|
|
62
|
-
const data = new Uint8ClampedArray(rawData);
|
|
58
|
+
// Get raw RGBA data from Jimp
|
|
59
|
+
// Jimp bitmap.data is already in RGBA format
|
|
60
|
+
const data = new Uint8ClampedArray(image.bitmap.data);
|
|
63
61
|
|
|
64
62
|
// Scan for QR code
|
|
65
63
|
const qrCode = jsQR(data, width, height);
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { extname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get MIME type from file extension
|
|
6
|
+
*/
|
|
7
|
+
export function getMimeType(filePath: string): string {
|
|
8
|
+
const ext = extname(filePath).toLowerCase().slice(1);
|
|
9
|
+
const mimeMap: Record<string, string> = {
|
|
10
|
+
jpg: 'image/jpeg',
|
|
11
|
+
jpeg: 'image/jpeg',
|
|
12
|
+
png: 'image/png',
|
|
13
|
+
gif: 'image/gif',
|
|
14
|
+
bmp: 'image/bmp',
|
|
15
|
+
tiff: 'image/tiff',
|
|
16
|
+
tif: 'image/tiff',
|
|
17
|
+
};
|
|
18
|
+
return mimeMap[ext] || 'image/png';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Write a Jimp image to a file with proper MIME type detection
|
|
23
|
+
*/
|
|
24
|
+
export async function writeImage(
|
|
25
|
+
// biome-ignore lint/suspicious/noExplicitAny: Jimp types are overly complex
|
|
26
|
+
image: any,
|
|
27
|
+
outputPath: string,
|
|
28
|
+
options?: { quality?: number }
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const mime = getMimeType(outputPath);
|
|
31
|
+
|
|
32
|
+
let buffer: Buffer;
|
|
33
|
+
|
|
34
|
+
if (mime === 'image/jpeg' && options?.quality) {
|
|
35
|
+
buffer = await image.getBuffer(mime, { quality: options.quality });
|
|
36
|
+
} else {
|
|
37
|
+
buffer = await image.getBuffer(mime);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
writeFileSync(outputPath, buffer);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Supported image formats for Jimp
|
|
45
|
+
*/
|
|
46
|
+
export const SUPPORTED_FORMATS = ['jpg', 'jpeg', 'png', 'bmp', 'gif', 'tiff'];
|