feat: load HRMS portal employee photos for enrollment

Config + download/validate portal JPEG by HRMS employee id.
main
SYED MUSTUFA AHMED NAQVI 2026-09-04 12:33:36 +05:00
parent b95fc02f74
commit 2b0f3df888
3 changed files with 312 additions and 0 deletions

View File

@ -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;
}
}
}
}

199
HrmsEmployeePhotoClient.cs Normal file
View File

@ -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
{
/// <param name="hrmsEmployeeId">employees.id used in the portal photo path. Not the device serial.</param>
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";
}
/// <param name="hrmsEmployeeId">employees.id for EmployeePhotoBaseUrl + id + ".jpeg".</param>
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;
}
}
}

View File

@ -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<bool> 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());
}
}
}