mirror of
https://github.com/microsoft/PowerToys.git
synced 2024-12-18 05:28:07 +08:00
[Peek]Adds QOI file support (#29919)
* Adds QOI support to Peek * Reduce allocations on QoiImage * Add to QOI to Peek's NOTICE as well. * Ensure file stream is closed after reading QOI
This commit is contained in:
parent
ee22581913
commit
4c3e5348f0
28
NOTICE.md
28
NOTICE.md
@ -788,6 +788,34 @@ SOFTWARE.
|
|||||||
|
|
||||||
## Utility: Peek
|
## 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
|
### UTF Unknown
|
||||||
|
|
||||||
We use the UTF.Unknown NuGet package for detecting encoding in text/code files.
|
We use the UTF.Unknown NuGet package for detecting encoding in text/code files.
|
||||||
|
@ -10,6 +10,9 @@
|
|||||||
<Product>PowerToys</Product>
|
<Product>PowerToys</Product>
|
||||||
<Description>PowerToys FilePreviewCommon</Description>
|
<Description>PowerToys FilePreviewCommon</Description>
|
||||||
<AssemblyName>PowerToys.FilePreviewCommon</AssemblyName>
|
<AssemblyName>PowerToys.FilePreviewCommon</AssemblyName>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -26,7 +26,7 @@ namespace Microsoft.PowerToys.FilePreviewCommon.Monaco.Formatters
|
|||||||
var stringBuilder = new StringBuilder();
|
var stringBuilder = new StringBuilder();
|
||||||
var xmlWriterSettings = new XmlWriterSettings()
|
var xmlWriterSettings = new XmlWriterSettings()
|
||||||
{
|
{
|
||||||
OmitXmlDeclaration = xmlDocument.FirstChild.NodeType != XmlNodeType.XmlDeclaration,
|
OmitXmlDeclaration = xmlDocument.FirstChild?.NodeType != XmlNodeType.XmlDeclaration,
|
||||||
Indent = true,
|
Indent = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Common.Utilities
|
namespace Microsoft.PowerToys.FilePreviewCommon
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gcode file helper class.
|
/// Gcode file helper class.
|
@ -5,9 +5,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using PreviewHandlerCommon.Utilities;
|
|
||||||
|
|
||||||
namespace Common.Utilities
|
namespace Microsoft.PowerToys.FilePreviewCommon
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a gcode thumbnail.
|
/// Represents a gcode thumbnail.
|
@ -2,7 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
namespace Common.Utilities
|
namespace Microsoft.PowerToys.FilePreviewCommon
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The gcode thumbnail image format.
|
/// The gcode thumbnail image format.
|
@ -30,7 +30,7 @@ namespace Microsoft.PowerToys.FilePreviewCommon
|
|||||||
|
|
||||||
// Extension to modify markdown AST.
|
// Extension to modify markdown AST.
|
||||||
HTMLParsingExtension extension = new HTMLParsingExtension(imagesBlockedCallBack);
|
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.
|
// 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
|
// while this is against spec, even GH supports this. Technically looks like GH just trims whitespace
|
||||||
|
@ -28,12 +28,12 @@ namespace Microsoft.PowerToys.FilePreviewCommon
|
|||||||
new XmlFormatter(),
|
new XmlFormatter(),
|
||||||
}.AsReadOnly();
|
}.AsReadOnly();
|
||||||
|
|
||||||
private static string _monacoDirectory;
|
private static string? _monacoDirectory;
|
||||||
|
|
||||||
public static string GetRuntimeMonacoDirectory()
|
public static string GetRuntimeMonacoDirectory()
|
||||||
{
|
{
|
||||||
string codeBase = Assembly.GetExecutingAssembly().Location;
|
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))
|
if (Path.Exists(path))
|
||||||
{
|
{
|
||||||
return path;
|
return path;
|
||||||
@ -41,7 +41,7 @@ namespace Microsoft.PowerToys.FilePreviewCommon
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
// We're likely in WinUI3Apps directory and need to go back to the base directory.
|
// 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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
177
src/common/FilePreviewCommon/QoiImage.cs
Normal file
177
src/common/FilePreviewCommon/QoiImage.cs
Normal file
@ -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
|
||||||
|
{
|
||||||
|
/// <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;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using System;
|
using System.Buffers.Binary;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@ -77,6 +77,31 @@ namespace Peek.Common.Extensions
|
|||||||
return size;
|
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)
|
public static ulong GetSizeInBytes(this IFileSystemItem item)
|
||||||
{
|
{
|
||||||
ulong sizeInBytes = 0;
|
ulong sizeInBytes = 0;
|
||||||
|
@ -18,7 +18,6 @@ namespace Peek.FilePreviewer.Previewers.Helpers
|
|||||||
public static async Task<BitmapSource> GetBitmapFromHBitmapAsync(IntPtr hbitmap, bool isSupportingTransparency, CancellationToken cancellationToken)
|
public static async Task<BitmapSource> GetBitmapFromHBitmapAsync(IntPtr hbitmap, bool isSupportingTransparency, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Bitmap? bitmap = null;
|
Bitmap? bitmap = null;
|
||||||
Bitmap? tempBitmapForDeletion = null;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -26,15 +25,29 @@ namespace Peek.FilePreviewer.Previewers.Helpers
|
|||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
return await BitmapToImageSource(bitmap, isSupportingTransparency, cancellationToken);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
bitmap?.Dispose();
|
||||||
|
|
||||||
|
// delete HBitmap to avoid memory leaks
|
||||||
|
NativeMethods.DeleteObject(hbitmap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<BitmapSource> BitmapToImageSource(Bitmap bitmap, bool isSupportingTransparency, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Bitmap? transparentBitmap = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
if (isSupportingTransparency && bitmap.PixelFormat == PixelFormat.Format32bppRgb)
|
if (isSupportingTransparency && bitmap.PixelFormat == PixelFormat.Format32bppRgb)
|
||||||
{
|
{
|
||||||
var bitmapRectangle = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
|
var bitmapRectangle = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
|
||||||
var bitmapData = bitmap.LockBits(bitmapRectangle, ImageLockMode.ReadOnly, bitmap.PixelFormat);
|
var bitmapData = bitmap.LockBits(bitmapRectangle, ImageLockMode.ReadOnly, bitmap.PixelFormat);
|
||||||
|
|
||||||
var transparentBitmap = new Bitmap(bitmapData.Width, bitmapData.Height, bitmapData.Stride, PixelFormat.Format32bppArgb, bitmapData.Scan0);
|
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;
|
|
||||||
bitmap = transparentBitmap;
|
bitmap = transparentBitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,11 +67,7 @@ namespace Peek.FilePreviewer.Previewers.Helpers
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
bitmap?.Dispose();
|
transparentBitmap?.Dispose();
|
||||||
tempBitmapForDeletion?.Dispose();
|
|
||||||
|
|
||||||
// delete HBitmap to avoid memory leaks
|
|
||||||
NativeMethods.DeleteObject(hbitmap);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using ManagedCommon;
|
using ManagedCommon;
|
||||||
|
using Microsoft.PowerToys.FilePreviewCommon;
|
||||||
using Microsoft.UI.Dispatching;
|
using Microsoft.UI.Dispatching;
|
||||||
using Microsoft.UI.Xaml.Media;
|
using Microsoft.UI.Xaml.Media;
|
||||||
using Microsoft.UI.Xaml.Media.Imaging;
|
using Microsoft.UI.Xaml.Media.Imaging;
|
||||||
@ -91,6 +92,14 @@ namespace Peek.FilePreviewer.Previewers
|
|||||||
ImageSize = size.Value;
|
ImageSize = size.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (IsQoi(Item))
|
||||||
|
{
|
||||||
|
var size = await Task.Run(Item.GetQoiSize);
|
||||||
|
if (size != null)
|
||||||
|
{
|
||||||
|
ImageSize = size.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ImageSize = await Task.Run(Item.GetImageSize);
|
ImageSize = await Task.Run(Item.GetImageSize);
|
||||||
@ -257,6 +266,12 @@ namespace Peek.FilePreviewer.Previewers
|
|||||||
|
|
||||||
Preview = source;
|
Preview = source;
|
||||||
}
|
}
|
||||||
|
else if (IsQoi(Item))
|
||||||
|
{
|
||||||
|
using var bitmap = QoiImage.FromStream(stream);
|
||||||
|
|
||||||
|
Preview = await BitmapHelper.BitmapToImageSource(bitmap, true, cancellationToken);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var bitmap = new BitmapImage();
|
var bitmap = new BitmapImage();
|
||||||
@ -286,6 +301,11 @@ namespace Peek.FilePreviewer.Previewers
|
|||||||
return item.Extension == ".svg";
|
return item.Extension == ".svg";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsQoi(IFileSystemItem item)
|
||||||
|
{
|
||||||
|
return item.Extension == ".qoi";
|
||||||
|
}
|
||||||
|
|
||||||
private void Clear()
|
private void Clear()
|
||||||
{
|
{
|
||||||
lowQualityThumbnailPreview = null;
|
lowQualityThumbnailPreview = null;
|
||||||
@ -367,6 +387,8 @@ namespace Peek.FilePreviewer.Previewers
|
|||||||
".cr3",
|
".cr3",
|
||||||
|
|
||||||
".svg",
|
".svg",
|
||||||
|
|
||||||
|
".qoi",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,6 +61,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\common\FilePreviewCommon\FilePreviewCommon.csproj" />
|
||||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||||
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using Common;
|
using Common;
|
||||||
using Common.Utilities;
|
using Microsoft.PowerToys.FilePreviewCommon;
|
||||||
using Microsoft.PowerToys.PreviewHandler.Gcode.Telemetry.Events;
|
using Microsoft.PowerToys.PreviewHandler.Gcode.Telemetry.Events;
|
||||||
using Microsoft.PowerToys.Telemetry;
|
using Microsoft.PowerToys.Telemetry;
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
using System.Drawing.Drawing2D;
|
using System.Drawing.Drawing2D;
|
||||||
using System.Drawing.Imaging;
|
using System.Drawing.Imaging;
|
||||||
using Common.Utilities;
|
using Microsoft.PowerToys.FilePreviewCommon;
|
||||||
|
|
||||||
namespace Microsoft.PowerToys.ThumbnailHandler.Gcode
|
namespace Microsoft.PowerToys.ThumbnailHandler.Gcode
|
||||||
{
|
{
|
||||||
|
@ -46,6 +46,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\common\FilePreviewCommon\FilePreviewCommon.csproj" />
|
||||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||||
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||||
<ProjectReference Include="..\Common\PreviewHandlerCommon.csproj" />
|
<ProjectReference Include="..\Common\PreviewHandlerCommon.csproj" />
|
||||||
|
@ -61,6 +61,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||||
|
<ProjectReference Include="..\..\..\common\FilePreviewCommon\FilePreviewCommon.csproj" />
|
||||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||||
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||||
|
@ -3,9 +3,9 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
using Common;
|
using Common;
|
||||||
|
using Microsoft.PowerToys.FilePreviewCommon;
|
||||||
using Microsoft.PowerToys.PreviewHandler.Qoi.Telemetry.Events;
|
using Microsoft.PowerToys.PreviewHandler.Qoi.Telemetry.Events;
|
||||||
using Microsoft.PowerToys.Telemetry;
|
using Microsoft.PowerToys.Telemetry;
|
||||||
using PreviewHandlerCommon.Utilities;
|
|
||||||
|
|
||||||
namespace Microsoft.PowerToys.PreviewHandler.Qoi
|
namespace Microsoft.PowerToys.PreviewHandler.Qoi
|
||||||
{
|
{
|
||||||
@ -63,7 +63,7 @@ namespace Microsoft.PowerToys.PreviewHandler.Qoi
|
|||||||
throw new ArgumentException($"{nameof(dataSource)} for {nameof(QoiPreviewHandlerControl)} must be a string but was a '{typeof(T)}'");
|
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);
|
thumbnail = QoiImage.FromStream(fs);
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
using System.Drawing.Drawing2D;
|
using System.Drawing.Drawing2D;
|
||||||
using System.Drawing.Imaging;
|
using System.Drawing.Imaging;
|
||||||
using PreviewHandlerCommon.Utilities;
|
using Microsoft.PowerToys.FilePreviewCommon;
|
||||||
|
|
||||||
namespace Microsoft.PowerToys.ThumbnailHandler.Qoi
|
namespace Microsoft.PowerToys.ThumbnailHandler.Qoi
|
||||||
{
|
{
|
||||||
|
@ -46,6 +46,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\common\FilePreviewCommon\FilePreviewCommon.csproj" />
|
||||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||||
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
<ProjectReference Include="..\..\..\common\ManagedTelemetry\Telemetry\ManagedTelemetry.csproj" />
|
||||||
<ProjectReference Include="..\Common\PreviewHandlerCommon.csproj" />
|
<ProjectReference Include="..\Common\PreviewHandlerCommon.csproj" />
|
||||||
|
@ -10,7 +10,6 @@
|
|||||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
@ -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
|
|
||||||
{
|
|
||||||
/// <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");
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user