Refactor media source handling and update collection options
- Updated `IMediaSourceCatalog` to support user-added media sources, allowing dynamic editing and management of sources. - Removed the `UrlListSource` class as its functionality is now integrated into the new catalog structure. - Enhanced `CollectOptions` to default `RequireProxy` to true, ensuring stricter handling of proxy requirements. - Improved error handling in `ParseError` to include a `Subject` field for better context on failures. - Adjusted dependency injection to reflect changes in media source management, removing old source registrations. - Introduced background proxy checks to ensure a more robust proxy pool management during collection processes. These changes streamline the media collection process and improve the overall user experience by providing clearer error reporting and more flexible source management.
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace AvParser.ImgTestService;
|
||||
|
||||
/// <summary>
|
||||
/// Renders a deterministic identicon PNG from an id, with a hand-written encoder.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// No image library and no native dependency on purpose: the container stays tiny and runs the same
|
||||
/// on any architecture. The same id always yields the same bytes, so a collector that fetches an id
|
||||
/// twice gets one blob, and a duplicate-detection test has something stable to assert against.
|
||||
/// </remarks>
|
||||
public static class Identicon
|
||||
{
|
||||
private const int Cells = 5;
|
||||
private const int CellSize = 48;
|
||||
private const int Margin = 20;
|
||||
private const int Size = (Cells * CellSize) + (2 * Margin);
|
||||
|
||||
/// <summary>Builds the PNG bytes for an id.</summary>
|
||||
public static byte[] Render(string id)
|
||||
{
|
||||
var hash = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(id));
|
||||
|
||||
// A saturated foreground from the first bytes, on a near-white background.
|
||||
var (fr, fg, fb) = Foreground(hash);
|
||||
byte br = 0xF2,
|
||||
bg = 0xF2,
|
||||
bb = 0xF4;
|
||||
|
||||
var pixels = new byte[Size * Size * 3];
|
||||
FillBackground(pixels, br, bg, bb);
|
||||
|
||||
// A 5x5 grid, mirrored left-to-right, so the icon reads as a single symmetric shape. Bit
|
||||
// source is the tail of the hash, one bit per cell in the left three columns.
|
||||
for (var col = 0; col < (Cells + 1) / 2; col++)
|
||||
{
|
||||
for (var row = 0; row < Cells; row++)
|
||||
{
|
||||
var bit = hash[(col * Cells) + row] & 1;
|
||||
if (bit == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PaintCell(pixels, col, row, fr, fg, fb);
|
||||
PaintCell(pixels, Cells - 1 - col, row, fr, fg, fb);
|
||||
}
|
||||
}
|
||||
|
||||
return Encode(pixels, Size, Size);
|
||||
}
|
||||
|
||||
private static (byte R, byte G, byte B) Foreground(byte[] hash)
|
||||
{
|
||||
// Pick a hue-ish colour that is never too pale: keep at least one channel low and one high.
|
||||
var r = (byte)(60 + (hash[0] % 160));
|
||||
var g = (byte)(60 + (hash[1] % 160));
|
||||
var b = (byte)(60 + (hash[2] % 160));
|
||||
return (r, g, b);
|
||||
}
|
||||
|
||||
private static void FillBackground(byte[] pixels, byte r, byte g, byte b)
|
||||
{
|
||||
for (var i = 0; i < pixels.Length; i += 3)
|
||||
{
|
||||
pixels[i] = r;
|
||||
pixels[i + 1] = g;
|
||||
pixels[i + 2] = b;
|
||||
}
|
||||
}
|
||||
|
||||
private static void PaintCell(byte[] pixels, int col, int row, byte r, byte g, byte b)
|
||||
{
|
||||
var x0 = Margin + (col * CellSize);
|
||||
var y0 = Margin + (row * CellSize);
|
||||
|
||||
for (var y = y0; y < y0 + CellSize; y++)
|
||||
{
|
||||
var rowStart = y * Size * 3;
|
||||
for (var x = x0; x < x0 + CellSize; x++)
|
||||
{
|
||||
var i = rowStart + (x * 3);
|
||||
pixels[i] = r;
|
||||
pixels[i + 1] = g;
|
||||
pixels[i + 2] = b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Encodes RGB pixels as a PNG (colour type 2, 8-bit).</summary>
|
||||
private static byte[] Encode(byte[] rgb, int width, int height)
|
||||
{
|
||||
using var output = new MemoryStream();
|
||||
|
||||
// Signature.
|
||||
output.Write([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
|
||||
|
||||
Span<byte> ihdr = stackalloc byte[13];
|
||||
WriteBigEndian(ihdr[..4], (uint)width);
|
||||
WriteBigEndian(ihdr.Slice(4, 4), (uint)height);
|
||||
ihdr[8] = 8; // bit depth
|
||||
ihdr[9] = 2; // colour type: truecolour RGB
|
||||
ihdr[10] = 0; // compression
|
||||
ihdr[11] = 0; // filter
|
||||
ihdr[12] = 0; // interlace
|
||||
WriteChunk(output, "IHDR"u8, ihdr);
|
||||
|
||||
// Raw scanlines: a leading filter byte (0 = none) then the row's RGB bytes.
|
||||
var stride = width * 3;
|
||||
var raw = new byte[height * (stride + 1)];
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
var src = y * stride;
|
||||
var dst = y * (stride + 1);
|
||||
raw[dst] = 0;
|
||||
Array.Copy(rgb, src, raw, dst + 1, stride);
|
||||
}
|
||||
|
||||
using (var compressed = new MemoryStream())
|
||||
{
|
||||
using (var zlib = new ZLibStream(compressed, CompressionLevel.Optimal, leaveOpen: true))
|
||||
{
|
||||
zlib.Write(raw, 0, raw.Length);
|
||||
}
|
||||
|
||||
WriteChunk(output, "IDAT"u8, compressed.ToArray());
|
||||
}
|
||||
|
||||
WriteChunk(output, "IEND"u8, []);
|
||||
|
||||
return output.ToArray();
|
||||
}
|
||||
|
||||
private static void WriteChunk(Stream stream, ReadOnlySpan<byte> type, ReadOnlySpan<byte> data)
|
||||
{
|
||||
Span<byte> length = stackalloc byte[4];
|
||||
WriteBigEndian(length, (uint)data.Length);
|
||||
stream.Write(length);
|
||||
stream.Write(type);
|
||||
stream.Write(data);
|
||||
|
||||
var crc = Crc32.Compute(type, data);
|
||||
Span<byte> crcBytes = stackalloc byte[4];
|
||||
WriteBigEndian(crcBytes, crc);
|
||||
stream.Write(crcBytes);
|
||||
}
|
||||
|
||||
private static void WriteBigEndian(Span<byte> destination, uint value)
|
||||
{
|
||||
destination[0] = (byte)(value >> 24);
|
||||
destination[1] = (byte)(value >> 16);
|
||||
destination[2] = (byte)(value >> 8);
|
||||
destination[3] = (byte)value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Minimal CRC-32 (PNG polynomial), enough to seal chunks.</summary>
|
||||
internal static class Crc32
|
||||
{
|
||||
private static readonly uint[] Table = BuildTable();
|
||||
|
||||
public static uint Compute(ReadOnlySpan<byte> type, ReadOnlySpan<byte> data)
|
||||
{
|
||||
var crc = 0xFFFFFFFFu;
|
||||
crc = Update(crc, type);
|
||||
crc = Update(crc, data);
|
||||
return crc ^ 0xFFFFFFFFu;
|
||||
}
|
||||
|
||||
private static uint Update(uint crc, ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
foreach (var b in bytes)
|
||||
{
|
||||
crc = Table[(crc ^ b) & 0xFF] ^ (crc >> 8);
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
private static uint[] BuildTable()
|
||||
{
|
||||
var table = new uint[256];
|
||||
for (var n = 0u; n < 256; n++)
|
||||
{
|
||||
var c = n;
|
||||
for (var k = 0; k < 8; k++)
|
||||
{
|
||||
c = (c & 1) != 0 ? 0xEDB88320u ^ (c >> 1) : c >> 1;
|
||||
}
|
||||
|
||||
table[n] = c;
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user