mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 03:37:59 +01:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request Make sure we account for Bitmap row length being a multiple of 4 when reading Qoi files. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] **Closes:** #31948 - [ ] **Communication:** Not discussed, but a bug fix - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end user facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed Built and run and restarted file explorer. Sample files from my comment on the issue (after rewriting each one to force the thumbnail to regenrate): 
189 lines
6.8 KiB
C#
189 lines
6.8 KiB
C#
// 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
|
|
{
|
|
/// <summary>
|
|
/// QOI Image helper.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a <see cref="Bitmap"/> from the specified QOI data stream.
|
|
/// </summary>
|
|
/// <param name="stream">A <see cref="Stream"/> that contains the QOI data.</param>
|
|
/// <returns>The <see cref="Bitmap"/> this method creates.</returns>
|
|
/// <exception cref="ArgumentException">The stream does not have a valid QOI image format.</exception>
|
|
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);
|
|
}
|
|
}
|
|
}
|