瀏覽代碼

新增图像边框移除器类

懒得勤快 4 月之前
父節點
當前提交
0a5d511f5a

+ 1 - 1
BenchmarkTest/BenchmarkTest.csproj

@@ -7,7 +7,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
+    <PackageReference Include="BenchmarkDotNet" Version="0.15.2" />
   </ItemGroup>
 
   <ItemGroup>

+ 1 - 1
Directory.Build.props

@@ -1,6 +1,6 @@
 <Project>
  <PropertyGroup>
-   <Version>2025.2</Version>
+   <Version>2025.3</Version>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
 </Project>

+ 13 - 13
Masuit.Tools.Abstractions/Masuit.Tools.Abstractions.csproj

@@ -51,11 +51,11 @@
         <PackageReference Include="AngleSharp.Css" Version="1.0.0-beta.151" />
         <PackageReference Include="DnsClient" Version="1.8.0" />
         <PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
-        <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.4" />
+        <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.6" />
         <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
-        <PackageReference Include="System.Management" Version="9.0.4" />
+        <PackageReference Include="System.Management" Version="9.0.6" />
         <PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
-        <PackageReference Include="SharpCompress" Version="0.39.0" />
+        <PackageReference Include="SharpCompress" Version="0.40.0" />
     </ItemGroup>
 
     <ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0'">
@@ -64,7 +64,7 @@
         <PackageReference Include="System.Memory" Version="4.6.3" />
         <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="[1.0.0]" />
         <PackageReference Include="SixLabors.ImageSharp" Version="[2.1.10]" />
-        <PackageReference Include="System.Collections.Immutable" Version="9.0.4" />
+        <PackageReference Include="System.Collections.Immutable" Version="9.0.6" />
         <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
         <PackageReference Include="System.Diagnostics.PerformanceCounter" Version="[7.0.0]" />
     </ItemGroup>
@@ -75,7 +75,7 @@
         <PackageReference Include="System.Memory" Version="4.6.3" />
         <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="[1.0.0]" />
         <PackageReference Include="SixLabors.ImageSharp" Version="[2.1.10]" />
-        <PackageReference Include="System.Collections.Immutable" Version="9.0.4" />
+        <PackageReference Include="System.Collections.Immutable" Version="9.0.6" />
         <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
         <PackageReference Include="System.Diagnostics.PerformanceCounter" Version="[7.0.0]" />
     </ItemGroup>
@@ -83,25 +83,25 @@
     <ItemGroup Condition=" '$(TargetFramework)' == 'net6'">
         <PackageReference Include="Castle.Core" Version="5.1.1" />
         <PackageReference Include="System.Diagnostics.PerformanceCounter" Version="[7.0.0]" />
-        <PackageReference Include="System.Collections.Immutable" Version="9.0.4" />
+        <PackageReference Include="System.Collections.Immutable" Version="9.0.6" />
         <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.6" />
-        <PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
+        <PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
     </ItemGroup>
 
     <ItemGroup Condition=" '$(TargetFramework)' == 'net8'">
         <PackageReference Include="Castle.Core" Version="5.2.1" />
-        <PackageReference Include="System.Diagnostics.PerformanceCounter" Version="9.0.4" />
-        <PackageReference Include="System.Collections.Immutable" Version="9.0.4" />
+        <PackageReference Include="System.Diagnostics.PerformanceCounter" Version="9.0.6" />
+        <PackageReference Include="System.Collections.Immutable" Version="9.0.6" />
         <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.6" />
-        <PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
+        <PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
     </ItemGroup>
 
     <ItemGroup Condition=" '$(TargetFramework)' == 'net9'">
         <PackageReference Include="Castle.Core" Version="5.2.1" />
-        <PackageReference Include="System.Diagnostics.PerformanceCounter" Version="9.0.4" />
-        <PackageReference Include="System.Collections.Immutable" Version="9.0.4" />
+        <PackageReference Include="System.Diagnostics.PerformanceCounter" Version="9.0.6" />
+        <PackageReference Include="System.Collections.Immutable" Version="9.0.6" />
         <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.6" />
