diff --git a/NOTICE.md b/NOTICE.md index 5649fced0a29..4d8a05c57d66 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -788,6 +788,34 @@ SOFTWARE. ## Utility: Peek +### The Quite OK Image Format reference decoder + +**Source**: https://github.com/phoboslab/qoi + +**Note**: [@pedrolamas](https://github.com/pedrolamas) translated and adapted the reference decoder code to C# that is in PowerToys from the original C++ implementation. + +MIT License + +Copyright (c) 2022 Dominic Szablewski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + ### UTF Unknown We use the UTF.Unknown NuGet package for detecting encoding in text/code files. diff --git a/src/common/FilePreviewCommon/FilePreviewCommon.csproj b/src/common/FilePreviewCommon/FilePreviewCommon.csproj index f10873559fd3..da6b7bdbdb85 100644 --- a/src/common/FilePreviewCommon/FilePreviewCommon.csproj +++ b/src/common/FilePreviewCommon/FilePreviewCommon.csproj @@ -4,12 +4,15 @@ net8.0-windows - win-x64;win-arm64 + win-x64;win-arm64 $(Version).0 Microsoft Corporation PowerToys PowerToys FilePreviewCommon PowerToys.FilePreviewCommon + true + true + enable @@ -32,7 +35,7 @@ Always - Always + Always Always diff --git a/src/common/FilePreviewCommon/Formatters/XmlFormatter.cs b/src/common/FilePreviewCommon/Formatters/XmlFormatter.cs index b282c802ae73..450e92a5b37e 100644 --- a/src/common/FilePreviewCommon/Formatters/XmlFormatter.cs +++ b/src/common/FilePreviewCommon/Formatters/XmlFormatter.cs @@ -26,7 +26,7 @@ public string Format(string value) var stringBuilder = new StringBuilder(); var xmlWriterSettings = new XmlWriterSettings() { - OmitXmlDeclaration = xmlDocument.FirstChild.NodeType != XmlNodeType.XmlDeclaration, + OmitXmlDeclaration = xmlDocument.FirstChild?.NodeType != XmlNodeType.XmlDeclaration, Indent = true, }; diff --git a/src/modules/previewpane/common/Utilities/GcodeHelper.cs b/src/common/FilePreviewCommon/GcodeHelper.cs similarity index 98% rename from src/modules/previewpane/common/Utilities/GcodeHelper.cs rename to src/common/FilePreviewCommon/GcodeHelper.cs index f614a099e4e6..62dc29554e84 100644 --- a/src/modules/previewpane/common/Utilities/GcodeHelper.cs +++ b/src/common/FilePreviewCommon/GcodeHelper.cs @@ -8,7 +8,7 @@ using System.Linq; using System.Text; -namespace Common.Utilities +namespace Microsoft.PowerToys.FilePreviewCommon { /// /// Gcode file helper class. diff --git a/src/modules/previewpane/common/Utilities/GcodeThumbnail.cs b/src/common/FilePreviewCommon/GcodeThumbnail.cs similarity index 97% rename from src/modules/previewpane/common/Utilities/GcodeThumbnail.cs rename to src/common/FilePreviewCommon/GcodeThumbnail.cs index 7715c3f53f90..545d7aa04ab9 100644 --- a/src/modules/previewpane/common/Utilities/GcodeThumbnail.cs +++ b/src/common/FilePreviewCommon/GcodeThumbnail.cs @@ -5,9 +5,8 @@ using System; using System.Drawing; using System.IO; -using PreviewHandlerCommon.Utilities; -namespace Common.Utilities +namespace Microsoft.PowerToys.FilePreviewCommon { /// /// Represents a gcode thumbnail. diff --git a/src/modules/previewpane/common/Utilities/GcodeThumbnailFormat.cs b/src/common/FilePreviewCommon/GcodeThumbnailFormat.cs similarity index 93% rename from src/modules/previewpane/common/Utilities/GcodeThumbnailFormat.cs rename to src/common/FilePreviewCommon/GcodeThumbnailFormat.cs index bb4d84e0bce5..1e471ed8c50d 100644 --- a/src/modules/previewpane/common/Utilities/GcodeThumbnailFormat.cs +++ b/src/common/FilePreviewCommon/GcodeThumbnailFormat.cs @@ -2,7 +2,7 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Common.Utilities +namespace Microsoft.PowerToys.FilePreviewCommon { /// /// The gcode thumbnail image format. diff --git a/src/common/FilePreviewCommon/MarkdownHelper.cs b/src/common/FilePreviewCommon/MarkdownHelper.cs index 31013419ed6f..bb8a98440ebe 100644 --- a/src/common/FilePreviewCommon/MarkdownHelper.cs +++ b/src/common/FilePreviewCommon/MarkdownHelper.cs @@ -30,7 +30,7 @@ public static string MarkdownHtml(string fileContent, string theme, string fileP // Extension to modify markdown AST. HTMLParsingExtension extension = new HTMLParsingExtension(imagesBlockedCallBack); - extension.FilePath = Path.GetDirectoryName(filePath); + extension.FilePath = Path.GetDirectoryName(filePath) ?? string.Empty; // if you have a string with double space, some people view it as a new line. // while this is against spec, even GH supports this. Technically looks like GH just trims whitespace diff --git a/src/common/FilePreviewCommon/MonacoHelper.cs b/src/common/FilePreviewCommon/MonacoHelper.cs index eac3826426fc..54b0ac93af5b 100644 --- a/src/common/FilePreviewCommon/MonacoHelper.cs +++ b/src/common/FilePreviewCommon/MonacoHelper.cs @@ -28,12 +28,12 @@ public static class MonacoHelper new XmlFormatter(), }.AsReadOnly(); - private static string _monacoDirectory; + private static string? _monacoDirectory; public static string GetRuntimeMonacoDirectory() { string codeBase = Assembly.GetExecutingAssembly().Location; - string path = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(codeBase), "Assets", "Monaco")); + string path = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(codeBase) ?? string.Empty, "Assets", "Monaco")); if (Path.Exists(path)) { return path; @@ -41,7 +41,7 @@ public static string GetRuntimeMonacoDirectory() else { // We're likely in WinUI3Apps directory and need to go back to the base directory. - return Path.GetFullPath(Path.Combine(Path.GetDirectoryName(codeBase), "..", "Assets", "Monaco")); + return Path.GetFullPath(Path.Combine(Path.GetDirectoryName(codeBase) ?? string.Empty, "..", "Assets", "Monaco")); } } diff --git a/src/common/FilePreviewCommon/QoiImage.cs b/src/common/FilePreviewCommon/QoiImage.cs new file mode 100644 index 000000000000..ac315eb8323e --- /dev/null +++ b/src/common/FilePreviewCommon/QoiImage.cs @@ -0,0 +1,177 @@ +// 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; + + 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; + } + } + } + + bitmap.UnlockBits(bitmapData); + + return bitmap; + } + catch + { + bitmap?.Dispose(); + + throw; + } + } + + private static uint ReadUInt32BigEndian(BinaryReader reader) + { + var buffer = reader.ReadBytes(4); + + return BinaryPrimitives.ReadUInt32BigEndian(buffer); + } + } +} diff --git a/src/modules/peek/Peek.Common/Extensions/IFileSystemItemExtensions.cs b/src/modules/peek/Peek.Common/Extensions/IFileSystemItemExtensions.cs index 0d976fd28814..5717e97aa867 100644 --- a/src/modules/peek/Peek.Common/Extensions/IFileSystemItemExtensions.cs +++ b/src/modules/peek/Peek.Common/Extensions/IFileSystemItemExtensions.cs @@ -2,7 +2,7 @@ // 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.Globalization; using System.IO; using System.Text.RegularExpressions; @@ -77,6 +77,31 @@ public static class IFileSystemItemExtensions return size; } + public static Size? GetQoiSize(this IFileSystemItem item) + { + Size? size = null; + using (FileStream stream = new FileStream(item.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) + { + if (stream.Length >= 12) + { + stream.Position = 4; + + using (var reader = new BinaryReader(stream)) + { + uint widthValue = BinaryPrimitives.ReadUInt32BigEndian(reader.ReadBytes(4)); + uint heightValue = BinaryPrimitives.ReadUInt32BigEndian(reader.ReadBytes(4)); + + if (widthValue > 0 && heightValue > 0) + { + size = new Size(widthValue, heightValue); + } + } + } + } + + return size; + } + public static ulong GetSizeInBytes(this IFileSystemItem item) { ulong sizeInBytes = 0; diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/BitmapHelper.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/BitmapHelper.cs index 30cb971ee69c..a8699a443443 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/BitmapHelper.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/BitmapHelper.cs @@ -18,7 +18,6 @@ public static class BitmapHelper public static async Task GetBitmapFromHBitmapAsync(IntPtr hbitmap, bool isSupportingTransparency, CancellationToken cancellationToken) { Bitmap? bitmap = null; - Bitmap? tempBitmapForDeletion = null; try { @@ -26,15 +25,29 @@ public static async Task GetBitmapFromHBitmapAsync(IntPtr hbitmap, cancellationToken.ThrowIfCancellationRequested(); + return await BitmapToImageSource(bitmap, isSupportingTransparency, cancellationToken); + } + finally + { + bitmap?.Dispose(); + + // delete HBitmap to avoid memory leaks + NativeMethods.DeleteObject(hbitmap); + } + } + + public static async Task BitmapToImageSource(Bitmap bitmap, bool isSupportingTransparency, CancellationToken cancellationToken) + { + Bitmap? transparentBitmap = null; + + try + { if (isSupportingTransparency && bitmap.PixelFormat == PixelFormat.Format32bppRgb) { var bitmapRectangle = new Rectangle(0, 0, bitmap.Width, bitmap.Height); var bitmapData = bitmap.LockBits(bitmapRectangle, ImageLockMode.ReadOnly, bitmap.PixelFormat); - var transparentBitmap = new Bitmap(bitmapData.Width, bitmapData.Height, bitmapData.Stride, PixelFormat.Format32bppArgb, bitmapData.Scan0); - - // Can't dispose of original bitmap yet as that causes crashes on png files. Saving it for later disposal after saving to stream. - tempBitmapForDeletion = bitmap; + transparentBitmap = new Bitmap(bitmapData.Width, bitmapData.Height, bitmapData.Stride, PixelFormat.Format32bppArgb, bitmapData.Scan0); bitmap = transparentBitmap; } @@ -54,11 +67,7 @@ public static async Task GetBitmapFromHBitmapAsync(IntPtr hbitmap, } finally { - bitmap?.Dispose(); - tempBitmapForDeletion?.Dispose(); - - // delete HBitmap to avoid memory leaks - NativeMethods.DeleteObject(hbitmap); + transparentBitmap?.Dispose(); } } diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/ImagePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/ImagePreviewer.cs index d01a60e56e34..ab42b5724cdb 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/ImagePreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/ImagePreviewer.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using ManagedCommon; +using Microsoft.PowerToys.FilePreviewCommon; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; @@ -91,6 +92,14 @@ public async Task GetPreviewSizeAsync(CancellationToken cancellatio ImageSize = size.Value; } } + else if (IsQoi(Item)) + { + var size = await Task.Run(Item.GetQoiSize); + if (size != null) + { + ImageSize = size.Value; + } + } else { ImageSize = await Task.Run(Item.GetImageSize); @@ -257,6 +266,12 @@ await Dispatcher.RunOnUiThread(async () => Preview = source; } + else if (IsQoi(Item)) + { + using var bitmap = QoiImage.FromStream(stream); + + Preview = await BitmapHelper.BitmapToImageSource(bitmap, true, cancellationToken); + } else { var bitmap = new BitmapImage(); @@ -286,6 +301,11 @@ private bool IsSvg(IFileSystemItem item) return item.Extension == ".svg"; } + private bool IsQoi(IFileSystemItem item) + { + return item.Extension == ".qoi"; + } + private void Clear() { lowQualityThumbnailPreview = null; @@ -367,6 +387,8 @@ private void Clear() ".cr3", ".svg", + + ".qoi", }; } } diff --git a/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandler.csproj b/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandler.csproj index 72afb9e10da4..1f58af39db1f 100644 --- a/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandler.csproj +++ b/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandler.csproj @@ -61,6 +61,7 @@ + diff --git a/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandlerControl.cs b/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandlerControl.cs index 63d9828d3848..67c6d9f42e72 100644 --- a/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandlerControl.cs +++ b/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandlerControl.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using Common; -using Common.Utilities; +using Microsoft.PowerToys.FilePreviewCommon; using Microsoft.PowerToys.PreviewHandler.Gcode.Telemetry.Events; using Microsoft.PowerToys.Telemetry; diff --git a/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.cs b/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.cs index 4afe553f3da2..d9b9026b8fa8 100644 --- a/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.cs +++ b/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Drawing.Drawing2D; using System.Drawing.Imaging; -using Common.Utilities; +using Microsoft.PowerToys.FilePreviewCommon; namespace Microsoft.PowerToys.ThumbnailHandler.Gcode { diff --git a/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.csproj b/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.csproj index 24ac171b7a6d..034e87d70e95 100644 --- a/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.csproj +++ b/src/modules/previewpane/GcodeThumbnailProvider/GcodeThumbnailProvider.csproj @@ -46,6 +46,7 @@ + diff --git a/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandler.csproj b/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandler.csproj index 462ba6cd7a4c..66936c38d3ca 100644 --- a/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandler.csproj +++ b/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandler.csproj @@ -61,6 +61,7 @@ + diff --git a/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandlerControl.cs b/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandlerControl.cs index 95d606ce82bc..047a3e614fa9 100644 --- a/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandlerControl.cs +++ b/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandlerControl.cs @@ -3,9 +3,9 @@ // See the LICENSE file in the project root for more information. using Common; +using Microsoft.PowerToys.FilePreviewCommon; using Microsoft.PowerToys.PreviewHandler.Qoi.Telemetry.Events; using Microsoft.PowerToys.Telemetry; -using PreviewHandlerCommon.Utilities; namespace Microsoft.PowerToys.PreviewHandler.Qoi { @@ -63,7 +63,7 @@ public override void DoPreview(T dataSource) throw new ArgumentException($"{nameof(dataSource)} for {nameof(QoiPreviewHandlerControl)} must be a string but was a '{typeof(T)}'"); } - FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); + using FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read); thumbnail = QoiImage.FromStream(fs); diff --git a/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.cs b/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.cs index b055a26d0bcf..7c0e7ead1c97 100644 --- a/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.cs +++ b/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Drawing.Drawing2D; using System.Drawing.Imaging; -using PreviewHandlerCommon.Utilities; +using Microsoft.PowerToys.FilePreviewCommon; namespace Microsoft.PowerToys.ThumbnailHandler.Qoi { diff --git a/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.csproj b/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.csproj index 9ae42bb515f7..6ab6ce01b8a0 100644 --- a/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.csproj +++ b/src/modules/previewpane/QoiThumbnailProvider/QoiThumbnailProvider.csproj @@ -46,6 +46,7 @@ + diff --git a/src/modules/previewpane/common/PreviewHandlerCommon.csproj b/src/modules/previewpane/common/PreviewHandlerCommon.csproj index 99a6b2ed43eb..bb1f8043d0dd 100644 --- a/src/modules/previewpane/common/PreviewHandlerCommon.csproj +++ b/src/modules/previewpane/common/PreviewHandlerCommon.csproj @@ -10,7 +10,6 @@ false true enable - true diff --git a/src/modules/previewpane/common/Utilities/QoiImage.cs b/src/modules/previewpane/common/Utilities/QoiImage.cs deleted file mode 100644 index 20a2ca14765a..000000000000 --- a/src/modules/previewpane/common/Utilities/QoiImage.cs +++ /dev/null @@ -1,177 +0,0 @@ -// 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; - -//// Based on https://github.com/phoboslab/qoi/blob/master/qoi.h - -namespace PreviewHandlerCommon.Utilities -{ - /// - /// 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"); - } - - using var reader = new BinaryReader(stream); - - 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 pixelsCount = width * height; - var pixels = new QoiPixel[pixelsCount]; - var index = new QoiPixel[64]; - - var pixel = new QoiPixel(0, 0, 0, 255); - - var run = 0; - var chunksLen = fileSize - QOI_PADDING_LENGTH; - - for (var pixelIndex = 0; pixelIndex < pixelsCount; pixelIndex++) - { - 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; - } - - pixels[pixelIndex] = pixel; - } - - return ConvertToBitmap(width, height, channels, pixels); - } - - private static Bitmap ConvertToBitmap(uint width, uint height, byte channels, QoiPixel[] pixels) - { - var pixelFormat = channels == 4 ? PixelFormat.Format32bppArgb : PixelFormat.Format24bppRgb; - var bitmap = new Bitmap((int)width, (int)height, pixelFormat); - - var bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.WriteOnly, pixelFormat); - - unsafe - { - for (var pixelIndex = 0; pixelIndex < pixels.Length; pixelIndex++) - { - var pixel = pixels[pixelIndex]; - var bitmapPixel = (byte*)bitmapData.Scan0 + (pixelIndex * channels); - - bitmapPixel[0] = pixel.B; - bitmapPixel[1] = pixel.G; - bitmapPixel[2] = pixel.R; - if (channels == 4) - { - bitmapPixel[3] = pixel.A; - } - } - } - - bitmap.UnlockBits(bitmapData); - - return bitmap; - } - - private static uint ReadUInt32BigEndian(BinaryReader reader) - { - var buffer = reader.ReadBytes(4); - - return BinaryPrimitives.ReadUInt32BigEndian(buffer); - } - } -}