Example #1
0
        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)
            });
        }
Example #2
0
        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);
        }