Files
DCIT/DCIT.Core/Services/BmpDiffCodec.cs
2026-07-13 10:04:36 +08:00

241 lines
8.8 KiB
C#

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using DCIT.Core.Utilities;
namespace DCIT.Core.Services
{
public class BmpDiffCodec
{
private const string Magic = "DCITBMP1";
private const ushort HeaderVersion = 1;
private const ushort Flags = 0;
private const int FixedWidth = 1024;
private const int HeaderSize = 8 + 2 + 2 + 8 + 32;
public void EncodeJson(string canonicalJson, string outputPath)
{
var payloadBytes = Encoding.UTF8.GetBytes(canonicalJson ?? string.Empty);
using (var stream = new MemoryStream(payloadBytes))
{
EncodeFromStream(stream, outputPath);
}
}
public void EncodeFromStream(Stream jsonStream, string outputPath)
{
if (jsonStream == null)
{
throw new ArgumentNullException("jsonStream");
}
byte[] hash;
long length;
if (jsonStream.CanSeek)
{
length = jsonStream.Length;
using (var sha = SHA256.Create())
{
hash = sha.ComputeHash(jsonStream);
}
jsonStream.Position = 0;
}
else
{
using (var buffered = new MemoryStream())
{
jsonStream.CopyTo(buffered);
length = buffered.Length;
using (var sha = SHA256.Create())
{
hash = sha.ComputeHash(buffered);
}
buffered.Position = 0;
WriteBitmapWithPayload(outputPath, length, hash, buffered);
return;
}
}
WriteBitmapWithPayload(outputPath, length, hash, jsonStream);
}
public string DecodeJson(string bmpPath)
{
using (var bitmap = new Bitmap(bmpPath))
{
var rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
var bitmapData = bitmap.LockBits(rect, ImageLockMode.ReadOnly, bitmap.PixelFormat);
try
{
var rowLength = bitmap.Width;
var pixelBytes = new byte[rowLength * bitmap.Height];
for (var row = 0; row < bitmap.Height; row++)
{
var source = IntPtr.Add(bitmapData.Scan0, row * bitmapData.Stride);
Marshal.Copy(source, pixelBytes, row * rowLength, rowLength);
}
var payloadBytes = ParseBlob(pixelBytes);
return Encoding.UTF8.GetString(payloadBytes);
}
finally
{
bitmap.UnlockBits(bitmapData);
}
}
}
private static void WriteBitmapWithPayload(string outputPath, long payloadLength, byte[] payloadHash, Stream payloadStream)
{
var totalBytes = HeaderSize + payloadLength;
var height = Math.Max(1, (int)Math.Ceiling(totalBytes / (double)FixedWidth));
var pixelCount = FixedWidth * height;
var pixelBytes = new byte[pixelCount];
var offset = 0;
var magicBytes = Encoding.ASCII.GetBytes(Magic);
Buffer.BlockCopy(magicBytes, 0, pixelBytes, offset, magicBytes.Length);
offset += magicBytes.Length;
pixelBytes[offset++] = (byte)(HeaderVersion & 0xFF);
pixelBytes[offset++] = (byte)((HeaderVersion >> 8) & 0xFF);
pixelBytes[offset++] = (byte)(Flags & 0xFF);
pixelBytes[offset++] = (byte)((Flags >> 8) & 0xFF);
pixelBytes[offset++] = (byte)(payloadLength & 0xFF);
pixelBytes[offset++] = (byte)((payloadLength >> 8) & 0xFF);
pixelBytes[offset++] = (byte)((payloadLength >> 16) & 0xFF);
pixelBytes[offset++] = (byte)((payloadLength >> 24) & 0xFF);
pixelBytes[offset++] = (byte)((payloadLength >> 32) & 0xFF);
pixelBytes[offset++] = (byte)((payloadLength >> 40) & 0xFF);
pixelBytes[offset++] = (byte)((payloadLength >> 48) & 0xFF);
pixelBytes[offset++] = (byte)((payloadLength >> 56) & 0xFF);
Buffer.BlockCopy(payloadHash, 0, pixelBytes, offset, 32);
offset += 32;
var buffer = new byte[65536];
int read;
while ((read = payloadStream.Read(buffer, 0, buffer.Length)) > 0)
{
if (offset + read > pixelBytes.Length)
{
throw new InvalidDataException("BMP 载荷长度超出预期像素缓冲。");
}
Buffer.BlockCopy(buffer, 0, pixelBytes, offset, read);
offset += read;
}
if (offset != totalBytes)
{
throw new InvalidDataException("BMP 载荷长度与声明的长度不一致。");
}
using (var bitmap = new Bitmap(FixedWidth, height, PixelFormat.Format8bppIndexed))
{
var palette = bitmap.Palette;
for (var index = 0; index < 256; index++)
{
palette.Entries[index] = Color.FromArgb(index, index, index);
}
bitmap.Palette = palette;
var rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
var bitmapData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format8bppIndexed);
try
{
for (var row = 0; row < height; row++)
{
var sourceOffset = row * FixedWidth;
var destination = IntPtr.Add(bitmapData.Scan0, row * bitmapData.Stride);
Marshal.Copy(pixelBytes, sourceOffset, destination, FixedWidth);
}
}
finally
{
bitmap.UnlockBits(bitmapData);
}
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? string.Empty);
bitmap.Save(outputPath, ImageFormat.Bmp);
}
}
private static byte[] BuildBlob(byte[] payloadBytes)
{
payloadBytes = payloadBytes ?? new byte[0];
var payloadHash = Convert.FromBase64String(HashUtility.ComputeSha256Base64(payloadBytes));
using (var stream = new MemoryStream())
using (var writer = new BinaryWriter(stream, Encoding.ASCII))
{
writer.Write(Encoding.ASCII.GetBytes(Magic));
writer.Write(HeaderVersion);
writer.Write(Flags);
writer.Write((ulong)payloadBytes.Length);
writer.Write(payloadHash);
writer.Write(payloadBytes);
writer.Flush();
return stream.ToArray();
}
}
private static byte[] ParseBlob(byte[] blobBytes)
{
using (var stream = new MemoryStream(blobBytes))
using (var reader = new BinaryReader(stream, Encoding.ASCII))
{
var magic = Encoding.ASCII.GetString(reader.ReadBytes(8));
if (!string.Equals(magic, Magic, StringComparison.Ordinal))
{
throw new InvalidDataException("BMP 差异文件魔数无效。");
}
var version = reader.ReadUInt16();
if (version != HeaderVersion)
{
throw new InvalidDataException("BMP 差异文件版本不受支持。");
}
_ = reader.ReadUInt16();
var payloadLength = reader.ReadUInt64();
var payloadHash = reader.ReadBytes(32);
var payloadBytes = reader.ReadBytes((int)payloadLength);
var actualHash = Convert.FromBase64String(HashUtility.ComputeSha256Base64(payloadBytes));
if (!BytesEqual(payloadHash, actualHash))
{
throw new InvalidDataException("BMP 差异文件载荷校验失败。");
}
return payloadBytes;
}
}
private static bool BytesEqual(byte[] left, byte[] right)
{
if (left == null || right == null || left.Length != right.Length)
{
return false;
}
for (var index = 0; index < left.Length; index++)
{
if (left[index] != right[index])
{
return false;
}
}
return true;
}
}
}