-        <PackageReference Include="SixLabors.ImageSharp" Version="3.1.8" />
+        <PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
     </ItemGroup>
 
     <ItemGroup Condition=" '$(TargetFramework)' == 'net461' ">

+ 631 - 0
Masuit.Tools.Abstractions/Media/ImageBorderRemover.cs

@@ -0,0 +1,631 @@
+using Masuit.Tools.Systems;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+
+namespace Masuit.Tools.Media;
+
+/// <summary>
+/// 图像边框移除器
+/// </summary>
+public class ImageBorderRemover
+{
+    /// <summary>
+    /// 检测图片边框信息(支持多色边框)
+    /// </summary>
+    /// <param name="imagePath">图片路径</param>
+    /// <param name="tolerance">颜色容差(0-100),默认10</param>
+    /// <param name="maxLayers">最大检测边框层数,默认3</param>
+    /// <param name="useDownscaling">是否使用缩小采样优化性能,默认true</param>
+    /// <param name="downscaleFactor">缩小采样比例(1-10),默认4</param>
+    /// <returns>边框检测结果</returns>
+    public static BorderDetectionResult DetectBorders(string imagePath, int tolerance = 10, int maxLayers = 3, bool useDownscaling = true, int downscaleFactor = 4)
+    {
+        using (Image<Rgba32> image = Image.Load<Rgba32>(imagePath))
+        {
+            return DetectBorders(image, tolerance, maxLayers, useDownscaling, downscaleFactor);
+        }
+    }
+
+    /// <summary>
+    /// 检测图片边框信息(从已加载的图像)
+    /// </summary>
+    /// <param name="image">已加载的图像</param>
+    /// <param name="tolerance">颜色容差(0-100),默认10</param>
+    /// <param name="maxLayers">最大检测边框层数,默认3</param>
+    /// <param name="useDownscaling">是否使用缩小采样优化性能,默认true</param>
+    /// <param name="downscaleFactor">缩小采样比例(1-10),默认4</param>
+    /// <returns>边框检测结果</returns>
+    public static BorderDetectionResult DetectBorders(Image<Rgba32> image, int tolerance = 10, int maxLayers = 3, bool useDownscaling = true, int downscaleFactor = 4)
+    {
+        var result = new BorderDetectionResult
+        {
+            ImageWidth = image.Width,
+            ImageHeight = image.Height,
+            BorderColors = new List<Rgba32>(),
+            BorderLayers = 0
+        };
+
+        byte toleranceValue = (byte)(tolerance * 2.55);
+
+        // 使用多层边框检测算法
+        var (top, bottom, left, right, layers, colors) = FindContentBordersWithLayers(image, toleranceValue, maxLayers, useDownscaling, downscaleFactor);
+
+        // 设置内容边界
+        result.ContentTop = top;
+        result.ContentBottom = bottom;
+        result.ContentLeft = left;
+        result.ContentRight = right;
+        result.BorderLayers = layers;
+        result.BorderColors = colors;
+
+        return result;
+    }
+
+    /// <summary>
+    /// 自动移除图片的多层边框(仅当至少有两边存在边框时才裁剪)
+    /// </summary>
+    /// <param name="inputPath">输入图片路径</param>
+    /// <param name="tolerance">颜色容差(0-100),默认10</param>
+    /// <param name="maxLayers">最大检测边框层数,默认3</param>
+    /// <returns>是否执行了裁剪操作</returns>
+    public static void RemoveBorders(string inputPath, int tolerance = 10, int maxLayers = 3)
+    {
+        RemoveBorders(inputPath, inputPath, tolerance, maxLayers);
+    }
+
+    /// <summary>
+    /// 自动移除图片的多层边框(仅当至少有两边存在边框时才裁剪)
+    /// </summary>
+    /// <param name="inputPath">输入图片路径</param>
+    /// <param name="outputPath">输出图片路径</param>
+    /// <param name="tolerance">颜色容差(0-100),默认10</param>
+    /// <param name="maxLayers">最大检测边框层数,默认3</param>
+    /// <param name="useDownscaling">是否使用缩小采样优化性能,默认true</param>
+    /// <param name="downscaleFactor">缩小采样比例(1-10),默认4</param>
+    /// <returns>是否执行了裁剪操作</returns>
+    public static void RemoveBorders(string inputPath, string outputPath, int tolerance = 10, int maxLayers = 3, bool useDownscaling = true, int downscaleFactor = 4)
+    {
+        using Image<Rgba32> image = Image.Load<Rgba32>(inputPath);
+        var hasCropped = RemoveBorders(image, tolerance, maxLayers, useDownscaling, downscaleFactor);
+
+        // 决定是否保存
+        if (hasCropped)
+        {
+            image.Save(outputPath);
+        }
+    }
+
+    /// <summary>
+    /// 自动移除图片的多层边框(仅当至少有两边存在边框时才裁剪)
+    /// </summary>
+    /// <param name="input">输入图片路径</param>
+    /// <param name="tolerance">颜色容差(0-100),默认10</param>
+    /// <param name="maxLayers">最大检测边框层数,默认3</param>
+    /// <param name="useDownscaling">是否使用缩小采样优化性能,默认true</param>
+    /// <param name="downscaleFactor">缩小采样比例(1-10),默认4</param>
+    /// <returns>是否执行了裁剪操作</returns>
+    public static PooledMemoryStream RemoveBorders(Stream input, int tolerance = 10, int maxLayers = 3, bool useDownscaling = true, int downscaleFactor = 4)
+    {
+        var format = Image.DetectFormat(input);
+        input.Seek(0, SeekOrigin.Begin);
+        Image<Rgba32> image = Image.Load<Rgba32>(input);
+        RemoveBorders(image, tolerance, maxLayers, useDownscaling, downscaleFactor);
+        var stream = new PooledMemoryStream();
+        image.Save(stream, format);
+        return stream;
+    }
+
+    private static bool RemoveBorders(Image<Rgba32> image, int tolerance, int maxLayers, bool useDownscaling, int downscaleFactor)
+    {
+        // 保存原始尺寸用于比较
+        int originalWidth = image.Width;
+        int originalHeight = image.Height;
+
+        // 使用多层检测方法获取边框信息
+        var borderInfo = DetectBorders(image, tolerance, maxLayers, useDownscaling, downscaleFactor);
+
+        bool hasCropped = false;
+        if (borderInfo.CanBeCropped)
+        {
+            int newWidth = borderInfo.ContentRight - borderInfo.ContentLeft + 1;
+            int newHeight = borderInfo.ContentBottom - borderInfo.ContentTop + 1;
+
+            if (newWidth > 0 && newHeight > 0 && (newWidth != originalWidth || newHeight != originalHeight))
+            {
+                image.Mutate(x => x.Crop(new Rectangle(borderInfo.ContentLeft, borderInfo.ContentTop, newWidth, newHeight)));
+                hasCropped = true;
+            }
+        }
+        return hasCropped;
+    }
+
+    /// <summary>
+    /// 查找内容边界(支持多层边框检测)
+    /// </summary>
+    private static (int top, int bottom, int left, int right, int layers, List<Rgba32> colors) FindContentBordersWithLayers(Image<Rgba32> image, byte tolerance, int maxLayers, bool useDownscaling, int downscaleFactor)
+    {
+        // 如果启用缩小采样且图像足够大
+        Image<Rgba32> workingImage;
+        float scale = 1f;
+        bool isDownscaled = false;
+
+        if (useDownscaling && image.Width > 500 && image.Height > 500)
+        {
+            // 计算缩小尺寸
+            int newWidth = image.Width / downscaleFactor;
+            int newHeight = image.Height / downscaleFactor;
+            scale = (float)image.Width / newWidth;
+
+            // 创建缩小版本用于检测
+            workingImage = image.Clone(ctx => ctx.Resize(newWidth, newHeight));
+            isDownscaled = true;
+        }
+        else
+        {
+            workingImage = image;
+        }
+
+        int width = workingImage.Width;
+        int height = workingImage.Height;
+
+        int top = 0;
+        int bottom = height - 1;
+        int left = 0;
+        int right = width - 1;
+
+        int layers = 0;
+        var borderColors = new List<Rgba32>();
+
+        // 检测多层边框
+        for (int layer = 0; layer < maxLayers; layer++)
+        {
+            bool borderFound = false;
+
+            // 并行检测四个方向的边框层
+            var results = new (int borderSize, Rgba32? color)[4];
+
+            Parallel.Invoke(() =>
+            {
+                if (top < height / 2)
+                {
+                    Rgba32? layerColor = null;
+                    int newTop = DetectLayerBorderTop(workingImage, top, bottom, left, right, tolerance, ref layerColor);
+                    results[0] = (newTop - top, layerColor);
+                    if (newTop > top) borderFound = true;
+                    top = newTop;
+                }
+            }, () =>
+            {
+                if (bottom > height / 2)
+                {
+                    Rgba32? layerColor = null;
+                    int newBottom = DetectLayerBorderBottom(workingImage, top, bottom, left, right, tolerance, ref layerColor);
+                    results[1] = (newBottom - bottom, layerColor);
+                    if (newBottom < bottom) borderFound = true;
+                    bottom = newBottom;
+                }
+            }, () =>
+            {
+                if (left < width / 2)
+                {
+                    Rgba32? layerColor = null;
+                    int newLeft = DetectLayerBorderLeft(workingImage, top, bottom, left, right, tolerance, ref layerColor);
+                    results[2] = (newLeft - left, layerColor);
+                    if (newLeft > left) borderFound = true;
+                    left = newLeft;
+                }
+            }, () =>
+            {
+                if (right > width / 2)
+                {
+                    Rgba32? layerColor = null;
+                    int newRight = DetectLayerBorderRight(workingImage, top, bottom, left, right, tolerance, ref layerColor);
+                    results[3] = (newRight - right, layerColor);
+                    if (newRight < right) borderFound = true;
+                    right = newRight;
+                }
+            });
+
+            // 收集检测到的边框颜色
+            foreach (var (borderSize, color) in results)
+            {
+                if (color.HasValue && borderSize > 0)
+                {
+                    borderColors.Add(color.Value);
+                }
+            }
+
+            if (borderFound)
+            {
+                layers++;
+            }
+            else
+            {
+                break; // 没有检测到更多边框层
+            }
+        }
+
+        // 如果是缩小采样版本,映射回原图坐标
+        if (isDownscaled)
+        {
+            top = (int)(top * scale);
+            bottom = (int)(bottom * scale);
+            left = (int)(left * scale);
+            right = (int)(right * scale);
+
+            // 确保边界在图像范围内
+            top = Clamp(top, 0, image.Height - 1);
+            bottom = Clamp(bottom, top, image.Height - 1);
+            left = Clamp(left, 0, image.Width - 1);
+            right = Clamp(right, left, image.Width - 1);
+
+            // 释放缩小图像
+            workingImage.Dispose();
+        }
+
+        return (top, bottom, left, right, layers, borderColors);
+    }
+
+    private static int Clamp(int value, int min, int max)
+    {
+        return value < min ? min : value > max ? max : value;
+    }
+
+    /// <summary>
+    /// 检测顶部边框层(优化版)
+    /// </summary>
+    private static int DetectLayerBorderTop(Image<Rgba32> image, int currentTop, int currentBottom, int currentLeft, int currentRight, byte tolerance, ref Rgba32? borderColor)
+    {
+        int newTop = currentTop;
+        Rgba32? detectedColor = null;
+
+        // 使用采样检测代替全行扫描
+        int sampleCount = Math.Min(50, currentRight - currentLeft + 1);
+        int stepX = Math.Max(1, (currentRight - currentLeft) / sampleCount);
+
+        // 从当前顶部开始向下扫描
+        for (int y = currentTop; y <= currentBottom; y++)
+        {
+            Rgba32? rowColor = null;
+            bool isUniform = true;
+
+            // 采样检查行是否统一颜色
+            for (int x = currentLeft; x <= currentRight; x += stepX)
+            {
+                if (!rowColor.HasValue)
+                {
+                    rowColor = image[x, y];
+                    continue;
+                }
+
+                if (!IsSimilarColor(image[x, y], rowColor.Value, tolerance))
+                {
+                    isUniform = false;
+                    break;
+                }
+            }
+
+            // 如果是统一颜色行
+            if (isUniform && rowColor.HasValue)
+            {
+                // 第一行总是被认为是边框
+                if (y == currentTop)
+                {
+                    detectedColor = rowColor;
+                    newTop = y + 1;
+                    continue;
+                }
+
+                // 后续行必须与第一行颜色相似
+                if (detectedColor.HasValue && IsSimilarColor(rowColor.Value, detectedColor.Value, tolerance))
+                {
+                    newTop = y + 1;
+                }
+                else
+                {
+                    break;
+                }
+            }
+            else
+            {
+                break;
+            }
+        }
+
+        if (newTop > currentTop)
+        {
+            borderColor = detectedColor;
+            return newTop;
+        }
+
+        return currentTop;
+    }
+
+    /// <summary>
+    /// 检测底部边框层(优化版)
+    /// </summary>
+    private static int DetectLayerBorderBottom(Image<Rgba32> image, int currentTop, int currentBottom, int currentLeft, int currentRight, byte tolerance, ref Rgba32? borderColor)
+    {
+        int newBottom = currentBottom;
+        Rgba32? detectedColor = null;
+
+        // 使用采样检测代替全行扫描
+        int sampleCount = Math.Min(50, currentRight - currentLeft + 1);
+        int stepX = Math.Max(1, (currentRight - currentLeft) / sampleCount);
+
+        // 从当前底部开始向上扫描
+        for (int y = currentBottom; y >= currentTop; y--)
+        {
+            Rgba32? rowColor = null;
+            bool isUniform = true;
+
+            // 采样检查行是否统一颜色
+            for (int x = currentLeft; x <= currentRight; x += stepX)
+            {
+                if (!rowColor.HasValue)
+                {
+                    rowColor = image[x, y];
+                    continue;
+                }
+
+                if (!IsSimilarColor(image[x, y], rowColor.Value, tolerance))
+                {
+                    isUniform = false;
+                    break;
+                }
+            }
+
+            if (isUniform && rowColor.HasValue)
+            {
+                if (y == currentBottom)
+                {
+                    detectedColor = rowColor;
+                    newBottom = y - 1;
+                    continue;
+                }
+
+                if (detectedColor.HasValue && IsSimilarColor(rowColor.Value, detectedColor.Value, tolerance))
+                {
+                    newBottom = y - 1;
+                }
+                else
+                {
+                    break;
+                }
+            }
+            else
+            {
+                break;
+            }
+        }
+
+        if (newBottom < currentBottom)
+        {
+            borderColor = detectedColor;
+            return newBottom;
+        }
+
+        return currentBottom;
+    }
+
+    /// <summary>
+    /// 检测左侧边框层(优化版)
+    /// </summary>
+    private static int DetectLayerBorderLeft(Image<Rgba32> image, int currentTop, int currentBottom, int currentLeft, int currentRight, byte tolerance, ref Rgba32? borderColor)
+    {
+        int newLeft = currentLeft;
+        Rgba32? detectedColor = null;
+
+        // 使用采样检测代替全列扫描
+        int sampleCount = Math.Min(50, currentBottom - currentTop + 1);
+        int stepY = Math.Max(1, (currentBottom - currentTop) / sampleCount);
+
+        // 从当前左侧开始向右扫描
+        for (int x = currentLeft; x <= currentRight; x++)
+        {
+            Rgba32? colColor = null;
+            bool isUniform = true;
+
+            // 采样检查列是否统一颜色
+            for (int y = currentTop; y <= currentBottom; y += stepY)
+            {
+                if (!colColor.HasValue)
+                {
+                    colColor = image[x, y];
+                    continue;
+                }
+
+                if (!IsSimilarColor(image[x, y], colColor.Value, tolerance))
+                {
+                    isUniform = false;
+                    break;
+                }
+            }
+
+            if (isUniform && colColor.HasValue)
+            {
+                if (x == currentLeft)
+                {
+                    detectedColor = colColor;
+                    newLeft = x + 1;
+                    continue;
+                }
+
+                if (detectedColor.HasValue && IsSimilarColor(colColor.Value, detectedColor.Value, tolerance))
+                {
+                    newLeft = x + 1;
+                }
+                else
+                {
+                    break;
+                }
+            }
+            else
+            {
+                break;
+            }
+        }
+
+        if (newLeft > currentLeft)
+        {
+            borderColor = detectedColor;
+            return newLeft;
+        }
+
+        return currentLeft;
+    }
+
+    /// <summary>
+    /// 检测右侧边框层(优化版)
+    /// </summary>
+    private static int DetectLayerBorderRight(Image<Rgba32> image, int currentTop, int currentBottom, int currentLeft, int currentRight, byte tolerance, ref Rgba32? borderColor)
+    {
+        int newRight = currentRight;
+        Rgba32? detectedColor = null;
+
+        // 使用采样检测代替全列扫描
+        int sampleCount = Math.Min(50, currentBottom - currentTop + 1);
+        int stepY = Math.Max(1, (currentBottom - currentTop) / sampleCount);
+
+        // 从当前右侧开始向左扫描
+        for (int x = currentRight; x >= currentLeft; x--)
+        {
+            Rgba32? colColor = null;
+            bool isUniform = true;
+
+            // 采样检查列是否统一颜色
+            for (int y = currentTop; y <= currentBottom; y += stepY)
+            {
+                if (!colColor.HasValue)
+                {
+                    colColor = image[x, y];
+                    continue;
+                }
+
+                if (!IsSimilarColor(image[x, y], colColor.Value, tolerance))
+                {
+                    isUniform = false;
+                    break;
+                }
+            }
+
+            if (isUniform && colColor.HasValue)
+            {
+                if (x == currentRight)
+                {
+                    detectedColor = colColor;
+                    newRight = x - 1;
+                    continue;
+                }
+
+                if (detectedColor.HasValue && IsSimilarColor(colColor.Value, detectedColor.Value, tolerance))
+                {
+                    newRight = x - 1;
+                }
+                else
+                {
+                    break;
+                }
+            }
+            else
+            {
+                break;
+            }
+        }
+
+        if (newRight < currentRight)
+        {
+            borderColor = detectedColor;
+            return newRight;
+        }
+
+        return currentRight;
+    }
+
+    /// <summary>
+    /// 颜色相似度比较(SIMD优化)
+    /// </summary>
+    private static bool IsSimilarColor(Rgba32 color1, Rgba32 color2, byte tolerance)
+    {
+        // 使用快速比较算法
+        int diffR = Math.Abs(color1.R - color2.R);
+        int diffG = Math.Abs(color1.G - color2.G);
+        int diffB = Math.Abs(color1.B - color2.B);
+
+        // 快速路径:如果任一通道差异超过容差
+        if (diffR > tolerance || diffG > tolerance || diffB > tolerance)
+            return false;
+
+        // 精确比较
+        return diffR <= tolerance && diffG <= tolerance && diffB <= tolerance;
+    }
+}
+
+/// <summary>
+/// 边框检测结果(包含多层边框信息)
+/// </summary>
+public struct BorderDetectionResult
+{
+    /// <summary>原始图片宽度</summary>
+    public int ImageWidth { get; set; }
+
+    /// <summary>原始图片高度</summary>
+    public int ImageHeight { get; set; }
+
+    /// <summary>内容上边界位置</summary>
+    public int ContentTop { get; set; }
+
+    /// <summary>内容下边界位置</summary>
+    public int ContentBottom { get; set; }
+
+    /// <summary>内容左边界位置</summary>
+    public int ContentLeft { get; set; }
+
+    /// <summary>内容右边界位置</summary>
+    public int ContentRight { get; set; }
+
+    /// <summary>检测到的边框层数</summary>
+    public int BorderLayers { get; set; }
+
+    /// <summary>边框颜色层次(从外到内)</summary>
+    public List<Rgba32> BorderColors { get; set; }
+
+    /// <summary>顶部边框总宽度(像素)</summary>
+    public int TopBorderWidth => ContentTop;
+
+    /// <summary>底部边框总宽度(像素)</summary>
+    public int BottomBorderWidth => ImageHeight - 1 - ContentBottom;
+
+    /// <summary>左侧边框总宽度(像素)</summary>
+    public int LeftBorderWidth => ContentLeft;
+
+    /// <summary>右侧边框总宽度(像素)</summary>
+    public int RightBorderWidth => ImageWidth - 1 - ContentRight;
+
+    /// <summary>是否有顶部边框</summary>
+    public bool HasTopBorder => TopBorderWidth > 0;
+
+    /// <summary>是否有底部边框</summary>
+    public bool HasBottomBorder => BottomBorderWidth > 0;
+
+    /// <summary>是否有左侧边框</summary>
+    public bool HasLeftBorder => LeftBorderWidth > 0;
+
+    /// <summary>是否有右侧边框</summary>
+    public bool HasRightBorder => RightBorderWidth > 0;
+
+    /// <summary>是否有任意边框</summary>
+    public bool HasAnyBorder => BorderCount > 0;
+
+    /// <summary>是否满足裁剪条件(至少两个边)</summary>
+    public bool CanBeCropped => BorderCount >= 2;
+
+    public int BorderCount => (HasTopBorder ? 1 : 0) + (HasBottomBorder ? 1 : 0) + (HasLeftBorder ? 1 : 0) + (HasRightBorder ? 1 : 0);
+
+    /// <summary>内容区域宽度</summary>
+    public int ContentWidth => ContentRight - ContentLeft + 1;
+
+    /// <summary>内容区域高度</summary>
+    public int ContentHeight => ContentBottom - ContentTop + 1;
+}

+ 3 - 3
Masuit.Tools.AspNetCore/Masuit.Tools.AspNetCore.csproj

@@ -44,7 +44,7 @@
 
     <ItemGroup>
         <FrameworkReference Include="Microsoft.AspNetCore.App" />
-        <PackageReference Include="FastExpressionCompiler" Version="5.1.1" />
+        <PackageReference Include="FastExpressionCompiler" Version="5.3.0" />
     </ItemGroup>
 
     <ItemGroup>
@@ -55,12 +55,12 @@
         <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[3.1.32]" />
     </ItemGroup>
     <ItemGroup Condition=" '$(TargetFramework)' == 'net9'">
-        <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.4" />
+        <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.6" />
     </ItemGroup>
     <ItemGroup Condition=" '$(TargetFramework)' == 'net6'">
         <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.36" />
     </ItemGroup>
     <ItemGroup Condition=" '$(TargetFramework)' == 'net8'">
-        <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.15" />
+        <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.17" />
     </ItemGroup>
 </Project>

+ 2 - 2
Masuit.Tools.Core/Masuit.Tools.Core.csproj

@@ -45,13 +45,13 @@ github:https://github.com/ldqk/Masuit.Tools
         <PackageReference Include="Microsoft.EntityFrameworkCore" Version="[3.1.32]" />
     </ItemGroup>
     <ItemGroup Condition=" '$(TargetFramework)' == 'net9'">
