feat: convert NEDO XML templates to Hanvon face photos
Detect NEDO blobs, extract/prepare JPEG for Hanvon upload.main
parent
7741523d25
commit
b95fc02f74
|
|
@ -0,0 +1,172 @@
|
||||||
|
using System;
|
||||||
|
using System.Configuration;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Drawing2D;
|
||||||
|
using System.Drawing.Imaging;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace HanvonF710XAttendanceService
|
||||||
|
{
|
||||||
|
internal static class NedoPhotoPreprocessor
|
||||||
|
{
|
||||||
|
public const int DefaultMinShortSide = 480;
|
||||||
|
public const int HanvonMaxSide = 1024;
|
||||||
|
public const int HanvonMaxBytes = 500 * 1024;
|
||||||
|
|
||||||
|
public static bool TryPrepareForHanvon(string photoBase64, out string preparedBase64, out int width, out int height, out string note)
|
||||||
|
{
|
||||||
|
preparedBase64 = null;
|
||||||
|
width = 0;
|
||||||
|
height = 0;
|
||||||
|
note = null;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(photoBase64))
|
||||||
|
{
|
||||||
|
note = "empty";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] sourceBytes;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sourceBytes = Convert.FromBase64String(photoBase64);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
note = "invalid_base64";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsCompleteJpeg(sourceBytes))
|
||||||
|
{
|
||||||
|
note = "incomplete_jpeg";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int minShortSide = GetIntSetting("NEDO_FACE_MIN_SHORT_SIDE", DefaultMinShortSide);
|
||||||
|
byte[] outputBytes;
|
||||||
|
using (var inputStream = new MemoryStream(sourceBytes))
|
||||||
|
using (var image = Image.FromStream(inputStream))
|
||||||
|
{
|
||||||
|
width = image.Width;
|
||||||
|
height = image.Height;
|
||||||
|
int shortSide = Math.Min(width, height);
|
||||||
|
if (shortSide >= minShortSide && sourceBytes.Length <= HanvonMaxBytes)
|
||||||
|
{
|
||||||
|
preparedBase64 = photoBase64;
|
||||||
|
note = "unchanged";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
double scale = 1.0;
|
||||||
|
if (shortSide < minShortSide)
|
||||||
|
{
|
||||||
|
scale = (double)minShortSide / shortSide;
|
||||||
|
}
|
||||||
|
|
||||||
|
int targetWidth = (int)Math.Round(width * scale);
|
||||||
|
int targetHeight = (int)Math.Round(height * scale);
|
||||||
|
int longSide = Math.Max(targetWidth, targetHeight);
|
||||||
|
if (longSide > HanvonMaxSide)
|
||||||
|
{
|
||||||
|
double fit = (double)HanvonMaxSide / longSide;
|
||||||
|
targetWidth = (int)Math.Round(targetWidth * fit);
|
||||||
|
targetHeight = (int)Math.Round(targetHeight * fit);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var bitmap = new Bitmap(targetWidth, targetHeight))
|
||||||
|
using (var graphics = Graphics.FromImage(bitmap))
|
||||||
|
{
|
||||||
|
graphics.CompositingQuality = CompositingQuality.HighQuality;
|
||||||
|
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||||
|
graphics.SmoothingMode = SmoothingMode.HighQuality;
|
||||||
|
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||||
|
graphics.DrawImage(image, 0, 0, targetWidth, targetHeight);
|
||||||
|
|
||||||
|
using (var outputStream = new MemoryStream())
|
||||||
|
{
|
||||||
|
var encoder = GetJpegEncoder();
|
||||||
|
if (encoder != null)
|
||||||
|
{
|
||||||
|
using (var parameters = new EncoderParameters(1))
|
||||||
|
{
|
||||||
|
parameters.Param[0] = new EncoderParameter(Encoder.Quality, 92L);
|
||||||
|
bitmap.Save(outputStream, encoder, parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bitmap.Save(outputStream, ImageFormat.Jpeg);
|
||||||
|
}
|
||||||
|
|
||||||
|
outputBytes = outputStream.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
width = targetWidth;
|
||||||
|
height = targetHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputBytes.Length > HanvonMaxBytes)
|
||||||
|
{
|
||||||
|
note = "too_large_after_prepare";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
preparedBase64 = Convert.ToBase64String(outputBytes);
|
||||||
|
note = "upscaled";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsCompleteJpeg(byte[] bytes)
|
||||||
|
{
|
||||||
|
if (bytes == null || bytes.Length < 4)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytes[0] != 0xFF || bytes[1] != 0xD8)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = bytes.Length - 2; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (bytes[i] == 0xFF && bytes[i + 1] == 0xD9)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ImageCodecInfo GetJpegEncoder()
|
||||||
|
{
|
||||||
|
ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();
|
||||||
|
foreach (var codec in codecs)
|
||||||
|
{
|
||||||
|
if (string.Equals(codec.MimeType, "image/jpeg", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return codec;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetIntSetting(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,375 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace HanvonF710XAttendanceService
|
||||||
|
{
|
||||||
|
internal static class NedoTemplateConverterTests
|
||||||
|
{
|
||||||
|
private const string SampleJpegBase64 =
|
||||||
|
"/9j/4AAQSkZJRgABAQEAOABkAAD/2wBDAA0JCgsKCA0LCgsODg0PEyAVExISEyccHhcgLikxMC4pLSwzOko+MzZGNywtQFdBRkxOUlNSMj5aYVpQYEpRUk//2wBDAQ4ODhMREyYVFSZPNS01T09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0//wAARCACgAHgDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIQAxAAAAGfAP/Z";
|
||||||
|
|
||||||
|
public static int RunAll()
|
||||||
|
{
|
||||||
|
int failed = 0;
|
||||||
|
failed += Run("1 detect NEDO XML", TestDetectNedoXml);
|
||||||
|
failed += Run("2 extract valid NEDO photo", TestExtractValidNedoPhoto);
|
||||||
|
failed += Run("3 missing NEDO photo", TestMissingNedoPhoto);
|
||||||
|
failed += Run("4 invalid NEDO photo", TestInvalidNedoPhoto);
|
||||||
|
failed += Run("5 NEDO plan uses face not record", TestNedoPlanUsesFace);
|
||||||
|
failed += Run("6 Hanvon-native template uses record", TestHanvonNativeUsesRecord);
|
||||||
|
failed += Run("7 NEDO plan uses profile verification", TestNedoUsesProfileVerification);
|
||||||
|
failed += Run("8 employee 5 style NEDO blob", TestEmployee5StyleBlob);
|
||||||
|
failed += Run("9 prepare upscales small NEDO photo", TestPrepareUpscalesSmallPhoto);
|
||||||
|
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 TestDetectNedoXml()
|
||||||
|
{
|
||||||
|
string xml = BuildNedoXml("5", SampleJpegBase64);
|
||||||
|
return NedoTemplateConverter.IsNedoXml(xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TestExtractValidNedoPhoto()
|
||||||
|
{
|
||||||
|
string xml = BuildNedoXml("5", SampleJpegBase64);
|
||||||
|
NedoPhotoExtractResult result = NedoTemplateConverter.TryExtractPhoto(xml, out string photo);
|
||||||
|
return result == NedoPhotoExtractResult.Found
|
||||||
|
&& photo == SampleJpegBase64
|
||||||
|
&& NedoTemplateConverter.GetDecodedJpegByteLength(photo) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TestMissingNedoPhoto()
|
||||||
|
{
|
||||||
|
string xml = "id=\"5\" name=\"\" check_type=\"face\" face_data=\"abc\"";
|
||||||
|
NedoPhotoExtractResult result = NedoTemplateConverter.TryExtractPhoto(xml, out string photo);
|
||||||
|
return result == NedoPhotoExtractResult.Missing && photo == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TestInvalidNedoPhoto()
|
||||||
|
{
|
||||||
|
string xml = BuildNedoXml("5", "not-valid-base64!!!");
|
||||||
|
NedoPhotoExtractResult result = NedoTemplateConverter.TryExtractPhoto(xml, out string photo);
|
||||||
|
return result == NedoPhotoExtractResult.Invalid && photo == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TestNedoPlanUsesFace()
|
||||||
|
{
|
||||||
|
string xml = BuildNedoXml("5", SampleJpegBase64);
|
||||||
|
var plan = DbToDeviceUploadPlan.FromBlob(Encoding.UTF8.GetBytes(xml));
|
||||||
|
return plan.Format == DbToDeviceTemplateFormat.NedoXml
|
||||||
|
&& NedoTemplateConverter.UsesFaceField(plan)
|
||||||
|
&& !NedoTemplateConverter.UsesRecordField(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TestHanvonNativeUsesRecord()
|
||||||
|
{
|
||||||
|
string record = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 });
|
||||||
|
var plan = DbToDeviceUploadPlan.FromBlob(Encoding.UTF8.GetBytes(record));
|
||||||
|
return plan.Format == DbToDeviceTemplateFormat.HanvonRecord
|
||||||
|
&& NedoTemplateConverter.UsesRecordField(plan)
|
||||||
|
&& !NedoTemplateConverter.UsesFaceField(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TestNedoUsesProfileVerification()
|
||||||
|
{
|
||||||
|
string xml = BuildNedoXml("5", SampleJpegBase64);
|
||||||
|
var plan = DbToDeviceUploadPlan.FromBlob(Encoding.UTF8.GetBytes(xml));
|
||||||
|
return plan.UseProfileVerification;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TestEmployee5StyleBlob()
|
||||||
|
{
|
||||||
|
string xml = "id=\"5\" name=\"\" authority=\"0X11\" card_num=\"0Xffffffff\" calid=\"0\" check_type=\"face\" opendoor_type=\"face\" password=\"\" alg_edition=\"3.1\" sn=\"6753718120000147\"photo=\"" + SampleJpegBase64 + "\"face_data=\"abc\"";
|
||||||
|
var plan = DbToDeviceUploadPlan.FromBlob(Encoding.UTF8.GetBytes(xml));
|
||||||
|
return plan.Format == DbToDeviceTemplateFormat.NedoXml
|
||||||
|
&& plan.HasUploadPayload
|
||||||
|
&& plan.NedoPhotoStatus == NedoPhotoExtractResult.Found;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TestPrepareUpscalesSmallPhoto()
|
||||||
|
{
|
||||||
|
var plan = DbToDeviceUploadPlan.FromBlob(Encoding.UTF8.GetBytes(BuildNedoXml("5", SampleJpegBase64)));
|
||||||
|
if (!plan.TryPrepareFacePayload(out string error))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Equals(plan.PrepareNote, "upscaled", StringComparison.OrdinalIgnoreCase)
|
||||||
|
&& plan.PreparedWidth >= NedoPhotoPreprocessor.DefaultMinShortSide
|
||||||
|
&& plan.PreparedHeight >= NedoPhotoPreprocessor.DefaultMinShortSide;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildNedoXml(string id, string photo)
|
||||||
|
{
|
||||||
|
return "id=\"" + id + "\" name=\"\" check_type=\"face\" photo=\"" + photo + "\" face_data=\"biometric-not-used\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue