// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Buffers.Binary;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Text;
//// Based on https://github.com/phoboslab/qoi/blob/master/qoi.h
namespace Microsoft.PowerToys.FilePreviewCommon
{
///
/// QOI Image helper.
///
public static class QoiImage
{
#pragma warning disable SA1310 // Field names should not contain underscore
private const byte QOI_OP_INDEX = 0x00; // 00xxxxxx
private const byte QOI_OP_DIFF = 0x40; // 01xxxxxx
private const byte QOI_OP_LUMA = 0x80; // 10xxxxxx
private const byte QOI_OP_RUN = 0xc0; // 11xxxxxx
private const byte QOI_OP_RGB = 0xfe; // 11111110
private const byte QOI_OP_RGBA = 0xff; // 11111111
private const byte QOI_MASK_2 = 0xc0; // 11000000
private const int QOI_MAGIC = 'q' << 24 | 'o' << 16 | 'i' << 8 | 'f';
private const int QOI_HEADER_SIZE = 14;
private const uint QOI_PIXELS_MAX = 400000000;
private const byte QOI_PADDING_LENGTH = 8;
#pragma warning restore SA1310 // Field names should not contain underscore
private record struct QoiPixel(byte R, byte G, byte B, byte A)
{
public readonly int GetColorHash() => (R * 3) + (G * 5) + (B * 7) + (A * 11);
}
///
/// Creates a from the specified QOI data stream.
///
/// A that contains the QOI data.
/// The this method creates.
/// The stream does not have a valid QOI image format.
public static Bitmap FromStream(Stream stream)
{
var fileSize = stream.Length;
if (fileSize < QOI_HEADER_SIZE + QOI_PADDING_LENGTH)
{
throw new ArgumentException("Not enough data for a QOI file");
}
Bitmap? bitmap = null;
try
{
using var reader = new BinaryReader(stream, Encoding.UTF8, true);
var headerMagic = ReadUInt32BigEndian(reader);
if (headerMagic != QOI_MAGIC)
{
throw new ArgumentException("Invalid QOI file header");
}
var width = ReadUInt32BigEndian(reader);
var height = ReadUInt32BigEndian(reader);
var channels = reader.ReadByte();
var colorSpace = reader.ReadByte();
if (width == 0 || height == 0 || channels < 3 || channels > 4 || colorSpace > 1 || height >= QOI_PIXELS_MAX / width)
{
throw new ArgumentException("Invalid QOI file data");
}
var pixelFormat = channels == 4 ? PixelFormat.Format32bppArgb : PixelFormat.Format24bppRgb;
bitmap = new Bitmap((int)width, (int)height, pixelFormat);
var bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.WriteOnly, pixelFormat);
var dataLength = bitmapData.Height * bitmapData.Stride;
var index = new QoiPixel[64];
var pixel = new QoiPixel(0, 0, 0, 255);
var run = 0;
var chunksLen = fileSize - QOI_PADDING_LENGTH;
var x = 0;
var rowAdd = bitmapData.Stride - (channels * bitmapData.Width);
for (var dataIndex = 0; dataIndex < dataLength; dataIndex += channels)
{
if (run > 0)
{
run--;
}
else if (stream.Position < chunksLen)
{
var b1 = reader.ReadByte();
if (b1 == QOI_OP_RGB)
{
pixel.R = reader.ReadByte();
pixel.G = reader.ReadByte();
pixel.B = reader.ReadByte();
}
else if (b1 == QOI_OP_RGBA)
{
pixel.R = reader.ReadByte();
pixel.G = reader.ReadByte();
pixel.B = reader.ReadByte();
pixel.A = reader.ReadByte();
}
else if ((b1 & QOI_MASK_2) == QOI_OP_INDEX)
{
pixel = index[b1];
}
else if ((b1 & QOI_MASK_2) == QOI_OP_DIFF)
{
pixel.R += (byte)(((b1 >> 4) & 0x03) - 2);
pixel.G += (byte)(((b1 >> 2) & 0x03) - 2);
pixel.B += (byte)((b1 & 0x03) - 2);
}
else if ((b1 & QOI_MASK_2) == QOI_OP_LUMA)
{
var b2 = reader.ReadByte();
var vg = (b1 & 0x3f) - 32;
pixel.R += (byte)(vg - 8 + ((b2 >> 4) & 0x0f));
pixel.G += (byte)vg;
pixel.B += (byte)(vg - 8 + (b2 & 0x0f));
}
else if ((b1 & QOI_MASK_2) == QOI_OP_RUN)
{
run = b1 & 0x3f;
}
index[pixel.GetColorHash() % 64] = pixel;
}
unsafe
{
var bitmapPixel = (byte*)bitmapData.Scan0 + dataIndex;
bitmapPixel[0] = pixel.B;
bitmapPixel[1] = pixel.G;
bitmapPixel[2] = pixel.R;
if (channels == 4)
{
bitmapPixel[3] = pixel.A;
}
}
x++;
if (x == bitmapData.Width)
{
// We align dataIndex with the stride
dataIndex += rowAdd;
x = 0;
}
}
bitmap.UnlockBits(bitmapData);
return bitmap;
}
catch
{
bitmap?.Dispose();
throw;
}
}
private static uint ReadUInt32BigEndian(BinaryReader reader)
{
var buffer = reader.ReadBytes(4);
return BinaryPrimitives.ReadUInt32BigEndian(buffer);
}
}
}