-        <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
+        <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
     </ItemGroup>
     <ItemGroup Condition=" '$(TargetFramework)' == 'net6'">
         <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.36" />
     </ItemGroup>
     <ItemGroup Condition=" '$(TargetFramework)' == 'net8'">
-        <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.15" />
+        <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.17" />
     </ItemGroup>
     <ItemGroup>
       <Compile Remove="..\Masuit.Tools.Abstractions\Mapping\**" />

+ 1 - 1
Masuit.Tools.Excel/Masuit.Tools.Excel.csproj

@@ -37,7 +37,7 @@
       </None>
     </ItemGroup>
     <ItemGroup>
-        <PackageReference Include="EPPlus" Version="8.0.3" />
+        <PackageReference Include="EPPlus" Version="8.0.5" />
         <PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
     </ItemGroup>
     <ItemGroup>

+ 1 - 1
Masuit.Tools/Masuit.Tools.Net.csproj

@@ -35,7 +35,7 @@
 
     <ItemGroup>
         <PackageReference Include="Microsoft.AspNet.Mvc" Version="5.3.0" />
-        <PackageReference Include="StackExchange.Redis" Version="2.8.37" />
+        <PackageReference Include="StackExchange.Redis" Version="2.8.41" />
         <ProjectReference Include="..\Masuit.Tools.Abstractions\Masuit.Tools.Abstractions.csproj" />
         <Reference Include="System.Web" />
     </ItemGroup>

