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);
}
}
}