screenshot-beautify 1.1.1 → 1.2.1

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.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/dist/index.js +198 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -64,18 +64,30 @@ screenshot-beautify presets
64
64
 
65
65
  | Category | Presets |
66
66
  |----------|---------|
67
+ | **Smart** | `auto` - analyzes your screenshot and creates a matching gradient |
67
68
  | Warm | `sunset`, `sunrise`, `peach` |
68
69
  | Cool | `ocean`, `sky`, `northern` |
69
70
  | Dark | `charcoal`, `midnight`, `space` |
70
71
  | Vibrant | `neon`, `fire`, `aurora` |
71
72
  | Soft | `lavender`, `mint`, `rose` |
72
73
 
74
+ ### Auto Preset
75
+
76
+ The `auto` preset extracts the dominant colors from your screenshot and generates a complementary gradient background. This ensures your beautified screenshot always looks cohesive without manual color selection.
77
+
78
+ ```bash
79
+ screenshot-beautify screenshot.png --preset auto
80
+ ```
81
+
73
82
  ## Examples
74
83
 
75
84
  ```bash
76
85
  # Basic usage
77
86
  screenshot-beautify screenshot.png
78
87
 
88
+ # Auto-match background to screenshot colors
89
+ screenshot-beautify screenshot.png --preset auto
90
+
79
91
  # With sunset gradient
80
92
  screenshot-beautify screenshot.png --preset sunset
81
93
 
package/dist/index.js CHANGED
@@ -11,6 +11,124 @@ import sharp2 from "sharp";
11
11
 
12
12
  // src/presets.ts
13
13
  import sharp from "sharp";
14
+ function rgbToHex(r, g, b) {
15
+ return "#" + [r, g, b].map((x) => x.toString(16).padStart(2, "0")).join("");
16
+ }
17
+ function rgbToHsl(r, g, b) {
18
+ r /= 255;
19
+ g /= 255;
20
+ b /= 255;
21
+ const max = Math.max(r, g, b);
22
+ const min = Math.min(r, g, b);
23
+ let h = 0, s = 0;
24
+ const l = (max + min) / 2;
25
+ if (max !== min) {
26
+ const d = max - min;
27
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
28
+ switch (max) {
29
+ case r:
30
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
31
+ break;
32
+ case g:
33
+ h = ((b - r) / d + 2) / 6;
34
+ break;
35
+ case b:
36
+ h = ((r - g) / d + 4) / 6;
37
+ break;
38
+ }
39
+ }
40
+ return { h: h * 360, s: s * 100, l: l * 100 };
41
+ }
42
+ function hslToRgb(h, s, l) {
43
+ h /= 360;
44
+ s /= 100;
45
+ l /= 100;
46
+ let r, g, b;
47
+ if (s === 0) {
48
+ r = g = b = l;
49
+ } else {
50
+ const hue2rgb = (p2, q2, t) => {
51
+ if (t < 0) t += 1;
52
+ if (t > 1) t -= 1;
53
+ if (t < 1 / 6) return p2 + (q2 - p2) * 6 * t;
54
+ if (t < 1 / 2) return q2;
55
+ if (t < 2 / 3) return p2 + (q2 - p2) * (2 / 3 - t) * 6;
56
+ return p2;
57
+ };
58
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
59
+ const p = 2 * l - q;
60
+ r = hue2rgb(p, q, h + 1 / 3);
61
+ g = hue2rgb(p, q, h);
62
+ b = hue2rgb(p, q, h - 1 / 3);
63
+ }
64
+ return {
65
+ r: Math.round(r * 255),
66
+ g: Math.round(g * 255),
67
+ b: Math.round(b * 255)
68
+ };
69
+ }
70
+ async function extractDominantColor(imagePath) {
71
+ const { data, info } = await sharp(imagePath).resize(50, 50, { fit: "cover" }).raw().toBuffer({ resolveWithObject: true });
72
+ const colorCounts = /* @__PURE__ */ new Map();
73
+ let totalSaturation = 0;
74
+ let pixelCount = 0;
75
+ for (let i = 0; i < data.length; i += info.channels) {
76
+ const r = data[i];
77
+ const g = data[i + 1];
78
+ const b = data[i + 2];
79
+ const pixelHsl = rgbToHsl(r, g, b);
80
+ totalSaturation += pixelHsl.s;
81
+ pixelCount++;
82
+ const qr = Math.round(r / 32) * 32;
83
+ const qg = Math.round(g / 32) * 32;
84
+ const qb = Math.round(b / 32) * 32;
85
+ const key = `${qr},${qg},${qb}`;
86
+ const existing = colorCounts.get(key);
87
+ if (existing) {
88
+ existing.count++;
89
+ } else {
90
+ colorCounts.set(key, { count: 1, r: qr, g: qg, b: qb });
91
+ }
92
+ }
93
+ const sorted = [...colorCounts.values()].sort((a, b) => b.count - a.count);
94
+ const dominant = sorted[0];
95
+ const hsl = rgbToHsl(dominant.r, dominant.g, dominant.b);
96
+ const avgSaturation = totalSaturation / pixelCount;
97
+ const rgbRange = Math.max(dominant.r, dominant.g, dominant.b) - Math.min(dominant.r, dominant.g, dominant.b);
98
+ const isGreyish = rgbRange < 40;
99
+ const isNeutral = avgSaturation < 25 || hsl.s < 15 || isGreyish;
100
+ let color1Hsl, color2Hsl;
101
+ if (isNeutral) {
102
+ color1Hsl = {
103
+ h: hsl.h,
104
+ s: 0,
105
+ l: Math.min(hsl.l + 8, 55)
106
+ };
107
+ color2Hsl = {
108
+ h: hsl.h,
109
+ s: 0,
110
+ l: Math.max(hsl.l - 5, 20)
111
+ };
112
+ } else {
113
+ color1Hsl = {
114
+ h: hsl.h,
115
+ s: Math.min(hsl.s + 5, 60),
116
+ l: Math.min(hsl.l + 10, 60)
117
+ };
118
+ color2Hsl = {
119
+ h: (hsl.h + 15) % 360,
120
+ // Slight hue shift
121
+ s: hsl.s,
122
+ l: Math.max(hsl.l - 5, 25)
123
+ };
124
+ }
125
+ const color1 = hslToRgb(color1Hsl.h, color1Hsl.s, color1Hsl.l);
126
+ const color2 = hslToRgb(color2Hsl.h, color2Hsl.s, color2Hsl.l);
127
+ return [
128
+ rgbToHex(color1.r, color1.g, color1.b),
129
+ rgbToHex(color2.r, color2.g, color2.b)
130
+ ];
131
+ }
14
132
  var GRADIENT_PRESETS = {
15
133
  // Warm tones
16
134
  sunset: {
@@ -94,7 +212,7 @@ var GRADIENT_PRESETS = {
94
212
  }
95
213
  };
96
214
  function listPresets() {
97
- return Object.keys(GRADIENT_PRESETS);
215
+ return ["auto", ...Object.keys(GRADIENT_PRESETS)];
98
216
  }
99
217
  async function createPresetBackground(presetName, width, height) {
100
218
  const preset = GRADIENT_PRESETS[presetName];
@@ -131,6 +249,7 @@ async function createBackground(options) {
131
249
  height,
132
250
  imagePath,
133
251
  preset,
252
+ sourceImagePath,
134
253
  // Dark charcoal gradient as default
135
254
  gradientStart = "#2d3436",
136
255
  gradientEnd = "#636e72"
@@ -141,6 +260,10 @@ async function createBackground(options) {
141
260
  position: "center"
142
261
  }).png().toBuffer();
143
262
  }
263
+ if (preset === "auto" && sourceImagePath) {
264
+ const [autoStart, autoEnd] = await extractDominantColor(sourceImagePath);
265
+ return createAutoBackground(width, height, autoStart, autoEnd);
266
+ }
144
267
  if (preset && GRADIENT_PRESETS[preset]) {
145
268
  return createPresetBackground(preset, width, height);
146
269
  }
@@ -157,6 +280,20 @@ async function createBackground(options) {
157
280
  `;
158
281
  return sharp2(Buffer.from(svg)).png().toBuffer();
159
282
  }
283
+ async function createAutoBackground(width, height, gradientStart, gradientEnd) {
284
+ const svg = `
285
+ <svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
286
+ <defs>
287
+ <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
288
+ <stop offset="0%" style="stop-color:${gradientStart};stop-opacity:1" />
289
+ <stop offset="100%" style="stop-color:${gradientEnd};stop-opacity:1" />
290
+ </linearGradient>
291
+ </defs>
292
+ <rect width="100%" height="100%" fill="url(#grad)" />
293
+ </svg>
294
+ `;
295
+ return sharp2(Buffer.from(svg)).png().toBuffer();
296
+ }
160
297
 
161
298
  // src/frame.ts
162
299
  import sharp3 from "sharp";
@@ -229,7 +366,8 @@ async function beautify(inputPath, outputPath, options = {}) {
229
366
  imagePath: backgroundImage,
230
367
  preset: backgroundPreset,
231
368
  gradientStart,
232
- gradientEnd
369
+ gradientEnd,
370
+ sourceImagePath: backgroundPreset === "auto" ? inputPath : void 0
233
371
  });
234
372
  const frame = await createFrame({
235
373
  width: framedWidth,
@@ -241,6 +379,35 @@ async function beautify(inputPath, outputPath, options = {}) {
241
379
  const frameY = padding;
242
380
  const screenshotX = frameX;
243
381
  const screenshotY = frameY + titleBarHeight;
382
+ let shadow = null;
383
+ if (backgroundPreset === "auto") {
384
+ const shadowBlur = 30;
385
+ const shadowOffsetX = 0;
386
+ const shadowOffsetY = 8;
387
+ const shadowOpacity = 0.4;
388
+ const shadowWidth = framedWidth + shadowBlur * 2;
389
+ const shadowHeight = framedHeight + shadowBlur * 2;
390
+ const shadowSvg = `
391
+ <svg width="${shadowWidth}" height="${shadowHeight}" xmlns="http://www.w3.org/2000/svg">
392
+ <defs>
393
+ <filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
394
+ <feGaussianBlur in="SourceGraphic" stdDeviation="${shadowBlur / 2}" />
395
+ </filter>
396
+ </defs>
397
+ <rect
398
+ x="${shadowBlur}"
399
+ y="${shadowBlur}"
400
+ width="${framedWidth}"
401
+ height="${framedHeight}"
402
+ rx="${cornerRadius}"
403
+ ry="${cornerRadius}"
404
+ fill="rgba(0, 0, 0, ${shadowOpacity})"
405
+ filter="url(#shadow)"
406
+ />
407
+ </svg>
408
+ `;
409
+ shadow = await sharp4(Buffer.from(shadowSvg)).png().toBuffer();
410
+ }
244
411
  const cornerMask = Buffer.from(`
245
412
  <svg width="${imgWidth}" height="${imgHeight}" xmlns="http://www.w3.org/2000/svg">
246
413
  <rect
@@ -263,20 +430,27 @@ async function beautify(inputPath, outputPath, options = {}) {
263
430
  blend: "dest-in"
264
431
  }
265
432
  ]).png().toBuffer();
266
- await sharp4(background).composite([
267
- // Window frame
268
- {
269
- input: frame,
270
- left: Math.round(frameX),
271
- top: Math.round(frameY)
272
- },
273
- // Screenshot
274
- {
275
- input: roundedScreenshot,
276
- left: Math.round(screenshotX),
277
- top: Math.round(screenshotY)
278
- }
279
- ]).png().toFile(outputPath);
433
+ const layers = [];
434
+ if (shadow) {
435
+ const shadowBlur = 30;
436
+ const shadowOffsetY = 8;
437
+ layers.push({
438
+ input: shadow,
439
+ left: Math.round(frameX - shadowBlur),
440
+ top: Math.round(frameY - shadowBlur + shadowOffsetY)
441
+ });
442
+ }
443
+ layers.push({
444
+ input: frame,
445
+ left: Math.round(frameX),
446
+ top: Math.round(frameY)
447
+ });
448
+ layers.push({
449
+ input: roundedScreenshot,
450
+ left: Math.round(screenshotX),
451
+ top: Math.round(screenshotY)
452
+ });
453
+ await sharp4(background).composite(layers).png().toFile(outputPath);
280
454
  }
281
455
 
282
456
  // src/tray.ts
@@ -463,7 +637,8 @@ var program = new Command();
463
637
  program.name("screenshot-beautify").description("Beautify screenshots with window frames, backgrounds, and shadows").version("1.0.0");
464
638
  program.command("presets").description("List available background presets").action(() => {
465
639
  console.log("Available background presets:\n");
466
- const presets = listPresets();
640
+ console.log(" - auto (analyzes image and creates matching gradient)");
641
+ const presets = listPresets().filter((p) => p !== "auto");
467
642
  presets.forEach((name) => {
468
643
  console.log(` - ${name}`);
469
644
  });
@@ -579,7 +754,7 @@ program.command("tray <source> <output>").description("Run as a system tray app
579
754
  deleteOriginal: opts.deleteOriginal
580
755
  });
581
756
  });
582
- program.command("file <input>", { isDefault: true }).description("Beautify a single screenshot").option("-o, --output <path>", "Output file path").option("--padding <number>", "Padding around the screenshot", "80").option("--background <path>", "Background image path").option("--preset <name>", "Background preset (run 'presets' to see options)").action(async (input, opts) => {
757
+ program.command("file <input>", { isDefault: true }).description("Beautify a single screenshot").option("-o, --output <path>", "Output file path").option("--padding <number>", "Padding around the screenshot", "80").option("--background <path>", "Background image path").option("--preset <name>", "Background preset (run 'presets' to see options)").option("--delete-original", "Delete original file after beautifying", false).action(async (input, opts) => {
583
758
  try {
584
759
  const inputPath = resolve2(input);
585
760
  const padding = parseInt(opts.padding || "80", 10);
@@ -592,6 +767,11 @@ program.command("file <input>", { isDefault: true }).description("Beautify a sin
592
767
  console.log(`Beautifying: ${inputPath}`);
593
768
  await beautify(inputPath, outputPath, { padding, backgroundImage, backgroundPreset });
594
769
  console.log(`Saved to: ${outputPath}`);
770
+ if (opts.deleteOriginal) {
771
+ const { unlink } = await import("fs/promises");
772
+ await unlink(inputPath);
773
+ console.log(`\u{1F5D1}\uFE0F Deleted original: ${basename2(inputPath)}`);
774
+ }
595
775
  } catch (error) {
596
776
  console.error("Error:", error instanceof Error ? error.message : error);
597
777
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screenshot-beautify",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "CLI tool to beautify screenshots with macOS-style window frames and gradient backgrounds",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",