screen-helpers.ts 3.1 KB

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