Этап 3

This commit is contained in:
2026-03-29 15:41:42 +03:00
parent 08b9039b58
commit cc5669a00a
2 changed files with 399 additions and 7 deletions

View File

@@ -0,0 +1,267 @@
using System.Text;
using Minint.Core.Models;
using Minint.Core.Services;
namespace Minint.Infrastructure.Serialization;
/// <summary>
/// Self-implemented binary reader/writer for the .minint container format.
/// All multi-byte integers are little-endian. Strings are UTF-8 with a 1-byte length prefix.
/// </summary>
public sealed class MinintSerializer : IMinintSerializer
{
private static readonly byte[] Signature = "MININT"u8.ToArray();
private const ushort CurrentVersion = 1;
private const int ReservedBytes = 8;
private const int MaxNameLength = 255;
#region Write
public void Write(Stream stream, MinintContainer container)
{
ArgumentNullException.ThrowIfNull(stream);
ArgumentNullException.ThrowIfNull(container);
using var w = new BinaryWriter(stream, Encoding.UTF8, leaveOpen: true);
WriteHeader(w, container);
foreach (var doc in container.Documents)
WriteDocument(w, doc, container.Width, container.Height);
}
private static void WriteHeader(BinaryWriter w, MinintContainer c)
{
w.Write(Signature);
w.Write(CurrentVersion);
w.Write((uint)c.Width);
w.Write((uint)c.Height);
w.Write((uint)c.Documents.Count);
w.Write(new byte[ReservedBytes]);
}
private static void WriteDocument(BinaryWriter w, MinintDocument doc, int width, int height)
{
WritePrefixedString(w, doc.Name);
w.Write(doc.FrameDelayMs);
w.Write((uint)doc.Palette.Count);
foreach (var color in doc.Palette)
{
w.Write(color.R);
w.Write(color.G);
w.Write(color.B);
w.Write(color.A);
}
w.Write((uint)doc.Layers.Count);
int byteWidth = doc.IndexByteWidth;
foreach (var layer in doc.Layers)
WriteLayer(w, layer, byteWidth, width * height);
}
private static void WriteLayer(BinaryWriter w, MinintLayer layer, int byteWidth, int pixelCount)
{
WritePrefixedString(w, layer.Name);
w.Write(layer.IsVisible ? (byte)1 : (byte)0);
w.Write(layer.Opacity);
if (layer.Pixels.Length != pixelCount)
throw new InvalidOperationException(
$"Layer '{layer.Name}' has {layer.Pixels.Length} pixels, expected {pixelCount}.");
for (int i = 0; i < pixelCount; i++)
WriteIndex(w, layer.Pixels[i], byteWidth);
}
private static void WriteIndex(BinaryWriter w, int index, int byteWidth)
{
switch (byteWidth)
{
case 1:
w.Write((byte)index);
break;
case 2:
w.Write((ushort)index);
break;
case 3:
w.Write((byte)(index & 0xFF));
w.Write((byte)((index >> 8) & 0xFF));
w.Write((byte)((index >> 16) & 0xFF));
break;
case 4:
w.Write(index);
break;
}
}
private static void WritePrefixedString(BinaryWriter w, string value)
{
var bytes = Encoding.UTF8.GetBytes(value);
if (bytes.Length > MaxNameLength)
throw new InvalidOperationException(
$"String '{value}' exceeds max length of {MaxNameLength} UTF-8 bytes.");
w.Write((byte)bytes.Length);
w.Write(bytes);
}
#endregion
#region Read
public MinintContainer Read(Stream stream)
{
ArgumentNullException.ThrowIfNull(stream);
using var r = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
var (width, height, docCount) = ReadHeader(r);
var container = new MinintContainer(width, height);
for (int i = 0; i < docCount; i++)
container.Documents.Add(ReadDocument(r, width, height));
return container;
}
private static (int Width, int Height, int DocCount) ReadHeader(BinaryReader r)
{
byte[] sig = ReadExact(r, Signature.Length, "file signature");
if (!sig.AsSpan().SequenceEqual(Signature))
throw new InvalidDataException(
"Invalid file signature. Expected 'MININT'.");
ushort version = r.ReadUInt16();
if (version != CurrentVersion)
throw new InvalidDataException(
$"Unsupported format version {version}. Only version {CurrentVersion} is supported.");
uint width = r.ReadUInt32();
uint height = r.ReadUInt32();
uint docCount = r.ReadUInt32();
if (width == 0 || height == 0)
throw new InvalidDataException("Width and height must be at least 1.");
if (width > 65_536 || height > 65_536)
throw new InvalidDataException(
$"Dimensions {width}x{height} exceed maximum supported size (65536).");
if (docCount == 0)
throw new InvalidDataException("Container must have at least 1 document.");
byte[] reserved = ReadExact(r, ReservedBytes, "reserved bytes");
for (int i = 0; i < reserved.Length; i++)
{
if (reserved[i] != 0)
break; // non-zero reserved bytes: tolerated for forward compat
}
return ((int)width, (int)height, (int)docCount);
}
private static MinintDocument ReadDocument(BinaryReader r, int width, int height)
{
string name = ReadPrefixedString(r);
uint frameDelay = r.ReadUInt32();
uint paletteCount = r.ReadUInt32();
if (paletteCount == 0)
throw new InvalidDataException("Palette must have at least 1 color.");
var palette = new List<RgbaColor>((int)paletteCount);
for (uint i = 0; i < paletteCount; i++)
{
byte cr = r.ReadByte();
byte cg = r.ReadByte();
byte cb = r.ReadByte();
byte ca = r.ReadByte();
palette.Add(new RgbaColor(cr, cg, cb, ca));
}
int byteWidth = GetIndexByteWidth((int)paletteCount);
uint layerCount = r.ReadUInt32();
if (layerCount == 0)
throw new InvalidDataException($"Document '{name}' must have at least 1 layer.");
var layers = new List<MinintLayer>((int)layerCount);
int pixelCount = width * height;
for (uint i = 0; i < layerCount; i++)
layers.Add(ReadLayer(r, byteWidth, pixelCount, (int)paletteCount));
return new MinintDocument(name, frameDelay, palette, layers);
}
private static MinintLayer ReadLayer(BinaryReader r, int byteWidth, int pixelCount, int paletteCount)
{
string name = ReadPrefixedString(r);
byte visByte = r.ReadByte();
if (visByte > 1)
throw new InvalidDataException(
$"Layer '{name}': invalid visibility flag {visByte} (expected 0 or 1).");
bool isVisible = visByte == 1;
byte opacity = r.ReadByte();
var pixels = new int[pixelCount];
for (int i = 0; i < pixelCount; i++)
{
int idx = ReadIndex(r, byteWidth);
if (idx < 0 || idx >= paletteCount)
throw new InvalidDataException(
$"Layer '{name}': pixel index {idx} at position {i} is out of palette range [0, {paletteCount}).");
pixels[i] = idx;
}
return new MinintLayer(name, isVisible, opacity, pixels);
}
private static int ReadIndex(BinaryReader r, int byteWidth)
{
return byteWidth switch
{
1 => r.ReadByte(),
2 => r.ReadUInt16(),
3 => r.ReadByte() | (r.ReadByte() << 8) | (r.ReadByte() << 16),
4 => r.ReadInt32(),
_ => throw new InvalidDataException($"Invalid index byte width: {byteWidth}")
};
}
private static string ReadPrefixedString(BinaryReader r)
{
byte len = r.ReadByte();
if (len == 0) return string.Empty;
byte[] bytes = ReadExact(r, len, "string data");
return Encoding.UTF8.GetString(bytes);
}
/// <summary>
/// Reads exactly <paramref name="count"/> bytes or throws on premature EOF.
/// </summary>
private static byte[] ReadExact(BinaryReader r, int count, string context)
{
byte[] buf = r.ReadBytes(count);
if (buf.Length < count)
throw new InvalidDataException(
$"Unexpected end of stream while reading {context} (expected {count} bytes, got {buf.Length}).");
return buf;
}
#endregion
#region Helpers
/// <summary>
/// Same logic as <see cref="MinintDocument.IndexByteWidth"/>,
/// usable when only the palette count is known (during deserialization).
/// </summary>
public static int GetIndexByteWidth(int paletteCount) => paletteCount switch
{
<= 255 => 1,
<= 65_535 => 2,
<= 16_777_215 => 3,
_ => 4
};
#endregion
}