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; } } } }