device-shots 0.2.0 → 0.3.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 +9 -9
- package/dist/index.js +149 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -117,10 +117,10 @@ Screenshots are organized by platform and screen size bucket, matching App Store
|
|
|
117
117
|
│ │ ├── dashboard_framed.png
|
|
118
118
|
│ │ ├── settings.png
|
|
119
119
|
│ │ └── settings_framed.png
|
|
120
|
-
│ ├── 6.
|
|
120
|
+
│ ├── 6.5/ # iPhone 14 Plus, 13 Pro Max
|
|
121
121
|
│ │ ├── dashboard.png
|
|
122
122
|
│ │ └── dashboard_framed.png
|
|
123
|
-
│ └── 6.3/ # iPhone 16 Pro
|
|
123
|
+
│ └── 6.3/ # iPhone 16 Pro, 16, 15 Pro, 15
|
|
124
124
|
│ └── ...
|
|
125
125
|
├── android/
|
|
126
126
|
│ └── phone/ # Pixel 9 Pro, etc.
|
|
@@ -135,14 +135,14 @@ Screenshots are organized by platform and screen size bucket, matching App Store
|
|
|
135
135
|
|
|
136
136
|
| Bucket | Devices |
|
|
137
137
|
|--------|---------|
|
|
138
|
-
| `6.9` | iPhone 16 Pro Max |
|
|
139
|
-
| `6.
|
|
140
|
-
| `6.
|
|
141
|
-
| `6.
|
|
142
|
-
| `6.1` | iPhone 16, 15, 15 Pro, 14, 14 Pro, 13, 13 Pro |
|
|
138
|
+
| `6.9` | iPhone 16 Pro Max, 16 Plus, 15 Pro Max, 15 Plus, 14 Pro Max |
|
|
139
|
+
| `6.5` | iPhone 14 Plus, 13 Pro Max, 12 Pro Max, 11 Pro Max, XS Max |
|
|
140
|
+
| `6.3` | iPhone 16 Pro, 16, 15 Pro, 15, 14 Pro |
|
|
141
|
+
| `6.1` | iPhone 14, 13, 12, X, XS, 12 mini, 13 mini |
|
|
143
142
|
| `5.5` | iPhone 8 Plus, 7 Plus |
|
|
144
|
-
| `
|
|
145
|
-
| `
|
|
143
|
+
| `4.7` | iPhone SE (3rd/2nd), iPhone 8 |
|
|
144
|
+
| `13` | iPad Pro 13", iPad Air 13" |
|
|
145
|
+
| `11` | iPad Pro 11", iPad Air 11", iPad mini 6th gen |
|
|
146
146
|
|
|
147
147
|
**Android** — Categorized by form factor:
|
|
148
148
|
|
package/dist/index.js
CHANGED
|
@@ -96,35 +96,40 @@ async function runOrFail(cmd, args, options) {
|
|
|
96
96
|
|
|
97
97
|
// src/devices/screen-sizes.ts
|
|
98
98
|
var IOS_RESOLUTION_MAP = {
|
|
99
|
-
// 6.9" — iPhone 16 Pro Max
|
|
99
|
+
// 6.9" — iPhone 16 Pro Max, 16 Plus, 15 Pro Max, 15 Plus, 14 Pro Max
|
|
100
100
|
"1320x2868": "6.9",
|
|
101
|
-
|
|
102
|
-
"
|
|
103
|
-
// 6.5" — iPhone 14 Plus, 13 Pro Max, 12 Pro Max, 11 Pro Max
|
|
101
|
+
"1290x2796": "6.9",
|
|
102
|
+
"1260x2736": "6.9",
|
|
103
|
+
// 6.5" — iPhone 14 Plus, 13 Pro Max, 12 Pro Max, 11 Pro Max, XS Max, XR
|
|
104
104
|
"1284x2778": "6.5",
|
|
105
105
|
"1242x2688": "6.5",
|
|
106
|
-
// 6.3" — iPhone 16 Pro
|
|
106
|
+
// 6.3" — iPhone 16 Pro, 16, 15 Pro, 15, 14 Pro
|
|
107
107
|
"1206x2622": "6.3",
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
"1179x2556": "6.3",
|
|
109
|
+
// 6.1" — iPhone 14, 13, 13 Pro, 12, 12 Pro, X, XS, 11 Pro, 12 mini, 13 mini
|
|
110
110
|
"1170x2532": "6.1",
|
|
111
|
-
|
|
112
|
-
"
|
|
111
|
+
"1125x2436": "6.1",
|
|
112
|
+
"1080x2340": "6.1",
|
|
113
113
|
// 5.5" — iPhone 8 Plus, 7 Plus, 6s Plus
|
|
114
114
|
"1242x2208": "5.5",
|
|
115
115
|
// 4.7" — iPhone SE (3rd/2nd), iPhone 8, 7, 6s
|
|
116
116
|
"750x1334": "4.7",
|
|
117
|
-
//
|
|
117
|
+
// 4.0" — iPhone SE (1st), iPhone 5/5s/5c
|
|
118
|
+
"640x1136": "4.0",
|
|
119
|
+
"640x1096": "4.0",
|
|
120
|
+
// iPad 13" — iPad Pro 13" (M4/M3/M2/M1), iPad Air 13", iPad Pro 12.9" (older)
|
|
118
121
|
"2064x2752": "13",
|
|
119
122
|
"2048x2732": "13",
|
|
120
|
-
// iPad 11" — iPad Pro 11", iPad Air
|
|
123
|
+
// iPad 11" — iPad Pro 11", iPad Air 11", iPad 10th gen, iPad mini 6th gen
|
|
124
|
+
"1488x2266": "11",
|
|
125
|
+
"1668x2420": "11",
|
|
121
126
|
"1668x2388": "11",
|
|
122
|
-
"1668x2224": "11",
|
|
123
127
|
"1640x2360": "11",
|
|
124
|
-
// iPad 10.
|
|
125
|
-
"
|
|
126
|
-
// iPad mini
|
|
127
|
-
"
|
|
128
|
+
// iPad 10.5" — iPad Pro 10.5", iPad Air 3rd gen, iPad 7th-9th gen
|
|
129
|
+
"1668x2224": "10.5",
|
|
130
|
+
// iPad 9.7" — iPad Pro 9.7", iPad Air 1-2, older iPad mini
|
|
131
|
+
"1536x2048": "9.7",
|
|
132
|
+
"1536x2008": "9.7"
|
|
128
133
|
};
|
|
129
134
|
function getIosScreenSize(width, height) {
|
|
130
135
|
const w = Math.min(width, height);
|
|
@@ -485,6 +490,64 @@ async function frameScreenshots(inputDir, outputDir, force = false) {
|
|
|
485
490
|
const rawFiles = existsSync3(inputDir) ? readdirSync(inputDir).filter((f) => f.endsWith(".png")).length : 0;
|
|
486
491
|
return { framed: framedFiles, skipped: rawFiles - framedFiles };
|
|
487
492
|
}
|
|
493
|
+
async function frameAndroidScreenshot(inputPath, outputPath) {
|
|
494
|
+
if (!await commandExists("magick")) {
|
|
495
|
+
throw new Error("ImageMagick is required for Android framing. Install with: brew install imagemagick");
|
|
496
|
+
}
|
|
497
|
+
const { stdout: identify } = await run("magick", [
|
|
498
|
+
"identify",
|
|
499
|
+
"-format",
|
|
500
|
+
"%wx%h",
|
|
501
|
+
inputPath
|
|
502
|
+
]);
|
|
503
|
+
const match = identify.match(/(\d+)x(\d+)/);
|
|
504
|
+
if (!match) return false;
|
|
505
|
+
const width = parseInt(match[1], 10);
|
|
506
|
+
const height = parseInt(match[2], 10);
|
|
507
|
+
const borderWidth = Math.round(width * 0.018);
|
|
508
|
+
const innerRadius = Math.round(width * 0.045);
|
|
509
|
+
const outerRadius = innerRadius + borderWidth;
|
|
510
|
+
const totalW = width + borderWidth * 2;
|
|
511
|
+
const totalH = height + borderWidth * 2;
|
|
512
|
+
try {
|
|
513
|
+
await runOrFail("magick", [
|
|
514
|
+
// Create black rounded rectangle background
|
|
515
|
+
"-size",
|
|
516
|
+
`${totalW}x${totalH}`,
|
|
517
|
+
"xc:none",
|
|
518
|
+
"-draw",
|
|
519
|
+
`fill black roundrectangle 0,0 ${totalW - 1},${totalH - 1} ${outerRadius},${outerRadius}`,
|
|
520
|
+
// Load screenshot and round its corners
|
|
521
|
+
"(",
|
|
522
|
+
inputPath,
|
|
523
|
+
"-alpha",
|
|
524
|
+
"set",
|
|
525
|
+
"(",
|
|
526
|
+
"+clone",
|
|
527
|
+
"-alpha",
|
|
528
|
+
"extract",
|
|
529
|
+
"-draw",
|
|
530
|
+
`fill black color 0,0 reset`,
|
|
531
|
+
"-draw",
|
|
532
|
+
`fill white roundrectangle 0,0 ${width - 1},${height - 1} ${innerRadius},${innerRadius}`,
|
|
533
|
+
")",
|
|
534
|
+
"-compose",
|
|
535
|
+
"DstIn",
|
|
536
|
+
"-composite",
|
|
537
|
+
")",
|
|
538
|
+
// Composite screenshot centered on black background
|
|
539
|
+
"-gravity",
|
|
540
|
+
"center",
|
|
541
|
+
"-compose",
|
|
542
|
+
"Over",
|
|
543
|
+
"-composite",
|
|
544
|
+
outputPath
|
|
545
|
+
]);
|
|
546
|
+
return true;
|
|
547
|
+
} catch {
|
|
548
|
+
return false;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
488
551
|
async function frameAllIosScreenshots(screenshotsDir, force = false) {
|
|
489
552
|
const iosDir = join3(screenshotsDir, "ios");
|
|
490
553
|
if (!existsSync3(iosDir)) return 0;
|
|
@@ -503,6 +566,29 @@ async function frameAllIosScreenshots(screenshotsDir, force = false) {
|
|
|
503
566
|
}
|
|
504
567
|
return totalFramed;
|
|
505
568
|
}
|
|
569
|
+
async function frameAllAndroidScreenshots(screenshotsDir, force = false) {
|
|
570
|
+
if (!await commandExists("magick")) return 0;
|
|
571
|
+
const androidDir = join3(screenshotsDir, "android");
|
|
572
|
+
if (!existsSync3(androidDir)) return 0;
|
|
573
|
+
let totalFramed = 0;
|
|
574
|
+
const sizeDirs = readdirSync(androidDir, { withFileTypes: true }).filter(
|
|
575
|
+
(d) => d.isDirectory()
|
|
576
|
+
);
|
|
577
|
+
for (const sizeDir of sizeDirs) {
|
|
578
|
+
const dirPath = join3(androidDir, sizeDir.name);
|
|
579
|
+
const rawFiles = readdirSync(dirPath).filter(
|
|
580
|
+
(f) => f.endsWith(".png") && !f.includes("_framed")
|
|
581
|
+
);
|
|
582
|
+
for (const file of rawFiles) {
|
|
583
|
+
const inputPath = join3(dirPath, file);
|
|
584
|
+
const outputPath = join3(dirPath, file.replace(".png", "_framed.png"));
|
|
585
|
+
if (!force && existsSync3(outputPath)) continue;
|
|
586
|
+
const success = await frameAndroidScreenshot(inputPath, outputPath);
|
|
587
|
+
if (success) totalFramed++;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return totalFramed;
|
|
591
|
+
}
|
|
506
592
|
|
|
507
593
|
// src/commands/capture.ts
|
|
508
594
|
async function captureCommand(options) {
|
|
@@ -654,16 +740,33 @@ Detected ${devices.length} device(s) with ${bundleId}:`)
|
|
|
654
740
|
Captured ${captured.length} screenshot(s) as '${screenshotName}'.`
|
|
655
741
|
)
|
|
656
742
|
);
|
|
657
|
-
if (shouldFrame
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
743
|
+
if (shouldFrame) {
|
|
744
|
+
if (iosDevices.length > 0) {
|
|
745
|
+
console.log("");
|
|
746
|
+
const s = ora("Framing iOS screenshots...").start();
|
|
747
|
+
try {
|
|
748
|
+
const framed = await frameAllIosScreenshots(outputDir);
|
|
749
|
+
s.succeed(`Framed ${framed} iOS screenshot(s)`);
|
|
750
|
+
} catch (error) {
|
|
751
|
+
s.fail(
|
|
752
|
+
`iOS framing failed: ${error instanceof Error ? error.message : error}`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
if (androidDevices.length > 0) {
|
|
757
|
+
const s = ora("Framing Android screenshots...").start();
|
|
758
|
+
try {
|
|
759
|
+
const framed = await frameAllAndroidScreenshots(outputDir);
|
|
760
|
+
if (framed > 0) {
|
|
761
|
+
s.succeed(`Framed ${framed} Android screenshot(s)`);
|
|
762
|
+
} else {
|
|
763
|
+
s.info("Android framing skipped (ImageMagick not found)");
|
|
764
|
+
}
|
|
765
|
+
} catch (error) {
|
|
766
|
+
s.fail(
|
|
767
|
+
`Android framing failed: ${error instanceof Error ? error.message : error}`
|
|
768
|
+
);
|
|
769
|
+
}
|
|
667
770
|
}
|
|
668
771
|
}
|
|
669
772
|
}
|
|
@@ -706,23 +809,32 @@ import pc2 from "picocolors";
|
|
|
706
809
|
async function frameCommand(dir, options) {
|
|
707
810
|
const config = await loadConfig();
|
|
708
811
|
const screenshotsDir = dir || options.input || config.output;
|
|
709
|
-
console.log(pc2.bold(`Framing
|
|
710
|
-
const
|
|
812
|
+
console.log(pc2.bold(`Framing screenshots in ${screenshotsDir}...`));
|
|
813
|
+
const iosSpinner = ora2("Framing iOS screenshots (device bezels)...").start();
|
|
711
814
|
try {
|
|
712
|
-
const framed = await frameAllIosScreenshots(
|
|
713
|
-
|
|
714
|
-
|
|
815
|
+
const framed = await frameAllIosScreenshots(screenshotsDir, options.force);
|
|
816
|
+
if (framed > 0) {
|
|
817
|
+
iosSpinner.succeed(`Framed ${framed} iOS screenshot(s)`);
|
|
818
|
+
} else {
|
|
819
|
+
iosSpinner.info("No new iOS screenshots to frame");
|
|
820
|
+
}
|
|
821
|
+
} catch (error) {
|
|
822
|
+
iosSpinner.fail(
|
|
823
|
+
`iOS framing failed: ${error instanceof Error ? error.message : error}`
|
|
715
824
|
);
|
|
825
|
+
}
|
|
826
|
+
const androidSpinner = ora2("Framing Android screenshots (black border)...").start();
|
|
827
|
+
try {
|
|
828
|
+
const framed = await frameAllAndroidScreenshots(screenshotsDir, options.force);
|
|
716
829
|
if (framed > 0) {
|
|
717
|
-
|
|
830
|
+
androidSpinner.succeed(`Framed ${framed} Android screenshot(s)`);
|
|
718
831
|
} else {
|
|
719
|
-
|
|
832
|
+
androidSpinner.info("No new Android screenshots to frame");
|
|
720
833
|
}
|
|
721
834
|
} catch (error) {
|
|
722
|
-
|
|
723
|
-
`
|
|
835
|
+
androidSpinner.fail(
|
|
836
|
+
`Android framing failed: ${error instanceof Error ? error.message : error}`
|
|
724
837
|
);
|
|
725
|
-
process.exit(1);
|
|
726
838
|
}
|
|
727
839
|
}
|
|
728
840
|
|
|
@@ -730,7 +842,7 @@ async function frameCommand(dir, options) {
|
|
|
730
842
|
var program = new Command();
|
|
731
843
|
program.name("device-shots").description(
|
|
732
844
|
"Capture and frame mobile app screenshots from iOS simulators and Android emulators"
|
|
733
|
-
).version("0.
|
|
845
|
+
).version("0.3.0");
|
|
734
846
|
program.command("capture").description("Capture screenshots from running devices").argument("[name]", "Screenshot name").option("-b, --bundle-id <id>", "App bundle ID").option("-o, --output <dir>", "Output directory").option("-p, --platform <platform>", "ios, android, or both").option("--no-frame", "Skip framing after capture").option("--time <time>", "Status bar time", "9:41").action(async (name, opts) => {
|
|
735
847
|
await captureCommand({ name, ...opts });
|
|
736
848
|
});
|