+ 1 - 1
NetCoreTest/NetCoreTest.csproj

@@ -6,7 +6,7 @@
     <ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
+    <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" />
   </ItemGroup>
   <ItemGroup>
     <Folder Include="Controllers\" />

+ 3 - 0
README.md

@@ -745,6 +745,9 @@ gif.GetFrames(@"D:\frames\"); // 解压gif每帧图片
 var marker=ImageWatermarker(stream);
 stream=maker.AddWatermark("水印文字","字体文件",字体大小,color,水印位置,边距); // 给图片添加水印
 stream=maker.AddWatermark(水印图片,水印位置,边距,字体大小,字体); // 给图片添加水印
+
+var borderInfo=ImageBorderRemover.DetectBorders(原始图片); // 检测图片是否包含纯色边框
+ImageBorderRemover.RemoveBorders(原始图片,保存图片); // 移除图片的纯色边框并另存为
 ```
 ```csharp
 // 图像相似度对比

+ 2 - 2
Test/Masuit.Tools.Abstractions.Test/Masuit.Tools.Abstractions.Test.csproj

@@ -13,10 +13,10 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
     <PackageReference Include="Moq" Version="4.20.72" />
     <PackageReference Include="xunit" Version="2.9.3" />
-    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.0">
+    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
     </PackageReference>

+ 1 - 1
Test/Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.csproj

@@ -23,7 +23,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.1" />
+    <PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.1" />
   </ItemGroup>
 
   <ItemGroup>

+ 5 - 5
Test/Masuit.Tools.Core.Test/Masuit.Tools.Core.Test.csproj

@@ -9,12 +9,12 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="9.0.4" />
-    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.4" />
-    <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
+    <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="9.0.6" />
+    <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.6" />
+    <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
     <PackageReference Include="xunit" Version="2.9.3" />
-    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.0">
+    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
     </PackageReference>

+ 2 - 2
Test/Masuit.Tools.Test/Masuit.Tools.Test.csproj

@@ -94,7 +94,7 @@
       <Version>4.20.72</Version>
     </PackageReference>
     <PackageReference Include="StackExchange.Redis">
-      <Version>2.8.37</Version>
+      <Version>2.8.41</Version>
     </PackageReference>
     <PackageReference Include="System.Runtime.CompilerServices.Unsafe">
       <Version>6.1.2</Version>
@@ -109,7 +109,7 @@
       <Version>2.0.3</Version>
     </PackageReference>
     <PackageReference Include="xunit.analyzers">
-      <Version>1.21.0</Version>
+      <Version>1.22.0</Version>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <PrivateAssets>all</PrivateAssets>
     </PackageReference>