Exporting very large PNGs

So, I have a need to export extremely large PNGs from my game (it makes procedurally generated world maps, and I want to dump an image of the whole map), far larger than I can store in a Texture2D, which is subject to the limits of the graphics card (on my laptop, maxing out at about 2048x2048).

The first thing I tried was using the System.Drawing.Bitmap class, as this is how you’d normally accomplish image exporting in .NET, only to discover that Unity can’t utilize the System.Drawing library, because it doesn’t have GdiPlus.dll.

So now I’m looking for other ways to export images from Unity/Mono. Any suggestions?

Thats very difficult in Unity, your options would be: Find a Mono Assembly which is encoding PNG files for you or write your own encoder. You could have a look into this, maybe you can use it:

http://www.koders.com/csharp/fidA4C7AAE93A23B5CD8AA90356FA12F7F5A856887D.aspx?s=mdef%3Ainsert

I cast a quick glanze over it but it looks understandable.

var pngBytes = PngEncoder.Encode(imageDataBytes, width, height).ToArray();
using (var fileStream= new FileStream("YourPathPath", FileMode.Open))
    fileStream.Write(pngBytes, 0, pngBytes.Length);

Depending on what you need the images for, you could just use in raw bytes format. It’s very easy to export to raw with BitConverter. Then you can use Photoshop or ImageMagick, etc. to convert to PNG, or just ocntinue to use them as raw.
Example is for a square, single channel, 16bits per-channel, should be easy to modify for whatever format you need:

	static public void Write(string filename, ushort[,] output) {
		int w = output.GetLength(0);
		using(BinaryWriter binWriter =new BinaryWriter(File.Open(filename, FileMode.Create)))
		{
			for (int y = 0; y < w; y++)
			{
				for (int x = 0; x < w; x++)
				{
					binWriter.Write(BitConverter.GetBytes(output[x,y]));
				}
			}
		}
	}

Is the texture2D really limited to your graphics card? I made some pretty big screenshots using the method from this thread http://forum.unity3d.com/threads/3880-Submitting-featured-content

Yes, Texture2D is limited to the GPU, its a not an image but a texture.
With modern cards, that means 8192x8192 max size (thats 256MB of VRAM without mipmap, over 300mb with mipmap) but with older hardware it goes down to 2048x2048 and worse

@pakfront: I intend for the map image exporter to be usable by end users when the game is released, so it really needs to be in a format that that’s practical for most people, not just something that I can finagle into shape with an image converter afterwards.

@Marrrk: Looks promising. I’ll need to mess with it a bit to make sure it works does what I need. I’m a bit nervous about the fact that it’s importing a bunch of System.Windows.* libraries, though. Will those be generally available to Unity, or will this then be limited to Windows-only?

@Yorick: What dreamora said. Given that the limit will vary depending on hardware, and I can’t count on it being all that large, it’s really not a feasible option. Also, creating and editing a very large Texture2D is extremely slow. My laptop card can support textures at 2048x2048, but it can take upwards of 10-20 seconds to create it, when it only takes a fraction of a second to build it in system memory.

I wish Unity would provide a means of storing and manipulating images in system memory, whether it’s tagging a Texture2D as a non-hardware texture, or creating some kind of separate Image class.

I just discovered the Atalasoft DOTimage library, which has a free license that’s still capable of doing everything I’d need. There’s no explicit mention of Mono or non-Windows support, but they do specify that it’s pure .NET, so that gives me hope that it will be compatible with Mono. I’ll give it a try and report back my results.

(Edit: The library is available here DotImage® - Atalasoft )

Pure .Net is not exactly the same as Mono, you will need to test it, but I doubt that this will work without a hassle.
The PNG Encoder Code I gave you does not need all the System.Windows usings. You need: System.IO for the MemoryStream, class and System for the BitConverter, everything else is not needed.

Marrk, have you tested the PNG Encoder? I get an error, but it may well be my writing the stream to a file is not done correctly.

here’s my test code:

//should be a single pixel RGBA of black
byte [] tmp = new byte[4];
Stream stream = PngEncoder.Encode(tmp, 1, 1);
Write("output.png", stream);

and the Write method:

static public void Write(string filename, Stream readStream) {
	FileStream writeStream = new FileStream(filename, FileMode.Create, FileAccess.Write);
	int Length = 256;
	Byte [] buffer = new Byte[Length];
	int bytesRead = readStream.Read(buffer,0,Length);
	// write the required bytes
	while( bytesRead > 0 )
	{
		writeStream.Write(buffer,0,bytesRead);
		bytesRead = readStream.Read(buffer,0,Length);
	}
	readStream.Close();
	writeStream.Close();
}

