HanvonAttendanceService/NedoTemplateConverter.cs

376 lines
12 KiB
C#

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; }
/// <param name="hrmsEmployeeId">employees.id used for portal photo URL. Must NOT be the device serial.</param>
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);
}
}
}