using System;
using System.Buffers;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Dpz.Core.Entity.Base.Image;

namespace Dpz.Core.Infrastructure.Imaging;

/// <summary>
/// 基于 magic-byte (文件头特征字节) 的图片格式识别器。
/// 支持 jpg / png / gif / webp / bmp / tiff / tga / heic / avif / ico 等常见格式。
/// </summary>
public sealed class MagicNumberImageFormatDetector : IImageFormatDetector
{
    /// <summary>
    /// 单例实例,无状态可复用。
    /// </summary>
    public static readonly Lazy<MagicNumberImageFormatDetector> Instance = new(() =>
        new MagicNumberImageFormatDetector()
    );

    // 二进制格式通常 12 字节足够;SVG 可能带 XML 声明或注释,因此多读一点文本头。
    private const int HeaderSize = 512;

    /// <inheritdoc />
    public ImageFormat Detect(ReadOnlySpan<byte> bytes)
    {
        switch (bytes.Length)
        {
            case < 2:
                return ImageFormat.Unknown;
            // JPEG: FF D8 FF
            case >= 3 when bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF:
                return ImageFormat.Jpeg;
            // PNG: 89 50 4E 47 0D 0A 1A 0A
            case >= 8
                when bytes[0] == 0x89
                    && bytes[1] == 0x50
                    && bytes[2] == 0x4E
                    && bytes[3] == 0x47
                    && bytes[4] == 0x0D
                    && bytes[5] == 0x0A
                    && bytes[6] == 0x1A
                    && bytes[7] == 0x0A:
                return ImageFormat.Png;
            // GIF: "GIF87a" or "GIF89a"
            case >= 6
                when bytes[0] == 0x47
                    && bytes[1] == 0x49
                    && bytes[2] == 0x46
                    && bytes[3] == 0x38
                    && (bytes[4] == 0x37 || bytes[4] == 0x39)
                    && bytes[5] == 0x61:
                return ImageFormat.Gif;
        }

        // BMP: "BM"
        if (bytes[0] == 0x42 && bytes[1] == 0x4D)
        {
            return ImageFormat.Bmp;
        }

        // RIFF container: "RIFF" .... "WEBP"
        if (
            bytes.Length >= 12
            && bytes[0] == 0x52
            && bytes[1] == 0x49
            && bytes[2] == 0x46
            && bytes[3] == 0x46
            && bytes[8] == 0x57
            && bytes[9] == 0x45
            && bytes[10] == 0x42
            && bytes[11] == 0x50
        )
        {
            return ImageFormat.Webp;
        }

        // TIFF: "II*\0" (little-endian) or "MM\0*" (big-endian)
        if (bytes.Length >= 4)
        {
            if (bytes[0] == 0x49 && bytes[1] == 0x49 && bytes[2] == 0x2A && bytes[3] == 0x00)
            {
                return ImageFormat.Tiff;
            }
            if (bytes[0] == 0x4D && bytes[1] == 0x4D && bytes[2] == 0x00 && bytes[3] == 0x2A)
            {
                return ImageFormat.Tiff;
            }
        }

        // ISO BMFF (heic/avif): bytes 4..7 = "ftyp", bytes 8..11 = brand
        if (
            bytes.Length >= 12
            && bytes[4] == 0x66
            && bytes[5] == 0x74
            && bytes[6] == 0x79
            && bytes[7] == 0x70
        )
        {
            // heic / heix / hevc / hevx / mif1 / msf1 -> HEIC family
            // avif / avis -> AVIF family
            var brand = (bytes[8], bytes[9], bytes[10], bytes[11]);
            switch (brand)
            {
                // msf1
                case
                (0x68, 0x65, 0x69, 0x63) // heic
                or
                (0x68, 0x65, 0x69, 0x78) // heix
                or
                (0x68, 0x65, 0x76, 0x63) // hevc
                or
                (0x68, 0x65, 0x76, 0x78) // hevx
                or
                (0x6D, 0x69, 0x66, 0x31) // mif1
                or (0x6D, 0x73, 0x66, 0x31):
                    return ImageFormat.Heic;
                case (0x61, 0x76, 0x69, 0x66) or (0x61, 0x76, 0x69, 0x73):
                    return ImageFormat.Avif;
            }
        }

        // ICO: 00 00 01 00
        if (
            bytes.Length >= 4
            && bytes[0] == 0x00
            && bytes[1] == 0x00
            && bytes[2] == 0x01
            && bytes[3] == 0x00
        )
        {
            return ImageFormat.Ico;
        }

        if (LooksLikeSvg(bytes))
        {
            return ImageFormat.Svg;
        }

        return ImageFormat.Unknown;
    }

