feat: load HRMS portal employee photos for enrollment
Config + download/validate portal JPEG by HRMS employee id.main
parent
b95fc02f74
commit
2b0f3df888
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue