device-shots 0.2.1 → 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 (2) hide show
  1. package/dist/index.js +128 -21
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -490,6 +490,64 @@ async function frameScreenshots(inputDir, outputDir, force = false) {
490
490
  const rawFiles = existsSync3(inputDir) ? readdirSync(inputDir).filter((f) => f.endsWith(".png")).length : 0;
491
491
  return { framed: framedFiles, skipped: rawFiles - framedFiles };
492
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
+ }
493
551
  async function frameAllIosScreenshots(screenshotsDir, force = false) {
494
552
  const iosDir = join3(screenshotsDir, "ios");
495
553
  if (!existsSync3(iosDir)) return 0;
@@ -508,6 +566,29 @@ async function frameAllIosScreenshots(screenshotsDir, force = false) {
508
566
  }
509
567
  return totalFramed;
510
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
+ }
511
592
 
512
593
  // src/commands/capture.ts
513
594
  async function captureCommand(options) {
@@ -659,16 +740,33 @@ Detected ${devices.length} device(s) with ${bundleId}:`)
659
740
  Captured ${captured.length} screenshot(s) as '${screenshotName}'.`
660
741
  )
661
742
  );
662
- if (shouldFrame && iosDevices.length > 0) {
663
- console.log("");
664
- const s = ora("Framing iOS screenshots...").start();
665
- try {
666
- const framed = await frameAllIosScreenshots(outputDir);
667
- s.succeed(`Framed ${framed} screenshot(s)`);
668
- } catch (error) {
669
- s.fail(
670
- `Framing failed: ${error instanceof Error ? error.message : error}`
671
- );
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
+ }
672
770
  }
673
771
  }
674
772
  }
@@ -711,23 +809,32 @@ import pc2 from "picocolors";
711
809
  async function frameCommand(dir, options) {
712
810
  const config = await loadConfig();
713
811
  const screenshotsDir = dir || options.input || config.output;
714
- console.log(pc2.bold(`Framing iOS screenshots in ${screenshotsDir}...`));
715
- 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();
716
814
  try {
717
- const framed = await frameAllIosScreenshots(
718
- screenshotsDir,
719
- 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}`
720
824
  );
825
+ }
826
+ const androidSpinner = ora2("Framing Android screenshots (black border)...").start();
827
+ try {
828
+ const framed = await frameAllAndroidScreenshots(screenshotsDir, options.force);
721
829
  if (framed > 0) {
722
- spinner.succeed(`Framed ${framed} screenshot(s)`);
830
+ androidSpinner.succeed(`Framed ${framed} Android screenshot(s)`);
723
831
  } else {
724
- spinner.info("No new screenshots to frame");
832
+ androidSpinner.info("No new Android screenshots to frame");
725
833
  }
726
834
  } catch (error) {
727
- spinner.fail(
728
- `Framing failed: ${error instanceof Error ? error.message : error}`
835
+ androidSpinner.fail(
836
+ `Android framing failed: ${error instanceof Error ? error.message : error}`
729
837
  );
730
- process.exit(1);
731
838
  }
732
839
  }
733
840
 
@@ -735,7 +842,7 @@ async function frameCommand(dir, options) {
735
842
  var program = new Command();
736
843
  program.name("device-shots").description(
737
844
  "Capture and frame mobile app screenshots from iOS simulators and Android emulators"
738
- ).version("0.2.1");
845
+ ).version("0.3.0");
739
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) => {
740
847
  await captureCommand({ name, ...opts });
741
848
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "device-shots",
3
- "version": "0.2.1",
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": {