screen-helpers.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. import * as path from 'node:path';
  2. import * as fs from 'node:fs';
  3. import { screen, desktopCapturer, NativeImage } from 'electron';
  4. const fixtures = path.resolve(__dirname, '..', 'fixtures');
  5. export enum HexColors {
  6. GREEN = '#00b140',
  7. PURPLE = '#6a0dad',
  8. RED = '#ff0000',
  9. BLUE = '#0000ff',
  10. WHITE = '#ffffff'
  11. };
  12. /**
  13. * Capture the screen at the given point.
  14. *
  15. * NOTE: Not yet supported on Linux in CI due to empty sources list.
  16. */
  17. export const captureScreen = async (point: Electron.Point = { x: 0, y: 0 }): Promise<NativeImage> => {
  18. const display = screen.getDisplayNearestPoint(point);
  19. const sources = await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: display.size });
  20. // Toggle to save screen captures for debugging.
  21. const DEBUG_CAPTURE = process.env.DEBUG_CAPTURE || false;
  22. if (DEBUG_CAPTURE) {
  23. for (const source of sources) {
  24. await fs.promises.writeFile(path.join(fixtures, `screenshot_${source.display_id}_${Date.now()}.png`), source.thumbnail.toPNG());
  25. }
  26. }
  27. const screenCapture = sources.find(source => source.display_id === `${display.id}`);
  28. // Fails when HDR is enabled on Windows.
  29. // https://bugs.chromium.org/p/chromium/issues/detail?id=1247730
  30. if (!screenCapture) {
  31. const displayIds = sources.map(source => source.display_id);
  32. throw new Error(`Unable to find screen capture for display '${display.id}'\n\tAvailable displays: ${displayIds.join(', ')}`);
  33. }
  34. return screenCapture.thumbnail;
  35. };
  36. const formatHexByte = (val: number): string => {
  37. const str = val.toString(16);
  38. return str.length === 2 ? str : `0${str}`;
  39. };
  40. /**
  41. * Get the hex color at the given pixel coordinate in an image.
  42. */
  43. export const getPixelColor = (image: Electron.NativeImage, point: Electron.Point): string => {
  44. // image.crop crashes if point is fractional, so round to prevent that crash
  45. const pixel = image.crop({ x: Math.round(point.x), y: Math.round(point.y), width: 1, height: 1 });
  46. // TODO(samuelmaddock): NativeImage.toBitmap() should return the raw pixel
  47. // color, but it sometimes differs. Why is that?
  48. const [b, g, r] = pixel.toBitmap();
  49. return `#${formatHexByte(r)}${formatHexByte(g)}${formatHexByte(b)}`;
  50. };
  51. const hexToRgba = (hexColor: string) => {
  52. const match = hexColor.match(/^#([0-9a-fA-F]{6,8})$/);
  53. if (!match) return;
  54. const colorStr = match[1];
  55. return [
  56. parseInt(colorStr.substring(0, 2), 16),
  57. parseInt(colorStr.substring(2, 4), 16),
  58. parseInt(colorStr.substring(4, 6), 16),
  59. parseInt(colorStr.substring(6, 8), 16) || 0xFF
  60. ];
  61. };
  62. /** Calculate euclidian distance between colors. */
  63. const colorDistance = (hexColorA: string, hexColorB: string) => {
  64. const colorA = hexToRgba(hexColorA);
  65. const colorB = hexToRgba(hexColorB);
  66. if (!colorA || !colorB) return -1;
  67. return Math.sqrt(
  68. Math.pow(colorB[0] - colorA[0], 2) +
  69. Math.pow(colorB[1] - colorA[1], 2) +
  70. Math.pow(colorB[2] - colorA[2], 2)
  71. );
  72. };
  73. /**
  74. * Determine if colors are similar based on distance. This can be useful when
  75. * comparing colors which may differ based on lossy compression.
  76. */
  77. export const areColorsSimilar = (
  78. hexColorA: string,
  79. hexColorB: string,
  80. distanceThreshold = 90
  81. ): boolean => {
  82. const distance = colorDistance(hexColorA, hexColorB);
  83. return distance <= distanceThreshold;
  84. };
  85. /**
  86. * Whether the current VM has a valid screen which can be used to capture.
  87. *
  88. * This is specific to Electron's CI test runners.
  89. * - Linux: virtual screen display is 0x0
  90. * - Win32 arm64 (WOA): virtual screen display is 0x0
  91. * - Win32 ia32: skipped
  92. */
  93. export const hasCapturableScreen = () => {
  94. return process.platform === 'darwin' ||
  95. (process.platform === 'win32' && process.arch === 'x64');
  96. };