376 lines
12 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|