GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
ImageHelper.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Drawing;
19using System.Drawing.Imaging;
20using System.IO;
21using System.Linq;
22using System.Windows.Forms;
24using OpenCvSharp;
25using OpenQA.Selenium;
26
27namespace GenerallyPositive
28{
34 {
38 public Browser.Browser Browser { get; set; }
39
43 public static Bitmap ScreenShot { get; set; }
44
45 private static string base64StringInternal = null;
46
47
48 /************** CAPTURE SCREEN ******************/
49 // https://stackoverflow.com/questions/4978157/how-to-search-for-an-image-on-screen-in-c
50 // TODO: Capture screen for Applications
51
76 public IAllowGraphicSettings CaptureScreen(object ApplicationOrBrowser = null)
77 {
78 bool headlessMode = false; // assume headless as we might be called before we even have a browser created
79 Bitmap image = new Bitmap(1920, 1080);
80 bool isBrowser = typeof(Browser.Browser) == ApplicationOrBrowser?.GetType();
81 if (true == isBrowser)
82 // a browser on a hidden desktop is headful, but the screen is showing the desktop the user is on
83 // and not that one, so copying the screen would quietly hand back a picture of their desktop. the
84 // browser is asked for the image instead, which is the same route headless takes
85 headlessMode = ((Browser.Browser)ApplicationOrBrowser).BrowserSettings.UseHeadless
86 || ((Browser.Browser)ApplicationOrBrowser).BrowserSettings.HiddenDesktop;
87
88 try
89 {
90 image = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height, PixelFormat.Format32bppArgb);
91 }
92 catch
93 {
94 try
95 {
96 image = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height, PixelFormat.Format32bppArgb);
97 }
98 catch
99 {
100 // can't use publishevent, would be recursive
101 //Console.WriteLine("new Bitmap failed: Unable to take screenshot");
102 System.Diagnostics.Debug.WriteLine("new Bitmap failed: Unable to take screenshot");
103 return this;
104 }
105 }
106
107 if (false == headlessMode)
108 {
109 var gfx = Graphics.FromImage(image);
110 try
111 {
112 gfx.CopyFromScreen(Screen.PrimaryScreen.Bounds.X, Screen.PrimaryScreen.Bounds.Y, 0, 0, Screen.PrimaryScreen.Bounds.Size, CopyPixelOperation.SourceCopy);
113 }
114 catch { }
115 }
116 else if (true == isBrowser)
117 {
118 Browser = (Browser.Browser)ApplicationOrBrowser;
119
120 try
121 {
122 byte[] imageBytes = null;
123
124 if (true == ((Browser.Browser)ApplicationOrBrowser).UseSelenium)
125 {
126 if (null != ((Browser.Browser)ApplicationOrBrowser).BrowserDriver)
127 {
128 Screenshot screenshot = ((ITakesScreenshot)((Browser.Browser)ApplicationOrBrowser).BrowserDriver).GetScreenshot();
129 // Convert the screenshot to a Bitmap
130 base64StringInternal = screenshot.AsBase64EncodedString;
131 }
132 }
133 else if (0 < GPAL.Browsers.Count())
134 {
135 foreach (Browser.Browser browser in GPAL.Browsers)
136 if (null != browser.BrowserSettings.Process)
137 {
138 if (true == ((Browser.Browser)ApplicationOrBrowser).UsePuppeteer)
139 {
140 base64StringInternal = ((Browser.Browser)ApplicationOrBrowser).PuppeteerClient.CaptureVisibleTab().Execute();
141 }
142 else if (((Browser.Browser)ApplicationOrBrowser).UseOttoMagic)
143 {
144 base64StringInternal = ((Browser.Browser)ApplicationOrBrowser).MagicHelper.CaptureVisibleTab();
145 }
146
147 break;
148 }
149 }
150 char[] ca = { ',' };
151
152 // remove the header info, we want the raw data
153 // "data:image/png;base64,iVBORw0 ... "
154 try
155 {
156 base64StringInternal = base64StringInternal.Trim()
157 .Trim('"') // remove surrounding quotes if present
158 .Split(ca, 2) // split on first comma only
159 [1]; // take everything after the comma
160 }
161 catch
162 {
163 base64StringInternal = base64StringInternal.Trim()
164 .Trim('"'); // remove surrounding quotes if present
165 }
166
167 // also trim any leftover whitespace (rare but safe)
168 base64StringInternal = base64StringInternal.Trim();
169
170 imageBytes = Convert.FromBase64String(base64StringInternal);
171 using (MemoryStream ms = new MemoryStream(imageBytes))
172 // Create an Image object from the MemoryStream
173 image = new Bitmap(ms);
174 }
175 catch (Exception ex)
176 {
177 string outMessageHeader = $@"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff")}][EXCEPTION][ImageHelper]: ";
178
179 // can't use publishevent, would be recursive: publishing an exception takes a screenshot, and
180 // this is the screenshot. written directly instead, and silent still means silent
181 if ("1" != Environment.GetEnvironmentVariable("GPAL_IS_SILENT"))
182 {
183 Console.WriteLine($"{outMessageHeader} [{Browser.AutomationEngine}][{Browser.BrowserType}][{(true == Browser.BrowserSettings.UseHeadless ? "Headless" : "Headful")}] Unable to take screenshot [{ex.Message}]");
184 System.Diagnostics.Debug.WriteLine($"{outMessageHeader} [{Browser.AutomationEngine}][{Browser.BrowserType}][{(true == Browser.BrowserSettings.UseHeadless ? "Headless" : "Headful")}] Unable to take screenshot [{ex.Message}]");
185 }
186 }
187 }
188 ScreenShot = image;
189
190 return this;
191 }
192
205 public IAllowGraphicSettings ToBase64String(out string base64String)
206 {
207 System.IO.MemoryStream ms = new MemoryStream();
208 if (null != base64StringInternal)
209 base64String = base64StringInternal;
210 else if (null != ScreenShot)
211 {
212 ScreenShot.Save(ms, ImageFormat.Jpeg);
213 byte[] byteImage = ms.ToArray();
214 base64String = Convert.ToBase64String(byteImage);
215 }
216 else
217 base64String = string.Empty;
218
219 return this;
220 }
221
227 public IAllowGraphicSettings ToBitmap(out Bitmap bitmap)
228 {
229 bitmap = ScreenShot;
230 return this;
231 }
232
244 [File(Description = "Save to file (overwrite)")]
246 {
247 ToBase64String(out string base64String);
248 SaveScreenshot(file.Next, base64String);
249
250 return this;
251 }
252
258 private static void SaveScreenshot(string fileName, object screenshotResponse)
259 {
260 // Implement logic to save the screenshot to a file
261 // The screenshotResponse should contain the base64-encoded image data
262
263 // Example: Convert base64 to bytes and save to a file
264 try
265 {
266 var base64Data = screenshotResponse?.ToString();
267 if (!string.IsNullOrEmpty(base64Data))
268 {
269 var imageBytes = Convert.FromBase64String(base64Data);
270 string saveToDirectory = Path.GetDirectoryName(fileName);
271 // ensure save to directory exists
272 if (false == string.IsNullOrEmpty(saveToDirectory))
273 Directory.CreateDirectory(saveToDirectory);
274 File.WriteAllBytes(fileName, imageBytes);
275
276 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"Screenshot saved to [{fileName}]");
277 }
278 else
279 GPAL.PublishSimpleEvent(Enums.GPALEventType.WARNING, "No screenshot to save");
280 }
281 catch (Exception ex)
282 {
283 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"No screenshot saved to [{fileName}]", null, Enums.GPALObjectType.None, ex);
284 }
285 }
286
287 #region Helpers
288
299 public static System.Drawing.Image LoadImageFromFile(string filename)
300 {
301 return System.Drawing.Image.FromFile(filename);
302 }
303
314 public static Image Base64ToImage(string base64String)
315 {
316 base64String = base64String.Trim().Trim('\"');
317
318 string pattern = @"data:[^;]+;base64,[A-Za-z0-9+/=]+";
319 base64String = System.Text.RegularExpressions.Regex.Replace(base64String, pattern, string.Empty);
320
321 // Convert Base64 String to byte[]
322 byte[] imageBytes = Convert.FromBase64String(base64String);
323 MemoryStream ms = new MemoryStream(imageBytes, 0, imageBytes.Length);
324
325 // Convert byte[] to Image
326 ms.Write(imageBytes, 0, imageBytes.Length);
327 Image image = Image.FromStream(ms, true);
328
329 return image;
330 }
331
347 public Rectangle FindImage(string imagePath, object ApplicationOrBrowser = null)
348 {
349 Rectangle screenRect = Screen.PrimaryScreen.Bounds;
350 Rect match = new Rect();
351
352 try
353 {
354 // pass the browser through so a headless run matches against a visible-tab capture instead of the
355 // physical screen (which has no window on it to find). the coordinates that come back are then
356 // viewport relative rather than screen absolute - see GPALElement.CoordinateSpace.
357 CaptureScreen(ApplicationOrBrowser);
358 Bitmap screenshot = ScreenShot;
359
360 // Convert Bitmaps to Mats and convert to grayscale
361 Mat sourceMat = OpenCvSharp.Extensions.BitmapConverter.ToMat(screenshot);
362 Mat templateMat = new Mat(imagePath);
363 Mat sourceGray = new Mat();
364 Mat templateGray = new Mat();
365
366 // Check for empty Mats
367 if (sourceMat.Empty())
368 {
369 GPAL.PublishSimpleEvent(Enums.GPALEventType.WARNING, "SourceMat is empty.", sourceMat, Enums.GPALObjectType.Other);
370 return new Rectangle();
371 }
372 if (templateMat.Empty())
373 {
374 GPAL.PublishSimpleEvent(Enums.GPALEventType.WARNING, "TemplateMat is empty.", templateMat, Enums.GPALObjectType.Other);
375 return new Rectangle();
376 }
377
378 // Convert to grayscale for better matching
379 Cv2.CvtColor(sourceMat, sourceGray, ColorConversionCodes.BGR2GRAY);
380 Cv2.CvtColor(templateMat, templateGray, ColorConversionCodes.BGR2GRAY);
381
382 // Perform template matching
383 Mat result = new Mat();
384 Cv2.MatchTemplate(sourceGray, templateGray, result, TemplateMatchModes.CCoeffNormed);
385
386 // Find match locations
387 Cv2.MinMaxLoc(result, out _, out double res, out _, out OpenCvSharp.Point maxLoc);
388
389 // Check if match meets threshold
390 if (res >= (GPAL.MatchingPercentage * 0.01))
391 match = new Rect(maxLoc.X, maxLoc.Y, templateMat.Width, templateMat.Height);
392 }
393 catch (Exception ex)
394 {
395 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Error locating [{imagePath}] on-screen.", null, Enums.GPALObjectType.None, ex);
396 }
397
398 return new Rectangle(match.X, match.Y, match.Width, match.Height);
399 }
400 #endregion Helpers
401
407 {
408 return this;
409 }
410 }
411
412 // extension ToByteArray for CV
416 internal static class ImageExtensions
417 {
424 public static byte[] ToByteArray(this System.Drawing.Image image, ImageFormat format)
425 {
426 using (MemoryStream ms = new MemoryStream())
427 {
428 image.Save(ms, format);
429 return ms.ToArray();
430 }
431 }
432 }
433}
Browser object that contains the fluent methods to create your Browser workflow. Instatiated using G...
Definition Browser.cs:68
IAllowPuppeteerExecution CaptureVisibleTab(ImageFormat imageFormat=ImageFormat.JPEG)
Configure this client to capture a screenshot of the visible tab in the given image format.
GPAL File object instantied with GPAL.File Used to load tokens into a GPALGrid [rows/columns].
Definition GPALFile.cs:36
GPALFile Next
Advances an internal cursor and returns the next filename to use. If there is exactly one file and Wi...
Definition GPALFile.cs:407
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...
Definition GPAL.cs:2406
Provides comprehensive image capture, conversion, and matching capabilities for automation scenarios....
static Bitmap ScreenShot
Static reference to the most recent screenshot captured by this helper.
IImageHelper ToGPALObject()
Returns this instance as IImageHelper for interface-based usage.
IAllowGraphicSettings SaveTo(GPALFile file)
Saves the captured screenshot to a file (overwrites if exists).
IAllowGraphicSettings CaptureScreen(object ApplicationOrBrowser=null)
Captures the current screen or visible browser tab. Works in both headful (desktop screenshot) and he...
IAllowGraphicSettings ToBitmap(out Bitmap bitmap)
Returns the last captured screenshot as a Bitmap.
IAllowGraphicSettings ToBase64String(out string base64String)
Converts the last captured screenshot to a Base64 string (JPEG format).
static Image Base64ToImage(string base64String)
Converts a Base64 string (with or without data URI prefix) to a System.Drawing.Image.
Rectangle FindImage(string imagePath, object ApplicationOrBrowser=null)
Finds the location of a template image within the current screen/browser screenshot using OpenCV temp...
Browser.Browser Browser
Gets or sets the associated Browser instance for headless capture operations.
static System.Drawing.Image LoadImageFromFile(string filename)
Loads an image from the specified file path.