    /// <inheritdoc />
    public async ValueTask<ImageFormat> DetectAsync(
        Stream stream,
        CancellationToken cancellationToken = default
    )
    {
        ArgumentNullException.ThrowIfNull(stream);

        var buffer = ArrayPool<byte>.Shared.Rent(HeaderSize);
        try
        {
            long? originalPosition = null;
            if (stream.CanSeek)
            {
                originalPosition = stream.Position;
            }

            var read = 0;
            while (read < HeaderSize)
            {
                var n = await stream
                    .ReadAsync(buffer.AsMemory(read, HeaderSize - read), cancellationToken)
                    .ConfigureAwait(false);
                if (n == 0)
                {
                    break;
                }
                read += n;
            }

            if (originalPosition.HasValue)
            {
                stream.Position = originalPosition.Value;
            }

            return Detect(buffer.AsSpan(0, read));
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(buffer);
        }
    }

    private static bool LooksLikeSvg(ReadOnlySpan<byte> bytes)
    {
        if (bytes.Length < 4)
        {
            return false;
        }

        var sampleLength = Math.Min(bytes.Length, HeaderSize);
        var text = Encoding.UTF8.GetString(bytes[..sampleLength]).TrimStart('\uFEFF').TrimStart();
        if (text.Length == 0)
        {
            return false;
        }

        return StartsWithSvgElement(SkipXmlPreamble(text));
    }

    private static bool StartsWithSvgElement(string text)
    {
        if (!text.StartsWith("<svg", StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        return text.Length == 4 || char.IsWhiteSpace(text[4]) || text[4] is '>' or '/';
    }

    private static string SkipXmlPreamble(string text)
    {
        while (text.Length > 0)
        {
            if (text.StartsWith("<?", StringComparison.Ordinal))
            {
                var end = text.IndexOf("?>", StringComparison.Ordinal);
                if (end < 0)
                {
                    return text;
                }
                text = text[(end + 2)..].TrimStart();
                continue;
            }

            if (text.StartsWith("<!--", StringComparison.Ordinal))
            {
                var end = text.IndexOf("-->", StringComparison.Ordinal);
                if (end < 0)
                {
                    return text;
                }
                text = text[(end + 3)..].TrimStart();
                continue;
            }

            if (text.StartsWith("<!DOCTYPE", StringComparison.OrdinalIgnoreCase))
            {
                var end = text.IndexOf('>');
                if (end < 0)
                {
                    return text;
                }
                text = text[(end + 1)..].TrimStart();
                continue;
            }

            return text;
        }

        return text;
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个专业的图像格式检测器类,基于文件头的"魔术字节"(magic bytes)来识别不同的图像格式。让我详细解释这个代码:

主要功能

这个类实现了 IImageFormatDetector 接口,用于识别各种常见的图像格式,包括:

  • JPEG, PNG, GIF, WebP, BMP, TIFF
  • 现代格式:HEIC, AVIF, ICO
  • 矢量格式:SVG

核心设计特点

1. 单例模式

public static readonly Lazy<MagicNumberImageFormatDetector> Instance = new(() =>
    new MagicNumberImageFormatDetector()
);

使用懒加载单例,因为这个检测器是无状态的,可以安全复用。

2. 魔术字节检测原理

每种图像格式都有特定的文件头标识:

  • JPEG: FF D8 FF
  • PNG: 89 50 4E 47 0D 0A 1A 0A ("PNG"的变体)
  • GIF: 47 49 46 38 37/39 61 ("GIF87a"或"GIF89a")
  • BMP: 42 4D ("BM")
  • WebP: RIFF容器格式,包含"RIFF"和"WEBP"标识

关键方法分析

3. 同步检测方法 Detect

使用 switch 表达式和模式匹配,高效地检测不同格式:

case >= 3 when bytes[0] == 0xFF && bytes[1] == 0xD8 && bytes[2] == 0xFF:
    return ImageFormat.Jpeg;

这种写法既检查了长度又验证了特定字节序列。

4. 现代格式支持

对于 HEIC/AVIF 等基于 ISO BMFF 容器的格式:

// 检测 "ftyp" 标识符,然后根据 brand 确定具体格式
var brand = (bytes[8], bytes[9], bytes[10], bytes[11]);

5. 异步流处理 DetectAsync

  • 使用 ArrayPool<byte>.Shared 避免频繁内存分配
  • 支持可定位流的位置恢复
  • 正确处理取消令牌
  • 循环读取确保获得足够的头部数据

6. SVG 特殊处理

SVG 是文本格式,需要特殊处理:

  • 跳过 UTF-8 BOM 标记
  • 处理 XML 声明 (<?xml ... ?>)
  • 处理注释 (<!-- ... -->)
  • 处理 DOCTYPE 声明
  • 最终检查是否以 <svg 开头

性能优化亮点

  1. 最小读取量: 只读取 512 字节的文件头,对大文件很高效
  2. 短路求值: 按照常见程度排序检测条件
  3. 内存池: 异步方法使用 ArrayPool 减少 GC 压力
  4. 零分配: 同步方法使用 ReadOnlySpan<byte> 避免不必要的内存分配

错误处理

  • 对空流参数抛出 ArgumentNullException
  • 优雅处理流读取异常
  • 确保资源正确释放(try-finally 模式)

这个实现展现了现代 C# 的最佳实践,包括性能优化、内存管理和异步编程模式。

评论加载中...