also tried this, which should be equivalent:

static public void Write(string filename, Stream readStream) {
	using (Stream writeStream = new FileStream(filename, FileMode.Create, FileAccess.Write)) {
		byte[] buffer = new byte[32*1024];
		int read;
	
		while ( (read=readStream.Read(buffer, 0, buffer.Length)) > 0)
		{
			writeStream.Write(buffer, 0, read);
		}		
	}
}

Break up the image into quadrants. Save each quadrant out to a file. Use GIMP or ImageMagic to stitch the resulting quadrant bitmaps together.

I did something similar a while back, creating poster-sized images. Running the external process to stitch large images took up to 1G of mem and around 5 min of processing time.

@Marrk: Haven’t had time to properly test it yet. I don’t know that it’ll work, but I figured that “pure .NET” meant there was at least a chance. That said, if I don’t need all those extra usings, then the PNG encoder you linked to becomes more attractive. I am, however, also a bit worried about license dependencies. The comments talk about zlib. Are you sure this code is not pulled at least partially from something that should be under a license of some kind?

@aoakenfo: As I mentioned above, the exporter is intended to be used by the end user. It’s not just for me to make pretty promo images. So I want it to output the results in a clean and ready-to-use format, which doesn’t require any further processing. The temporary storage for the image, before I encode it, will unfortunately require a very large chunk of memory, but there’s really no way around that.

You are right it does crash, for me too. But I found another PNG Encoder implementation:

http://blogs.msdn.com/b/delay/archive/2011/02/07/what-it-lacks-in-efficiency-it-makes-up-for-in-efficiency-silverlight-ready-png-encoder-implementation-shows-one-way-to-use-net-ienumerables-effectively.aspx

http://cesso.org/Samples/PngEncoder/PngEncoder.zip

Just take toe content of the PngEncoder.cs File and everything should work very well, I testet it with the demo project which is provided in the archive. Maybe small image sizes willnot work too.

Regarding the license, zlib should be ok with you, just read the license text on wikipedia or in the file and you see that you very much have every right to do with it you want (some minor exceptions there, but … read it)

The link I gave you now uses this license: http://opensource.org/licenses/ms-pl.html Have a look if it is ok for you, or write your own encoder based upon of of the encoders presented in this thread.

Awesome, thanks. You’ve been extremely helpful.

For me this produces corrupt PNG files, using my own data or his test from PngEncoderDemo.
Image Magick identify says:

