using System; using System.Text; using System.Text.RegularExpressions; namespace HanvonF710XAttendanceService { internal enum DbToDeviceTemplateFormat { Empty, NedoXml, HanvonJpegFace, HanvonRecord } internal enum NedoPhotoExtractResult { Found, Missing, Invalid } internal sealed class DbToDeviceUploadPlan { public DbToDeviceTemplateFormat Format { get; set; } = DbToDeviceTemplateFormat.Empty; public string PushMode { get; set; } public string Payload { get; set; } public bool UseProfileVerification { get; set; } public NedoPhotoExtractResult? NedoPhotoStatus { get; set; } public int PhotoByteLength { get; set; } public int PhotoBase64Length { get; set; } public int SourceBlobLength { get; set; } public string PrepareNote { get; set; } public int PreparedWidth { get; set; } public int PreparedHeight { get; set; } public string PhotoSourceLabel { get; set; } public string PhotoSourceUrl { get; set; } /// employees.id used for portal photo URL. Must NOT be the device serial. public bool TryResolveEmployeePhoto(string hrmsEmployeeId, out string error) { error = null; bool hadNedoPayload = HasUploadPayload; if (Format != DbToDeviceTemplateFormat.NedoXml || !EmployeePhotoSourceSettings.IsEnabled()) { if (Format == DbToDeviceTemplateFormat.NedoXml && HasUploadPayload) { PhotoSourceLabel = "NEDO_XML"; } return HasUploadPayload; } if (string.IsNullOrWhiteSpace(hrmsEmployeeId)) { error = "hrms_employee_id_missing"; if (hadNedoPayload) { PhotoSourceLabel = "NEDO_XML"; return true; } return false; } if (HrmsEmployeePhotoClient.TryDownloadEmployeePhoto(hrmsEmployeeId, out HrmsEmployeePhotoDownloadResult portalPhoto)) { Payload = Convert.ToBase64String(portalPhoto.ImageBytes); PhotoByteLength = portalPhoto.ImageBytes.Length; PhotoBase64Length = portalPhoto.Base64Length; PhotoSourceLabel = "HRMS_PORTAL"; PhotoSourceUrl = portalPhoto.Url; PreparedWidth = portalPhoto.Width; PreparedHeight = portalPhoto.Height; return true; } error = portalPhoto?.Error; if (hadNedoPayload) { PhotoSourceLabel = "NEDO_XML"; return true; } return false; } public bool TryPrepareFacePayload(out string error) { error = null; if (Format != DbToDeviceTemplateFormat.NedoXml && Format != DbToDeviceTemplateFormat.HanvonJpegFace) { return true; } if (string.IsNullOrWhiteSpace(Payload)) { error = "empty"; return false; } if (!NedoPhotoPreprocessor.TryPrepareForHanvon(Payload, out string prepared, out int width, out int height, out string note)) { error = note ?? "prepare_failed"; return false; } Payload = prepared; PrepareNote = note; PreparedWidth = width; PreparedHeight = height; PhotoBase64Length = prepared?.Length ?? 0; PhotoByteLength = NedoTemplateConverter.GetDecodedJpegByteLength(prepared); return true; } public string FormatLabel { get { switch (Format) { case DbToDeviceTemplateFormat.NedoXml: return "NEDO_XML"; case DbToDeviceTemplateFormat.HanvonJpegFace: return "HANVON_JPEG"; case DbToDeviceTemplateFormat.HanvonRecord: return "HANVON_RECORD"; default: return "EMPTY"; } } } public bool HasUploadPayload => !string.IsNullOrWhiteSpace(Payload); public static DbToDeviceUploadPlan FromBlob(byte[] blob) { var plan = new DbToDeviceUploadPlan(); plan.SourceBlobLength = blob?.Length ?? 0; if (blob == null || blob.Length == 0) { plan.Format = DbToDeviceTemplateFormat.Empty; return plan; } if (blob.Length >= 3 && blob[0] == 0xFF && blob[1] == 0xD8 && blob[2] == 0xFF) { plan.Format = DbToDeviceTemplateFormat.HanvonJpegFace; plan.PushMode = "face"; plan.Payload = Convert.ToBase64String(blob); plan.PhotoByteLength = blob.Length; plan.UseProfileVerification = true; return plan; } string text = Encoding.UTF8.GetString(blob).Trim(); if (string.IsNullOrWhiteSpace(text)) { plan.Format = DbToDeviceTemplateFormat.Empty; return plan; } if (NedoTemplateConverter.IsNedoXml(text)) { plan.Format = DbToDeviceTemplateFormat.NedoXml; plan.PushMode = "face"; plan.UseProfileVerification = true; string photo; NedoPhotoExtractResult photoResult = NedoTemplateConverter.TryExtractPhoto(text, out photo); plan.NedoPhotoStatus = photoResult; if (photoResult == NedoPhotoExtractResult.Found) { plan.Payload = photo; plan.PhotoBase64Length = photo?.Length ?? 0; plan.PhotoByteLength = NedoTemplateConverter.GetDecodedJpegByteLength(photo); } return plan; } string normalized = HanvonHttpApiClient.NormalizeFaceTemplatePayload(blob, out string pushMode); plan.Payload = normalized; plan.PushMode = pushMode; plan.Format = string.Equals(pushMode, "face", StringComparison.OrdinalIgnoreCase) ? DbToDeviceTemplateFormat.HanvonJpegFace : DbToDeviceTemplateFormat.HanvonRecord; plan.UseProfileVerification = plan.Format == DbToDeviceTemplateFormat.HanvonJpegFace; if (plan.Format == DbToDeviceTemplateFormat.HanvonJpegFace && !string.IsNullOrWhiteSpace(normalized)) { plan.PhotoByteLength = NedoTemplateConverter.GetDecodedJpegByteLength(normalized); } return plan; } } internal static class NedoTemplateConverter { private static readonly Regex PhotoAttributeRegex = new Regex( @"photo\s*=\s*""([^""]*)""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); public static bool IsNedoXml(string text) { if (string.IsNullOrWhiteSpace(text)) { return false; } bool hasPhoto = text.IndexOf("photo=\"", StringComparison.OrdinalIgnoreCase) >= 0 || text.IndexOf("photo =", StringComparison.OrdinalIgnoreCase) >= 0; if (!hasPhoto) { return false; } bool hasFaceData = text.IndexOf("face_data=\"", StringComparison.OrdinalIgnoreCase) >= 0; bool hasCheckTypeFace = text.IndexOf("check_type=\"face\"", StringComparison.OrdinalIgnoreCase) >= 0; bool hasIdAttribute = Regex.IsMatch(text, @"\bid\s*=\s*""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); return hasFaceData || hasCheckTypeFace || hasIdAttribute; } public static NedoPhotoExtractResult TryExtractPhoto(string nedoText, out string photoBase64) { photoBase64 = null; if (string.IsNullOrWhiteSpace(nedoText)) { return NedoPhotoExtractResult.Missing; } int photoAttrIndex = FindPhotoAttributeIndex(nedoText, out int valueStart); if (photoAttrIndex < 0) { return NedoPhotoExtractResult.Missing; } if (valueStart >= nedoText.Length) { return NedoPhotoExtractResult.Missing; } int faceDataIndex = nedoText.IndexOf("\"face_data", valueStart, StringComparison.OrdinalIgnoreCase); if (faceDataIndex < 0) { faceDataIndex = nedoText.IndexOf("\" face_data", valueStart, StringComparison.OrdinalIgnoreCase); } int valueEnd; if (faceDataIndex > valueStart) { valueEnd = faceDataIndex; } else { valueEnd = nedoText.IndexOf('"', valueStart); } if (valueEnd <= valueStart) { Match match = PhotoAttributeRegex.Match(nedoText); if (!match.Success || match.Groups.Count < 2) { return NedoPhotoExtractResult.Missing; } return ValidateAndNormalizePhoto(match.Groups[1].Value, out photoBase64); } string raw = nedoText.Substring(valueStart, valueEnd - valueStart); return ValidateAndNormalizePhoto(raw, out photoBase64); } private static int FindPhotoAttributeIndex(string text, out int valueStart) { valueStart = -1; Match spaced = Regex.Match(text, @"photo\s*=\s*""", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); if (!spaced.Success) { return -1; } valueStart = spaced.Index + spaced.Length; return spaced.Index; } private static NedoPhotoExtractResult ValidateAndNormalizePhoto(string raw, out string photoBase64) { photoBase64 = null; if (string.IsNullOrWhiteSpace(raw)) { return NedoPhotoExtractResult.Missing; } raw = raw.Trim(); if (raw.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) { int comma = raw.IndexOf(','); if (comma >= 0) { raw = raw.Substring(comma + 1); } } raw = raw.Replace("\r", "").Replace("\n", "").Trim(); if (!IsValidJpegBase64(raw)) { return NedoPhotoExtractResult.Invalid; } try { byte[] decoded = Convert.FromBase64String(raw); if (!NedoPhotoPreprocessor.IsCompleteJpeg(decoded)) { return NedoPhotoExtractResult.Invalid; } } catch { return NedoPhotoExtractResult.Invalid; } photoBase64 = raw; return NedoPhotoExtractResult.Found; } public static bool IsValidJpegBase64(string base64) { if (string.IsNullOrWhiteSpace(base64)) { return false; } try { byte[] decoded = Convert.FromBase64String(base64); return decoded.Length >= 3 && decoded[0] == 0xFF && decoded[1] == 0xD8 && decoded[2] == 0xFF; } catch { return false; } } public static int GetDecodedJpegByteLength(string base64) { if (string.IsNullOrWhiteSpace(base64)) { return 0; } try { return Convert.FromBase64String(base64).Length; } catch { return 0; } } public static bool UsesFaceField(DbToDeviceUploadPlan plan) { return plan != null && string.Equals(plan.PushMode, "face", StringComparison.OrdinalIgnoreCase); } public static bool UsesRecordField(DbToDeviceUploadPlan plan) { return plan != null && string.Equals(plan.PushMode, "record", StringComparison.OrdinalIgnoreCase); } } }