screenshot-beautify 1.1.1 → 1.2.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.
Files changed (2) hide show
  1. package/dist/index.js +192 -17
  2. package/package.json +1 -1
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "screenshot-beautify",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
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",