You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用C#创建支持读写增删的简易图片存储包?

Absolutely, there are several minimalist, library-style approaches you can use in C# to build a custom image storage package perfect for your 20,000-image use case. Let’s break down the most practical options, with actionable code snippets to get you up and running:


1. Build a Custom Lightweight Binary Container (Most Minimal & Controllable)

If you want full control, zero external dependencies, and a format tailored exactly to your needs, rolling your own binary container is the way to go. The core idea is simple:

  • The container file has two parts: a header (stores metadata like total file count, and each image's offset, size, and unique ID) and a data block (stores all image binaries consecutively).
  • Operations work by reading the header to locate images, then directly reading/writing the relevant byte ranges—super efficient for large batches of small files.

Core Implementation Code

First, define serializable structs for metadata (using StructLayout to ensure binary compatibility):

using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ImageEntry
{
    public uint ImageId; // Use a hash of the original filename to save space
    public long FileSize;
    public long DataOffset;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ContainerHeader
{
    public uint MagicNumber; // Unique identifier (e.g., 0x494D4750 = "IMGP")
    public uint EntryCount;
}

Then create your reusable container class (this is your custom library):

public class ImageContainer : IDisposable
{
    private readonly string _containerPath;
    private ContainerHeader _header;
    private List<ImageEntry> _entries = new();
    private bool _isOpen = false;

    public ImageContainer(string containerFilePath)
    {
        _containerPath = containerFilePath;
    }

    // "Enter" the container
    public void Open()
    {
        if (_isOpen) return;

        using var fs = new FileStream(_containerPath, FileMode.Open);
        // Read header
        var headerBytes = new byte[Marshal.SizeOf<ContainerHeader>()];
        fs.Read(headerBytes, 0, headerBytes.Length);
        _header = BytesToStruct<ContainerHeader>(headerBytes);

        // Read all image entries
        _entries.Clear();
        var entrySize = Marshal.SizeOf<ImageEntry>();
        for (int i = 0; i < _header.EntryCount; i++)
        {
            var entryBytes = new byte[entrySize];
            fs.Read(entryBytes, 0, entrySize);
            _entries.Add(BytesToStruct<ImageEntry>(entryBytes));
        }

        _isOpen = true;
    }

    // "Exit" the container
    public void Close()
    {
        _isOpen = false;
        _entries.Clear();
    }

    // Create a new empty container
    public void CreateNew()
    {
        using var fs = new FileStream(_containerPath, FileMode.Create);
        _header = new ContainerHeader { MagicNumber = 0x494D4750, EntryCount = 0 };
        fs.Write(StructToBytes(_header), 0, Marshal.SizeOf<ContainerHeader>());
    }

    // Add/write an image to the container
    public void AddImage(uint imageId, byte[] imageBytes)
    {
        if (!_isOpen) throw new InvalidOperationException("Container is not open.");

        using var fs = new FileStream(_containerPath, FileMode.Open);
        // Write image data to the end of the file
        fs.Seek(0, SeekOrigin.End);
        var newOffset = fs.Position;
        fs.Write(imageBytes, 0, imageBytes.Length);

        // Update metadata
        _header.EntryCount++;
        _entries.Add(new ImageEntry { ImageId = imageId, FileSize = imageBytes.Length, DataOffset = newOffset });

        // Rewrite header and entries to the start of the file
        fs.Seek(0, SeekOrigin.Begin);
        fs.Write(StructToBytes(_header), 0, Marshal.SizeOf<ContainerHeader>());
        foreach (var entry in _entries)
        {
            fs.Write(StructToBytes(entry), 0, Marshal.SizeOf<ImageEntry>());
        }
    }

    // Read an image from the container
    public byte[] GetImage(uint imageId)
    {
        if (!_isOpen) throw new InvalidOperationException("Container is not open.");

        var entry = _entries.FirstOrDefault(e => e.ImageId == imageId);
        if (entry.FileSize == 0) throw new FileNotFoundException("Image not found in container.");

        using var fs = new FileStream(_containerPath, FileMode.Open);
        fs.Seek(entry.DataOffset, SeekOrigin.Begin);
        var buffer = new byte[entry.FileSize];
        fs.Read(buffer, 0, buffer.Length);
        return buffer;
    }

    // Delete an image from the container
    public void RemoveImage(uint imageId)
    {
        if (!_isOpen) throw new InvalidOperationException("Container is not open.");

        var entryToRemove = _entries.FirstOrDefault(e => e.ImageId == imageId);
        if (entryToRemove.FileSize == 0) return;

        // Create a temporary file to copy remaining content
        var tempPath = Path.GetTempFileName();
        using (var fsSource = new FileStream(_containerPath, FileMode.Open))
        using (var fsDest = new FileStream(tempPath, FileMode.Create))
        {
            // Copy updated header and entries
            var newEntries = _entries.Where(e => e.ImageId != imageId).ToList();
            _header.EntryCount = (uint)newEntries.Count;
            fsDest.Write(StructToBytes(_header), 0, Marshal.SizeOf<ContainerHeader>());

            // Adjust offsets for entries after the deleted image
            long offsetAdjustment = entryToRemove.FileSize;
            foreach (var entry in newEntries)
            {
                var adjustedEntry = entry;
                if (entry.DataOffset > entryToRemove.DataOffset)
                {
                    adjustedEntry.DataOffset -= offsetAdjustment;
                }
                fsDest.Write(StructToBytes(adjustedEntry), 0, Marshal.SizeOf<ImageEntry>());
            }

            // Copy remaining image data
            foreach (var entry in newEntries)
            {
                fsSource.Seek(entry.DataOffset, SeekOrigin.Begin);
                var buffer = new byte[entry.FileSize];
                fsSource.Read(buffer, 0, buffer.Length);
                fsDest.Write(buffer, 0, buffer.Length);
            }
        }

        // Replace original container with temp file
        File.Delete(_containerPath);
        File.Move(tempPath, _containerPath);
        _entries = newEntries;
    }

    // Helper: Struct to byte array
    private byte[] StructToBytes<T>(T structure) where T : struct
    {
        var size = Marshal.SizeOf(structure);
        var buffer = new byte[size];
        var ptr = Marshal.AllocHGlobal(size);
        Marshal.StructureToPtr(structure, ptr, false);
        Marshal.Copy(ptr, buffer, 0, size);
        Marshal.FreeHGlobal(ptr);
        return buffer;
    }

    // Helper: Byte array to struct
    private T BytesToStruct<T>(byte[] bytes) where T : struct
    {
        var size = Marshal.SizeOf<T>();
        var ptr = Marshal.AllocHGlobal(size);
        Marshal.Copy(bytes, 0, ptr, size);
        var structure = Marshal.PtrToStructure<T>(ptr);
        Marshal.FreeHGlobal(ptr);
        return structure;
    }

    public void Dispose()
    {
        Close();
    }
}

2. Use .NET's Built-in System.IO.Packaging (Quickest Implementation)

If you want to skip writing low-level code, .NET's built-in System.IO.Packaging (a lightweight OLE compound file container) is perfect. It acts like a simple, uncompressed Zip file, requires only a NuGet package reference, and handles all the heavy lifting for you.

Implementation Code

First, add the System.IO.Packaging NuGet package to your project. Then create your wrapper class:

using System.IO.Packaging;

public class ImagePackage : IDisposable
{
    private readonly string _packagePath;
    private Package _package;
    private bool _isOpen = false;

    public ImagePackage(string packageFilePath)
    {
        _packagePath = packageFilePath;
    }

    // "Enter" the package
    public void Open()
    {
        if (_isOpen) return;
        _package = Package.Open(_packagePath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
        _isOpen = true;
    }

    // "Exit" the package
    public void Close()
    {
        _package?.Close();
        _isOpen = false;
    }

    // Add/write an image
    public void AddImage(string imageName, byte[] imageBytes, string mimeType = "image/jpeg")
    {
        if (!_isOpen) throw new InvalidOperationException("Package is not open.");

        var imageUri = new Uri($"/Images/{imageName}", UriKind.Relative);
        // Replace existing image if it exists
        if (_package.PartExists(imageUri))
        {
            _package.DeletePart(imageUri);
        }

        var part = _package.CreatePart(imageUri, mimeType);
        using var stream = part.GetStream();
        stream.Write(imageBytes, 0, imageBytes.Length);
    }

    // Read an image
    public byte[] GetImage(string imageName)
    {
        if (!_isOpen) throw new InvalidOperationException("Package is not open.");

        var imageUri = new Uri($"/Images/{imageName}", UriKind.Relative);
        if (!_package.PartExists(imageUri))
        {
            throw new FileNotFoundException("Image not found in package.");
        }

        var part = _package.GetPart(imageUri);
        using var stream = part.GetStream();
        var buffer = new byte[stream.Length];
        stream.Read(buffer, 0, buffer.Length);
        return buffer;
    }

    // Delete an image
    public void RemoveImage(string imageName)
    {
        if (!_isOpen) throw new InvalidOperationException("Package is not open.");

        var imageUri = new Uri($"/Images/{imageName}", UriKind.Relative);
        if (_package.PartExists(imageUri))
        {
            _package.DeletePart(imageUri);
        }
    }

    public void Dispose()
    {
        Close();
    }
}

Final Recommendation

  • Choose Option 1 if you need absolute minimalism, full control over the container format, and zero external dependencies. It’s optimized for your 20,000-image use case and easy to extend.
  • Choose Option 2 if you want to build something working quickly with minimal custom code. The .NET packaging API is stable, well-documented, and handles all edge cases for you.

内容的提问来源于stack exchange,提问作者teodoric8 .

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 07:52:50