using System.Buffers.Binary;
using System.Drawing;
using System.Drawing.Imaging;
using XFEWorkflow.Abstractions;
namespace XFEWorkflow.Plugins.Vision;
public sealed record TemplateImageData(string Value, int Width, int Height);
// PNG data stored in the node travels with copy/paste, undo and workflow export.
// Existing file paths remain valid for workflows and code written before 1.1.
public static class TemplateImage
{
public const string Prefix = "data:image/png;base64,";
public const int MaxBytes = 2 * 1024 * 1024, MaxPixels = 1_048_576;
private static void ValidateSize(int width, int height)
{
if (width <= 0 || height <= 0 || width > 8192 || height > 8192 || (long)width * height > MaxPixels)
throw new ArgumentException("模板最多 1048576 像素,单边最多 8192 像素;请框选目标控件附近的小区域。");
}
public static TemplateImageData Encode(Bitmap bitmap)
{
ValidateSize(bitmap.Width, bitmap.Height);
using var data = new MemoryStream(); bitmap.Save(data, ImageFormat.Png);
if (data.Length > MaxBytes) throw new ArgumentException("模板 PNG 超过 2 MiB,请缩小框选区域。");
return new(Prefix + Convert.ToBase64String(data.GetBuffer(), 0, (int)data.Length), bitmap.Width, bitmap.Height);
}
public static TemplateImageData Crop(ScreenFrame frozen, ScreenRegion region)
{
region.RequireInside(frozen.Region); ValidateSize(region.Width, region.Height);
using var bitmap = frozen.Bitmap.Clone(new Rectangle(region.X - frozen.Region.X, region.Y - frozen.Region.Y, region.Width, region.Height), PixelFormat.Format24bppRgb);
return Encode(bitmap);
}
public static TemplateImageData Import(string path)
{
using var frame = ScreenCapture.Load(path); return Encode(frame.Bitmap);
}
public static ScreenFrame Load(string value)
{
if (!value.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) return ScreenCapture.Load(value);
var bytes = Decode(value);
using var data = new MemoryStream(bytes, false);
using var original = Image.FromStream(data, false, true);
var bitmap = new Bitmap(original.Width, original.Height, PixelFormat.Format24bppRgb);
try
{
using var graphics = Graphics.FromImage(bitmap); graphics.Clear(Color.White);
graphics.DrawImage(original, new Rectangle(0, 0, bitmap.Width, bitmap.Height), new Rectangle(0, 0, original.Width, original.Height), GraphicsUnit.Pixel);
return new(bitmap, new(0, 0, bitmap.Width, bitmap.Height), "image");
}
catch { bitmap.Dispose(); throw; }
}
public static byte[] Decode(string value)
{
if (!value.StartsWith(Prefix, StringComparison.Ordinal) || value.Length > Prefix.Length + (MaxBytes + 2) / 3 * 4)
throw new ArgumentException("内嵌模板必须是最多 2 MiB 的 PNG 图像。");
byte[] bytes;
try { bytes = Convert.FromBase64String(value[Prefix.Length..]); }
catch (FormatException ex) { throw new ArgumentException("模板图像数据损坏。", ex); }
if (bytes.Length is < 33 or > MaxBytes || !bytes.AsSpan(0, 8).SequenceEqual(new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 }) ||
BinaryPrimitives.ReadInt32BigEndian(bytes.AsSpan(8, 4)) != 13 || !bytes.AsSpan(12, 4).SequenceEqual("IHDR"u8))
throw new ArgumentException("模板 PNG 头部损坏。");
ValidateSize(BinaryPrimitives.ReadInt32BigEndian(bytes.AsSpan(16, 4)), BinaryPrimitives.ReadInt32BigEndian(bytes.AsSpan(20, 4)));
return bytes;
}
}
using System.Buffers.Binary;
using System.Drawing;
using System.Drawing.Imaging;
using XFEWorkflow.Abstractions;
namespace XFEWorkflow.Plugins.Vision;
public sealed record TemplateImageData(string Value, int Width, int Height);
// PNG data stored in the node travels with copy/paste, undo and workflow export.
// Existing file paths remain valid for workflows and code written before 1.1.
public static class TemplateImage
{
public const string Prefix = "data:image/png;base64,";
public const int MaxBytes = 2 * 1024 * 1024, MaxPixels = 1_048_576;
private static void ValidateSize(int width, int height)
{
if (width <= 0 || height <= 0 || width > 8192 || height > 8192 || (long)width * height > MaxPixels)
throw new ArgumentException("模板最多 1048576 像素,单边最多 8192 像素;请框选目标控件附近的小区域。");
}
public static TemplateImageData Encode(Bitmap bitmap)
{
ValidateSize(bitmap.Width, bitmap.Height);
using var data = new MemoryStream(); bitmap.Save(data, ImageFormat.Png);
if (data.Length > MaxBytes) throw new ArgumentException("模板 PNG 超过 2 MiB,请缩小框选区域。");
return new(Prefix + Convert.ToBase64String(data.GetBuffer(), 0, (int)data.Length), bitmap.Width, bitmap.Height);
}
public static TemplateImageData Crop(ScreenFrame frozen, ScreenRegion region)
{
region.RequireInside(frozen.Region); ValidateSize(region.Width, region.Height);
using var bitmap = frozen.Bitmap.Clone(new Rectangle(region.X - frozen.Region.X, region.Y - frozen.Region.Y, region.Width, region.Height), PixelFormat.Format24bppRgb);
return Encode(bitmap);
}
public static TemplateImageData Import(string path)
{
using var frame = ScreenCapture.Load(path); return Encode(frame.Bitmap);
}
public static ScreenFrame Load(string value)
{
if (!value.StartsWith("data:", StringComparison.OrdinalIgnoreCase)) return ScreenCapture.Load(value);
var bytes = Decode(value);
using var data = new MemoryStream(bytes, false);
using var original = Image.FromStream(data, false, true);
var bitmap = new Bitmap(original.Width, original.Height, PixelFormat.Format24bppRgb);
try
{
using var graphics = Graphics.FromImage(bitmap); graphics.Clear(Color.White);
graphics.DrawImage(original, new Rectangle(0, 0, bitmap.Width, bitmap.Height), new Rectangle(0, 0, original.Width, original.Height), GraphicsUnit.Pixel);
return new(bitmap, new(0, 0, bitmap.Width, bitmap.Height), "image");
}
catch { bitmap.Dispose(); throw; }
}
public static byte[] Decode(string value)
{
if (!value.StartsWith(Prefix, StringComparison.Ordinal) || value.Length > Prefix.Length + (MaxBytes + 2) / 3 * 4)
throw new ArgumentException("内嵌模板必须是最多 2 MiB 的 PNG 图像。");
byte[] bytes;
try { bytes = Convert.FromBase64String(value[Prefix.Length..]); }
catch (FormatException ex) { throw new ArgumentException("模板图像数据损坏。", ex); }
if (bytes.Length is < 33 or > MaxBytes || !bytes.AsSpan(0, 8).SequenceEqual(new byte[] { 137, 80, 78, 71, 13, 10, 26, 10 }) ||
BinaryPrimitives.ReadInt32BigEndian(bytes.AsSpan(8, 4)) != 13 || !bytes.AsSpan(12, 4).SequenceEqual("IHDR"u8))
throw new ArgumentException("模板 PNG 头部损坏。");
ValidateSize(BinaryPrimitives.ReadInt32BigEndian(bytes.AsSpan(16, 4)), BinaryPrimitives.ReadInt32BigEndian(bytes.AsSpan(20, 4)));
return bytes;
}
}