From c6ba443c23301b7591b9cfc2ca03bb972ae875ef Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Sat, 18 Nov 2023 01:10:15 +0000 Subject: [PATCH 1/4] Adds QOI support to Peek --- .../FilePreviewCommon.csproj | 7 +++-- .../Formatters/XmlFormatter.cs | 2 +- .../FilePreviewCommon}/GcodeHelper.cs | 2 +- .../FilePreviewCommon}/GcodeThumbnail.cs | 3 +- .../GcodeThumbnailFormat.cs | 2 +- .../FilePreviewCommon/MarkdownHelper.cs | 2 +- src/common/FilePreviewCommon/MonacoHelper.cs | 6 ++-- .../FilePreviewCommon}/QoiImage.cs | 2 +- .../Extensions/IFileSystemItemExtensions.cs | 27 ++++++++++++++++- .../Previewers/Helpers/BitmapHelper.cs | 29 ++++++++++++------- .../MediaPreviewer/ImagePreviewer.cs | 22 ++++++++++++++ .../GcodePreviewHandler.csproj | 1 + .../GcodePreviewHandlerControl.cs | 2 +- .../GcodeThumbnailProvider.cs | 2 +- .../GcodeThumbnailProvider.csproj | 1 + .../QoiPreviewHandler.csproj | 1 + .../QoiPreviewHandlerControl.cs | 2 +- .../QoiThumbnailProvider.cs | 2 +- .../QoiThumbnailProvider.csproj | 1 + .../common/PreviewHandlerCommon.csproj | 1 - 20 files changed, 89 insertions(+), 28 deletions(-) rename src/{modules/previewpane/common/Utilities => common/FilePreviewCommon}/GcodeHelper.cs (98%) rename src/{modules/previewpane/common/Utilities => common/FilePreviewCommon}/GcodeThumbnail.cs (97%) rename src/{modules/previewpane/common/Utilities => common/FilePreviewCommon}/GcodeThumbnailFormat.cs (93%) rename src/{modules/previewpane/common/Utilities => common/FilePreviewCommon}/QoiImage.cs (99%) diff --git a/src/common/FilePreviewCommon/FilePreviewCommon.csproj b/src/common/FilePreviewCommon/FilePreviewCommon.csproj index b0e64db6f6fa..955c9914d827 100644 --- a/src/common/FilePreviewCommon/FilePreviewCommon.csproj +++ b/src/common/FilePreviewCommon/FilePreviewCommon.csproj @@ -4,12 +4,15 @@ net7.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/modules/previewpane/common/Utilities/QoiImage.cs b/src/common/FilePreviewCommon/QoiImage.cs similarity index 99% rename from src/modules/previewpane/common/Utilities/QoiImage.cs rename to src/common/FilePreviewCommon/QoiImage.cs index 20a2ca14765a..519e6aff0ca7 100644 --- a/src/modules/previewpane/common/Utilities/QoiImage.cs +++ b/src/common/FilePreviewCommon/QoiImage.cs @@ -10,7 +10,7 @@ //// Based on https://github.com/phoboslab/qoi/blob/master/qoi.h -namespace PreviewHandlerCommon.Utilities +namespace Microsoft.PowerToys.FilePreviewCommon { /// /// QOI Image helper. 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 f02f703c0473..54623a9014e4 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/ImagePreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/ImagePreviewer.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.PowerToys.FilePreviewCommon; using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; @@ -90,6 +91,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); @@ -256,6 +265,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(); @@ -285,6 +300,11 @@ private bool IsSvg(IFileSystemItem item) return item.Extension == ".svg"; } + private bool IsQoi(IFileSystemItem item) + { + return item.Extension == ".qoi"; + } + private void Clear() { lowQualityThumbnailPreview = null; @@ -366,6 +386,8 @@ private void Clear() ".cr3", ".svg", + + ".qoi", }; } } diff --git a/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandler.csproj b/src/modules/previewpane/GcodePreviewHandler/GcodePreviewHandler.csproj index 2f49a7f54ad1..f2117face173 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 e75a48227d2e..1acbab1db07e 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 8ba906c5e047..7ec323fb3340 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..3e513ae8c557 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 { 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 e78b84b355cf..049241c81802 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 30a7f4896243..92b344527dca 100644 --- a/src/modules/previewpane/common/PreviewHandlerCommon.csproj +++ b/src/modules/previewpane/common/PreviewHandlerCommon.csproj @@ -10,7 +10,6 @@ false true enable - true From e0613acf8a962a1ee748298f9265bbbe16206398 Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Mon, 20 Nov 2023 11:42:34 +0000 Subject: [PATCH 2/4] Reduce allocations on QoiImage --- src/common/FilePreviewCommon/QoiImage.cs | 178 +++++++++++------------ 1 file changed, 89 insertions(+), 89 deletions(-) diff --git a/src/common/FilePreviewCommon/QoiImage.cs b/src/common/FilePreviewCommon/QoiImage.cs index 519e6aff0ca7..ac315eb8323e 100644 --- a/src/common/FilePreviewCommon/QoiImage.cs +++ b/src/common/FilePreviewCommon/QoiImage.cs @@ -7,6 +7,7 @@ using System.Drawing; using System.Drawing.Imaging; using System.IO; +using System.Text; //// Based on https://github.com/phoboslab/qoi/blob/master/qoi.h @@ -55,116 +56,115 @@ public static Bitmap FromStream(Stream stream) throw new ArgumentException("Not enough data for a QOI file"); } - using var reader = new BinaryReader(stream); + Bitmap? bitmap = null; - 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) + try { - 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); + using var reader = new BinaryReader(stream, Encoding.UTF8, true); - var run = 0; - var chunksLen = fileSize - QOI_PADDING_LENGTH; + var headerMagic = ReadUInt32BigEndian(reader); - for (var pixelIndex = 0; pixelIndex < pixelsCount; pixelIndex++) - { - if (run > 0) + if (headerMagic != QOI_MAGIC) { - run--; + throw new ArgumentException("Invalid QOI file header"); } - 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; - } + var width = ReadUInt32BigEndian(reader); + var height = ReadUInt32BigEndian(reader); + var channels = reader.ReadByte(); + var colorSpace = reader.ReadByte(); - index[pixel.GetColorHash() % 64] = pixel; + if (width == 0 || height == 0 || channels < 3 || channels > 4 || colorSpace > 1 || height >= QOI_PIXELS_MAX / width) + { + throw new ArgumentException("Invalid QOI file data"); } - pixels[pixelIndex] = pixel; - } + var pixelFormat = channels == 4 ? PixelFormat.Format32bppArgb : PixelFormat.Format24bppRgb; - return ConvertToBitmap(width, height, channels, pixels); - } + bitmap = new Bitmap((int)width, (int)height, pixelFormat); - 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); + var dataLength = bitmapData.Height * bitmapData.Stride; - var bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.WriteOnly, pixelFormat); + var index = new QoiPixel[64]; + var pixel = new QoiPixel(0, 0, 0, 255); - unsafe - { - for (var pixelIndex = 0; pixelIndex < pixels.Length; pixelIndex++) + var run = 0; + var chunksLen = fileSize - QOI_PADDING_LENGTH; + + for (var dataIndex = 0; dataIndex < dataLength; dataIndex += channels) { - var pixel = pixels[pixelIndex]; - var bitmapPixel = (byte*)bitmapData.Scan0 + (pixelIndex * 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; + } - bitmapPixel[0] = pixel.B; - bitmapPixel[1] = pixel.G; - bitmapPixel[2] = pixel.R; - if (channels == 4) + unsafe { - bitmapPixel[3] = pixel.A; + 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); + bitmap.UnlockBits(bitmapData); - return bitmap; + return bitmap; + } + catch + { + bitmap?.Dispose(); + + throw; + } } private static uint ReadUInt32BigEndian(BinaryReader reader) From c420f9f4a8655cb7be57b4fce958a379afa8d921 Mon Sep 17 00:00:00 2001 From: Jaime Bernardo Date: Mon, 18 Dec 2023 11:56:02 +0000 Subject: [PATCH 3/4] Add to QOI to Peek's NOTICE as well. --- NOTICE.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) 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. From 15e0a5dd92c2d7c40b1c8a7508eb3c103f44ae66 Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Mon, 18 Dec 2023 12:35:20 +0000 Subject: [PATCH 4/4] Ensure file stream is closed after reading QOI --- .../previewpane/QoiPreviewHandler/QoiPreviewHandlerControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandlerControl.cs b/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandlerControl.cs index 3e513ae8c557..047a3e614fa9 100644 --- a/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandlerControl.cs +++ b/src/modules/previewpane/QoiPreviewHandler/QoiPreviewHandlerControl.cs @@ -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);