diff --git a/EmployeePhotoSourceSettings.cs b/EmployeePhotoSourceSettings.cs new file mode 100644 index 0000000..9a352ec --- /dev/null +++ b/EmployeePhotoSourceSettings.cs @@ -0,0 +1,61 @@ +using System; +using System.Configuration; + +namespace HanvonF710XAttendanceService +{ + internal static class EmployeePhotoSourceSettings + { + public static bool IsEnabled() + { + return GetBool("EnableEmployeePhotoSource", false); + } + + public static string GetBaseUrl() + { + return ConfigurationManager.AppSettings["EmployeePhotoBaseUrl"]?.Trim() ?? ""; + } + + public static int GetMinShortSide() + { + return GetInt("HRMS_PHOTO_MIN_SHORT_SIDE", 80); + } + + public static int GetMaxShortSide() + { + return GetInt("HRMS_PHOTO_MAX_SHORT_SIDE", 4096); + } + + public static int GetTimeoutMs() + { + return GetInt("HRMS_PHOTO_HTTP_TIMEOUT_MS", 30000); + } + + private static bool GetBool(string key, bool defaultValue) + { + try + { + var raw = ConfigurationManager.AppSettings[key]; + if (string.IsNullOrWhiteSpace(raw)) return defaultValue; + return bool.TryParse(raw.Trim(), out var v) ? v : defaultValue; + } + catch + { + return defaultValue; + } + } + + private static int GetInt(string key, int defaultValue) + { + try + { + var raw = ConfigurationManager.AppSettings[key]; + if (string.IsNullOrWhiteSpace(raw)) return defaultValue; + return int.TryParse(raw.Trim(), out var v) && v > 0 ? v : defaultValue; + } + catch + { + return defaultValue; + } + } + } +} diff --git a/HrmsEmployeePhotoClient.cs b/HrmsEmployeePhotoClient.cs new file mode 100644 index 0000000..75a36a8 --- /dev/null +++ b/HrmsEmployeePhotoClient.cs @@ -0,0 +1,199 @@ +using System; +using System.Drawing; +using System.IO; +using System.Net; + +namespace HanvonF710XAttendanceService +{ + internal sealed class HrmsEmployeePhotoDownloadResult + { + public bool Success { get; set; } + public int StatusCode { get; set; } + public string ContentType { get; set; } + public string Url { get; set; } + public byte[] ImageBytes { get; set; } + public int Width { get; set; } + public int Height { get; set; } + public int Base64Length { get; set; } + public string Error { get; set; } + } + + internal static class HrmsEmployeePhotoClient + { + /// employees.id used in the portal photo path. Not the device serial. + public static string BuildPhotoUrl(string baseUrl, string hrmsEmployeeId) + { + if (string.IsNullOrWhiteSpace(baseUrl)) + { + throw new ArgumentException("Employee photo base URL is required."); + } + + if (string.IsNullOrWhiteSpace(hrmsEmployeeId)) + { + throw new ArgumentException("HRMS employee ID (employees.id) is required."); + } + + string trimmedBase = baseUrl.Trim(); + if (!trimmedBase.EndsWith("/", StringComparison.Ordinal)) + { + trimmedBase += "/"; + } + + return trimmedBase + hrmsEmployeeId.Trim() + ".jpeg"; + } + + /// employees.id for EmployeePhotoBaseUrl + id + ".jpeg". + public static bool TryDownloadEmployeePhoto(string hrmsEmployeeId, out HrmsEmployeePhotoDownloadResult result) + { + result = new HrmsEmployeePhotoDownloadResult(); + if (!EmployeePhotoSourceSettings.IsEnabled()) + { + result.Error = "employee_photo_source_disabled"; + return false; + } + + string baseUrl = EmployeePhotoSourceSettings.GetBaseUrl(); + if (string.IsNullOrWhiteSpace(baseUrl)) + { + result.Error = "employee_photo_base_url_missing"; + return false; + } + + if (string.IsNullOrWhiteSpace(hrmsEmployeeId)) + { + result.Error = "hrms_employee_id_missing"; + return false; + } + + result.Url = BuildPhotoUrl(baseUrl, hrmsEmployeeId); + int timeoutMs = EmployeePhotoSourceSettings.GetTimeoutMs(); + + try + { + var request = (HttpWebRequest)WebRequest.Create(result.Url); + request.Method = "GET"; + request.Timeout = timeoutMs; + request.ReadWriteTimeout = timeoutMs; + request.AllowAutoRedirect = true; + request.UserAgent = "HanvonF710XAttendanceService/1.0"; + + using (var response = (HttpWebResponse)request.GetResponse()) + using (var stream = response.GetResponseStream()) + using (var memory = new MemoryStream()) + { + result.StatusCode = (int)response.StatusCode; + result.ContentType = response.ContentType; + + if (response.StatusCode != HttpStatusCode.OK) + { + result.Error = "http_status_" + result.StatusCode; + return false; + } + + if (stream == null) + { + result.Error = "empty_response_stream"; + return false; + } + + stream.CopyTo(memory); + result.ImageBytes = memory.ToArray(); + } + } + catch (WebException wex) + { + if (wex.Response is HttpWebResponse errResp) + { + result.StatusCode = (int)errResp.StatusCode; + result.ContentType = errResp.ContentType; + } + + result.Error = wex.Message; + return false; + } + catch (Exception ex) + { + result.Error = ex.Message; + return false; + } + + if (!TryValidateDownloadedImage(result, out string validationError)) + { + result.Error = validationError; + result.ImageBytes = null; + return false; + } + + result.Base64Length = Convert.ToBase64String(result.ImageBytes).Length; + result.Success = true; + return true; + } + + public static bool TryValidateDownloadedImage(HrmsEmployeePhotoDownloadResult result, out string error) + { + error = null; + byte[] bytes = result?.ImageBytes; + if (bytes == null || bytes.Length == 0) + { + error = "empty_image"; + return false; + } + + if (bytes.Length < 4 || bytes[0] != 0xFF || bytes[1] != 0xD8) + { + error = "not_jpeg"; + return false; + } + + if (!NedoPhotoPreprocessor.IsCompleteJpeg(bytes)) + { + error = "incomplete_jpeg"; + return false; + } + + string contentType = result.ContentType ?? ""; + if (!string.IsNullOrWhiteSpace(contentType) + && contentType.IndexOf("image", StringComparison.OrdinalIgnoreCase) < 0 + && contentType.IndexOf("jpeg", StringComparison.OrdinalIgnoreCase) < 0 + && contentType.IndexOf("octet-stream", StringComparison.OrdinalIgnoreCase) < 0) + { + error = "invalid_content_type"; + return false; + } + + try + { + using (var input = new MemoryStream(bytes)) + using (var image = Image.FromStream(input)) + { + result.Width = image.Width; + result.Height = image.Height; + } + } + catch (Exception ex) + { + error = "invalid_image_data:" + ex.Message; + return false; + } + + int minShort = EmployeePhotoSourceSettings.GetMinShortSide(); + int maxShort = EmployeePhotoSourceSettings.GetMaxShortSide(); + int shortSide = Math.Min(result.Width, result.Height); + int longSide = Math.Max(result.Width, result.Height); + + if (shortSide < minShort) + { + error = "image_too_small"; + return false; + } + + if (longSide > maxShort) + { + error = "image_too_large"; + return false; + } + + return true; + } + } +} diff --git a/HrmsEmployeePhotoClientTests.cs b/HrmsEmployeePhotoClientTests.cs new file mode 100644 index 0000000..50b340b --- /dev/null +++ b/HrmsEmployeePhotoClientTests.cs @@ -0,0 +1,52 @@ +using System; + +namespace HanvonF710XAttendanceService +{ + internal static class HrmsEmployeePhotoClientTests + { + public static int RunAll() + { + int failed = 0; + failed += Run("1 build portal photo URL", TestBuildPhotoUrl); + failed += Run("2 disabled source returns false", TestDisabledSource); + return failed; + } + + private static int Run(string name, Func test) + { + try + { + if (!test()) + { + Console.WriteLine("FAIL: " + name); + return 1; + } + + Console.WriteLine("PASS: " + name); + return 0; + } + catch (Exception ex) + { + Console.WriteLine("FAIL: " + name + " err=" + ex.Message); + return 1; + } + } + + private static bool TestBuildPhotoUrl() + { + // Photo path uses employees.id (84321), not device serial (15057). + string url = HrmsEmployeePhotoClient.BuildPhotoUrl( + "https://portal.utopiaindustries.pk/uind/employee-photo/", + "84321"); + return string.Equals( + url, + "https://portal.utopiaindustries.pk/uind/employee-photo/84321.jpeg", + StringComparison.OrdinalIgnoreCase); + } + + private static bool TestDisabledSource() + { + return !string.IsNullOrWhiteSpace(EmployeePhotoSourceSettings.GetBaseUrl()); + } + } +}