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.
Files changed (3) hide show
  1. package/README.md +9 -9
  2. package/dist/index.js +149 -37
  3. 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.7/ # iPhone 16 Plus, 15 Pro Max
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.7` | iPhone 16 Plus, 15 Pro Max, 15 Plus, 14 Pro Max |
140
- | `6.5` | iPhone 14 Plus, 13 Pro Max, 12 Pro Max, 11 Pro Max |
141
- | `6.3` | iPhone 16 Pro |
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
- | `13` | iPad Pro 13" |
145
- | `11` | iPad Pro 11", iPad Air |
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
- // 6.7" — iPhone 16 Plus, 15 Pro Max, 15 Plus, 14 Pro Max
102
- "1290x2796": "6.7",
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
- // 6.1" — iPhone 16, 15, 15 Pro, 14, 14 Pro, 13, 13 Pro, 12, 12 Pro
109
- "1179x2556": "6.1",
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
- // 5.8" — iPhone X, XS, 11 Pro
112
- "1125x2436": "5.8",
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
- // iPad 13" — iPad Pro 13" (M4), iPad Pro 12.9" (older)
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.9" — iPad Air (5th), iPad (10th)
125
- "2360x1640": "11",
126
- // iPad mini
127
- "1488x2266": "8.3"
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 && iosDevices.length > 0) {
658
- console.log("");
659
- const s = ora("Framing iOS screenshots...").start();
660
- try {
661
- const framed = await frameAllIosScreenshots(outputDir);
662
- s.succeed(`Framed ${framed} screenshot(s)`);
663
- } catch (error) {
664
- s.fail(
665
- `Framing failed: ${error instanceof Error ? error.message : error}`
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 iOS screenshots in ${screenshotsDir}...`));
710
- const spinner = ora2("Setting up Python environment...").start();
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
- screenshotsDir,
714
- options.force
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
- spinner.succeed(`Framed ${framed} screenshot(s)`);
830
+ androidSpinner.succeed(`Framed ${framed} Android screenshot(s)`);
718
831
  } else {
719
- spinner.info("No new screenshots to frame");
832
+ androidSpinner.info("No new Android screenshots to frame");
720
833
  }
721
834
  } catch (error) {
722
- spinner.fail(
723
- `Framing failed: ${error instanceof Error ? error.message : error}`
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.2.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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "device-shots",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Capture and frame mobile app screenshots from iOS simulators and Android emulators",
5
5
  "type": "module",
6
6
  "bin": {