I’m currently testing it under Mono (C# compiler version 2011.105.0.0)
on Linux, so maybe it works in Unity builds.

Okay, so 2 weeks and a lot of work later, I’m on the verge of abandoning this feature.

This latest PNG encoder was not usable to me, in it’s raw form, because unlike him I need compression. My images are huge, so exporting them raw and uncompressed is not practical. Besides, based on my reading of the PNG specification, not compressing it actually breaks the spec, which is probably why the images it’s exported are seen as “corrupt” by some viewers.

So, I set out to write my own encoder, using this encoder, the official spec and the Wikipedia article as reference points. I got it basically done, using the .NET class DeflateStream to manage the compression. Then I got this message:

DllNotFoundException: MonoPosixHelper
System.IO.Compression.DeflateStream..ctor (System.IO.Stream compressedStream, CompressionMode mode, Boolean leaveOpen, Boolean gzip)
System.IO.Compression.DeflateStream..ctor (System.IO.Stream compressedStream, CompressionMode mode)
(wrapper remoting-invoke-with-check) System.IO.Compression.DeflateStream:.ctor (System.IO.Stream,System.IO.Compression.CompressionMode)
SOI.Common.Image.Image.ExportPNG (System.String filename)
GenesisDemo.Start ()

So, uh, like with the .NET image libraries, apparently DeflateStream also relies on DLL which Unity does not support. This means that, in order to implement my own PNG encoder, I’d need to also make my own implementation of Deflate. This feature isn’t worth that much effort.

Is there somewhere I can put in a feature request to Unity? I’d like to ask that they provide some optional means of including a larger subset of the Mono.NET libraries in their final executable. I don’t want to have them always there, if you don’t need them, because they’ll add a lot of size of the executable, but it would be extremely useful, for a lot of different projects and purposes, to be able to access the rest of the .NET functionality which is available to Mono, outside of just the stuff in system.dll.

Usually I go to http://feedback.unity3d.com/forums/15792-unity for feature requests. You should check if they already have a similar topic or add it yourself.

Just saw this posted: http://forum.unity3d.com/threads/85803-MegaGrab

I know this is an old thread, but I thought I’d post my solution to this problem. I got the PNG encoder from http://upokecenter.dreamhosters.com/articles/2011/04/png-image-encoder-in-c/ and found it didn’t work in Unity because it uses System.IO.Compression.DeflateStream, which Unity’s version of Mono does not include. To solve this, I swapped that out for zlib.net. It works in Unity now. And I also added a SetPixels function that takes a Unity Color[ ] array and writes a block of pixels to the target, inverted on Y because Unity stores textures bottom-up and PNG stores them top-down.

However, I wanted to mention something about memory. My tests show that compared to working in a Texture2D and then converting to a PNG with Texture2D.EncodeToPNG, this method eats up a TON of memory. In Unity 3x, memory just continues to grow (memory leak), but in Unity 4x it does not. Still, when doing some atlasing tests, Unity 4.3’s memory footprint using this went to 1GB, whereas working with Texture2Ds it stayed around 300MB. Both ways require you convert your Texture2D to a Color[ ] array, and both end up creating a byte[ ] array for the final PNG. There are no extra allocations in this code which could cause the massive increase in memory, so I believe its probably got to do with the Z-lib compression making large allocations.

Still, if you need to be able to make PNGs and for some reason you can’t use Unity’s Texture2D to do it, this will work.

Just in case you’re wondering, zlib.net is a totally C# .net port of zlib, so you don’t need Pro to use the DLL. Or you can get the source and include it in your project too if you want.

using System;
using System.IO;
using UnityEngine;

/*
 * This file is in the public domain.
 *
 * Originally Created by Peter O.
 * Source: http://upokecenter.dreamhosters.com/articles/2011/04/png-image-encoder-in-c/
 *
 * Modified by Guavaman:
 * Changed some variable names, added more comments.
 * Added SetPixels function for writing pixels from a Unity Color[] array in Unity bottom-up pixel order
 * Replaced System.IO.Compression.DeflateStream with Zlib compression.
 *
 * Requires zlib.net library: http://www.componentace.com/zlib_.NET.htm
 */

/// <summary>
/// A simple class for encoding PNG image files.
/// </summary>
public class PNG {

    private int bytesPerPixel = 4;

    byte[] imageData, data;
    uint IDATA_crc;
    byte[] pngHeader, IHDR_chunkPart;
    uint[] crcTable;
    int width, height, realRowSize, blockSize, rowSize;

    /// Gets the height of the image.
    public int Height {
        get { return height; }
    }

    /// Gets the width of the image.
    public int Width {
        get { return width; }
    }

    /// Creates a new PNG image with the given
    /// width and height.
    public PNG(int width, int height, int bitsPerPixel) {
        if(width > 65535 || width <= 0)
            throw new ArgumentOutOfRangeException("width");
        if(height > 65535 || height <= 0)
            throw new ArgumentOutOfRangeException("height");
        if(bitsPerPixel != 24  bitsPerPixel != 32) throw new System.ArgumentOutOfRangeException("bitsPerPixel must be either 24 or 32!");
        bytesPerPixel = bitsPerPixel / 8;

        // Create header of PNG file + part of first chunk
        pngHeader = new byte[] {
            0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a // Header: The first eight bytes of a PNG datastream always contain the following (decimal) values: 137 80 78 71 13 10 26 10
        };

        // First chunk: IHDR chunk
        IHDR_chunkPart = new byte[] {
            0,0,0,0xd, // Chunk Length: 4 bytes = 13   Does not include Length or CRC in this count
            0x49,0x48,0x44,0x52, // Chunk Type: 4 bytes = IHDR
            
            // Chunk Data:
            0,0,(byte)(width>>8),(byte)(width255), // Width: 4 bytes = width of image in bytes
            0,0,(byte)(height>>8),(byte)(height255), // Height: 4 bytes = width of image in bytes
            8, // Bit depth: 1 byte = 256 values per color
            (byte)((bytesPerPixel == 4) ? 6 : 2), // Colour Type: 1 byte : 6 = Truecolor + alpha, 4 = Truecolor
            0, // Compression method: 1 byte
            0, // Filter method: 1 byte
            0  // Interlace method: 1 byte

            // CRC chunk will be added later
        };

        this.width = width;
        this.height = height;
        this.realRowSize = (this.width * bytesPerPixel) + 1;
        this.blockSize = this.realRowSize;
        this.rowSize = this.realRowSize;
        this.imageData = new byte[this.rowSize * this.height];
        this.data = this.imageData;

        IDATA_crc = Crc32(
            new byte[] {
                0x49,0x44,0x41,0x54
            }, 0, 4, 0);
    }

    private uint Crc32(byte[] stream, int offset, int length, uint crc) {
        uint c;
        if(crcTable == null) {
            crcTable = new uint[256];
            for(uint n = 0; n <= 255; n++) {
                c = n;
                for(var k = 0; k <= 7; k++) {
                    if((c  1) == 1)
                        c = 0xEDB88320 ^ ((c >> 1)  0x7FFFFFFF);
                    else
                        c = ((c >> 1)  0x7FFFFFFF);
                }
                crcTable[n] = c;
            }
        }
        c = crc ^ 0xffffffff;
        var endOffset = offset + length;
        for(var i = offset; i < endOffset; i++) {
            c = crcTable[(c ^ stream[i])  255] ^ ((c >> 8)  0xFFFFFF);
        }
        return c ^ 0xffffffff;
    }

    /// Sets the PNG filter for the current row.
    public void SetFilter(int y, byte filter) {
        this.data[y * this.realRowSize] = filter;
    }

    /// Gets the PNG filter for the current row.
    public byte GetFilter(int y) {
        return this.data[y * this.realRowSize];
    }

    ///
    ///  Sets the pixel located at the X and Y coordinates of the
    ///  image.  The pixel byte array is either 3 or 4 elements
    ///  long and contains the red, green, blue, and optionally
    ///  alpha components in that order.  If the byte array is 3
    ///  elements long, sets alpha to 255.
    ///  Because the row may use a PNG filter, those components may
    ///  not actually represent the intensity of each color returned.
    ///
    public void SetPixel(int x, int y, byte[] pixel) {
        if(pixel == null)
            throw new ArgumentNullException("pixel");
        if(pixel.Length >= 4) {
            SetPixel(x, y, pixel[0], pixel[1], pixel[2], pixel[3]);
        } else if(pixel.Length == 3) {
            SetPixel(x, y, pixel[0], pixel[1], pixel[2], (byte)255);
        } else {
            throw new ArgumentException("'pixel' has an improper length");
        }
    }

    ///
    ///  Sets the pixel located at the X and Y coordinates of the
    ///  image.  r, g, b are the red, green, and blue components.
    ///  Sets alpha to 255.  This function should only be used if the
    ///  PNG filter for the row is 0 (or not set), since this class
    ///  currently does not apply PNG filters.
    ///
    public void SetPixel(int x, int y, byte r, byte g, byte b) {
        SetPixel(x, y, r, g, b, (byte)255);
    }
    ///
    ///  Sets the pixel located at the X and Y coordinates of the
    ///  image.  r, g, b are the red, green, and blue components.
    ///  a is the alpha component.
    ///  Because the row may use a PNG filter, those components may
    ///  not actually represent the intensity of each color returned.
    ///
    public void SetPixel(int x, int y, byte r, byte g, byte b, byte a) {
        if(x < 0 || x >= width) throw new ArgumentOutOfRangeException("x");
        if(y < 0 || y >= height) throw new ArgumentOutOfRangeException("y");
        int offset = (y * this.realRowSize) + (x * bytesPerPixel) + 1;
        this.data[offset] = r;
        this.data[offset + 1] = g;
        this.data[offset + 2] = b;
        if(bytesPerPixel >= 4) this.data[offset + 3] = a;
    }


    /// <summary>
    /// Set a block of pixels using UnityEngine.Color class
    /// </summary>
    /// <param name="x">Starting X in the target texture to write in Unity format: 0,0 = lower left!</param>
    /// <param name="y">Starting Y in the target texture to write in Unity format: 0,0 = lower left!</param>
    /// <param name="blockWidth">Width of the pixel block to write.</param>
    /// <param name="blockHeight">Height of the pixel block to write.</param>
    /// <param name="pixels"></param>
    public void SetPixels(int x, int y, int blockWidth, int blockHeight, Color[] pixels) {
        if(x < 0 || x >= this.width) throw new ArgumentOutOfRangeException("x");
        if(y < 0 || y >= this.height) throw new ArgumentOutOfRangeException("y");
        if(pixels == null) throw new ArgumentNullException("pixels");
        if(blockWidth * blockHeight != pixels.Length) throw new ArgumentException("pixels.Length must = blockWidth * blockHeight!");

        int targetRows = this.height;
        int targetCols = this.width;
        int sourceRows = blockHeight;
        int sourceCols = blockWidth;
        int targetRow, targetCol, realTargetByteCol, sourceIndex, targetIndex;

        // Invert because Unity stores textures from bottom to top
        y = this.height - 1 - y;

        for(int row = 0; row < sourceRows; row++) {
            targetRow = y - row; // invert
            if(targetRow >= targetRows) break; // target has no more space vertically, done

            for(int col = 0; col < sourceCols; col++) {
                targetCol = x + col; // offset for extra byte at beginning of each row
                if(targetCol >= targetCols) break; // target has no more space horizontally, go to next row
                realTargetByteCol = targetCol * bytesPerPixel + 1; // get the real column in the byte row including the extra byte at beginning

                // find the indices of the pixels in each image
                sourceIndex = row * sourceCols + col;
                targetIndex = targetRow * realRowSize + realTargetByteCol;

                // write the pixels to the bytes
                for(int i = 0; i < bytesPerPixel; i++) {
                    this.data[targetIndex + i] = (byte)((int)(pixels[sourceIndex][i] * 255));
                }
            }
        }
    }

    ///
    ///  Gets the pixel located at the X and Y coordinates of the
    ///  image.  Returns a bit array containing four elements for
    ///  the red, green, blue, and alpha components, in that order.
    ///  Because the row may use a PNG filter, the returned data may
    ///  not actually represent the intensity of each color returned.
    ///
    public byte[] GetPixel(int x, int y) {
        if(x < 0 || x >= width) throw new ArgumentOutOfRangeException("x");
        if(y < 0 || y >= height) throw new ArgumentOutOfRangeException("y");
        int offset = (y * this.realRowSize) + (x * bytesPerPixel) + 1;
        return new byte[] {
            this.data[offset],
            this.data[offset+1],
            this.data[offset+2],
            (byte)((bytesPerPixel>=4) ? this.data[offset+3] : 255)
        };
    }

    private byte[] GetBE(uint crc) {
        return new byte[] {
            (byte)((crc>>24)255),
            (byte)((crc>>16)255),
            (byte)((crc>>8)255),
            (byte)((crc>>0)255)
        };
    }

    /// Saves the image to a file.
    public void Save(string filename) {
        byte[] compressedData = null;

        using(FileStream fs = new FileStream(filename, FileMode.Create)) {

            // Write PNG header
            fs.Write(this.pngHeader, 0, this.pngHeader.Length); // write the header and first part of the IHDR chunk to the stream

            // Write IHDR chunk
            fs.Write(this.IHDR_chunkPart, 0, this.IHDR_chunkPart.Length); // write the IHDR partial chunk
            uint crc32 = Crc32(IHDR_chunkPart, 4, 17, 0); // get CRC of IHDR chunk, skipping Length
            fs.Write(GetBE(crc32), 0, 4); // convert to bytes and write to stream

            // Compress image - requires zlib.net because Unity doesn't have access to System.IO.Compression
            using(MemoryStream ms = new MemoryStream()) {
                zlib.ZOutputStream zos = new zlib.ZOutputStream(ms, zlib.zlibConst.Z_DEFAULT_COMPRESSION); // put ms in as the output reciever
                zos.Write(this.data, 0, this.data.Length); // copy the pixels into the compressed stream
                zos.Flush();
                zos.finish(); // copies data back to ms
                compressedData = ms.ToArray();
                zos.Close();
            }

            // Convert data length to bytes
                byte[] compressedDataLength = new byte[] {
                (byte)((compressedData.Length>>24)255),
                (byte)((compressedData.Length>>16)255),
                (byte)((compressedData.Length>>8)255),
                (byte)((compressedData.Length>>0)255)
            };

            // Write length of compressed data
            fs.Write(compressedDataLength, 0, compressedDataLength.Length);

            // Write IDATA header
            fs.Write(new byte[] { 0x49, 0x44, 0x41, 0x54 }, 0, 4);

            // Write IDATA data
            fs.Write(compressedData, 0, compressedData.Length);

            // Write the CRC of the data chunk
            uint crc = Crc32(compressedData, 0, compressedData.Length, this.IDATA_crc);
            byte[] subdcrc = GetBE(crc);
            fs.Write(subdcrc, 0, subdcrc.Length);

            // Write the IEND chunk
            byte[] IEND_chunk = new byte[] {
                0,0,0,0, // Length
                0x49,0x45,0x4e,0x44, // Chunk Type: IEND
                // No Chunk Data in this chunk
                0xae,0x42,0x60,0x82 // CRC
            };
            fs.Write(IEND_chunk, 0, IEND_chunk.Length);
        }
    }
}