如何用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:
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(); } }
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 .

