/*
Copyright (c) 2016, 2017 Philip Holzmann.
This file is part of BuddhabrotMax.
BuddhabrotMax is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
BuddhabrotMax is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with BuddhabrotMax. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.IO.Compression;
using System.Diagnostics.Contracts;
namespace BuddhabrotMax
{
/// <summary>
/// An OpenEXR file writer for the ImageGrayscale class.
///
/// Based on <http://www.openexr.com/openexrfilelayout.pdf>
/// and <http://www.openexr.com/TechnicalIntroduction.pdf>.
///
/// Zip Compression based on <https://github.com/openexr/openexr/blob/develop/OpenEXR/IlmImf/ImfZip.cpp>
/// (only data reordering and predictor, could not find documentation on how it works).
/// </summary>
//TODO: make compression multi threaded?
public static class ExrWriter
{
public enum ExrPixelType
{
Half,
Float
}
public enum ExrCompression
{
None,
Zip
}
public interface ISaveProgress
{
void LogProgress(double progress, long writtenBytes);
void ReportFinished();
}
public interface IImage
{
int Width { get; }
int Height { get; }
void GetFloat(int x, int y, out float red, out float green, out float blue);
}
private static bool isGrayscale(IImage image)
{
int w = image.Width;
int h = image.Height;
for (int y = 0; y < h; y++)
for (int x = 0; x < w; x++)
{
float r, g, b;
image.GetFloat(x, y, out r, out g, out b);
if (r != g || g != b)
return false;
}
return true;
}
private struct ExrWriteContext
{
public readonly IImage Image;
public readonly Stream Stream;
public readonly ExrPixelType PixelType;
public readonly bool Grayscale;
public readonly ISaveProgress Progress;
public int ChannelCount => Grayscale ? 1 : 3;
public int BytesPerChannel => PixelType == ExrPixelType.Float ? 4 : 2;
public int BytesPerPixel => BytesPerChannel * ChannelCount;
public ExrWriteContext(IImage image, Stream stream, ExrPixelType pixelType, bool grayscale, ISaveProgress progress)
{
Contract.Requires(pixelType == ExrPixelType.Float || pixelType == ExrPixelType.Half);
this.Image = image;
this.Stream = stream;
this.PixelType = pixelType;
this.Grayscale = grayscale;
this.Progress = progress;
}
}
//not zero zerminated
private static byte[] toAsciiString(String s)
{
return Encoding.ASCII.GetBytes(s);
}
private static void writeAttributeHeader(String name, String type, BinaryWriter writer)
{
writer.Write(toAsciiString(name));
writer.Write((byte)0);
writer.Write(toAsciiString(type));
writer.Write((byte)0);
}
private static void writePixelDataZip(ExrWriteContext c, long[] offsetTable)
{
Contract.Requires(offsetTable != null);
Contract.Requires(offsetTable.Length == (c.Image.Height + 15) / 16);
int width = c.Image.Width;
int height = c.Image.Height;
int bufferSize = c.BytesPerPixel * width * 16;
byte[] dataBuffer = new byte[(int)Math.Ceiling(bufferSize * 1.1) + 100]; //make buffer a bit bigger in case compression actually makes data bigger
byte[] tmpBuffer = new byte[bufferSize];
MemoryStream bufR, bufG = null, bufB = null;
BinaryWriter writerR, writerG = null, writerB = null;
bufR = new MemoryStream(c.BytesPerChannel * width);
writerR = new BinaryWriter(bufR);
if (!c.Grayscale)
{
bufG = new MemoryStream(c.BytesPerChannel * width);
writerG = new BinaryWriter(bufG);
bufB = new MemoryStream(c.BytesPerChannel * width);
writerB = new BinaryWriter(bufB);
}
using (bufR)
using (writerR)
using (bufG)
using (writerG)
using (bufB)
using (writerB)
using (BinaryWriter mainWriter = new BinaryWriter(c.Stream, Encoding.Default, true))
using (MemoryStream dataBufferStream = new MemoryStream(dataBuffer, true))
for (int y = 0; y < height; y += 16)
{
offsetTable[y / 16] = mainWriter.Seek(0, SeekOrigin.Current);
mainWriter.Write(y);
dataBufferStream.SetLength(0);
//using (BinaryWriter innerWriter = new BinaryWriter(dataBufferStream, Encoding.Default, true))
for (int y2 = y; y2 < height && y2 < y + 16; y2++)
{
bufR.SetLength(0);
if (!c.Grayscale)
{
bufG.SetLength(0);
bufB.SetLength(0);
}
for (int x = 0; x < width; x++)
{
float r, g, b;
c.Image.GetFloat(x, y2, out r, out g, out b);
if (c.PixelType == ExrPixelType.Half)
{
writerR.Write(HalfHelper.SingleToHalf(r));
if (!c.Grayscale)
{
writerG.Write(HalfHelper.SingleToHalf(g));
writerB.Write(HalfHelper.SingleToHalf(b));
}
}
else
{
writerR.Write(r);
if (!c.Grayscale)
{
writerG.Write(g);
writerB.Write(b);
}
}
}
//write buffered line
if (!c.Grayscale)
{
bufB.WriteTo(dataBufferStream);
bufG.WriteTo(dataBufferStream);
}
bufR.WriteTo(dataBufferStream);
}
int rawSize = (int)dataBufferStream.Length;
//reorder data
{
int t1 = 0;
int t2 = (rawSize + 1) / 2;
int raw = 0;
for (;;)
{
Contract.Assume(t1 < tmpBuffer.Length);
Contract.Assume(raw < dataBuffer.Length);
if (raw < rawSize)
tmpBuffer[t1++] = dataBuffer[raw++];
else
break;
Contract.Assume(t2 < tmpBuffer.Length);
Contract.Assume(raw < dataBuffer.Length);
if (raw < rawSize)
tmpBuffer[t2++] = dataBuffer[raw++];
else
break;
}
}
//predictor
{
int t = 1;
int stop = rawSize;
int p = tmpBuffer[t - 1];
while (t < stop)
{
Contract.Assume(t < tmpBuffer.Length);
int d = tmpBuffer[t + 0] - p + (128 + 256);
p = tmpBuffer[t + 0];
tmpBuffer[t + 0] = (byte)d;
t++;
}
}
//reuse dataBuffer for compression
dataBufferStream.SetLength(0);
using (ZlibStream deflate = new ZlibStream(dataBufferStream, CompressionLevel.Fastest, true))
{
deflate.Write(tmpBuffer, 0, rawSize);
}
mainWriter.Write((int)dataBufferStream.Length);
dataBufferStream.WriteTo(c.Stream); //write compressed data
c.Progress.LogProgress((double)y / height, c.Stream.Length);
}
}
public static void WriteExr(IImage image, Stream stream, ExrCompression compression, ExrPixelType pixelType, bool forceGrayscale, ISaveProgress progress)
{
Contract.Requires(image != null);
Contract.Requires(stream != null);
Contract.Requires(stream.CanWrite);
Contract.Requires(stream.CanSeek);
Contract.Requires(compression == ExrCompression.None || compression == ExrCompression.Zip);
Contract.Requires(pixelType == ExrPixelType.Float || pixelType == ExrPixelType.Half);
Contract.Requires(progress != null);
bool grayscale = forceGrayscale || isGrayscale(image);
ExrWriteContext c = new ExrWriteContext(image, stream, pixelType, grayscale, progress);
const int floatPixelType = 2;
const int halfPixelType = 1;
const byte noneCompression = 0;
const byte zipCompression = 3;
int width = image.Width;
int height = image.Height;
BinaryWriter binWriter = new BinaryWriter(stream, Encoding.Default, true);
binWriter.Write(20000630); //magic number
{
int version = 0;
version |= 2 << 0; //version number
version |= 0 << 9; //regular single-part scan line file
version |= 0 << 10; //no long names
version |= 0 << 11; //regular
version |= 0 << 12; //no multi part file
binWriter.Write(version); //write version
}
//== Header
//Attributes
writeAttributeHeader("channels", "chlist", binWriter);
binWriter.Write((int)(18 * c.ChannelCount + 1)); //attribute size
String[] channels = grayscale ? new String[] { "Y" } : new String[] { "B", "G", "R" };
foreach (String channel in channels)
{
Contract.Assume(channel.Length == 1); //otherwise attribute size is incorrect
binWriter.Write(toAsciiString(channel)); //channel name
binWriter.Write((byte)0); //zero-terminate
binWriter.Write((int)(pixelType == ExrPixelType.Half ? halfPixelType : floatPixelType)); //pixel type
binWriter.Write((byte)1); //pLinear
binWriter.Write((byte)0); //three bytes padding
binWriter.Write((byte)0);
binWriter.Write((byte)0);
binWriter.Write((int)1); //xSampling
binWriter.Write((int)1); //ySampling
}
binWriter.Write((byte)0); //null byte
writeAttributeHeader("compression", "compression", binWriter);
binWriter.Write((int)1); //attribute size
byte chosenCompression = 0;
if (compression == ExrCompression.None)
chosenCompression = noneCompression;
else if (compression == ExrCompression.Zip)
chosenCompression = zipCompression;
binWriter.Write((byte)chosenCompression); //NO_COMPRESSION
writeAttributeHeader("dataWindow", "box2i", binWriter);
binWriter.Write((int)16); //attribute size
binWriter.Write((int)0); //xMin
binWriter.Write((int)0); //yMin
binWriter.Write((int)width - 1); //xMax
binWriter.Write((int)height - 1); //yMax
writeAttributeHeader("displayWindow", "box2i", binWriter);
binWriter.Write((int)16); //attribute size
binWriter.Write((int)0); //xMin
binWriter.Write((int)0); //yMin
binWriter.Write((int)width - 1); //xMax
binWriter.Write((int)height - 1); //yMax
writeAttributeHeader("lineOrder", "lineOrder", binWriter);
binWriter.Write((int)1); //attribute size
binWriter.Write((byte)0); //INCREASING_Y
writeAttributeHeader("pixelAspectRatio", "float", binWriter);
binWriter.Write((int)4);
binWriter.Write(1f);
writeAttributeHeader("screenWindowCenter", "v2f", binWriter);
binWriter.Write((int)8);
binWriter.Write(0f);
binWriter.Write(0f);
writeAttributeHeader("screenWindowWidth", "float", binWriter);
binWriter.Write((int)4);
binWriter.Write(1f);
binWriter.Write((byte)0); //end of header
//== Offset table
int offsetTableLines = height;
if (compression == ExrCompression.Zip)
offsetTableLines = (height + 15) / 16;
long[] offsetTable = new long[offsetTableLines];
long currentOffset = binWriter.Seek(0, SeekOrigin.Current); //maybe a bad way to get the current data offset
long offsetTableSize = sizeof(long) * height;
long firstScanlineOffset = currentOffset + offsetTableSize;
long scanlineLength = width * c.BytesPerPixel + 4 + 4; //4 for line number, 4 for pixel data size
//write the table dummy
for (int i = 0; i < offsetTableLines; i++)
binWriter.Write((long)0); //8 bytes per entry
//== Pixel Data
binWriter.Flush();
if (compression == ExrCompression.None)
{
MemoryStream bufR, bufG = null, bufB = null;
BinaryWriter writerR, writerG = null, writerB = null;
bufR = new MemoryStream(c.BytesPerChannel * width);
writerR = new BinaryWriter(bufR);
if (!grayscale)
{
bufG = new MemoryStream(c.BytesPerChannel * width);
writerG = new BinaryWriter(bufG);
bufB = new MemoryStream(c.BytesPerChannel * width);
writerB = new BinaryWriter(bufB);
}
using (bufR)
using (writerR)
using (bufG)
using (writerG)
using (bufB)
using (writerB)
for (int y = 0; y < height; y++)
{
bufR.SetLength(0);
if (!grayscale)
{
bufG.SetLength(0);
bufB.SetLength(0);
}
offsetTable[y] = firstScanlineOffset + scanlineLength * y;
binWriter.Write(y); //line number
binWriter.Write(width * c.BytesPerPixel); //pixel data size
for (int x = 0; x < width; x++)
{
float r, g, b;
image.GetFloat(x, y, out r, out g, out b);
if (pixelType == ExrPixelType.Half)
{
writerR.Write(HalfHelper.SingleToHalf(r));
if (!grayscale)
{
writerG.Write(HalfHelper.SingleToHalf(g));
writerB.Write(HalfHelper.SingleToHalf(b));
}
}
else
{
writerR.Write(r);
if (!grayscale)
{
writerG.Write(g);
writerB.Write(b);
}
}
}
//write buffered line
if (!grayscale)
{
bufB.WriteTo(stream);
bufG.WriteTo(stream);
}
bufR.WriteTo(stream);
progress.LogProgress((double)y / height, stream.Length);
}
}
else if (compression == ExrCompression.Zip)
writePixelDataZip(c, offsetTable);
binWriter.Dispose();
binWriter = new BinaryWriter(stream, Encoding.Default, true);
//write the actual offset table
binWriter.Seek((int)currentOffset, SeekOrigin.Begin);
for (int i = 0; i < offsetTableLines; i++)
binWriter.Write(offsetTable[i]); //8 bytes per entry
binWriter.Flush();
stream.Dispose();
progress.ReportFinished();
}
}
}