screenshot-beautify 1.0.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.
- package/README.md +102 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +647 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# screenshot-beautify
|
|
2
|
+
|
|
3
|
+
A CLI tool to beautify screenshots with macOS-style window frames, shadows, and gradient backgrounds.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g screenshot-beautify
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Single File
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
screenshot-beautify image.png
|
|
17
|
+
screenshot-beautify image.png --preset sunset
|
|
18
|
+
screenshot-beautify image.png --background ~/Pictures/wallpaper.jpg
|
|
19
|
+
screenshot-beautify image.png -o output.png --padding 100
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Watch Mode
|
|
23
|
+
|
|
24
|
+
Automatically beautify screenshots as they're added to a directory:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
screenshot-beautify watch ~/Desktop ~/Desktop/beautified --preset ocean
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### System Tray Mode (macOS)
|
|
31
|
+
|
|
32
|
+
Run in the background with a menu bar icon:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
screenshot-beautify tray ~/Desktop ~/Desktop/beautified --preset sunset
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The tray icon lets you:
|
|
39
|
+
- See status and processed count
|
|
40
|
+
- Start/Stop watching
|
|
41
|
+
- Open output folder
|
|
42
|
+
- Quit the app
|
|
43
|
+
|
|
44
|
+
## Options
|
|
45
|
+
|
|
46
|
+
| Option | Description |
|
|
47
|
+
|--------|-------------|
|
|
48
|
+
| `--preset <name>` | Use a built-in gradient preset |
|
|
49
|
+
| `--background <path>` | Use a custom image as background |
|
|
50
|
+
| `--padding <number>` | Padding around screenshot (default: 80) |
|
|
51
|
+
| `-o, --output <path>` | Output file path |
|
|
52
|
+
| `--delete-original` | Delete original after beautifying |
|
|
53
|
+
| `--foreground` | Run tray in foreground (for debugging) |
|
|
54
|
+
|
|
55
|
+
## Presets
|
|
56
|
+
|
|
57
|
+
List available presets:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
screenshot-beautify presets
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Available presets:**
|
|
64
|
+
|
|
65
|
+
| Category | Presets |
|
|
66
|
+
|----------|---------|
|
|
67
|
+
| Warm | `sunset`, `sunrise`, `peach` |
|
|
68
|
+
| Cool | `ocean`, `sky`, `northern` |
|
|
69
|
+
| Dark | `charcoal`, `midnight`, `space` |
|
|
70
|
+
| Vibrant | `neon`, `fire`, `aurora` |
|
|
71
|
+
| Soft | `lavender`, `mint`, `rose` |
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Basic usage
|
|
77
|
+
screenshot-beautify screenshot.png
|
|
78
|
+
|
|
79
|
+
# With sunset gradient
|
|
80
|
+
screenshot-beautify screenshot.png --preset sunset
|
|
81
|
+
|
|
82
|
+
# Watch Desktop and save to a subfolder
|
|
83
|
+
screenshot-beautify watch ~/Desktop ~/Desktop/beautified --preset ocean
|
|
84
|
+
|
|
85
|
+
# Run in system tray with custom wallpaper background
|
|
86
|
+
screenshot-beautify tray ~/Desktop ~/Desktop/beautified --background ~/wallpaper.jpg
|
|
87
|
+
|
|
88
|
+
# Custom padding
|
|
89
|
+
screenshot-beautify screenshot.png --padding 120 --preset midnight
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Output
|
|
93
|
+
|
|
94
|
+
The tool adds:
|
|
95
|
+
- macOS-style window frame with traffic light buttons
|
|
96
|
+
- Rounded corners
|
|
97
|
+
- Drop shadow
|
|
98
|
+
- Gradient or custom image background
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/beautify.ts
|
|
7
|
+
import sharp5 from "sharp";
|
|
8
|
+
|
|
9
|
+
// src/background.ts
|
|
10
|
+
import sharp2 from "sharp";
|
|
11
|
+
|
|
12
|
+
// src/presets.ts
|
|
13
|
+
import sharp from "sharp";
|
|
14
|
+
var GRADIENT_PRESETS = {
|
|
15
|
+
// Warm tones
|
|
16
|
+
sunset: {
|
|
17
|
+
name: "Sunset",
|
|
18
|
+
colors: ["#ff9a9e", "#fecfef", "#fecfef", "#fad0c4"],
|
|
19
|
+
angle: 135
|
|
20
|
+
},
|
|
21
|
+
sunrise: {
|
|
22
|
+
name: "Sunrise",
|
|
23
|
+
colors: ["#f093fb", "#f5576c"],
|
|
24
|
+
angle: 135
|
|
25
|
+
},
|
|
26
|
+
peach: {
|
|
27
|
+
name: "Peach",
|
|
28
|
+
colors: ["#ffecd2", "#fcb69f"],
|
|
29
|
+
angle: 135
|
|
30
|
+
},
|
|
31
|
+
// Cool tones
|
|
32
|
+
ocean: {
|
|
33
|
+
name: "Ocean",
|
|
34
|
+
colors: ["#667eea", "#764ba2"],
|
|
35
|
+
angle: 135
|
|
36
|
+
},
|
|
37
|
+
sky: {
|
|
38
|
+
name: "Sky",
|
|
39
|
+
colors: ["#a1c4fd", "#c2e9fb"],
|
|
40
|
+
angle: 135
|
|
41
|
+
},
|
|
42
|
+
northern: {
|
|
43
|
+
name: "Northern Lights",
|
|
44
|
+
colors: ["#43e97b", "#38f9d7"],
|
|
45
|
+
angle: 135
|
|
46
|
+
},
|
|
47
|
+
// Dark tones
|
|
48
|
+
charcoal: {
|
|
49
|
+
name: "Charcoal",
|
|
50
|
+
colors: ["#2d3436", "#636e72"],
|
|
51
|
+
angle: 135
|
|
52
|
+
},
|
|
53
|
+
midnight: {
|
|
54
|
+
name: "Midnight",
|
|
55
|
+
colors: ["#232526", "#414345"],
|
|
56
|
+
angle: 135
|
|
57
|
+
},
|
|
58
|
+
space: {
|
|
59
|
+
name: "Space",
|
|
60
|
+
colors: ["#0f0c29", "#302b63", "#24243e"],
|
|
61
|
+
angle: 135
|
|
62
|
+
},
|
|
63
|
+
// Vibrant
|
|
64
|
+
neon: {
|
|
65
|
+
name: "Neon",
|
|
66
|
+
colors: ["#fc00ff", "#00dbde"],
|
|
67
|
+
angle: 135
|
|
68
|
+
},
|
|
69
|
+
fire: {
|
|
70
|
+
name: "Fire",
|
|
71
|
+
colors: ["#f12711", "#f5af19"],
|
|
72
|
+
angle: 135
|
|
73
|
+
},
|
|
74
|
+
aurora: {
|
|
75
|
+
name: "Aurora",
|
|
76
|
+
colors: ["#00c6fb", "#005bea"],
|
|
77
|
+
angle: 135
|
|
78
|
+
},
|
|
79
|
+
// Soft/Muted
|
|
80
|
+
lavender: {
|
|
81
|
+
name: "Lavender",
|
|
82
|
+
colors: ["#e0c3fc", "#8ec5fc"],
|
|
83
|
+
angle: 135
|
|
84
|
+
},
|
|
85
|
+
mint: {
|
|
86
|
+
name: "Mint",
|
|
87
|
+
colors: ["#d4fc79", "#96e6a1"],
|
|
88
|
+
angle: 135
|
|
89
|
+
},
|
|
90
|
+
rose: {
|
|
91
|
+
name: "Rose",
|
|
92
|
+
colors: ["#eecda3", "#ef629f"],
|
|
93
|
+
angle: 135
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
function listPresets() {
|
|
97
|
+
return Object.keys(GRADIENT_PRESETS);
|
|
98
|
+
}
|
|
99
|
+
async function createPresetBackground(presetName, width, height) {
|
|
100
|
+
const preset = GRADIENT_PRESETS[presetName];
|
|
101
|
+
if (!preset) {
|
|
102
|
+
throw new Error(`Unknown preset: ${presetName}. Available: ${listPresets().join(", ")}`);
|
|
103
|
+
}
|
|
104
|
+
const { colors, angle = 135 } = preset;
|
|
105
|
+
const angleRad = angle * Math.PI / 180;
|
|
106
|
+
const x1 = Math.round(50 - Math.cos(angleRad) * 50);
|
|
107
|
+
const y1 = Math.round(50 - Math.sin(angleRad) * 50);
|
|
108
|
+
const x2 = Math.round(50 + Math.cos(angleRad) * 50);
|
|
109
|
+
const y2 = Math.round(50 + Math.sin(angleRad) * 50);
|
|
110
|
+
const stops = colors.map((color, i) => {
|
|
111
|
+
const offset = i / (colors.length - 1) * 100;
|
|
112
|
+
return `<stop offset="${offset}%" style="stop-color:${color};stop-opacity:1" />`;
|
|
113
|
+
}).join("\n ");
|
|
114
|
+
const svg = `
|
|
115
|
+
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
116
|
+
<defs>
|
|
117
|
+
<linearGradient id="grad" x1="${x1}%" y1="${y1}%" x2="${x2}%" y2="${y2}%">
|
|
118
|
+
${stops}
|
|
119
|
+
</linearGradient>
|
|
120
|
+
</defs>
|
|
121
|
+
<rect width="100%" height="100%" fill="url(#grad)" />
|
|
122
|
+
</svg>
|
|
123
|
+
`;
|
|
124
|
+
return sharp(Buffer.from(svg)).png().toBuffer();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/background.ts
|
|
128
|
+
async function createBackground(options) {
|
|
129
|
+
const {
|
|
130
|
+
width,
|
|
131
|
+
height,
|
|
132
|
+
imagePath,
|
|
133
|
+
preset,
|
|
134
|
+
// Dark charcoal gradient as default
|
|
135
|
+
gradientStart = "#2d3436",
|
|
136
|
+
gradientEnd = "#636e72"
|
|
137
|
+
} = options;
|
|
138
|
+
if (imagePath) {
|
|
139
|
+
return sharp2(imagePath).resize(width, height, {
|
|
140
|
+
fit: "cover",
|
|
141
|
+
position: "center"
|
|
142
|
+
}).png().toBuffer();
|
|
143
|
+
}
|
|
144
|
+
if (preset && GRADIENT_PRESETS[preset]) {
|
|
145
|
+
return createPresetBackground(preset, width, height);
|
|
146
|
+
}
|
|
147
|
+
const svg = `
|
|
148
|
+
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
149
|
+
<defs>
|
|
150
|
+
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
151
|
+
<stop offset="0%" style="stop-color:${gradientStart};stop-opacity:1" />
|
|
152
|
+
<stop offset="100%" style="stop-color:${gradientEnd};stop-opacity:1" />
|
|
153
|
+
</linearGradient>
|
|
154
|
+
</defs>
|
|
155
|
+
<rect width="100%" height="100%" fill="url(#grad)" />
|
|
156
|
+
</svg>
|
|
157
|
+
`;
|
|
158
|
+
return sharp2(Buffer.from(svg)).png().toBuffer();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/frame.ts
|
|
162
|
+
import sharp3 from "sharp";
|
|
163
|
+
async function createFrame(options) {
|
|
164
|
+
const {
|
|
165
|
+
width,
|
|
166
|
+
height,
|
|
167
|
+
titleBarHeight = 32,
|
|
168
|
+
cornerRadius = 10,
|
|
169
|
+
buttonSize = 12
|
|
170
|
+
} = options;
|
|
171
|
+
const buttonY = titleBarHeight / 2;
|
|
172
|
+
const buttonSpacing = 20;
|
|
173
|
+
const firstButtonX = 16;
|
|
174
|
+
const closeColor = "#FF5F56";
|
|
175
|
+
const minimizeColor = "#FFBD2E";
|
|
176
|
+
const maximizeColor = "#27C93F";
|
|
177
|
+
const svg = `
|
|
178
|
+
<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
|
|
179
|
+
<defs>
|
|
180
|
+
<clipPath id="roundedCorners">
|
|
181
|
+
<rect x="0" y="0" width="${width}" height="${height}" rx="${cornerRadius}" ry="${cornerRadius}" />
|
|
182
|
+
</clipPath>
|
|
183
|
+
</defs>
|
|
184
|
+
|
|
185
|
+
<!-- Window background with rounded corners -->
|
|
186
|
+
<rect x="0" y="0" width="${width}" height="${height}" rx="${cornerRadius}" ry="${cornerRadius}" fill="#FFFFFF" />
|
|
187
|
+
|
|
188
|
+
<!-- Title bar -->
|
|
189
|
+
<rect x="0" y="0" width="${width}" height="${titleBarHeight}" rx="${cornerRadius}" ry="${cornerRadius}" fill="#E8E8E8" />
|
|
190
|
+
<!-- Fill bottom corners of title bar -->
|
|
191
|
+
<rect x="0" y="${cornerRadius}" width="${width}" height="${titleBarHeight - cornerRadius}" fill="#E8E8E8" />
|
|
192
|
+
|
|
193
|
+
<!-- Window buttons -->
|
|
194
|
+
<circle cx="${firstButtonX}" cy="${buttonY}" r="${buttonSize / 2}" fill="${closeColor}" />
|
|
195
|
+
<circle cx="${firstButtonX + buttonSpacing}" cy="${buttonY}" r="${buttonSize / 2}" fill="${minimizeColor}" />
|
|
196
|
+
<circle cx="${firstButtonX + buttonSpacing * 2}" cy="${buttonY}" r="${buttonSize / 2}" fill="${maximizeColor}" />
|
|
197
|
+
</svg>
|
|
198
|
+
`;
|
|
199
|
+
return sharp3(Buffer.from(svg)).png().toBuffer();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/shadow.ts
|
|
203
|
+
import sharp4 from "sharp";
|
|
204
|
+
async function createShadow(options) {
|
|
205
|
+
const {
|
|
206
|
+
width,
|
|
207
|
+
height,
|
|
208
|
+
blur = 30,
|
|
209
|
+
opacity = 0.5,
|
|
210
|
+
cornerRadius = 10
|
|
211
|
+
} = options;
|
|
212
|
+
const shadowWidth = width + blur;
|
|
213
|
+
const shadowHeight = height + blur;
|
|
214
|
+
const offsetForSize = blur / 2;
|
|
215
|
+
const svg = `
|
|
216
|
+
<svg width="${shadowWidth}" height="${shadowHeight}" xmlns="http://www.w3.org/2000/svg">
|
|
217
|
+
<rect
|
|
218
|
+
x="${offsetForSize}"
|
|
219
|
+
y="${offsetForSize}"
|
|
220
|
+
width="${width}"
|
|
221
|
+
height="${height}"
|
|
222
|
+
rx="${cornerRadius}"
|
|
223
|
+
ry="${cornerRadius}"
|
|
224
|
+
fill="rgba(0, 0, 0, ${opacity})"
|
|
225
|
+
/>
|
|
226
|
+
</svg>
|
|
227
|
+
`;
|
|
228
|
+
return sharp4(Buffer.from(svg)).blur(blur).png().toBuffer();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/beautify.ts
|
|
232
|
+
async function beautify(inputPath, outputPath, options = {}) {
|
|
233
|
+
const {
|
|
234
|
+
padding = 80,
|
|
235
|
+
cornerRadius = 10,
|
|
236
|
+
titleBarHeight = 32,
|
|
237
|
+
shadowBlur = 30,
|
|
238
|
+
shadowOffsetX = 0,
|
|
239
|
+
shadowOffsetY = 20,
|
|
240
|
+
backgroundImage,
|
|
241
|
+
backgroundPreset,
|
|
242
|
+
gradientStart = "#2d3436",
|
|
243
|
+
gradientEnd = "#636e72"
|
|
244
|
+
} = options;
|
|
245
|
+
const inputImage = sharp5(inputPath);
|
|
246
|
+
const metadata = await inputImage.metadata();
|
|
247
|
+
if (!metadata.width || !metadata.height) {
|
|
248
|
+
throw new Error("Could not determine image dimensions");
|
|
249
|
+
}
|
|
250
|
+
const imgWidth = metadata.width;
|
|
251
|
+
const imgHeight = metadata.height;
|
|
252
|
+
const framedWidth = imgWidth;
|
|
253
|
+
const framedHeight = imgHeight + titleBarHeight;
|
|
254
|
+
const totalWidth = framedWidth + padding * 2;
|
|
255
|
+
const totalHeight = framedHeight + padding * 2;
|
|
256
|
+
const shadowPadding = shadowBlur * 2 + Math.abs(shadowOffsetY);
|
|
257
|
+
const canvasWidth = totalWidth + shadowPadding;
|
|
258
|
+
const canvasHeight = totalHeight + shadowPadding;
|
|
259
|
+
const background = await createBackground({
|
|
260
|
+
width: canvasWidth,
|
|
261
|
+
height: canvasHeight,
|
|
262
|
+
imagePath: backgroundImage,
|
|
263
|
+
preset: backgroundPreset,
|
|
264
|
+
gradientStart,
|
|
265
|
+
gradientEnd
|
|
266
|
+
});
|
|
267
|
+
const frame = await createFrame({
|
|
268
|
+
width: framedWidth,
|
|
269
|
+
height: framedHeight,
|
|
270
|
+
titleBarHeight,
|
|
271
|
+
cornerRadius
|
|
272
|
+
});
|
|
273
|
+
const shadow = await createShadow({
|
|
274
|
+
width: framedWidth,
|
|
275
|
+
height: framedHeight,
|
|
276
|
+
blur: shadowBlur,
|
|
277
|
+
cornerRadius
|
|
278
|
+
});
|
|
279
|
+
const frameX = padding + shadowPadding / 2;
|
|
280
|
+
const frameY = padding + shadowPadding / 2;
|
|
281
|
+
const shadowX = frameX + shadowOffsetX - shadowBlur / 2;
|
|
282
|
+
const shadowY = frameY + shadowOffsetY - shadowBlur / 2;
|
|
283
|
+
const screenshotX = frameX;
|
|
284
|
+
const screenshotY = frameY + titleBarHeight;
|
|
285
|
+
const cornerMask = Buffer.from(`
|
|
286
|
+
<svg width="${imgWidth}" height="${imgHeight}" xmlns="http://www.w3.org/2000/svg">
|
|
287
|
+
<rect
|
|
288
|
+
x="0"
|
|
289
|
+
y="0"
|
|
290
|
+
width="${imgWidth}"
|
|
291
|
+
height="${imgHeight}"
|
|
292
|
+
rx="${cornerRadius}"
|
|
293
|
+
ry="${cornerRadius}"
|
|
294
|
+
fill="white"
|
|
295
|
+
/>
|
|
296
|
+
<!-- Fill top corners to make them square (only bottom should be rounded) -->
|
|
297
|
+
<rect x="0" y="0" width="${cornerRadius}" height="${cornerRadius}" fill="white"/>
|
|
298
|
+
<rect x="${imgWidth - cornerRadius}" y="0" width="${cornerRadius}" height="${cornerRadius}" fill="white"/>
|
|
299
|
+
</svg>
|
|
300
|
+
`);
|
|
301
|
+
const roundedScreenshot = await sharp5(inputPath).ensureAlpha().composite([
|
|
302
|
+
{
|
|
303
|
+
input: await sharp5(cornerMask).png().toBuffer(),
|
|
304
|
+
blend: "dest-in"
|
|
305
|
+
}
|
|
306
|
+
]).png().toBuffer();
|
|
307
|
+
await sharp5(background).composite([
|
|
308
|
+
// Shadow layer
|
|
309
|
+
{
|
|
310
|
+
input: shadow,
|
|
311
|
+
left: Math.round(shadowX),
|
|
312
|
+
top: Math.round(shadowY)
|
|
313
|
+
},
|
|
314
|
+
// Window frame
|
|
315
|
+
{
|
|
316
|
+
input: frame,
|
|
317
|
+
left: Math.round(frameX),
|
|
318
|
+
top: Math.round(frameY)
|
|
319
|
+
},
|
|
320
|
+
// Screenshot
|
|
321
|
+
{
|
|
322
|
+
input: roundedScreenshot,
|
|
323
|
+
left: Math.round(screenshotX),
|
|
324
|
+
top: Math.round(screenshotY)
|
|
325
|
+
}
|
|
326
|
+
]).png().toFile(outputPath);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/tray.ts
|
|
330
|
+
import { createRequire } from "module";
|
|
331
|
+
import chokidar from "chokidar";
|
|
332
|
+
import { resolve, basename, extname, join } from "path";
|
|
333
|
+
import { existsSync, mkdirSync } from "fs";
|
|
334
|
+
import { exec } from "child_process";
|
|
335
|
+
var require2 = createRequire(import.meta.url);
|
|
336
|
+
var SysTray = require2("systray2").default;
|
|
337
|
+
var IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", ".webp", ".gif"];
|
|
338
|
+
function isImageFile(filePath) {
|
|
339
|
+
const ext = extname(filePath).toLowerCase();
|
|
340
|
+
return IMAGE_EXTENSIONS.includes(ext);
|
|
341
|
+
}
|
|
342
|
+
function isAlreadyBeautified(filePath) {
|
|
343
|
+
return basename(filePath).includes("_beautified");
|
|
344
|
+
}
|
|
345
|
+
var ICON_BASE64 = `iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAACXBIWXMAAAsTAAALEwEAmpwYAAAA6UlEQVR4nO2TywmDQBCG7UkF7SAhJ7UD12JUxEcP7lW0hpAifJySnLQG/YMDOYQVkTWEQPLBD8ssfMwMu4ryZw0AF7xyVt4BFvg+MYATgDvkuQE4Lolv2M910/hPmqZBnufgnKOua6yxSTxNE9I0ha7rUFWVomkafN+nO2lxURQki6IIwzBQwjCkWlmW8mLLsuB5njAFYwy2bcuLDcNAkiRCPY5jmKa5r2PGmNCx67pwHEdezDmnfc577fueEgQB1aqqkheP4yi8ivmcZdmidLP4Sdu21P2cruuwxkd/3nGn/ArgIIiVn+cBsJOTNPMb6GYAAAAASUVORK5CYII=`;
|
|
346
|
+
var watcher = null;
|
|
347
|
+
var processedCount = 0;
|
|
348
|
+
var systray = null;
|
|
349
|
+
var trayReady = false;
|
|
350
|
+
var config;
|
|
351
|
+
async function processFile(filePath) {
|
|
352
|
+
if (!isImageFile(filePath) || isAlreadyBeautified(filePath)) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const fileName = basename(filePath, extname(filePath));
|
|
356
|
+
const beautifiedPath = join(config.outputPath, `${fileName}_beautified.png`);
|
|
357
|
+
try {
|
|
358
|
+
await beautify(filePath, beautifiedPath, {
|
|
359
|
+
padding: config.padding || 80,
|
|
360
|
+
backgroundImage: config.backgroundImage,
|
|
361
|
+
backgroundPreset: config.backgroundPreset
|
|
362
|
+
});
|
|
363
|
+
processedCount++;
|
|
364
|
+
updateMenu();
|
|
365
|
+
if (config.deleteOriginal) {
|
|
366
|
+
const { unlink } = await import("fs/promises");
|
|
367
|
+
await unlink(filePath);
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function startWatching() {
|
|
373
|
+
if (watcher) {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
watcher = chokidar.watch(config.sourcePath, {
|
|
377
|
+
ignored: /(^|[\/\\])\../,
|
|
378
|
+
persistent: true,
|
|
379
|
+
ignoreInitial: true,
|
|
380
|
+
awaitWriteFinish: {
|
|
381
|
+
stabilityThreshold: 500,
|
|
382
|
+
pollInterval: 100
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
watcher.on("ready", () => {
|
|
386
|
+
updateMenu();
|
|
387
|
+
});
|
|
388
|
+
watcher.on("add", processFile);
|
|
389
|
+
watcher.on("error", () => {
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function stopWatching() {
|
|
393
|
+
if (watcher) {
|
|
394
|
+
watcher.close();
|
|
395
|
+
watcher = null;
|
|
396
|
+
updateMenu();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function openOutputFolder() {
|
|
400
|
+
const cmd = process.platform === "darwin" ? `open "${config.outputPath}"` : process.platform === "win32" ? `explorer "${config.outputPath}"` : `xdg-open "${config.outputPath}"`;
|
|
401
|
+
exec(cmd);
|
|
402
|
+
}
|
|
403
|
+
function updateMenu() {
|
|
404
|
+
if (!systray || !trayReady) return;
|
|
405
|
+
const isWatching = watcher !== null;
|
|
406
|
+
try {
|
|
407
|
+
systray.sendAction({
|
|
408
|
+
type: "update-item",
|
|
409
|
+
item: {
|
|
410
|
+
title: isWatching ? `\u2713 Watching (${processedCount} processed)` : "\u25CB Not watching",
|
|
411
|
+
enabled: false
|
|
412
|
+
},
|
|
413
|
+
seq_id: 0
|
|
414
|
+
});
|
|
415
|
+
systray.sendAction({
|
|
416
|
+
type: "update-item",
|
|
417
|
+
item: {
|
|
418
|
+
title: isWatching ? "Stop Watching" : "Start Watching",
|
|
419
|
+
enabled: true
|
|
420
|
+
},
|
|
421
|
+
seq_id: 1
|
|
422
|
+
});
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
async function createTray() {
|
|
427
|
+
systray = new SysTray({
|
|
428
|
+
menu: {
|
|
429
|
+
icon: ICON_BASE64,
|
|
430
|
+
title: "",
|
|
431
|
+
tooltip: "Screenshot Beautify",
|
|
432
|
+
items: [
|
|
433
|
+
{
|
|
434
|
+
title: "\u25CB Not watching",
|
|
435
|
+
enabled: false
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
title: "Start Watching",
|
|
439
|
+
enabled: true
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
title: "Open Output Folder",
|
|
443
|
+
enabled: true
|
|
444
|
+
},
|
|
445
|
+
SysTray.separator,
|
|
446
|
+
{
|
|
447
|
+
title: "Quit",
|
|
448
|
+
enabled: true
|
|
449
|
+
}
|
|
450
|
+
]
|
|
451
|
+
},
|
|
452
|
+
debug: false,
|
|
453
|
+
copyDir: true
|
|
454
|
+
});
|
|
455
|
+
await systray.ready();
|
|
456
|
+
trayReady = true;
|
|
457
|
+
systray.onClick((action) => {
|
|
458
|
+
switch (action.seq_id) {
|
|
459
|
+
case 1:
|
|
460
|
+
if (watcher) {
|
|
461
|
+
stopWatching();
|
|
462
|
+
} else {
|
|
463
|
+
startWatching();
|
|
464
|
+
}
|
|
465
|
+
break;
|
|
466
|
+
case 2:
|
|
467
|
+
openOutputFolder();
|
|
468
|
+
break;
|
|
469
|
+
case 4:
|
|
470
|
+
stopWatching();
|
|
471
|
+
systray?.kill(false);
|
|
472
|
+
process.exit(0);
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
startWatching();
|
|
477
|
+
}
|
|
478
|
+
async function startTray(options) {
|
|
479
|
+
config = {
|
|
480
|
+
sourcePath: resolve(options.sourcePath),
|
|
481
|
+
outputPath: resolve(options.outputPath),
|
|
482
|
+
padding: options.padding || 80,
|
|
483
|
+
backgroundImage: options.backgroundImage,
|
|
484
|
+
backgroundPreset: options.backgroundPreset,
|
|
485
|
+
deleteOriginal: options.deleteOriginal || false
|
|
486
|
+
};
|
|
487
|
+
if (!existsSync(config.sourcePath)) {
|
|
488
|
+
console.error(`Source directory does not exist: ${config.sourcePath}`);
|
|
489
|
+
process.exit(1);
|
|
490
|
+
}
|
|
491
|
+
if (!existsSync(config.outputPath)) {
|
|
492
|
+
mkdirSync(config.outputPath, { recursive: true });
|
|
493
|
+
}
|
|
494
|
+
await createTray();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/index.ts
|
|
498
|
+
import { resolve as resolve2, basename as basename2, dirname, extname as extname2, join as join2 } from "path";
|
|
499
|
+
import chokidar2 from "chokidar";
|
|
500
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
501
|
+
var IMAGE_EXTENSIONS2 = [".png", ".jpg", ".jpeg", ".webp", ".gif"];
|
|
502
|
+
function isImageFile2(filePath) {
|
|
503
|
+
const ext = extname2(filePath).toLowerCase();
|
|
504
|
+
return IMAGE_EXTENSIONS2.includes(ext);
|
|
505
|
+
}
|
|
506
|
+
function isAlreadyBeautified2(filePath) {
|
|
507
|
+
return basename2(filePath).includes("_beautified");
|
|
508
|
+
}
|
|
509
|
+
var program = new Command();
|
|
510
|
+
program.name("screenshot-beautify").description("Beautify screenshots with window frames, backgrounds, and shadows").version("1.0.0");
|
|
511
|
+
program.command("presets").description("List available background presets").action(() => {
|
|
512
|
+
console.log("Available background presets:\n");
|
|
513
|
+
const presets = listPresets();
|
|
514
|
+
presets.forEach((name) => {
|
|
515
|
+
console.log(` - ${name}`);
|
|
516
|
+
});
|
|
517
|
+
console.log("\nUsage: screenshot-beautify <input> --preset sunset");
|
|
518
|
+
});
|
|
519
|
+
program.command("watch <source> <output>").description("Watch a directory and auto-beautify new screenshots").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 (source, output, opts) => {
|
|
520
|
+
try {
|
|
521
|
+
const sourcePath = resolve2(source);
|
|
522
|
+
const outputPath = resolve2(output);
|
|
523
|
+
const padding = parseInt(opts.padding, 10);
|
|
524
|
+
const backgroundImage = opts.background ? resolve2(opts.background) : void 0;
|
|
525
|
+
const backgroundPreset = opts.preset;
|
|
526
|
+
if (!existsSync2(sourcePath)) {
|
|
527
|
+
console.error(`Source directory does not exist: ${sourcePath}`);
|
|
528
|
+
process.exit(1);
|
|
529
|
+
}
|
|
530
|
+
if (!existsSync2(outputPath)) {
|
|
531
|
+
mkdirSync2(outputPath, { recursive: true });
|
|
532
|
+
console.log(`Created output directory: ${outputPath}`);
|
|
533
|
+
}
|
|
534
|
+
console.log(`
|
|
535
|
+
\u{1F50D} Watching for screenshots in: ${sourcePath}`);
|
|
536
|
+
console.log(`\u{1F4C1} Beautified screenshots will be saved to: ${outputPath}`);
|
|
537
|
+
console.log(`\u2699\uFE0F Options: padding=${padding}${backgroundPreset ? `, preset=${backgroundPreset}` : ""}${backgroundImage ? `, background=${backgroundImage}` : ""}${opts.deleteOriginal ? ", delete-original=true" : ""}`);
|
|
538
|
+
console.log(`
|
|
539
|
+
\u2705 File watcher is now running...`);
|
|
540
|
+
console.log(` Press Ctrl+C to stop
|
|
541
|
+
`);
|
|
542
|
+
const watcher2 = chokidar2.watch(sourcePath, {
|
|
543
|
+
ignored: /(^|[\/\\])\../,
|
|
544
|
+
persistent: true,
|
|
545
|
+
ignoreInitial: true,
|
|
546
|
+
awaitWriteFinish: {
|
|
547
|
+
stabilityThreshold: 500,
|
|
548
|
+
pollInterval: 100
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
const timestamp = () => (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
552
|
+
const processFile2 = async (filePath) => {
|
|
553
|
+
if (!isImageFile2(filePath) || isAlreadyBeautified2(filePath)) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const fileName = basename2(filePath, extname2(filePath));
|
|
557
|
+
const beautifiedPath = join2(outputPath, `${fileName}_beautified.png`);
|
|
558
|
+
try {
|
|
559
|
+
console.log(`[${timestamp()}] \u{1F4F8} New screenshot detected: ${basename2(filePath)}`);
|
|
560
|
+
await beautify(filePath, beautifiedPath, { padding, backgroundImage, backgroundPreset });
|
|
561
|
+
console.log(`[${timestamp()}] \u2728 Beautified: ${basename2(beautifiedPath)}`);
|
|
562
|
+
if (opts.deleteOriginal) {
|
|
563
|
+
const { unlink } = await import("fs/promises");
|
|
564
|
+
await unlink(filePath);
|
|
565
|
+
console.log(`[${timestamp()}] \u{1F5D1}\uFE0F Deleted original: ${basename2(filePath)}`);
|
|
566
|
+
}
|
|
567
|
+
} catch (err) {
|
|
568
|
+
console.error(`[${timestamp()}] \u274C Failed to beautify ${basename2(filePath)}:`, err instanceof Error ? err.message : err);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
watcher2.on("ready", () => {
|
|
572
|
+
console.log(`[${timestamp()}] \u{1F440} Watching for new files...
|
|
573
|
+
`);
|
|
574
|
+
});
|
|
575
|
+
watcher2.on("add", processFile2);
|
|
576
|
+
watcher2.on("error", (error) => {
|
|
577
|
+
console.error(`[${timestamp()}] \u274C Watcher error:`, error);
|
|
578
|
+
});
|
|
579
|
+
process.on("SIGINT", () => {
|
|
580
|
+
console.log("\n\u{1F6D1} Stopping watcher...");
|
|
581
|
+
watcher2.close();
|
|
582
|
+
process.exit(0);
|
|
583
|
+
});
|
|
584
|
+
} catch (error) {
|
|
585
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
program.command("tray <source> <output>").description("Run as a system tray app with auto-beautify").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).option("--foreground", "Run in foreground (don't detach)", false).option("--_daemon", "Internal: running as daemon", false).action(async (source, output, opts) => {
|
|
590
|
+
const padding = parseInt(opts.padding, 10);
|
|
591
|
+
const backgroundImage = opts.background ? resolve2(opts.background) : void 0;
|
|
592
|
+
const backgroundPreset = opts.preset;
|
|
593
|
+
if (!opts._daemon && !opts.foreground) {
|
|
594
|
+
const { spawn } = await import("child_process");
|
|
595
|
+
const args = [
|
|
596
|
+
process.argv[1],
|
|
597
|
+
"tray",
|
|
598
|
+
source,
|
|
599
|
+
output,
|
|
600
|
+
"--padding",
|
|
601
|
+
opts.padding,
|
|
602
|
+
"--_daemon"
|
|
603
|
+
];
|
|
604
|
+
if (backgroundImage) args.push("--background", backgroundImage);
|
|
605
|
+
if (backgroundPreset) args.push("--preset", backgroundPreset);
|
|
606
|
+
if (opts.deleteOriginal) args.push("--delete-original");
|
|
607
|
+
const child = spawn(process.execPath, args, {
|
|
608
|
+
detached: true,
|
|
609
|
+
stdio: "ignore",
|
|
610
|
+
env: process.env
|
|
611
|
+
});
|
|
612
|
+
child.unref();
|
|
613
|
+
console.log("\u{1F4F7} Screenshot Beautify started in system tray");
|
|
614
|
+
console.log(` Watching: ${resolve2(source)}`);
|
|
615
|
+
console.log(` Output: ${resolve2(output)}`);
|
|
616
|
+
console.log("\nThe app is now running in the background.");
|
|
617
|
+
console.log("Click the tray icon to control it, or use 'Quit' to stop.");
|
|
618
|
+
process.exit(0);
|
|
619
|
+
}
|
|
620
|
+
await startTray({
|
|
621
|
+
sourcePath: source,
|
|
622
|
+
outputPath: output,
|
|
623
|
+
padding,
|
|
624
|
+
backgroundImage,
|
|
625
|
+
backgroundPreset,
|
|
626
|
+
deleteOriginal: opts.deleteOriginal
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
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) => {
|
|
630
|
+
try {
|
|
631
|
+
const inputPath = resolve2(input);
|
|
632
|
+
const padding = parseInt(opts.padding || "80", 10);
|
|
633
|
+
const backgroundImage = opts.background ? resolve2(opts.background) : void 0;
|
|
634
|
+
const backgroundPreset = opts.preset;
|
|
635
|
+
const outputPath = opts.output ? resolve2(opts.output) : join2(
|
|
636
|
+
dirname(inputPath),
|
|
637
|
+
`${basename2(inputPath, extname2(inputPath))}_beautified.png`
|
|
638
|
+
);
|
|
639
|
+
console.log(`Beautifying: ${inputPath}`);
|
|
640
|
+
await beautify(inputPath, outputPath, { padding, backgroundImage, backgroundPreset });
|
|
641
|
+
console.log(`Saved to: ${outputPath}`);
|
|
642
|
+
} catch (error) {
|
|
643
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "screenshot-beautify",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool to beautify screenshots with window frames, backgrounds, and shadows",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"screenshot-beautify": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"screenshot",
|
|
20
|
+
"beautify",
|
|
21
|
+
"cli",
|
|
22
|
+
"image",
|
|
23
|
+
"frame",
|
|
24
|
+
"shadow"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"chokidar": "^5.0.0",
|
|
30
|
+
"commander": "^12.1.0",
|
|
31
|
+
"sharp": "^0.33.5",
|
|
32
|
+
"systray2": "^2.1.4"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.10.0",
|
|
36
|
+
"tsup": "^8.3.5",
|
|
37
|
+
"tsx": "^4.19.2",
|
|
38
|
+
"typescript": "^5.7.2"
|
|
39
|
+
}
|
|
40
|
+
}
|