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 OpenQA.Selenium;
25
26namespace GenerallyPositive
27{
32 public interface IImageMatcher
33 {
39 Rectangle FindImage(Bitmap screen, string templatePath, int matchingPercentage);
40 }
41
47 {
51 public Browser.Browser Browser { get; set; }
52
56 public static Bitmap ScreenShot { get; set; }
57
58 private static string base64StringInternal = null;
59
60
61 /************** CAPTURE SCREEN ******************/
62 // https://stackoverflow.com/questions/4978157/how-to-search-for-an-image-on-screen-in-c
63 // TODO: Capture screen for Applications
64
89 public IAllowGraphicSettings CaptureScreen(object ApplicationOrBrowser = null)
90 {
91 // what a browser handed back last time belongs to that capture and to nothing after it. Left set,
92 // it is what ToBase64String and SaveTo return forever, so a desktop capture that follows a browser
93 // one hands back the browser picture
94 base64StringInternal = null;
95
96 bool headlessMode = false; // assume headless as we might be called before we even have a browser created
97 Bitmap image = new Bitmap(1920, 1080);
98 bool isBrowser = typeof(Browser.Browser) == ApplicationOrBrowser?.GetType();
99 if (true == isBrowser)
100 // a browser on a hidden desktop is headful, but the screen is showing the desktop the user is on
101 // and not that one, so copying the screen would quietly hand back a picture of their desktop. the
102 // browser is asked for the image instead, which is the same route headless takes
103 // an image search that will not click with the mouse wants the page, not the screen, windowed too
104 headlessMode = ((Browser.Browser)ApplicationOrBrowser).BrowserSettings.UseHeadless
105 || ((Browser.Browser)ApplicationOrBrowser).BrowserSettings.HiddenDesktop
106 || ((Browser.Browser)ApplicationOrBrowser).BrowserSettings.ImageSearchInPage;
107
108 try
109 {
110 image = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height, PixelFormat.Format32bppArgb);
111 }
112 catch
113 {
114 try
115 {
116 image = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height, PixelFormat.Format32bppArgb);
117 }
118 catch
119 {
120 // can't use publishevent, would be recursive
121 //Console.WriteLine("new Bitmap failed: Unable to take screenshot");
122 System.Diagnostics.Debug.WriteLine("new Bitmap failed: Unable to take screenshot");
123 return this;
124 }
125 }
126
127 if (false == headlessMode)
128 {
129 var gfx = Graphics.FromImage(image);
130 try
131 {
132 gfx.CopyFromScreen(Screen.PrimaryScreen.Bounds.X, Screen.PrimaryScreen.Bounds.Y, 0, 0, Screen.PrimaryScreen.Bounds.Size, CopyPixelOperation.SourceCopy);
133 }
134 catch { }
135 }
136 else if (true == isBrowser)
137 {
138 Browser = (Browser.Browser)ApplicationOrBrowser;
139
140 try
141 {
142 byte[] imageBytes = null;
143
144 if (true == ((Browser.Browser)ApplicationOrBrowser).UseSelenium)
145 {
146 if (null != ((Browser.Browser)ApplicationOrBrowser).BrowserDriver)
147 {
148 Screenshot screenshot = ((ITakesScreenshot)((Browser.Browser)ApplicationOrBrowser).BrowserDriver).GetScreenshot();
149 // Convert the screenshot to a Bitmap
150 base64StringInternal = screenshot.AsBase64EncodedString;
151 }
152 }
153 else if (0 < GPAL.Browsers.Count())
154 {
155 foreach (Browser.Browser browser in GPAL.Browsers)
156 if (null != browser.BrowserSettings.Process)
157 {
158 if (true == ((Browser.Browser)ApplicationOrBrowser).UsePuppeteer)
159 {
160 base64StringInternal = ((Browser.Browser)ApplicationOrBrowser).PuppeteerClient.CaptureVisibleTab().Execute();
161 }
162 else if (((Browser.Browser)ApplicationOrBrowser).UseOttoMagic)
163 {
164 base64StringInternal = ((Browser.Browser)ApplicationOrBrowser).MagicHelper.CaptureVisibleTab();
165 }
166
167 break;
168 }
169 }
170 char[] ca = { ',' };
171
172 // remove the header info, we want the raw data
173 // "data:image/png;base64,iVBORw0 ... "
174 try
175 {
176 base64StringInternal = base64StringInternal.Trim()
177 .Trim('"') // remove surrounding quotes if present
178 .Split(ca, 2) // split on first comma only
179 [1]; // take everything after the comma
180 }
181 catch
182 {
183 base64StringInternal = base64StringInternal.Trim()
184 .Trim('"'); // remove surrounding quotes if present
185 }
186
187 // also trim any leftover whitespace (rare but safe)
188 base64StringInternal = base64StringInternal.Trim();
189
190 imageBytes = Convert.FromBase64String(base64StringInternal);
191 using (MemoryStream ms = new MemoryStream(imageBytes))
192 // Create an Image object from the MemoryStream
193 image = new Bitmap(ms);
194 }
195 catch (Exception ex)
196 {
197 string outMessageHeader = $@"[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff")}][EXCEPTION][ImageHelper]: ";
198
199 // can't use publishevent, would be recursive: publishing an exception takes a screenshot, and
200 // this is the screenshot. written directly instead, and silent still means silent
201 if ("1" != Environment.GetEnvironmentVariable("GPAL_IS_SILENT"))
202 {
203 Console.WriteLine($"{outMessageHeader} [{Browser.AutomationEngine}][{Browser.BrowserType}][{(true == Browser.BrowserSettings.UseHeadless ? "Headless" : "Headful")}] Unable to take screenshot [{ex.Message}]");
204 System.Diagnostics.Debug.WriteLine($"{outMessageHeader} [{Browser.AutomationEngine}][{Browser.BrowserType}][{(true == Browser.BrowserSettings.UseHeadless ? "Headless" : "Headful")}] Unable to take screenshot [{ex.Message}]");
205 }
206 }
207 }
208 ScreenShot = image;
209
210 return this;
211 }
212
226 public IAllowGraphicSettings ToBase64String(out string base64String)
227 {
228 System.IO.MemoryStream ms = new MemoryStream();
229 if (null != base64StringInternal)
230 base64String = base64StringInternal;
231 else if (null != ScreenShot)
232 {
233 ScreenShot.Save(ms, ImageFormat.Jpeg);
234 byte[] byteImage = ms.ToArray();
235 base64String = Convert.ToBase64String(byteImage);
236 }
237 else
238 base64String = string.Empty;
239
240 return this;
241 }
242
248 public IAllowGraphicSettings ToBitmap(out Bitmap bitmap)
249 {
250 bitmap = ScreenShot;
251 return this;
252 }
253
265 [File(Description = "Save to file (overwrite)")]
267 {
268 SaveScreenshot(file.Next, ScreenShot);
269
270 return this;
271 }
272
278 private static void SaveScreenshot(string fileName, Image screenshot)
279 {
280 try
281 {
282 if (null != screenshot)
283 {
284 string saveToDirectory = Path.GetDirectoryName(fileName);
285
286 // ensure save to directory exists
287 if (false == string.IsNullOrEmpty(saveToDirectory))
288 Directory.CreateDirectory(saveToDirectory);
289
290 // the extension says the format, so a .png really is a PNG. Encoding one way whatever the
291 // name says leaves a file that lies about what is in it
292 screenshot.Save(fileName, FormatForFile(fileName));
293
294 GPAL.PublishSimpleEvent(Enums.GPALEventType.INFO, $"Screenshot saved to [{fileName}]");
295 }
296 else
297 GPAL.PublishSimpleEvent(Enums.GPALEventType.WARNING, "No screenshot to save");
298 }
299 catch (Exception ex)
300 {
301 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"No screenshot saved to [{fileName}]", null, Enums.GPALObjectType.None, ex);
302 }
303 }
304
310 internal static System.Drawing.Imaging.ImageFormat FormatForFile(string filename)
311 {
312 System.Drawing.Imaging.ImageFormat retVal;
313
314 switch (Path.GetExtension(filename ?? string.Empty).ToLower())
315 {
316 case ".jpg":
317 case ".jpeg":
318 retVal = System.Drawing.Imaging.ImageFormat.Jpeg;
319 break;
320 case ".bmp":
321 retVal = System.Drawing.Imaging.ImageFormat.Bmp;
322 break;
323 case ".gif":
324 retVal = System.Drawing.Imaging.ImageFormat.Gif;
325 break;
326 case ".tif":
327 case ".tiff":
328 retVal = System.Drawing.Imaging.ImageFormat.Tiff;
329 break;
330 default:
331 retVal = System.Drawing.Imaging.ImageFormat.Png;
332 break;
333 }
334
335 return retVal;
336 }
337
338 #region Helpers
339
350 public static System.Drawing.Image LoadImageFromFile(string filename)
351 {
352 return System.Drawing.Image.FromFile(filename);
353 }
354
365 public static Image Base64ToImage(string base64String)
366 {
367 base64String = base64String.Trim().Trim('\"');
368
369 // a data URI names the media type ahead of the payload, so only that header comes off. The pattern
370 // is anchored to the front and stops at the comma: matching the payload too would take the image
371 // with the header and leave nothing to decode
372 string pattern = @"^data:[^;]+;base64,";
373 base64String = System.Text.RegularExpressions.Regex.Replace(base64String, pattern, string.Empty);
374
375 // Convert Base64 String to byte[]
376 byte[] imageBytes = Convert.FromBase64String(base64String);
377
378 // the stream holds the bytes already and is left open, because an Image reads its pixels from the
379 // stream underneath it and throws once that is gone
380 MemoryStream ms = new MemoryStream(imageBytes, 0, imageBytes.Length);
381
382 Image image = Image.FromStream(ms, true);
383
384 return image;
385 }
386
402 public Rectangle FindImage(string imagePath, object ApplicationOrBrowser = null)
403 {
404 Rectangle retVal = Rectangle.Empty;
405
406 if (null == GPAL.ImageMatcher)
407 {
408 // said once, since a search pages down the whole document asking again at every step
409 if (false == matcherMissingSaid)
410 {
411 matcherMissingSaid = true;
412 GPAL.PublishSimpleEvent(Enums.GPALEventType.WARNING, $"Image matching needs the GPAL.Image package, added and registered with GPALImage.Register() at startup, so [{imagePath}] was not looked for");
413 }
414 }
415 else
416 {
417 try
418 {
419 // pass the browser through so a headless run matches against a visible-tab capture instead of the
420 // physical screen (which has no window on it to find). the coordinates that come back are then
421 // viewport relative rather than screen absolute - see GPALElement.CoordinateSpace.
422 CaptureScreen(ApplicationOrBrowser);
423
424 retVal = GPAL.ImageMatcher.FindImage(ScreenShot, imagePath, GPAL.MatchingPercentage);
425 }
426 catch (Exception ex)
427 {
428 GPAL.PublishSimpleEvent(Enums.GPALEventType.EXCEPTION, $"Error locating [{imagePath}] on-screen.", null, Enums.GPALObjectType.None, ex);
429 }
430 }
431
432 return retVal;
433 }
434
435 // whether the missing matcher has been reported yet
436 private static bool matcherMissingSaid;
437 #endregion Helpers
438
444 {
445 return this;
446 }
447 }
448
449 // extension ToByteArray for CV
453 internal static class ImageExtensions
454 {
461 public static byte[] ToByteArray(this System.Drawing.Image image, ImageFormat format)
462 {
463 using (MemoryStream ms = new MemoryStream())
464 {
465 image.Save(ms, format);
466 return ms.ToArray();
467 }
468 }
469 }
470}
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:401
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:2568
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. A browser capture comes back in whatever th...
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.
Finds a picture on a capture of the screen or of a browser. The GPAL.Image package provides one and r...
Rectangle FindImage(Bitmap screen, string templatePath, int matchingPercentage)
Where the picture in templatePath is on screen .