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
package/package.json
CHANGED
|
@@ -1,33 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "noupload",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Privacy-first CLI for PDF, Image, Audio, and QR operations - all processed locally",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"noupload": "./dist/index.js"
|
|
9
9
|
},
|
|
10
|
-
"pkg": {
|
|
11
|
-
"assets": [
|
|
12
|
-
"node_modules/sharp/**/*"
|
|
13
|
-
],
|
|
14
|
-
"targets": [
|
|
15
|
-
"node18-linux-x64",
|
|
16
|
-
"node18-linux-arm64",
|
|
17
|
-
"node18-macos-x64",
|
|
18
|
-
"node18-macos-arm64",
|
|
19
|
-
"node18-win-x64"
|
|
20
|
-
]
|
|
21
|
-
},
|
|
22
10
|
"scripts": {
|
|
23
11
|
"dev": "bun run src/index.ts",
|
|
24
|
-
"build": "bun build src/index.ts --
|
|
12
|
+
"build": "bun build src/index.ts --outfile dist/index.js --target node",
|
|
25
13
|
"build:compile": "bun build src/index.ts --compile --outfile dist/noupload",
|
|
26
14
|
"build:all": "bun run scripts/build.ts",
|
|
27
15
|
"test": "bun test",
|
|
28
16
|
"lint": "bunx @biomejs/biome check .",
|
|
29
17
|
"format": "bunx @biomejs/biome format --write .",
|
|
30
|
-
"typecheck": "tsc --noEmit"
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"prepublishOnly": "bun run build"
|
|
31
20
|
},
|
|
32
21
|
"dependencies": {
|
|
33
22
|
"citty": "^0.1.6",
|
|
@@ -35,25 +24,41 @@
|
|
|
35
24
|
"consola": "^3.2.3",
|
|
36
25
|
"filesize": "^10.1.6",
|
|
37
26
|
"fluent-ffmpeg": "^2.1.3",
|
|
27
|
+
"jimp": "^1.6.0",
|
|
38
28
|
"jsqr": "^1.4.0",
|
|
39
29
|
"ora": "^8.1.1",
|
|
40
30
|
"pdf-lib": "^1.17.1",
|
|
41
31
|
"qrcode": "^1.5.4",
|
|
42
|
-
"sharp": "^0.33.5",
|
|
43
32
|
"tesseract.js": "^5.1.1"
|
|
44
33
|
},
|
|
45
34
|
"devDependencies": {
|
|
35
|
+
"@biomejs/biome": "^1.9.4",
|
|
46
36
|
"@types/bun": "latest",
|
|
47
37
|
"@types/cli-progress": "^3.11.6",
|
|
48
38
|
"@types/fluent-ffmpeg": "^2.1.27",
|
|
49
39
|
"@types/qrcode": "^1.5.5",
|
|
50
40
|
"@types/which": "^3.0.4",
|
|
51
|
-
"@biomejs/biome": "^1.9.4",
|
|
52
41
|
"typescript": "^5.7.2"
|
|
53
42
|
},
|
|
54
43
|
"engines": {
|
|
55
|
-
"bun": ">=1.0.0"
|
|
44
|
+
"bun": ">=1.0.0",
|
|
45
|
+
"node": ">=18.0.0"
|
|
56
46
|
},
|
|
57
47
|
"license": "MIT",
|
|
58
|
-
"
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "https://github.com/shockz09/nouploadcli.git"
|
|
51
|
+
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"pdf",
|
|
54
|
+
"image",
|
|
55
|
+
"audio",
|
|
56
|
+
"qr",
|
|
57
|
+
"cli",
|
|
58
|
+
"privacy",
|
|
59
|
+
"offline",
|
|
60
|
+
"compress",
|
|
61
|
+
"convert",
|
|
62
|
+
"ocr"
|
|
63
|
+
]
|
|
59
64
|
}
|
|
@@ -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 adjust = defineCommand({
|
|
10
11
|
meta: {
|
|
@@ -25,27 +26,12 @@ export const adjust = defineCommand({
|
|
|
25
26
|
brightness: {
|
|
26
27
|
type: 'string',
|
|
27
28
|
alias: 'b',
|
|
28
|
-
description: 'Brightness
|
|
29
|
-
},
|
|
30
|
-
saturation: {
|
|
31
|
-
type: 'string',
|
|
32
|
-
alias: 's',
|
|
33
|
-
description: 'Saturation multiplier (1.0 = no change)',
|
|
34
|
-
},
|
|
35
|
-
hue: {
|
|
36
|
-
type: 'string',
|
|
37
|
-
alias: 'h',
|
|
38
|
-
description: 'Hue rotation in degrees',
|
|
29
|
+
description: 'Brightness adjustment (-1.0 to 1.0, 0 = no change)',
|
|
39
30
|
},
|
|
40
31
|
contrast: {
|
|
41
32
|
type: 'string',
|
|
42
33
|
alias: 'c',
|
|
43
|
-
description: 'Contrast adjustment (-
|
|
44
|
-
},
|
|
45
|
-
gamma: {
|
|
46
|
-
type: 'string',
|
|
47
|
-
alias: 'g',
|
|
48
|
-
description: 'Gamma correction (0.1 to 10)',
|
|
34
|
+
description: 'Contrast adjustment (-1.0 to 1.0, 0 = no change)',
|
|
49
35
|
},
|
|
50
36
|
},
|
|
51
37
|
async run({ args }) {
|
|
@@ -57,6 +43,10 @@ export const adjust = defineCommand({
|
|
|
57
43
|
throw new FileNotFoundError(input as string);
|
|
58
44
|
}
|
|
59
45
|
|
|
46
|
+
if (!args.brightness && !args.contrast) {
|
|
47
|
+
throw new Error('Specify --brightness or --contrast');
|
|
48
|
+
}
|
|
49
|
+
|
|
60
50
|
const outputPath = generateOutputPath(inputPath, args.output, '-adjusted');
|
|
61
51
|
ensureDir(outputPath);
|
|
62
52
|
|
|
@@ -66,48 +56,21 @@ export const adjust = defineCommand({
|
|
|
66
56
|
await withSpinner(
|
|
67
57
|
'Adjusting image...',
|
|
68
58
|
async () => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// Apply modulate for brightness, saturation, hue
|
|
72
|
-
const modulate: { brightness?: number; saturation?: number; hue?: number } = {};
|
|
59
|
+
const image = await Jimp.read(inputPath);
|
|
73
60
|
|
|
74
61
|
if (args.brightness) {
|
|
75
|
-
|
|
62
|
+
const brightness = Number.parseFloat(args.brightness);
|
|
63
|
+
image.brightness(brightness);
|
|
76
64
|
adjustments.push(`brightness: ${args.brightness}`);
|
|
77
65
|
}
|
|
78
66
|
|
|
79
|
-
if (args.saturation) {
|
|
80
|
-
modulate.saturation = Number.parseFloat(args.saturation);
|
|
81
|
-
adjustments.push(`saturation: ${args.saturation}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (args.hue) {
|
|
85
|
-
modulate.hue = Number.parseInt(args.hue, 10);
|
|
86
|
-
adjustments.push(`hue: ${args.hue}°`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (Object.keys(modulate).length > 0) {
|
|
90
|
-
pipeline = pipeline.modulate(modulate);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Apply linear for contrast
|
|
94
67
|
if (args.contrast) {
|
|
95
68
|
const contrast = Number.parseFloat(args.contrast);
|
|
96
|
-
|
|
97
|
-
const multiplier = 1 + contrast / 100;
|
|
98
|
-
const offset = 128 * (1 - multiplier);
|
|
99
|
-
pipeline = pipeline.linear(multiplier, offset);
|
|
69
|
+
image.contrast(contrast);
|
|
100
70
|
adjustments.push(`contrast: ${args.contrast}`);
|
|
101
71
|
}
|
|
102
72
|
|
|
103
|
-
|
|
104
|
-
if (args.gamma) {
|
|
105
|
-
const gamma = Number.parseFloat(args.gamma);
|
|
106
|
-
pipeline = pipeline.gamma(gamma);
|
|
107
|
-
adjustments.push(`gamma: ${args.gamma}`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
await pipeline.toFile(outputPath);
|
|
73
|
+
await writeImage(image, outputPath);
|
|
111
74
|
},
|
|
112
75
|
'Image adjusted successfully'
|
|
113
76
|
);
|
|
@@ -1,21 +1,22 @@
|
|
|
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
|
-
function
|
|
10
|
+
function hexToRgba(hex: string): number {
|
|
10
11
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
11
12
|
if (!result) {
|
|
12
|
-
return
|
|
13
|
+
return 0xffffffff; // White default
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const r = Number.parseInt(result[1], 16);
|
|
16
|
+
const g = Number.parseInt(result[2], 16);
|
|
17
|
+
const b = Number.parseInt(result[3], 16);
|
|
18
|
+
// Jimp uses RGBA format: 0xRRGGBBAA
|
|
19
|
+
return ((r << 24) | (g << 16) | (b << 8) | 0xff) >>> 0;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export const border = defineCommand({
|
|
@@ -61,20 +62,23 @@ export const border = defineCommand({
|
|
|
61
62
|
|
|
62
63
|
const inputSize = getFileSize(inputPath);
|
|
63
64
|
const borderSize = Number.parseInt(args.size || '10', 10);
|
|
64
|
-
const
|
|
65
|
+
const borderColor = hexToRgba(args.color || '#ffffff');
|
|
65
66
|
|
|
66
67
|
await withSpinner(
|
|
67
68
|
`Adding ${borderSize}px border...`,
|
|
68
69
|
async () => {
|
|
69
|
-
await
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
const sourceImage = await Jimp.read(inputPath);
|
|
71
|
+
|
|
72
|
+
// Create new image with border
|
|
73
|
+
const newWidth = sourceImage.width + borderSize * 2;
|
|
74
|
+
const newHeight = sourceImage.height + borderSize * 2;
|
|
75
|
+
|
|
76
|
+
const newImage = new Jimp({ width: newWidth, height: newHeight, color: borderColor });
|
|
77
|
+
|
|
78
|
+
// Composite original image onto the new image with border offset
|
|
79
|
+
newImage.composite(sourceImage, borderSize, borderSize);
|
|
80
|
+
|
|
81
|
+
await writeImage(newImage, outputPath);
|
|
78
82
|
},
|
|
79
83
|
'Border added successfully'
|
|
80
84
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { defineCommand } from 'citty';
|
|
3
|
-
import
|
|
3
|
+
import { Jimp } from 'jimp';
|
|
4
4
|
import { handleError, requireArg } from '../../utils/errors';
|
|
5
5
|
import {
|
|
6
6
|
ensureOutputDir,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from '../../utils/files';
|
|
13
13
|
import { bulkResult, info, success } from '../../utils/logger';
|
|
14
14
|
import { withProgress } from '../../utils/progress';
|
|
15
|
+
import { writeImage } from '../../utils/image';
|
|
15
16
|
|
|
16
17
|
export const bulkCompress = defineCommand({
|
|
17
18
|
meta: {
|
|
@@ -49,7 +50,7 @@ export const bulkCompress = defineCommand({
|
|
|
49
50
|
// Find all image files
|
|
50
51
|
let pattern = inputPath;
|
|
51
52
|
if (!inputPath.includes('*')) {
|
|
52
|
-
pattern = `${inputPath}/**/*.{jpg,jpeg,png,
|
|
53
|
+
pattern = `${inputPath}/**/*.{jpg,jpeg,png,bmp,gif,tiff}`;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
const files = await globFiles(pattern);
|
|
@@ -72,27 +73,8 @@ export const bulkCompress = defineCommand({
|
|
|
72
73
|
|
|
73
74
|
const inputSize = getFileSize(file);
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
switch (ext) {
|
|
78
|
-
case 'jpg':
|
|
79
|
-
case 'jpeg':
|
|
80
|
-
pipeline = pipeline.jpeg({ quality, mozjpeg: true });
|
|
81
|
-
break;
|
|
82
|
-
case 'png':
|
|
83
|
-
pipeline = pipeline.png({ quality, compressionLevel: 9 });
|
|
84
|
-
break;
|
|
85
|
-
case 'webp':
|
|
86
|
-
pipeline = pipeline.webp({ quality });
|
|
87
|
-
break;
|
|
88
|
-
case 'avif':
|
|
89
|
-
pipeline = pipeline.avif({ quality });
|
|
90
|
-
break;
|
|
91
|
-
default:
|
|
92
|
-
pipeline = pipeline.jpeg({ quality, mozjpeg: true });
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
await pipeline.toFile(outputPath);
|
|
76
|
+
const image = await Jimp.read(file);
|
|
77
|
+
await writeImage(image, outputPath, { quality });
|
|
96
78
|
|
|
97
79
|
const outputSize = getFileSize(outputPath);
|
|
98
80
|
totalSaved += inputSize - outputSize;
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { defineCommand } from 'citty';
|
|
3
|
-
import
|
|
3
|
+
import { Jimp } from 'jimp';
|
|
4
4
|
import { handleError, requireArg } from '../../utils/errors';
|
|
5
5
|
import { ensureOutputDir, getBasename, globFiles, resolvePath } from '../../utils/files';
|
|
6
6
|
import { bulkResult, info, success } from '../../utils/logger';
|
|
7
7
|
import { withProgress } from '../../utils/progress';
|
|
8
|
-
|
|
9
|
-
const SUPPORTED_FORMATS = ['jpg', 'jpeg', 'png', 'webp', 'avif', 'gif', 'tiff'];
|
|
8
|
+
import { SUPPORTED_FORMATS, writeImage } from '../../utils/image';
|
|
10
9
|
|
|
11
10
|
export const bulkConvert = defineCommand({
|
|
12
11
|
meta: {
|
|
@@ -45,7 +44,9 @@ export const bulkConvert = defineCommand({
|
|
|
45
44
|
const format = requireArg(args.format, 'format').toLowerCase();
|
|
46
45
|
|
|
47
46
|
if (!SUPPORTED_FORMATS.includes(format)) {
|
|
48
|
-
throw new Error(
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Unsupported format: ${format}. Supported: ${SUPPORTED_FORMATS.join(', ')}`
|
|
49
|
+
);
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
const inputPath = resolvePath(input as string);
|
|
@@ -55,7 +56,7 @@ export const bulkConvert = defineCommand({
|
|
|
55
56
|
// Find all image files
|
|
56
57
|
let pattern = inputPath;
|
|
57
58
|
if (!inputPath.includes('*')) {
|
|
58
|
-
pattern = `${inputPath}/**/*.{jpg,jpeg,png,
|
|
59
|
+
pattern = `${inputPath}/**/*.{jpg,jpeg,png,bmp,gif,tiff}`;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
const files = await globFiles(pattern);
|
|
@@ -74,31 +75,8 @@ export const bulkConvert = defineCommand({
|
|
|
74
75
|
const basename = getBasename(file);
|
|
75
76
|
const outputPath = join(outputDir, `${basename}.${format}`);
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
switch (format) {
|
|
80
|
-
case 'jpg':
|
|
81
|
-
case 'jpeg':
|
|
82
|
-
pipeline = pipeline.jpeg({ quality, mozjpeg: true });
|
|
83
|
-
break;
|
|
84
|
-
case 'png':
|
|
85
|
-
pipeline = pipeline.png({ quality });
|
|
86
|
-
break;
|
|
87
|
-
case 'webp':
|
|
88
|
-
pipeline = pipeline.webp({ quality });
|
|
89
|
-
break;
|
|
90
|
-
case 'avif':
|
|
91
|
-
pipeline = pipeline.avif({ quality });
|
|
92
|
-
break;
|
|
93
|
-
case 'gif':
|
|
94
|
-
pipeline = pipeline.gif();
|
|
95
|
-
break;
|
|
96
|
-
case 'tiff':
|
|
97
|
-
pipeline = pipeline.tiff({ quality });
|
|
98
|
-
break;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
await pipeline.toFile(outputPath);
|
|
78
|
+
const image = await Jimp.read(file);
|
|
79
|
+
await writeImage(image, outputPath, { quality });
|
|
102
80
|
processed++;
|
|
103
81
|
} catch {
|
|
104
82
|
failed++;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { defineCommand } from 'citty';
|
|
3
|
-
import
|
|
3
|
+
import { Jimp } from 'jimp';
|
|
4
4
|
import { handleError, requireArg } from '../../utils/errors';
|
|
5
5
|
import {
|
|
6
6
|
ensureOutputDir,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '../../utils/files';
|
|
12
12
|
import { bulkResult, info, success } from '../../utils/logger';
|
|
13
13
|
import { withProgress } from '../../utils/progress';
|
|
14
|
+
import { writeImage } from '../../utils/image';
|
|
14
15
|
|
|
15
16
|
export const bulkResize = defineCommand({
|
|
16
17
|
meta: {
|
|
@@ -61,7 +62,7 @@ export const bulkResize = defineCommand({
|
|
|
61
62
|
// Find all image files
|
|
62
63
|
let pattern = inputPath;
|
|
63
64
|
if (!inputPath.includes('*')) {
|
|
64
|
-
pattern = `${inputPath}/**/*.{jpg,jpeg,png,
|
|
65
|
+
pattern = `${inputPath}/**/*.{jpg,jpeg,png,bmp,gif,tiff}`;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
const files = await globFiles(pattern);
|
|
@@ -80,22 +81,30 @@ export const bulkResize = defineCommand({
|
|
|
80
81
|
const ext = getExtension(file);
|
|
81
82
|
const outputPath = join(outputDir, `${basename}.${ext}`);
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
let
|
|
84
|
+
const image = await Jimp.read(file);
|
|
85
|
+
let targetWidth: number;
|
|
86
|
+
let targetHeight: number;
|
|
85
87
|
|
|
86
88
|
if (args.scale) {
|
|
87
|
-
const metadata = await sharp(file).metadata();
|
|
88
89
|
const scale = Number.parseFloat(args.scale);
|
|
89
|
-
targetWidth = Math.round(
|
|
90
|
-
targetHeight = Math.round(
|
|
90
|
+
targetWidth = Math.round(image.width * scale);
|
|
91
|
+
targetHeight = Math.round(image.height * scale);
|
|
91
92
|
} else {
|
|
92
|
-
targetWidth = args.width ? Number.parseInt(args.width, 10) :
|
|
93
|
-
targetHeight = args.height ? Number.parseInt(args.height, 10) :
|
|
93
|
+
targetWidth = args.width ? Number.parseInt(args.width, 10) : image.width;
|
|
94
|
+
targetHeight = args.height ? Number.parseInt(args.height, 10) : image.height;
|
|
95
|
+
|
|
96
|
+
// Maintain aspect ratio if only one dimension specified
|
|
97
|
+
if (args.width && !args.height) {
|
|
98
|
+
const ratio = targetWidth / image.width;
|
|
99
|
+
targetHeight = Math.round(image.height * ratio);
|
|
100
|
+
} else if (!args.width && args.height) {
|
|
101
|
+
const ratio = targetHeight / image.height;
|
|
102
|
+
targetWidth = Math.round(image.width * ratio);
|
|
103
|
+
}
|
|
94
104
|
}
|
|
95
105
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
.toFile(outputPath);
|
|
106
|
+
image.resize({ w: targetWidth, h: targetHeight });
|
|
107
|
+
await writeImage(image, outputPath);
|
|
99
108
|
|
|
100
109
|
processed++;
|
|
101
110
|
} catch {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { existsSync } 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 {
|
|
5
6
|
ensureDir,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
} from '../../utils/files';
|
|
11
12
|
import { fileResult, success } from '../../utils/logger';
|
|
12
13
|
import { withSpinner } from '../../utils/progress';
|
|
14
|
+
import { writeImage } from '../../utils/image';
|
|
13
15
|
|
|
14
16
|
export const compressCmd = defineCommand({
|
|
15
17
|
meta: {
|
|
@@ -48,35 +50,12 @@ export const compressCmd = defineCommand({
|
|
|
48
50
|
|
|
49
51
|
const inputSize = getFileSize(inputPath);
|
|
50
52
|
const quality = Number.parseInt(args.quality || '80', 10);
|
|
51
|
-
const ext = getExtension(inputPath);
|
|
52
|
-
|
|
53
|
-
// Lazy load sharp
|
|
54
|
-
const { default: sharp } = await import('sharp');
|
|
55
53
|
|
|
56
54
|
await withSpinner(
|
|
57
55
|
`Compressing image (quality: ${quality})...`,
|
|
58
56
|
async () => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
switch (ext) {
|
|
62
|
-
case 'jpg':
|
|
63
|
-
case 'jpeg':
|
|
64
|
-
pipeline = pipeline.jpeg({ quality, mozjpeg: true });
|
|
65
|
-
break;
|
|
66
|
-
case 'png':
|
|
67
|
-
pipeline = pipeline.png({ quality, compressionLevel: 9 });
|
|
68
|
-
break;
|
|
69
|
-
case 'webp':
|
|
70
|
-
pipeline = pipeline.webp({ quality });
|
|
71
|
-
break;
|
|
72
|
-
case 'avif':
|
|
73
|
-
pipeline = pipeline.avif({ quality });
|
|
74
|
-
break;
|
|
75
|
-
default:
|
|
76
|
-
pipeline = pipeline.jpeg({ quality, mozjpeg: true });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
await pipeline.toFile(outputPath);
|
|
57
|
+
const image = await Jimp.read(inputPath);
|
|
58
|
+
await writeImage(image, outputPath, { quality });
|
|
80
59
|
},
|
|
81
60
|
'Image compressed successfully'
|
|
82
61
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
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 {
|
|
6
6
|
ensureDir,
|
|
@@ -11,8 +11,7 @@ import {
|
|
|
11
11
|
} from '../../utils/files';
|
|
12
12
|
import { fileResult, info, success } from '../../utils/logger';
|
|
13
13
|
import { withSpinner } from '../../utils/progress';
|
|
14
|
-
|
|
15
|
-
const SUPPORTED_FORMATS = ['jpg', 'jpeg', 'png', 'webp', 'avif', 'gif', 'tiff', 'heif'];
|
|
14
|
+
import { SUPPORTED_FORMATS, writeImage } from '../../utils/image';
|
|
16
15
|
|
|
17
16
|
export const convert = defineCommand({
|
|
18
17
|
meta: {
|
|
@@ -39,7 +38,7 @@ export const convert = defineCommand({
|
|
|
39
38
|
quality: {
|
|
40
39
|
type: 'string',
|
|
41
40
|
alias: 'q',
|
|
42
|
-
description: 'Quality (1-100)',
|
|
41
|
+
description: 'Quality (1-100, for JPEG)',
|
|
43
42
|
default: '85',
|
|
44
43
|
},
|
|
45
44
|
},
|
|
@@ -69,34 +68,8 @@ export const convert = defineCommand({
|
|
|
69
68
|
await withSpinner(
|
|
70
69
|
`Converting ${inputExt.toUpperCase()} to ${format.toUpperCase()}...`,
|
|
71
70
|
async () => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
switch (format) {
|
|
75
|
-
case 'jpg':
|
|
76
|
-
case 'jpeg':
|
|
77
|
-
pipeline = pipeline.jpeg({ quality, mozjpeg: true });
|
|
78
|
-
break;
|
|
79
|
-
case 'png':
|
|
80
|
-
pipeline = pipeline.png({ quality });
|
|
81
|
-
break;
|
|
82
|
-
case 'webp':
|
|
83
|
-
pipeline = pipeline.webp({ quality });
|
|
84
|
-
break;
|
|
85
|
-
case 'avif':
|
|
86
|
-
pipeline = pipeline.avif({ quality });
|
|
87
|
-
break;
|
|
88
|
-
case 'gif':
|
|
89
|
-
pipeline = pipeline.gif();
|
|
90
|
-
break;
|
|
91
|
-
case 'tiff':
|
|
92
|
-
pipeline = pipeline.tiff({ quality });
|
|
93
|
-
break;
|
|
94
|
-
case 'heif':
|
|
95
|
-
pipeline = pipeline.heif({ quality });
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
await pipeline.toFile(outputPath);
|
|
71
|
+
const image = await Jimp.read(inputPath);
|
|
72
|
+
await writeImage(image, outputPath, { quality });
|
|
100
73
|
},
|
|
101
74
|
'Image converted successfully'
|
|
102
75
|
);
|
|
@@ -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 crop = defineCommand({
|
|
10
11
|
meta: {
|
|
@@ -65,18 +66,18 @@ export const crop = defineCommand({
|
|
|
65
66
|
const cropWidth = Number.parseInt(width, 10);
|
|
66
67
|
const cropHeight = Number.parseInt(height, 10);
|
|
67
68
|
|
|
69
|
+
const image = await Jimp.read(inputPath);
|
|
70
|
+
|
|
68
71
|
// Validate crop area
|
|
69
|
-
|
|
70
|
-
if (left + cropWidth > (metadata.width || 0) || top + cropHeight > (metadata.height || 0)) {
|
|
72
|
+
if (left + cropWidth > image.width || top + cropHeight > image.height) {
|
|
71
73
|
throw new Error('Crop area exceeds image boundaries');
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
await withSpinner(
|
|
75
77
|
`Cropping image (${cropWidth}x${cropHeight} at ${left},${top})...`,
|
|
76
78
|
async () => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.toFile(outputPath);
|
|
79
|
+
image.crop({ x: left, y: top, w: cropWidth, h: cropHeight });
|
|
80
|
+
await writeImage(image, outputPath);
|
|
80
81
|
},
|
|
81
82
|
'Image cropped successfully'
|
|
82
83
|
);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { defineCommand } from 'citty';
|
|
4
|
-
import
|
|
4
|
+
import { Jimp } from 'jimp';
|
|
5
5
|
import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
|
|
6
|
-
import { ensureOutputDir,
|
|
6
|
+
import { ensureOutputDir, resolvePath } from '../../utils/files';
|
|
7
7
|
import { info, success } from '../../utils/logger';
|
|
8
8
|
import { withSpinner } from '../../utils/progress';
|
|
9
9
|
import { c } from '../../utils/style';
|
|
10
|
+
import { writeImage } from '../../utils/image';
|
|
10
11
|
|
|
11
12
|
const FAVICON_SIZES = [16, 32, 48, 64, 128, 180, 192, 512];
|
|
12
13
|
|
|
@@ -44,26 +45,26 @@ export const favicon = defineCommand({
|
|
|
44
45
|
await withSpinner(
|
|
45
46
|
`Generating ${FAVICON_SIZES.length} favicon sizes...`,
|
|
46
47
|
async () => {
|
|
48
|
+
const sourceImage = await Jimp.read(inputPath);
|
|
49
|
+
|
|
47
50
|
for (const size of FAVICON_SIZES) {
|
|
51
|
+
const image = sourceImage.clone();
|
|
52
|
+
// Use contain to fit within size while maintaining aspect ratio
|
|
53
|
+
image.contain({ w: size, h: size });
|
|
54
|
+
|
|
48
55
|
const outputPath = join(outputDir, `favicon-${size}x${size}.png`);
|
|
49
|
-
await
|
|
50
|
-
.resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
|
51
|
-
.png()
|
|
52
|
-
.toFile(outputPath);
|
|
56
|
+
await writeImage(image, outputPath);
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
// Also generate apple-touch-icon
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.toFile(join(outputDir, 'apple-touch-icon.png'));
|
|
60
|
+
const appleTouchImage = sourceImage.clone();
|
|
61
|
+
appleTouchImage.contain({ w: 180, h: 180 });
|
|
62
|
+
await writeImage(appleTouchImage, join(outputDir, 'apple-touch-icon.png'));
|
|
60
63
|
|
|
61
|
-
// Generate favicon.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
.png()
|
|
66
|
-
.toFile(join(outputDir, 'favicon.png'));
|
|
64
|
+
// Generate favicon.png (32x32)
|
|
65
|
+
const faviconImage = sourceImage.clone();
|
|
66
|
+
faviconImage.contain({ w: 32, h: 32 });
|
|
67
|
+
await writeImage(faviconImage, join(outputDir, 'favicon.png'));
|
|
67
68
|
},
|
|
68
69
|
'Favicons generated successfully'
|
|
69
70
|
);
|