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/package.json CHANGED
@@ -1,33 +1,22 @@
1
1
  {
2
2
  "name": "noupload",
3
- "version": "1.0.1",
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 --outdir dist --target bun --minify",
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
- "keywords": ["pdf", "image", "audio", "qr", "cli", "privacy", "offline", "compress", "convert", "ocr"]
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 sharp from 'sharp';
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 multiplier (1.0 = no change)',
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 (-100 to 100)',
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
- let pipeline = sharp(inputPath);
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
- modulate.brightness = Number.parseFloat(args.brightness);
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
- // Convert -100 to 100 range to multiplier
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
- // Apply gamma
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 sharp from 'sharp';
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 hexToRgb(hex: string): { r: number; g: number; b: number } {
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 { r: 0, g: 0, b: 0 };
13
+ return 0xffffffff; // White default
13
14
  }
14
- return {
15
- r: Number.parseInt(result[1], 16),
16
- g: Number.parseInt(result[2], 16),
17
- b: Number.parseInt(result[3], 16),
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 color = hexToRgb(args.color || '#ffffff');
65
+ const borderColor = hexToRgba(args.color || '#ffffff');
65
66
 
66
67
  await withSpinner(
67
68
  `Adding ${borderSize}px border...`,
68
69
  async () => {
69
- await sharp(inputPath)
70
- .extend({
71
- top: borderSize,
72
- bottom: borderSize,
73
- left: borderSize,
74
- right: borderSize,
75
- background: color,
76
- })
77
- .toFile(outputPath);
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 sharp from 'sharp';
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,webp,avif}`;
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
- let pipeline = sharp(file);
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 sharp from 'sharp';
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(`Unsupported format: ${format}`);
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,webp,avif,gif,tiff,heic}`;
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
- let pipeline = sharp(file);
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 sharp from 'sharp';
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,webp,avif,gif}`;
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
- let targetWidth: number | undefined;
84
- let targetHeight: number | undefined;
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((metadata.width || 0) * scale);
90
- targetHeight = Math.round((metadata.height || 0) * scale);
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) : undefined;
93
- targetHeight = args.height ? Number.parseInt(args.height, 10) : undefined;
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
- await sharp(file)
97
- .resize(targetWidth, targetHeight, { fit: 'inside', withoutEnlargement: true })
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
- let pipeline = sharp(inputPath);
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 sharp from 'sharp';
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
- let pipeline = sharp(inputPath);
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 sharp from 'sharp';
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
- const metadata = await sharp(inputPath).metadata();
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
- await sharp(inputPath)
78
- .extract({ left, top, width: cropWidth, height: cropHeight })
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 sharp from 'sharp';
4
+ import { Jimp } from 'jimp';
5
5
  import { FileNotFoundError, handleError, requireArg } from '../../utils/errors';
6
- import { ensureOutputDir, getBasename, resolvePath } from '../../utils/files';
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 sharp(inputPath)
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
- await sharp(inputPath)
57
- .resize(180, 180, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
58
- .png()
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.ico (16x16 and 32x32 combined would need ICO format)
62
- // For simplicity, we'll just create a 32x32 PNG named favicon.png
63
- await sharp(inputPath)
64
- .resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
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
  );