public static SerializeWorldResult Serialize(SerializeWorldRequest request) { // figure out how much data we need and allocate it var(pngSize, zlibSize) = AllocationSize(request); var array = new byte[pngSize + zlibSize]; var png = new Span <byte>(array, 0, pngSize); var zlib = new Span <byte>(array, pngSize, zlibSize); // CHUNK ORDER: https://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.Summary-of-standard-chunks // using constants to prevent lots of slicing on the span, might save half a nanosecond :) WritePngHeader(png); WriteIHDRChunk(png.Slice(PngHeaderSize, IHDRSize), request.Width * request.Scale, request.Height * request.Scale); WriteTEXTChunk(png.Slice(PngHeaderSize + IHDRSize, TEXTSize)); var written = WriteIDATChunk(png.Slice(PngHeaderSize + IHDRSize + TEXTSize), zlib, request.Width, request.Blocks.Span, request.Palette.Span[0], request.Palette.Span, request.Scale); WriteIENDChunk(png.Slice(PngHeaderSize + IHDRSize + TEXTSize + written, IENDSize)); var totalSize = PngHeaderSize + IHDRSize + TEXTSize + written + IENDSize; return(new SerializeWorldResult { Array = array, Png = new Memory <byte>(array, 0, totalSize) }); }
private static (int PngTarget, int RawPngData) AllocationSize(SerializeWorldRequest request) { var artificialWidth = request.Width * request.Scale; var artificialHeight = request.Height * request.Scale; // `n` byte(s) per block, plus 1 byte per row (`+ height`). // // This addition 1 byte per row comes from the PNG standard, // the filter type per scanline (https://www.w3.org/TR/2003/REC-PNG-20031110/#4Concepts.EncodingFiltering) int rawPngStreamLength = // scale the width & height by what they need to get // the correct amount of blocks. that will then be multiplied by 4, // because of each RGBA pixel per block. (artificialWidth * artificialHeight * 4) // 1 byte per scanline, and there well be `scale` more scanlines if the // world is scaled. + artificialHeight; // Quote from https://zlib.net/zlib_tech.html: // "In the worst possible case, [...] the only expansion is an overhead of five bytes per 16 KB block (about 0.03%), // plus a one-time overhead of six bytes for the entire stream" // // This will allocate enough bytes to satisfy the worst case scenario. // '16KB' does indeed refer to 16KiB. const int ZlibBlockSize = 16384; int zlibWorstCase = // 6 bytes overhead for the stream // (2 bytes header, 4 bytes alder32) 6 + // 5 bytes per 16KiB block // Integer rounding up: https://stackoverflow.com/a/2422722 // calc blocks (((rawPngStreamLength + (ZlibBlockSize - 1)) / ZlibBlockSize) // 5 bytes per block * 5) // after the zlib overhead comes the actual data + rawPngStreamLength; // `zlib_worst_case` is the amount of bytes necessary to be allocated, just for compressing the PNG data. // There must also be bytes allocated for the PNG itself. // // These header sizes are directly from the C# implementation: // https://github.com/SirJosh3917/EEUMinimapApi/blob/c9d25d660ccbab0d8e09fc840e9207ccd5ab4883/EEUMinimapApi/EEUMinimapApi/WorldToImage.cs#L335-L340 const int CPI_PNG_HEADER_SIZE = (1 + 3 + 2 + 2); const int CPI_PNG_tEXt_SIZE = (4 + 4 + /* strlen('Software') */ 8 + 1 + /* strlen('EEUMinimapApi') */ 13 + 4); const int CPI_PNG_IHDR_SIZE = (4 + 4 + 4 + 4 + 1 + 1 + 1 + 1 + 1 + 4); // PLTE unused now. int CPI_PNG_PLTE_SIZE(int uniqueBlocks) => 0; // (4 + 4 + (3 * (uniqueBlocks /* + 1: implicitly implied Id0 color */ + 1)) + 4); int CPI_PNG_IDAT_SIZE(int idatSize) => (4 + 4 + idatSize + 4); const int CPI_PNG_IEND_SIZE = (4 + 4 + 4); int png_size_with_zlib_worst_case = CPI_PNG_HEADER_SIZE + CPI_PNG_tEXt_SIZE + CPI_PNG_IHDR_SIZE + CPI_PNG_PLTE_SIZE(request.Palette.Length) + CPI_PNG_IDAT_SIZE(zlibWorstCase) + CPI_PNG_IEND_SIZE; return(png_size_with_zlib_worst_case, rawPngStreamLength); }