diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01fac699..a0b74a14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,16 @@ and the packages use [Semantic Versioning](https://semver.org/). Finding IDs suc
Version 3.0 fixes a large number of interop bugs and deliberately breaks APIs whose concept was wrong.
Read the [migration guide](doc/articles/migration-3.0.md) before upgrading.
+## [3.18.0]
+
+### Added
+- New package Dapplo.Windows.Imaging: the Windows Imaging Component (WIC) without WPF, System.Drawing or other pixel libraries, see [Images through WIC](doc/articles/imaging.md).
+- `WicCodecs.GetDecoders(refresh)` / `GetEncoders(refresh)`: every codec WIC reports (`IWICImagingFactory::CreateComponentEnumerator`), also the built-in codecs and the packaged (Store) codecs like HEIF/AVIF, WebP, Raw and JPEG XL, which the registry key of the WIC decoder category doesn't list on newer Windows versions. `WicCodecInfo` has the CLSID, container format, vendor, friendly name, author, version, file extensions (lower case with the dot) and MIME types (lower case, without duplicates) and the signing status. A codec which can't be read is skipped and logged.
+- `WicImage.Encode`: from memory (pointer and stride, e.g. GDI+ `BitmapData.Scan0`, read without a copy through an own `IWICBitmapSource`) or from a `WicRowProvider` callback (the encoder pulls the rows, e.g. for ImageSharp images which aren't contiguous), into a .NET `Stream`. `WicEncodeOptions` with the container format, `ImageQuality`, `Lossless`, any other encoder option by name (`WicImage.GetEncoderOptions` lists them, unknown options are skipped) and the resolution. WIC converts the pixels when the encoder wants another pixel format, indexed formats (GIF) get a palette from the pixels.
+- `WicImage.GetFrameInfo` and `WicImage.Decode` (into memory, into memory supplied once the size is known, or to a `WicRowConsumer` in bands): the first frame, converted to the requested pixel format, optionally with the EXIF orientation applied (`IWICBitmapFlipRotator`).
+- `WicCodecNotFoundException` when there is no codec for the container format or the stream (WINCODEC_ERR_COMPONENTNOTFOUND), or the codec can't be initialized (WINCODEC_ERR_COMPONENTINITIALIZEFAILURE), so callers can fall back.
+- Metadata through the WIC metadata query language: `WicFrameInfo.Metadata` (`WicMetadata`: every item with its path, `TryGet`, `TryGetRational`, `Orientation`, the ICC `ColorProfile` and the EXIF/XMP/IPTC blocks as `RawBlocks`), and `WicEncodeOptions.Metadata` with a `WicMetadataWriter` (`Set`, `Remove`, `SetColorProfile`, `AddRawBlock`); what an encoder doesn't accept is reported in `WicEncodeResult.MetadataIssues`, never a failure. `WicMetadataPaths` has the paths per container, `WicPixelFormats`, `WicContainerFormats`, `WicMetadataFormats` and `WicVendors` the GUIDs.
+
## [3.17.0]
### Added
diff --git a/README.md b/README.md
index 0ed9d8e0..91c8811c 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@ Upgrading from 2.x? Version 3.0 changes many APIs on purpose: read the
| [Dapplo.Windows.InstallerManager](https://www.nuget.org/packages/Dapplo.Windows.InstallerManager) | [](https://www.nuget.org/packages/Dapplo.Windows.InstallerManager) | For installers: find, close and restart the processes which lock files | - |
| [Dapplo.Windows.Dialogs](https://www.nuget.org/packages/Dapplo.Windows.Dialogs) | [](https://www.nuget.org/packages/Dapplo.Windows.Dialogs) | File open, file save and folder dialogs without WinForms or WPF | Common |
| [Dapplo.Windows.Icons](https://www.nuget.org/packages/Dapplo.Windows.Icons) | [](https://www.nuget.org/packages/Dapplo.Windows.Icons) | Extract icons, write ICO and CUR files, capture the mouse cursor | Common, Dpi, Gdi32, Kernel32, Messages, Shell32, User32 |
+| [Dapplo.Windows.Imaging](https://www.nuget.org/packages/Dapplo.Windows.Imaging) | [](https://www.nuget.org/packages/Dapplo.Windows.Imaging) | WIC: the codecs of the PC (also HEIF, WebP, JPEG XL), encode and decode images, metadata | Com, Common |
| [Dapplo.Windows.Devices](https://www.nuget.org/packages/Dapplo.Windows.Devices) | [](https://www.nuget.org/packages/Dapplo.Windows.Devices) | Device and volume arrival / removal notifications | Messages |
| [Dapplo.Windows.DesktopWindowsManager](https://www.nuget.org/packages/Dapplo.Windows.DesktopWindowsManager) | [](https://www.nuget.org/packages/Dapplo.Windows.DesktopWindowsManager) | DWM: frame bounds, cloaking, accent color, corners | Common |
| [Dapplo.Windows.Citrix](https://www.nuget.org/packages/Dapplo.Windows.Citrix) | [](https://www.nuget.org/packages/Dapplo.Windows.Citrix) | Citrix session detection and client information | Common |
@@ -114,6 +115,7 @@ short and use `ObserveOn` for UI work. [Getting started](doc/articles/intro.md#h
- [Power and system state](doc/articles/system-state.md)
- [Restart Manager](doc/articles/restart-manager.md)
- [Icons and cursors](doc/articles/icons.md)
+- [Images through WIC](doc/articles/imaging.md)
- [File and folder dialogs](doc/articles/dialogs.md)
- [Windows Forms and WPF](doc/articles/forms-and-wpf.md)
- [More packages](doc/articles/more-packages.md)
diff --git a/doc/articles/imaging.md b/doc/articles/imaging.md
new file mode 100644
index 00000000..35509e8f
--- /dev/null
+++ b/doc/articles/imaging.md
@@ -0,0 +1,267 @@
+# Images through WIC
+
+**Dapplo.Windows.Imaging** uses the Windows Imaging Component (WIC) directly: it lists the image codecs of the PC, encodes
+and decodes images, and reads and writes their metadata. It needs no WPF, no System.Drawing and no other pixel library:
+pixels are passed as memory (a pointer and a stride) or row by row through a callback, and the input and output are .NET
+streams.
+
+```powershell
+dotnet add package Dapplo.Windows.Imaging
+```
+
+Namespace used on this page: `Dapplo.Windows.Imaging`.
+
+Every COM object is created per call and released before the call returns, and every method works from STA and MTA
+threads.
+
+## Which formats the PC can read and write
+
+WIC lists every codec it has, including the packaged (Store) codecs like HEIF (also AVIF), WebP, Raw and JPEG XL. The
+registry key of the WIC decoder category (`HKCR\CLSID\{7ED96837-96F0-4812-B211-F13C24117ED3}\Instance`) doesn't list
+the built-in codecs on newer Windows versions and never lists the packaged ones, `WicCodecs` asks WIC itself.
+
+
+```csharp
+// Every decoder WIC has, also the packaged (Store) codecs like HEIF, AVIF, WebP and JPEG XL
+foreach (var decoder in WicCodecs.GetDecoders())
+{
+ Console.WriteLine($"{decoder.FriendlyName}: {string.Join(" ", decoder.FileExtensions)} ({string.Join(" ", decoder.MimeTypes)})");
+}
+
+// Can this PC write JPEG XL?
+bool canWriteJpegXl = WicCodecs.GetEncoders().Any(encoder => encoder.ContainerFormat == WicContainerFormats.JpegXl);
+```
+
+Each `WicCodecInfo` has the CLSID, the container format, the vendor, the friendly name, author and version, the file
+extensions (lower case with the dot, e.g. `.heic`) and MIME types (lower case), and the signing status. `IsFromMicrosoft`
+is true for the codecs of Windows and for the Store codecs of Microsoft. A codec which can't be read is left out (and
+logged), it doesn't break the list. `GetDecoders(refresh: true)` lets WIC refresh its list, e.g. after a codec was
+installed.
+
+## Encoding
+
+`WicImage.Encode` takes the pixels as memory, for example the `Scan0` of a locked GDI+ bitmap or a contiguous ImageSharp
+buffer, and reads them without a copy of the image.
+
+
+```csharp
+// Save a GDI+ bitmap as lossless JPEG XL, the pixels are read straight from the locked bitmap
+var bitmapData = bitmap.LockBits(new Rectangle(Point.Empty, bitmap.Size), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
+try
+{
+ var options = new WicEncodeOptions
+ {
+ ContainerFormat = WicContainerFormats.JpegXl,
+ Lossless = true,
+ DpiX = bitmap.HorizontalResolution,
+ DpiY = bitmap.VerticalResolution,
+ Metadata = metadata => metadata.Set("System.ApplicationName", "Greenshot")
+ };
+ // Format32bppArgb has the byte order B, G, R, A: WicPixelFormats.Format32bppBgra
+ WicEncodeResult result = WicImage.Encode(destination, options, bitmapData.Scan0, bitmapData.Width, bitmapData.Height, bitmapData.Stride, WicPixelFormats.Format32bppBgra);
+}
+catch (WicCodecNotFoundException)
+{
+ // The JPEG XL extension isn't installed (or can't be used), fall back to PNG
+}
+finally
+{
+ bitmap.UnlockBits(bitmapData);
+}
+```
+
+The pixel formats are WIC GUIDs, `WicPixelFormats` has the common ones: `Format32bppBgra` (GDI+ `Format32bppArgb`,
+ImageSharp `Bgra32`), `Format32bppPbgra`, `Format24bppBgr` (GDI+ `Format24bppRgb`, ImageSharp `Bgr24`),
+`Format32bppRgba` (ImageSharp `Rgba32`), and `Format64bppRgbaHalf` and `Format128bppRgbaFloat` for HDR. When the encoder
+wants another pixel format (e.g. JPEG has no alpha), WIC converts the pixels while it reads them;
+`WicEncodeResult.PixelFormat` is the format which was written. For an indexed format (GIF) a palette is made from the
+pixels, with a transparent color when the source has alpha.
+
+When the pixels aren't in one buffer (ImageSharp can split a large image), the other overload takes a callback. The
+encoder pulls the rows it needs, no copy of the image is made.
+
+
+```csharp
+// Pixels which aren't in one buffer (e.g. ImageSharp ProcessPixelRows): the encoder pulls the rows it needs
+WicImage.Encode(destination, new WicEncodeOptions { ContainerFormat = WicContainerFormats.Png }, width, height, WicPixelFormats.Format32bppBgra,
+ (top, count, rows, stride) =>
+ {
+ for (int row = 0; row < count; row++)
+ {
+ getRow(top + row).AsSpan().CopyTo(rows.Slice(row * stride));
+ }
+ });
+```
+
+`ImageQuality` (0..1) and `Lossless` are set when the encoder has them, `EncoderOptions` takes any other option by
+name. An option the encoder doesn't have is skipped; `WicEncodeResult.AppliedOptions` lists what was set and
+`WicImage.GetEncoderOptions(containerFormat)` lists what an encoder has:
+
+| Encoder | Options |
+|---|---|
+| JPEG | ImageQuality, BitmapTransform, SuppressApp0, JpegYCrCbSubsampling, Luminance, Chrominance and the Huffman tables |
+| TIFF | TiffCompressionMethod, CompressionQuality |
+| PNG | InterlaceOption, FilterOption |
+| JPEG XR | ImageQuality, Lossless, BitmapTransform, UseCodecOptions, Quality, Overlap, Subsampling, tile slices, FrequencyOrder, InterleavedAlpha, AlphaQuality ... |
+| BMP | EnableV5Header32bppBGRA |
+| GIF | none |
+
+A stream which can't seek is written through a `MemoryStream`. A seekable stream is written from its current position
+and ends up after the image.
+
+## Decoding
+
+Decoding reads the first frame and converts it to the pixel format you ask for. `GetFrameInfo` reads only the size,
+resolution, pixel format and metadata. `Decode` writes the pixels into your memory, either memory you allocated with
+the size from `GetFrameInfo`, or memory you supply once the size is known, so the stream is read once:
+
+
+```csharp
+// Load any image WIC can read into a new GDI+ bitmap, upright, without WPF and without an extra copy
+Bitmap bitmap = null;
+BitmapData bitmapData = null;
+try
+{
+ WicFrameInfo info = WicImage.Decode(source, WicPixelFormats.Format32bppBgra, (WicFrameInfo frame, out int stride) =>
+ {
+ bitmap = new Bitmap(frame.Width, frame.Height, PixelFormat.Format32bppArgb);
+ bitmap.SetResolution((float)frame.DpiX, (float)frame.DpiY);
+ bitmapData = bitmap.LockBits(new Rectangle(0, 0, frame.Width, frame.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
+ stride = bitmapData.Stride;
+ return bitmapData.Scan0;
+ }, applyOrientation: true);
+}
+finally
+{
+ if (bitmapData != null)
+ {
+ bitmap.UnlockBits(bitmapData);
+ }
+}
+```
+
+With `applyOrientation: true` the EXIF orientation is applied (rotated and flipped before the pixel format conversion),
+`WicFrameInfo.Width` and `Height` are then those of the upright image. A third overload hands the rows to a
+`WicRowConsumer`, in bands from a pooled buffer.
+
+## When there is no codec
+
+When WIC has no codec for the container format or doesn't recognize the stream, a `WicCodecNotFoundException` (a
+`COMException`) is thrown, so you can fall back. It is also thrown when a codec is registered but can't be initialized:
+on the CI runner (Windows Server) the HEIF and JPEG XL encoders are listed but fail with
+`WINCODEC_ERR_COMPONENTINITIALIZEFAILURE` (0x88982F8B); HEIF encoding needs the HEVC video extension. Other WIC
+failures are a `COMException` with the HRESULT, and an exception of your stream or row callback reaches you unchanged.
+
+## Metadata
+
+The metadata uses the [WIC metadata query language](https://learn.microsoft.com/en-us/windows/win32/wic/-wic-codec-metadataquerylanguage),
+so it is not a fixed set of properties. The same EXIF tag has a different path in each container, `WicMetadataPaths` has
+them, e.g. `Software(WicContainerFormats.Jpeg)` is `/app1/ifd/{ushort=305}` and `Software(WicContainerFormats.Tiff)` is
+`/ifd/{ushort=305}`.
+
+### Writing
+
+`WicEncodeOptions.Metadata` is called after the frame is initialized and before the pixels are written. Everything the
+encoder doesn't accept is skipped and listed in `WicEncodeResult.MetadataIssues` (and logged), it never fails the
+encoding.
+
+
+```csharp
+var container = WicContainerFormats.Jpeg;
+var options = new WicEncodeOptions
+{
+ ContainerFormat = container,
+ ImageQuality = 0.9f,
+ Metadata = metadata =>
+ {
+ // The EXIF ASCII tags are VT_LPSTR
+ metadata.Set(WicMetadataPaths.Software(container), WicMetadataValue.AnsiString("Greenshot"));
+ metadata.Set(WicMetadataPaths.DateTimeOriginal(container), WicMetadataValue.AnsiString(DateTime.Now.ToString("yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture)));
+ // dc:title becomes a language alternative with x-default
+ metadata.Set(WicMetadataPaths.XmpProperty(container, "dc:title"), "Capture of the Explorer window");
+ metadata.Set(WicMetadataPaths.XmpProperty(container, "xmp:CreatorTool"), "Greenshot");
+ metadata.SetColorProfile(iccProfile);
+ }
+};
+var result = WicImage.Encode(destination, options, pixels, width, height, stride, WicPixelFormats.Format32bppBgra);
+foreach (var issue in result.MetadataIssues)
+{
+ Console.WriteLine($"Not written: {issue}");
+}
+```
+
+Values: `string` is written as VT_LPWSTR, `WicMetadataValue.AnsiString` as VT_LPSTR (the EXIF ASCII tags like Software,
+DateTime and DateTimeOriginal), `bool`, the integer types, `float`, `double`, `WicRational` and `WicSignedRational` (EXIF
+RATIONAL and SRATIONAL), `DateTime` (FILETIME), `Guid`, `byte[]` (VT_BLOB, or `WicMetadataValue.ByteVector` for
+VT_VECTOR | VT_UI1), `ushort[]`, `uint[]`, `int[]` and `string[]`.
+
+The photo metadata policies of Windows, e.g. `System.ApplicationName`, `System.Title`, `System.Comment` and
+`System.Photo.Orientation`, write every location the container has: `System.ApplicationName` writes the EXIF Software
+and the XMP xmp:CreatorTool, `System.Title` the EXIF ImageDescription and the XMP dc:title as language alternative. A
+plain string at `/xmp/dc:title` makes a simple XMP property.
+
+### Reading
+
+`WicFrameInfo.Metadata` is a snapshot of every readable item of the first frame, walked through the nested blocks.
+
+
+```csharp
+WicFrameInfo info = WicImage.GetFrameInfo(source);
+WicMetadata metadata = info.Metadata;
+if (metadata.TryGet(WicMetadataPaths.Software(info.ContainerFormat), out var software))
+{
+ Console.WriteLine($"Written by {software}");
+}
+int? orientation = metadata.Orientation; // 1..8
+byte[] icc = metadata.ColorProfile; // null without a profile
+foreach (var item in metadata.Items)
+{
+ // e.g. "/app1/ifd/exif/{ushort=36867}" = "2026:11:02 12:34:56"
+ Console.WriteLine($"{item.Key} = {item.Value}");
+}
+```
+
+The paths use the names of the blocks (`/app1/ifd/exif`), the paths with the tags as WIC enumerates them
+(`/app1/{ushort=0}/{ushort=34665}`) work for `TryGet` too. EXIF rationals are stored by WIC as `ulong` / `long`
+(numerator in the low 32 bits), `TryGetRational` converts them. A nested block is listed as a `WicMetadataBlock` with its
+metadata format. `Orientation` is read with the path of the container or `System.Photo.Orientation`, `ColorProfile` is
+the ICC profile of the first color context.
+
+### Raw blocks
+
+To carry metadata over unchanged, `RawBlocks` has the EXIF, XMP and IPTC blocks as bytes, and `AddRawBlock` writes one:
+
+
+```csharp
+// Copy the XMP block of the original unchanged into the new file
+var original = WicImage.GetFrameInfo(source).Metadata;
+WicImage.Encode(destination, new WicEncodeOptions
+{
+ ContainerFormat = WicContainerFormats.Jpeg,
+ Metadata = metadata =>
+ {
+ foreach (var block in original.RawBlocks.Where(block => block.Format == WicMetadataFormats.Xmp))
+ {
+ metadata.AddRawBlock(new WicMetadataRawBlock(WicMetadataPaths.Xmp(WicContainerFormats.Jpeg), block.Format, block.Data));
+ }
+ }
+}, pixels, width, height, stride, WicPixelFormats.Format32bppBgra);
+```
+
+### What the codecs of Windows accept
+
+Tested with the codecs of Windows on the CI runner (Windows Server 2025, `windows-latest`), see `WicMetadataTests`:
+
+| | JPEG | TIFF | PNG | JPEG XR | GIF | BMP |
+|---|---|---|---|---|---|---|
+| EXIF tags (Software, ImageDescription, Orientation, XResolution) | `/app1/ifd/{ushort=n}` | `/ifd/{ushort=n}` | - | `/ifd/{ushort=n}` | - | - |
+| EXIF sub IFD (DateTimeOriginal, ExposureTime) | `/app1/ifd/exif/{ushort=n}` | `/ifd/exif/{ushort=n}` | - | `/ifd/exif/{ushort=n}` | - | - |
+| XMP | `/xmp/...` | `/ifd/xmp/...` | `/xmp/...` | `/ifd/xmp/...` | - | - |
+| PNG text | - | - | `/tEXt/{str=keyword}`, `/[1]tEXt/...`, `/iTXt/...` | - | - | - |
+| Policies (System.ApplicationName, System.Title, System.Comment, System.Photo.Orientation) | yes | yes | yes | yes | - | - |
+| Color profile | yes | yes | yes (iCCP) | yes | - | - |
+| Raw blocks | `/app1`, `/xmp` | `/ifd/xmp` (tag 700) | `/xmp` | `/ifd/xmp` (tag 700) | - | - |
+
+GIF has only its own blocks (`/imgdesc`, `/grctlext`, ...), BMP has no metadata. A PNG tEXt chunk holds one keyword: a
+second keyword at `/tEXt` replaces the first, give each its own chunk index (`WicMetadataPaths.PngText("Description", 1)`).
+HEIF and JPEG XL couldn't be tested on the CI runner (their encoders don't initialize there); their decoders are listed.
diff --git a/doc/articles/intro.md b/doc/articles/intro.md
index bde02cfd..0e5d8451 100644
--- a/doc/articles/intro.md
+++ b/doc/articles/intro.md
@@ -30,6 +30,7 @@ Install the package for the feature you need, NuGet brings in its dependencies.
| **Dapplo.Windows.InstallerManager** | For installers: find the processes which lock files, close and restart them (Restart Manager) | - |
| **Dapplo.Windows.Dialogs** | Modern file open, file save and folder picker dialogs, without WinForms or WPF | Common |
| **Dapplo.Windows.Icons** | Extract icons, write ICO and CUR files, capture the mouse cursor | Common, Dpi, Gdi32, Kernel32, Messages, Shell32, User32 |
+| **Dapplo.Windows.Imaging** | WIC: the image codecs of the PC (also HEIF, WebP, JPEG XL), encode and decode images, read and write metadata | Com, Common |
| **Dapplo.Windows.Devices** | Device arrival and removal notifications (USB, volumes, ...) | Messages |
| **Dapplo.Windows.DesktopWindowsManager** | DWM: extended frame bounds, cloaking, accent color, corner preference | Common |
| **Dapplo.Windows.Citrix** | Detect a Citrix session and query client information | Common |
@@ -174,6 +175,7 @@ Version 3.0 changes many APIs on purpose. Read the [migration guide](migration-3
- [Power and system state](system-state.md)
- [Restart Manager](restart-manager.md)
- [Icons and cursors](icons.md)
+- [Images through WIC](imaging.md)
- [File and folder dialogs](dialogs.md)
- [Windows Forms and WPF](forms-and-wpf.md)
- [More packages](more-packages.md)
diff --git a/doc/articles/toc.yml b/doc/articles/toc.yml
index 996cbf7b..4c8a0f30 100644
--- a/doc/articles/toc.yml
+++ b/doc/articles/toc.yml
@@ -16,6 +16,8 @@
href: restart-manager.md
- name: Icons and cursors
href: icons.md
+- name: Images through WIC
+ href: imaging.md
- name: File and folder dialogs
href: dialogs.md
- name: Windows Forms and WPF
diff --git a/doc/index.md b/doc/index.md
index 5d820ddf..c487faa3 100644
--- a/doc/index.md
+++ b/doc/index.md
@@ -23,6 +23,7 @@ packages for .NET Framework 4.8 and .NET 10.
| [Keep the PC awake, sleep, shut down, wake timers, power events](articles/system-state.md) | Dapplo.Windows.SystemState |
| [Survive updates: restart registration and installer support](articles/restart-manager.md) | Dapplo.Windows.AppRestartManager, .InstallerManager |
| [Extract icons, write ICO / CUR files, capture the cursor](articles/icons.md) | Dapplo.Windows.Icons |
+| [Read and write images and their metadata through WIC](articles/imaging.md) | Dapplo.Windows.Imaging |
| [File and folder dialogs without WinForms or WPF](articles/dialogs.md) | Dapplo.Windows.Dialogs |
| [Windows Forms and WPF integration](articles/forms-and-wpf.md) | Dapplo.Windows.Forms, .Wpf |
| [Citrix, DWM, devices, registry, sounds and more](articles/more-packages.md) | several |
diff --git a/src/Dapplo.Windows.Example.DocSamples/Dapplo.Windows.Example.DocSamples.csproj b/src/Dapplo.Windows.Example.DocSamples/Dapplo.Windows.Example.DocSamples.csproj
index a1423712..b246dfaf 100644
--- a/src/Dapplo.Windows.Example.DocSamples/Dapplo.Windows.Example.DocSamples.csproj
+++ b/src/Dapplo.Windows.Example.DocSamples/Dapplo.Windows.Example.DocSamples.csproj
@@ -29,6 +29,7 @@
+
diff --git a/src/Dapplo.Windows.Example.DocSamples/ImagingSamples.cs b/src/Dapplo.Windows.Example.DocSamples/ImagingSamples.cs
new file mode 100644
index 00000000..a20a3312
--- /dev/null
+++ b/src/Dapplo.Windows.Example.DocSamples/ImagingSamples.cs
@@ -0,0 +1,169 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using Dapplo.Windows.Imaging;
+
+namespace Dapplo.Windows.Example.DocSamples;
+
+///
+/// Samples for doc/articles/imaging.md
+///
+public static class ImagingSamples
+{
+ public static void DiscoverCodecs()
+ {
+ #region DiscoverCodecs
+ // Every decoder WIC has, also the packaged (Store) codecs like HEIF, AVIF, WebP and JPEG XL
+ foreach (var decoder in WicCodecs.GetDecoders())
+ {
+ Console.WriteLine($"{decoder.FriendlyName}: {string.Join(" ", decoder.FileExtensions)} ({string.Join(" ", decoder.MimeTypes)})");
+ }
+
+ // Can this PC write JPEG XL?
+ bool canWriteJpegXl = WicCodecs.GetEncoders().Any(encoder => encoder.ContainerFormat == WicContainerFormats.JpegXl);
+ #endregion
+ }
+
+ public static void EncodeBitmap(Bitmap bitmap, Stream destination)
+ {
+ #region EncodeBitmap
+ // Save a GDI+ bitmap as lossless JPEG XL, the pixels are read straight from the locked bitmap
+ var bitmapData = bitmap.LockBits(new Rectangle(Point.Empty, bitmap.Size), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
+ try
+ {
+ var options = new WicEncodeOptions
+ {
+ ContainerFormat = WicContainerFormats.JpegXl,
+ Lossless = true,
+ DpiX = bitmap.HorizontalResolution,
+ DpiY = bitmap.VerticalResolution,
+ Metadata = metadata => metadata.Set("System.ApplicationName", "Greenshot")
+ };
+ // Format32bppArgb has the byte order B, G, R, A: WicPixelFormats.Format32bppBgra
+ WicEncodeResult result = WicImage.Encode(destination, options, bitmapData.Scan0, bitmapData.Width, bitmapData.Height, bitmapData.Stride, WicPixelFormats.Format32bppBgra);
+ }
+ catch (WicCodecNotFoundException)
+ {
+ // The JPEG XL extension isn't installed (or can't be used), fall back to PNG
+ }
+ finally
+ {
+ bitmap.UnlockBits(bitmapData);
+ }
+ #endregion
+ }
+
+ public static Bitmap DecodeIntoBitmap(Stream source)
+ {
+ #region DecodeIntoBitmap
+ // Load any image WIC can read into a new GDI+ bitmap, upright, without WPF and without an extra copy
+ Bitmap bitmap = null;
+ BitmapData bitmapData = null;
+ try
+ {
+ WicFrameInfo info = WicImage.Decode(source, WicPixelFormats.Format32bppBgra, (WicFrameInfo frame, out int stride) =>
+ {
+ bitmap = new Bitmap(frame.Width, frame.Height, PixelFormat.Format32bppArgb);
+ bitmap.SetResolution((float)frame.DpiX, (float)frame.DpiY);
+ bitmapData = bitmap.LockBits(new Rectangle(0, 0, frame.Width, frame.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
+ stride = bitmapData.Stride;
+ return bitmapData.Scan0;
+ }, applyOrientation: true);
+ }
+ finally
+ {
+ if (bitmapData != null)
+ {
+ bitmap.UnlockBits(bitmapData);
+ }
+ }
+ #endregion
+ return bitmap;
+ }
+
+ public static void EncodeRows(Stream destination, int width, int height, Func getRow)
+ {
+ #region EncodeRows
+ // Pixels which aren't in one buffer (e.g. ImageSharp ProcessPixelRows): the encoder pulls the rows it needs
+ WicImage.Encode(destination, new WicEncodeOptions { ContainerFormat = WicContainerFormats.Png }, width, height, WicPixelFormats.Format32bppBgra,
+ (top, count, rows, stride) =>
+ {
+ for (int row = 0; row < count; row++)
+ {
+ getRow(top + row).AsSpan().CopyTo(rows.Slice(row * stride));
+ }
+ });
+ #endregion
+ }
+
+ public static void WriteMetadata(Stream destination, IntPtr pixels, int width, int height, int stride, byte[] iccProfile)
+ {
+ #region WriteMetadata
+ var container = WicContainerFormats.Jpeg;
+ var options = new WicEncodeOptions
+ {
+ ContainerFormat = container,
+ ImageQuality = 0.9f,
+ Metadata = metadata =>
+ {
+ // The EXIF ASCII tags are VT_LPSTR
+ metadata.Set(WicMetadataPaths.Software(container), WicMetadataValue.AnsiString("Greenshot"));
+ metadata.Set(WicMetadataPaths.DateTimeOriginal(container), WicMetadataValue.AnsiString(DateTime.Now.ToString("yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture)));
+ // dc:title becomes a language alternative with x-default
+ metadata.Set(WicMetadataPaths.XmpProperty(container, "dc:title"), "Capture of the Explorer window");
+ metadata.Set(WicMetadataPaths.XmpProperty(container, "xmp:CreatorTool"), "Greenshot");
+ metadata.SetColorProfile(iccProfile);
+ }
+ };
+ var result = WicImage.Encode(destination, options, pixels, width, height, stride, WicPixelFormats.Format32bppBgra);
+ foreach (var issue in result.MetadataIssues)
+ {
+ Console.WriteLine($"Not written: {issue}");
+ }
+ #endregion
+ }
+
+ public static void ReadMetadata(Stream source)
+ {
+ #region ReadMetadata
+ WicFrameInfo info = WicImage.GetFrameInfo(source);
+ WicMetadata metadata = info.Metadata;
+ if (metadata.TryGet(WicMetadataPaths.Software(info.ContainerFormat), out var software))
+ {
+ Console.WriteLine($"Written by {software}");
+ }
+ int? orientation = metadata.Orientation; // 1..8
+ byte[] icc = metadata.ColorProfile; // null without a profile
+ foreach (var item in metadata.Items)
+ {
+ // e.g. "/app1/ifd/exif/{ushort=36867}" = "2026:11:02 12:34:56"
+ Console.WriteLine($"{item.Key} = {item.Value}");
+ }
+ #endregion
+ }
+
+ public static void CarryOverXmp(Stream source, Stream destination, IntPtr pixels, int width, int height, int stride)
+ {
+ #region CarryOverXmp
+ // Copy the XMP block of the original unchanged into the new file
+ var original = WicImage.GetFrameInfo(source).Metadata;
+ WicImage.Encode(destination, new WicEncodeOptions
+ {
+ ContainerFormat = WicContainerFormats.Jpeg,
+ Metadata = metadata =>
+ {
+ foreach (var block in original.RawBlocks.Where(block => block.Format == WicMetadataFormats.Xmp))
+ {
+ metadata.AddRawBlock(new WicMetadataRawBlock(WicMetadataPaths.Xmp(WicContainerFormats.Jpeg), block.Format, block.Data));
+ }
+ }
+ }, pixels, width, height, stride, WicPixelFormats.Format32bppBgra);
+ #endregion
+ }
+}
diff --git a/src/Dapplo.Windows.Imaging/Dapplo.Windows.Imaging.csproj b/src/Dapplo.Windows.Imaging/Dapplo.Windows.Imaging.csproj
new file mode 100644
index 00000000..4bf9ccb5
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Dapplo.Windows.Imaging.csproj
@@ -0,0 +1,15 @@
+
+
+ Dapplo.Windows.Imaging has supporting code for the Windows Imaging Component (WIC), e.g. to find the image formats which can be read and written.
+ windows native wic imaging codecs
+ net480;net10.0-windows
+ true
+
+
+
+
+
+
+
+
+
diff --git a/src/Dapplo.Windows.Imaging/Internal/IFailureSource.cs b/src/Dapplo.Windows.Imaging/Internal/IFailureSource.cs
new file mode 100644
index 00000000..28720ccd
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Internal/IFailureSource.cs
@@ -0,0 +1,15 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Dapplo.Windows.Imaging.Internal;
+
+///
+/// A managed object called by WIC (stream, pixel source) which keeps the exception it turned into an HRESULT,
+/// so the original exception (e.g. an IOException of the stream or one of a row callback) reaches the caller.
+///
+internal interface IFailureSource
+{
+ Exception LastException { get; }
+}
diff --git a/src/Dapplo.Windows.Imaging/Internal/ManagedIStream.cs b/src/Dapplo.Windows.Imaging/Internal/ManagedIStream.cs
new file mode 100644
index 00000000..05480471
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Internal/ManagedIStream.cs
@@ -0,0 +1,188 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using STATSTG = System.Runtime.InteropServices.ComTypes.STATSTG;
+
+namespace Dapplo.Windows.Imaging.Internal;
+
+///
+/// COM IStream over a .NET Stream, the position of the stream when this is created is offset 0 for COM,
+/// so a decoder or encoder can work on a part of a larger stream. The stream must be seekable.
+/// Exceptions of the stream are kept in and become an HRESULT for COM.
+///
+[ComVisible(true)]
+internal sealed class ManagedIStream : IStream, IFailureSource
+{
+ private const int StreamSeekSet = 0;
+ private const int StreamSeekCur = 1;
+ private const int StreamSeekEnd = 2;
+ private const int StgTyStream = 2;
+ private const int StgmRead = 0x0;
+ private const int StgmWrite = 0x1;
+ private const int StgmReadWrite = 0x2;
+ private const int StgEInvalidFunction = unchecked((int)0x80030001);
+
+ private readonly Stream _stream;
+ private readonly long _origin;
+
+ public ManagedIStream(Stream stream)
+ {
+ _stream = stream ?? throw new ArgumentNullException(nameof(stream));
+ if (!stream.CanSeek)
+ {
+ throw new ArgumentException("The stream must be seekable", nameof(stream));
+ }
+ _origin = stream.Position;
+ }
+
+ public Exception LastException { get; private set; }
+
+ ///
+ /// The highest position (relative to the origin) which was written, the end of what an encoder wrote
+ ///
+ public long WrittenLength { get; private set; }
+
+ ///
+ /// Position the stream after what was written
+ ///
+ public void SeekToEndOfWritten() => _stream.Position = _origin + WrittenLength;
+
+ private T Guard(Func action)
+ {
+ try
+ {
+ return action();
+ }
+ catch (Exception ex)
+ {
+ LastException = ex;
+ throw;
+ }
+ }
+
+ public void Read(byte[] pv, int cb, IntPtr pcbRead)
+ {
+ var total = Guard(() =>
+ {
+ int read = 0;
+ while (read < cb)
+ {
+ var count = _stream.Read(pv, read, cb - read);
+ if (count <= 0)
+ {
+ break;
+ }
+ read += count;
+ }
+ return read;
+ });
+ if (pcbRead != IntPtr.Zero)
+ {
+ Marshal.WriteInt32(pcbRead, total);
+ }
+ }
+
+ public void Write(byte[] pv, int cb, IntPtr pcbWritten)
+ {
+ Guard(() =>
+ {
+ _stream.Write(pv, 0, cb);
+ WrittenLength = Math.Max(WrittenLength, _stream.Position - _origin);
+ return cb;
+ });
+ if (pcbWritten != IntPtr.Zero)
+ {
+ Marshal.WriteInt32(pcbWritten, cb);
+ }
+ }
+
+ public void Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition)
+ {
+ var position = Guard(() =>
+ {
+ long target = dwOrigin switch
+ {
+ StreamSeekSet => _origin + dlibMove,
+ StreamSeekCur => _stream.Position + dlibMove,
+ StreamSeekEnd => _stream.Length + dlibMove,
+ _ => throw new ArgumentOutOfRangeException(nameof(dwOrigin))
+ };
+ if (target < _origin)
+ {
+ throw new IOException("Seek before the start of the stream");
+ }
+ _stream.Position = target;
+ return target - _origin;
+ });
+ if (plibNewPosition != IntPtr.Zero)
+ {
+ Marshal.WriteInt64(plibNewPosition, position);
+ }
+ }
+
+ public void SetSize(long libNewSize) => Guard(() =>
+ {
+ _stream.SetLength(_origin + libNewSize);
+ return 0;
+ });
+
+ public void CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten)
+ {
+ var buffer = new byte[81920];
+ long copied = Guard(() =>
+ {
+ long total = 0;
+ while (total < cb)
+ {
+ var read = _stream.Read(buffer, 0, (int)Math.Min(buffer.Length, cb - total));
+ if (read <= 0)
+ {
+ break;
+ }
+ pstm.Write(buffer, read, IntPtr.Zero);
+ total += read;
+ }
+ return total;
+ });
+ if (pcbRead != IntPtr.Zero)
+ {
+ Marshal.WriteInt64(pcbRead, copied);
+ }
+ if (pcbWritten != IntPtr.Zero)
+ {
+ Marshal.WriteInt64(pcbWritten, copied);
+ }
+ }
+
+ public void Commit(int grfCommitFlags) => Guard(() =>
+ {
+ _stream.Flush();
+ return 0;
+ });
+
+ public void Revert()
+ {
+ // Not transacted, nothing to revert
+ }
+
+ public void LockRegion(long libOffset, long cb, int dwLockType) => throw new COMException("LockRegion is not supported", StgEInvalidFunction);
+
+ public void UnlockRegion(long libOffset, long cb, int dwLockType) => throw new COMException("UnlockRegion is not supported", StgEInvalidFunction);
+
+ public void Stat(out STATSTG pstatstg, int grfStatFlag)
+ {
+ var length = Guard(() => _stream.Length - _origin);
+ pstatstg = new STATSTG
+ {
+ type = StgTyStream,
+ cbSize = length,
+ grfMode = _stream.CanRead && _stream.CanWrite ? StgmReadWrite : _stream.CanWrite ? StgmWrite : StgmRead
+ };
+ }
+
+ public void Clone(out IStream ppstm) => throw new COMException("Clone is not supported", StgEInvalidFunction);
+}
diff --git a/src/Dapplo.Windows.Imaging/Internal/Wic.cs b/src/Dapplo.Windows.Imaging/Internal/Wic.cs
new file mode 100644
index 00000000..1d9b820e
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Internal/Wic.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Runtime.ExceptionServices;
+using System.Runtime.InteropServices;
+using Dapplo.Windows.Common.Enums;
+using Dapplo.Windows.Common.Extensions;
+using Dapplo.Windows.Imaging.Interop;
+
+namespace Dapplo.Windows.Imaging.Internal;
+
+///
+/// Shared helpers for the WIC calls
+///
+internal static class Wic
+{
+ private static readonly ConcurrentDictionary BitsPerPixelCache = new ConcurrentDictionary();
+
+ ///
+ /// Throw for a failed HRESULT: the exception of a managed stream or pixel source which caused it,
+ /// a for WINCODEC_ERR_COMPONENTNOTFOUND, otherwise a COMException.
+ ///
+ public static void Check(HResult hResult, string operation, IFailureSource first = null, IFailureSource second = null)
+ {
+ if (hResult.Succeeded())
+ {
+ return;
+ }
+ var cause = first?.LastException ?? second?.LastException;
+ if (cause != null)
+ {
+ ExceptionDispatchInfo.Capture(cause).Throw();
+ }
+ var message = $"{operation} failed with 0x{(int)hResult:X8}";
+ if (hResult == WicErrors.ComponentNotFound || hResult == WicErrors.ComponentInitializeFailure)
+ {
+ throw new WicCodecNotFoundException(message, (int)hResult);
+ }
+ throw new COMException(message, (int)hResult);
+ }
+
+ ///
+ /// Release a runtime callable wrapper, null and managed objects are ignored
+ ///
+ public static void Release(object comObject)
+ {
+ if (comObject != null && Marshal.IsComObject(comObject))
+ {
+ Marshal.ReleaseComObject(comObject);
+ }
+ }
+
+ ///
+ /// The bits per pixel of a WIC pixel format, from IWICPixelFormatInfo
+ ///
+ public static int GetBitsPerPixel(IWICImagingFactory factory, Guid pixelFormat)
+ {
+ if (BitsPerPixelCache.TryGetValue(pixelFormat, out var cached))
+ {
+ return cached;
+ }
+ var format = pixelFormat;
+ object componentInfo = null;
+ try
+ {
+ var hResult = factory.CreateComponentInfo(ref format, out componentInfo);
+ if (!hResult.Succeeded() || componentInfo is not IWICPixelFormatInfo pixelFormatInfo)
+ {
+ throw new ArgumentException($"{pixelFormat} is not a WIC pixel format (0x{(int)hResult:X8})", nameof(pixelFormat));
+ }
+ Check(pixelFormatInfo.GetBitsPerPixel(out var bitsPerPixel), "IWICPixelFormatInfo.GetBitsPerPixel");
+ BitsPerPixelCache[pixelFormat] = (int)bitsPerPixel;
+ return (int)bitsPerPixel;
+ }
+ finally
+ {
+ Release(componentInfo);
+ }
+ }
+
+ ///
+ /// The bytes of one row of pixels
+ ///
+ public static int RowBytes(int width, int bitsPerPixel) => checked((int)(((long)width * bitsPerPixel + 7) / 8));
+}
diff --git a/src/Dapplo.Windows.Imaging/Internal/WicMetadataReading.cs b/src/Dapplo.Windows.Imaging/Internal/WicMetadataReading.cs
new file mode 100644
index 00000000..feeacc93
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Internal/WicMetadataReading.cs
@@ -0,0 +1,371 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using Dapplo.Log;
+using Dapplo.Windows.Common.Extensions;
+using Dapplo.Windows.Imaging.Interop;
+
+namespace Dapplo.Windows.Imaging.Internal;
+
+///
+/// Reads the metadata of a decoded frame into a snapshot
+///
+internal static class WicMetadataReading
+{
+ private static readonly LogSource Log = new LogSource();
+
+ ///
+ /// Nested readers deeper than this are not walked (protection against broken files)
+ ///
+ private const int MaxDepth = 16;
+
+ ///
+ /// The names the metadata query language has for the nested blocks which WIC enumerates by their tag, e.g. "/app1/{ushort=0}" is "/app1/ifd"
+ ///
+ private static readonly Dictionary BlockAliases = new Dictionary
+ {
+ { WicMetadataFormats.Ifd, "ifd" },
+ { WicMetadataFormats.Exif, "exif" },
+ { WicMetadataFormats.Gps, "gps" },
+ { new Guid("ED686F8E-681F-4C8B-BD41-A8ADDBF6B3FC"), "interop" },
+ { WicMetadataFormats.Xmp, "xmp" },
+ { WicMetadataFormats.Iptc, "iptc" },
+ { new Guid("16100D66-8570-4BB9-B92D-FDA4B23ECE67"), "irb" },
+ { new Guid("243DCEE9-8703-40EE-8EF0-22A600B8058C"), "thumb" },
+ { WicMetadataFormats.App1, "app1" },
+ { WicMetadataFormats.App13, "app13" },
+ { new Guid("79007028-268D-45D6-A3C2-354E6A504BC9"), "app0" },
+ };
+
+ ///
+ /// The top-level blocks which are kept as raw bytes: the self-contained EXIF, XMP and IPTC blocks, not the image structure of a TIFF IFD or the JPEG tables
+ ///
+ private static readonly HashSet RawTopLevelFormats = new HashSet { WicMetadataFormats.App1, WicMetadataFormats.App13, WicMetadataFormats.Xmp, WicMetadataFormats.Iptc };
+
+ ///
+ /// The nested blocks which are kept as raw bytes, e.g. the XMP packet in the IFD of a TIFF
+ ///
+ private static readonly HashSet RawNestedFormats = new HashSet { WicMetadataFormats.Xmp, WicMetadataFormats.Iptc };
+
+ public static WicMetadata Read(IWICImagingFactory factory, IWICBitmapFrameDecode frame, Guid containerFormat)
+ {
+ var items = new List>();
+ var aliases = new List>();
+ int? orientation = null;
+
+ IWICMetadataQueryReader queryReader = null;
+ try
+ {
+ if (frame.GetMetadataQueryReader(out queryReader).Succeeded() && queryReader != null)
+ {
+ Walk(queryReader, string.Empty, string.Empty, items, aliases, 0);
+ orientation = ReadOrientation(queryReader, containerFormat);
+ }
+ }
+ catch (Exception ex) when (ex is COMException || ex is InvalidCastException)
+ {
+ Log.Warn().WriteLine(ex, "Reading the metadata failed, keeping {0} items", items.Count);
+ }
+ finally
+ {
+ Wic.Release(queryReader);
+ }
+
+ var rawBlocks = ReadRawBlocks(frame, containerFormat);
+ var colorProfile = ReadColorProfile(factory, frame);
+ return new WicMetadata(items, aliases, orientation, colorProfile, rawBlocks);
+ }
+
+ ///
+ /// Walk a query reader, the items get the friendly path (e.g. "/app1/ifd/exif/{ushort=36867}"), the path as WIC enumerates it (e.g. "/app1/{ushort=0}/{ushort=34665}/{ushort=36867}") is an alias for the lookup
+ ///
+ private static void Walk(IWICMetadataQueryReader reader, string prefix, string rawPrefix, List> items, List> aliases, int depth)
+ {
+ if (depth > MaxDepth || !reader.GetEnumerator(out var enumerator).Succeeded() || enumerator == null)
+ {
+ return;
+ }
+ var usedAliases = new HashSet(StringComparer.OrdinalIgnoreCase);
+ try
+ {
+ var names = new string[1];
+ while (enumerator.Next(1, names, IntPtr.Zero) == 0 && names[0] != null)
+ {
+ var name = names[0];
+ names[0] = null;
+ var rawPath = rawPrefix + name;
+ var propVariant = PropVariant.Allocate();
+ object value = null;
+ try
+ {
+ if (!reader.GetMetadataByName(name, propVariant).Succeeded())
+ {
+ continue;
+ }
+ if (!PropVariant.TryRead(propVariant, out value))
+ {
+ Log.Debug().WriteLine("Metadata {0} has the unsupported type {1}, skipping it", rawPath, PropVariant.GetVarType(propVariant));
+ continue;
+ }
+ if (value != null && Marshal.IsComObject(value))
+ {
+ if (value is IWICMetadataQueryReader nested)
+ {
+ nested.GetContainerFormat(out var format);
+ var friendlyName = name;
+ // Only a tag like "/{ushort=34665}" gets the name of its block, and only once per level
+ if (name.StartsWith("/{", StringComparison.Ordinal) && BlockAliases.TryGetValue(format, out var alias) && usedAliases.Add(alias))
+ {
+ friendlyName = "/" + alias;
+ }
+ var path = prefix + friendlyName;
+ var block = new WicMetadataBlock(path, format);
+ items.Add(new KeyValuePair(path, block));
+ if (!string.Equals(path, rawPath, StringComparison.OrdinalIgnoreCase))
+ {
+ aliases.Add(new KeyValuePair(rawPath, block));
+ }
+ Walk(nested, path, rawPath, items, aliases, depth + 1);
+ }
+ continue;
+ }
+ var itemPath = prefix + name;
+ items.Add(new KeyValuePair(itemPath, value));
+ if (!string.Equals(itemPath, rawPath, StringComparison.OrdinalIgnoreCase))
+ {
+ aliases.Add(new KeyValuePair(rawPath, value));
+ }
+ }
+ finally
+ {
+ Wic.Release(value);
+ PropVariant.Free(propVariant);
+ }
+ }
+ }
+ finally
+ {
+ Wic.Release(enumerator);
+ }
+ }
+
+ ///
+ /// The EXIF, XMP and IPTC blocks as raw bytes: the top-level blocks of IWICMetadataBlockReader and the XMP / IPTC blocks nested in them
+ ///
+ private static List ReadRawBlocks(IWICBitmapFrameDecode frame, Guid containerFormat)
+ {
+ var rawBlocks = new List();
+ if (frame is not IWICMetadataBlockReader blockReader || !blockReader.GetCount(out var count).Succeeded())
+ {
+ return rawBlocks;
+ }
+ // In other containers than TIFF and JPEG XR the top-level IFD is only metadata (e.g. the EXIF of HEIF)
+ var ifdIsMetadata = containerFormat != WicContainerFormats.Tiff && containerFormat != WicContainerFormats.Wmp;
+ for (uint i = 0; i < count; i++)
+ {
+ IWICMetadataReader reader = null;
+ try
+ {
+ if (!blockReader.GetReaderByIndex(i, out reader).Succeeded() || reader == null || !reader.GetMetadataFormat(out var format).Succeeded())
+ {
+ continue;
+ }
+ var path = BlockAliases.TryGetValue(format, out var alias) ? "/" + alias : null;
+ if (RawTopLevelFormats.Contains(format) || ifdIsMetadata && format == WicMetadataFormats.Ifd)
+ {
+ AddRawBlock(reader, path, format, rawBlocks);
+ }
+ if (path != null)
+ {
+ WalkRawBlocks(reader, path, rawBlocks, 0);
+ }
+ }
+ catch (COMException ex)
+ {
+ Log.Debug().WriteLine("Couldn't read the metadata block {0}: {1}", i, ex.Message);
+ }
+ finally
+ {
+ Wic.Release(reader);
+ }
+ }
+ return rawBlocks;
+ }
+
+ ///
+ /// Find the XMP and IPTC blocks nested in a block, e.g. tag 700 in the IFD of a TIFF
+ ///
+ private static void WalkRawBlocks(IWICMetadataReader reader, string path, List rawBlocks, int depth)
+ {
+ if (depth > MaxDepth || !reader.GetCount(out var count).Succeeded())
+ {
+ return;
+ }
+ for (uint i = 0; i < count; i++)
+ {
+ var id = PropVariant.Allocate();
+ var value = PropVariant.Allocate();
+ object nestedObject = null;
+ try
+ {
+ if (!reader.GetValueByIndex(i, IntPtr.Zero, id, value).Succeeded() || PropVariant.GetVarType(value) != 13 || !PropVariant.TryRead(value, out nestedObject))
+ {
+ continue;
+ }
+ if (nestedObject is not IWICMetadataReader nested || !nested.GetMetadataFormat(out var format).Succeeded())
+ {
+ continue;
+ }
+ var name = BlockAliases.TryGetValue(format, out var alias) ? alias : PropVariant.TryRead(id, out var idValue) && idValue is ushort tag ? $"{{ushort={tag}}}" : null;
+ if (name == null)
+ {
+ continue;
+ }
+ var nestedPath = $"{path}/{name}";
+ if (RawNestedFormats.Contains(format))
+ {
+ AddRawBlock(nested, nestedPath, format, rawBlocks);
+ }
+ WalkRawBlocks(nested, nestedPath, rawBlocks, depth + 1);
+ }
+ finally
+ {
+ Wic.Release(nestedObject);
+ PropVariant.Free(id);
+ PropVariant.Free(value);
+ }
+ }
+ }
+
+ ///
+ /// Save the block with IWICPersistStream
+ ///
+ private static void AddRawBlock(IWICMetadataReader reader, string path, Guid format, List rawBlocks)
+ {
+ if (reader is not IWICPersistStream persistStream)
+ {
+ return;
+ }
+ try
+ {
+ using var memoryStream = new MemoryStream();
+ var stream = new ManagedIStream(memoryStream);
+ if (persistStream.SaveEx(stream, 0, 0).Succeeded())
+ {
+ rawBlocks.Add(new WicMetadataRawBlock(path, format, memoryStream.ToArray()));
+ }
+ }
+ catch (Exception ex) when (ex is COMException || ex is IOException)
+ {
+ Log.Debug().WriteLine("Couldn't save the metadata block {0}: {1}", path, ex.Message);
+ }
+ }
+
+ private static int? ReadOrientation(IWICMetadataQueryReader reader, Guid containerFormat)
+ {
+ foreach (var query in WicMetadataPaths.OrientationQueries(containerFormat))
+ {
+ var propVariant = PropVariant.Allocate();
+ try
+ {
+ if (reader.GetMetadataByName(query, propVariant).Succeeded() && PropVariant.TryRead(propVariant, out var value) && value != null && value is IConvertible)
+ {
+ var orientation = Convert.ToInt32(value);
+ if (orientation >= 1 && orientation <= 8)
+ {
+ return orientation;
+ }
+ }
+ }
+ catch (Exception ex) when (ex is FormatException || ex is InvalidCastException || ex is OverflowException)
+ {
+ // Not a number
+ }
+ finally
+ {
+ PropVariant.Free(propVariant);
+ }
+ }
+ return null;
+ }
+
+ private static byte[] ReadColorProfile(IWICImagingFactory factory, IWICBitmapFrameDecode frame)
+ {
+ if (!frame.GetColorContexts(0, IntPtr.Zero, out var count).Succeeded() || count == 0)
+ {
+ return null;
+ }
+ var contexts = new IWICColorContext[count];
+ var pointers = new nint[count];
+ try
+ {
+ for (int i = 0; i < count; i++)
+ {
+ Wic.Check(factory.CreateColorContext(out contexts[i]), "IWICImagingFactory.CreateColorContext");
+ pointers[i] = Marshal.GetComInterfaceForObject(contexts[i], typeof(IWICColorContext));
+ }
+ var handle = GCHandle.Alloc(pointers, GCHandleType.Pinned);
+ try
+ {
+ if (!frame.GetColorContexts(count, handle.AddrOfPinnedObject(), out count).Succeeded())
+ {
+ return null;
+ }
+ }
+ finally
+ {
+ handle.Free();
+ }
+ for (int i = 0; i < count; i++)
+ {
+ // 1 = WICColorContextProfile
+ if (!contexts[i].GetType(out var type).Succeeded() || type != 1)
+ {
+ continue;
+ }
+ if (!contexts[i].GetProfileBytes(0, IntPtr.Zero, out var size).Succeeded() || size == 0)
+ {
+ continue;
+ }
+ var profile = new byte[size];
+ var profileHandle = GCHandle.Alloc(profile, GCHandleType.Pinned);
+ try
+ {
+ if (contexts[i].GetProfileBytes(size, profileHandle.AddrOfPinnedObject(), out size).Succeeded())
+ {
+ return profile;
+ }
+ }
+ finally
+ {
+ profileHandle.Free();
+ }
+ }
+ return null;
+ }
+ catch (COMException ex)
+ {
+ Log.Warn().WriteLine(ex, "Reading the color profile failed");
+ return null;
+ }
+ finally
+ {
+ foreach (var pointer in pointers)
+ {
+ if (pointer != IntPtr.Zero)
+ {
+ Marshal.Release(pointer);
+ }
+ }
+ foreach (var context in contexts)
+ {
+ Wic.Release(context);
+ }
+ }
+ }
+}
diff --git a/src/Dapplo.Windows.Imaging/Internal/WicPixelSource.cs b/src/Dapplo.Windows.Imaging/Internal/WicPixelSource.cs
new file mode 100644
index 00000000..915379f7
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Internal/WicPixelSource.cs
@@ -0,0 +1,168 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Buffers;
+using System.Runtime.InteropServices;
+using Dapplo.Windows.Common.Enums;
+using Dapplo.Windows.Imaging.Interop;
+
+namespace Dapplo.Windows.Imaging.Internal;
+
+///
+/// IWICBitmapSource implemented in .NET, WIC (an encoder or a format converter) pulls the rows it needs with CopyPixels.
+/// Exceptions become an HRESULT for WIC and are kept in .
+///
+[ComVisible(true)]
+internal abstract class WicPixelSource : IWICBitmapSource, IFailureSource
+{
+ protected WicPixelSource(int width, int height, Guid pixelFormat, int bitsPerPixel, double dpiX, double dpiY)
+ {
+ Width = width;
+ Height = height;
+ PixelFormat = pixelFormat;
+ BitsPerPixel = bitsPerPixel;
+ DpiX = dpiX;
+ DpiY = dpiY;
+ RowBytes = Wic.RowBytes(width, bitsPerPixel);
+ }
+
+ public int Width { get; }
+ public int Height { get; }
+ public Guid PixelFormat { get; }
+ public int BitsPerPixel { get; }
+ public int RowBytes { get; }
+ public double DpiX { get; }
+ public double DpiY { get; }
+ public Exception LastException { get; private set; }
+
+ public HResult GetSize(out uint width, out uint height)
+ {
+ width = (uint)Width;
+ height = (uint)Height;
+ return HResult.S_OK;
+ }
+
+ public HResult GetPixelFormat(out Guid pixelFormat)
+ {
+ pixelFormat = PixelFormat;
+ return HResult.S_OK;
+ }
+
+ public HResult GetResolution(out double dpiX, out double dpiY)
+ {
+ dpiX = DpiX;
+ dpiY = DpiY;
+ return HResult.S_OK;
+ }
+
+ public HResult CopyPalette(nint palette) => WicErrors.PaletteUnavailable;
+
+ public unsafe HResult CopyPixels(nint rectangle, uint stride, uint bufferSize, nint buffer)
+ {
+ try
+ {
+ var rect = rectangle == IntPtr.Zero ? new WicRect(0, 0, Width, Height) : *(WicRect*)rectangle;
+ if (rect.X < 0 || rect.Y < 0 || rect.Width < 0 || rect.Height < 0 || rect.X + rect.Width > Width || rect.Y + rect.Height > Height)
+ {
+ return HResult.E_INVALIDARG;
+ }
+ if (rect.Width == 0 || rect.Height == 0)
+ {
+ return HResult.S_OK;
+ }
+ if (buffer == IntPtr.Zero || ((long)rect.X * BitsPerPixel) % 8 != 0)
+ {
+ return HResult.E_INVALIDARG;
+ }
+ var rectRowBytes = Wic.RowBytes(rect.Width, BitsPerPixel);
+ if (stride < rectRowBytes || bufferSize < (long)(rect.Height - 1) * stride + rectRowBytes)
+ {
+ return HResult.E_INVALIDARG;
+ }
+ CopyRows(rect, (int)((long)rect.X * BitsPerPixel / 8), rectRowBytes, (int)stride, bufferSize, (byte*)buffer);
+ return HResult.S_OK;
+ }
+ catch (Exception ex)
+ {
+ LastException = ex;
+ return ex.HResult < 0 ? (HResult)ex.HResult : HResult.E_FAIL;
+ }
+ }
+
+ ///
+ /// Copy the rows of the rectangle into the destination
+ ///
+ /// the rectangle, validated
+ /// the byte offset of rect.X in a row
+ /// bytes of a row of the rectangle
+ /// bytes per row in the destination
+ /// size of the destination
+ /// the destination
+ protected abstract unsafe void CopyRows(WicRect rect, int xOffset, int rectRowBytes, int destinationStride, uint destinationSize, byte* destination);
+}
+
+///
+/// Pixels in memory, e.g. GDI+ BitmapData.Scan0 or a contiguous ImageSharp buffer, read without a copy of the image
+///
+internal sealed class WicMemoryPixelSource : WicPixelSource
+{
+ private readonly nint _pixels;
+ private readonly int _stride;
+
+ public WicMemoryPixelSource(nint pixels, int width, int height, int stride, Guid pixelFormat, int bitsPerPixel, double dpiX, double dpiY)
+ : base(width, height, pixelFormat, bitsPerPixel, dpiX, dpiY)
+ {
+ _pixels = pixels;
+ _stride = stride;
+ }
+
+ protected override unsafe void CopyRows(WicRect rect, int xOffset, int rectRowBytes, int destinationStride, uint destinationSize, byte* destination)
+ {
+ var source = (byte*)_pixels;
+ for (int row = 0; row < rect.Height; row++)
+ {
+ Buffer.MemoryCopy(source + (long)(rect.Y + row) * _stride + xOffset, destination + (long)row * destinationStride, rectRowBytes, rectRowBytes);
+ }
+ }
+}
+
+///
+/// Pixels supplied by a , for buffers which aren't contiguous (e.g. ImageSharp ProcessPixelRows)
+///
+internal sealed class WicRowPixelSource : WicPixelSource
+{
+ private readonly WicRowProvider _rows;
+
+ public WicRowPixelSource(WicRowProvider rows, int width, int height, Guid pixelFormat, int bitsPerPixel, double dpiX, double dpiY)
+ : base(width, height, pixelFormat, bitsPerPixel, dpiX, dpiY)
+ {
+ _rows = rows;
+ }
+
+ protected override unsafe void CopyRows(WicRect rect, int xOffset, int rectRowBytes, int destinationStride, uint destinationSize, byte* destination)
+ {
+ if (rect.X == 0 && rect.Width == Width)
+ {
+ // The whole width: the callback writes straight into the WIC buffer
+ var length = (int)Math.Min(destinationSize, (long)(rect.Height - 1) * destinationStride + rectRowBytes);
+ _rows(rect.Y, rect.Height, new Span(destination, length), destinationStride);
+ return;
+ }
+
+ // Part of the width: get each full row and copy the requested part
+ var rowBuffer = ArrayPool.Shared.Rent(RowBytes);
+ try
+ {
+ for (int row = 0; row < rect.Height; row++)
+ {
+ _rows(rect.Y + row, 1, new Span(rowBuffer, 0, RowBytes), RowBytes);
+ new ReadOnlySpan(rowBuffer, xOffset, rectRowBytes).CopyTo(new Span(destination + (long)row * destinationStride, rectRowBytes));
+ }
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(rowBuffer);
+ }
+ }
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/IEnumUnknown.cs b/src/Dapplo.Windows.Imaging/Interop/IEnumUnknown.cs
new file mode 100644
index 00000000..9b920506
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/IEnumUnknown.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Runtime.InteropServices;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// Enumerates objects with the IUnknown interface, here the components returned by IWICImagingFactory::CreateComponentEnumerator.
+/// See IEnumUnknown interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("00000100-0000-0000-C000-000000000046")]
+internal interface IEnumUnknown
+{
+ ///
+ /// Retrieves the next item, this is only used with celt = 1 so a single out parameter covers the array.
+ /// Returns S_OK when an item was fetched, S_FALSE at the end of the enumeration.
+ ///
+ [PreserveSig]
+ HResult Next(uint celt, [MarshalAs(UnmanagedType.IUnknown)] out object rgelt, out uint pceltFetched);
+
+ [PreserveSig]
+ HResult Skip(uint celt);
+
+ [PreserveSig]
+ HResult Reset();
+
+ [PreserveSig]
+ HResult Clone(out IEnumUnknown ppenum);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/IPropertyBag2.cs b/src/Dapplo.Windows.Imaging/Interop/IPropertyBag2.cs
new file mode 100644
index 00000000..480078c8
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/IPropertyBag2.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// The property bag with the encoder options, methods in the order of ocidl.idl.
+/// See IPropertyBag2 interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("22F55882-280B-11D0-A8A9-00A0C90C2004")]
+internal interface IPropertyBag2
+{
+ [PreserveSig] HResult Read(uint cProperties, nint pPropBag, nint pErrLog, nint pvarValue, nint phrError);
+
+ ///
+ /// Write one property, the value is marshalled as VARIANT (float = VT_R4, bool = VT_BOOL, byte = VT_UI1 ...)
+ ///
+ [PreserveSig] HResult Write(uint cProperties, [In] ref PropBag2 pPropBag, [In, MarshalAs(UnmanagedType.Struct)] ref object pvarValue);
+
+ [PreserveSig] HResult CountProperties(out uint pcProperties);
+
+ ///
+ /// Get the information of one property, the caller must free pstrName with CoTaskMemFree
+ ///
+ [PreserveSig] HResult GetPropertyInfo(uint iProperty, uint cProperties, out PropBag2 pPropBag, out uint pcProperties);
+
+ [PreserveSig] HResult LoadObject(nint pstrName, uint dwHint, nint pUnkObject, nint pErrLog);
+}
+
+///
+/// PROPBAG2 from ocidl.idl
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct PropBag2
+{
+ public uint Type;
+ public ushort VarType;
+ public ushort ClipboardFormat;
+ public uint Hint;
+ public nint Name;
+ public Guid Clsid;
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/IWICBitmapCodecInfo.cs b/src/Dapplo.Windows.Imaging/Interop/IWICBitmapCodecInfo.cs
new file mode 100644
index 00000000..d9506131
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/IWICBitmapCodecInfo.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// Information about a WIC codec (decoder or encoder).
+/// IWICBitmapCodecInfo inherits from IWICComponentInfo, with InterfaceIsIUnknown the inherited methods must be repeated: they come first, in the order of wincodec.idl.
+/// The string methods are called twice: with cch = 0 and a null buffer for the length (including the terminating 0), then with a buffer of that length.
+/// See IWICBitmapCodecInfo interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("E87A44C4-B76E-4C47-8B09-298EB12A2714")]
+internal interface IWICBitmapCodecInfo
+{
+ // IWICComponentInfo (23BC3F0A-698B-4357-886B-F24D50671334)
+ [PreserveSig] HResult GetComponentType(out WicComponentType pType);
+ [PreserveSig] HResult GetCLSID(out Guid pclsid);
+ [PreserveSig] HResult GetSigningStatus(out uint pStatus);
+ [PreserveSig] HResult GetAuthor(uint cchAuthor, nint wzAuthor, out uint pcchActual);
+ [PreserveSig] HResult GetVendorGUID(out Guid pguidVendor);
+ [PreserveSig] HResult GetVersion(uint cchVersion, nint wzVersion, out uint pcchActual);
+ [PreserveSig] HResult GetSpecVersion(uint cchSpecVersion, nint wzSpecVersion, out uint pcchActual);
+ [PreserveSig] HResult GetFriendlyName(uint cchFriendlyName, nint wzFriendlyName, out uint pcchActual);
+
+ // IWICBitmapCodecInfo
+ [PreserveSig] HResult GetContainerFormat(out Guid pguidContainerFormat);
+ [PreserveSig] HResult GetPixelFormats(uint cFormats, nint pguidPixelFormats, out uint pcActual);
+ [PreserveSig] HResult GetColorManagementVersion(uint cchColorManagementVersion, nint wzColorManagementVersion, out uint pcchActual);
+ [PreserveSig] HResult GetDeviceManufacturer(uint cchDeviceManufacturer, nint wzDeviceManufacturer, out uint pcchActual);
+ [PreserveSig] HResult GetDeviceModels(uint cchDeviceModels, nint wzDeviceModels, out uint pcchActual);
+ [PreserveSig] HResult GetMimeTypes(uint cchMimeTypes, nint wzMimeTypes, out uint pcchActual);
+ [PreserveSig] HResult GetFileExtensions(uint cchFileExtensions, nint wzFileExtensions, out uint pcchActual);
+ [PreserveSig] HResult DoesSupportAnimation(out int pfSupportAnimation);
+ [PreserveSig] HResult DoesSupportChromakey(out int pfSupportChromakey);
+ [PreserveSig] HResult DoesSupportLossless(out int pfSupportLossless);
+ [PreserveSig] HResult DoesSupportMultiframe(out int pfSupportMultiframe);
+ [PreserveSig] HResult MatchesMimeType([MarshalAs(UnmanagedType.LPWStr)] string wzMimeType, out int pfMatches);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/IWICBitmapDecoder.cs b/src/Dapplo.Windows.Imaging/Interop/IWICBitmapDecoder.cs
new file mode 100644
index 00000000..d65ea95e
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/IWICBitmapDecoder.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// A WIC decoder, methods in the order of wincodec.idl.
+/// See IWICBitmapDecoder interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("9EDDE9E7-8DEE-47ea-99DF-E6FAF2ED44BF")]
+internal interface IWICBitmapDecoder
+{
+ [PreserveSig] HResult QueryCapability(IStream pIStream, out uint pdwCapability);
+ [PreserveSig] HResult Initialize(IStream pIStream, WicDecodeOptions cacheOptions);
+ [PreserveSig] HResult GetContainerFormat(out Guid pguidContainerFormat);
+ [PreserveSig] HResult GetDecoderInfo(out nint ppIDecoderInfo);
+ [PreserveSig] HResult CopyPalette(nint pIPalette);
+ [PreserveSig] HResult GetMetadataQueryReader(out nint ppIMetadataQueryReader);
+ [PreserveSig] HResult GetPreview(out nint ppIBitmapSource);
+ [PreserveSig] HResult GetColorContexts(uint cCount, nint ppIColorContexts, out uint pcActualCount);
+ [PreserveSig] HResult GetThumbnail(out nint ppIThumbnail);
+ [PreserveSig] HResult GetFrameCount(out uint pCount);
+ [PreserveSig] HResult GetFrame(uint index, out IWICBitmapFrameDecode ppIBitmapFrame);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/IWICBitmapEncoder.cs b/src/Dapplo.Windows.Imaging/Interop/IWICBitmapEncoder.cs
new file mode 100644
index 00000000..5073f80e
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/IWICBitmapEncoder.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// A WIC encoder, methods in the order of wincodec.idl.
+/// See IWICBitmapEncoder interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("00000103-a8f2-4877-ba0a-fd2b6645fb94")]
+internal interface IWICBitmapEncoder
+{
+ [PreserveSig] HResult Initialize(IStream pIStream, WicBitmapEncoderCacheOption cacheOption);
+ [PreserveSig] HResult GetContainerFormat(out Guid pguidContainerFormat);
+ [PreserveSig] HResult GetEncoderInfo(out nint ppIEncoderInfo);
+ [PreserveSig] HResult SetColorContexts(uint cCount, nint ppIColorContext);
+ [PreserveSig] HResult SetPalette(nint pIPalette);
+ [PreserveSig] HResult SetThumbnail(nint pIThumbnail);
+ [PreserveSig] HResult SetPreview(nint pIPreview);
+
+ ///
+ /// Creates a new frame, ppIEncoderOptions is [in, out, unique]: passing a pointer to null makes the encoder return its property bag
+ ///
+ [PreserveSig] HResult CreateNewFrame(out IWICBitmapFrameEncode ppIFrameEncode, out IPropertyBag2 ppIEncoderOptions);
+ [PreserveSig] HResult Commit();
+ [PreserveSig] HResult GetMetadataQueryWriter(out nint ppIMetadataQueryWriter);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/IWICBitmapFlipRotator.cs b/src/Dapplo.Windows.Imaging/Interop/IWICBitmapFlipRotator.cs
new file mode 100644
index 00000000..4e3ee016
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/IWICBitmapFlipRotator.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// Flips and rotates a source, inherits IWICBitmapSource: those methods are repeated first, in the order of wincodec.idl.
+/// See IWICBitmapFlipRotator interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("5009834F-2D6A-41ce-9E1B-17C5AFF7A782")]
+internal interface IWICBitmapFlipRotator
+{
+ // IWICBitmapSource
+ [PreserveSig] HResult GetSize(out uint width, out uint height);
+ [PreserveSig] HResult GetPixelFormat(out Guid pixelFormat);
+ [PreserveSig] HResult GetResolution(out double dpiX, out double dpiY);
+ [PreserveSig] HResult CopyPalette(nint palette);
+ [PreserveSig] HResult CopyPixels(nint rectangle, uint stride, uint bufferSize, nint buffer);
+
+ // IWICBitmapFlipRotator
+ [PreserveSig] HResult Initialize(IWICBitmapSource pISource, WicBitmapTransformOptions options);
+}
+
+///
+/// WICBitmapTransformOptions from wincodec.idl
+///
+[Flags]
+internal enum WicBitmapTransformOptions : uint
+{
+ Rotate0 = 0x0,
+ Rotate90 = 0x1,
+ Rotate180 = 0x2,
+ Rotate270 = 0x3,
+ FlipHorizontal = 0x8,
+ FlipVertical = 0x10
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/IWICBitmapFrameDecode.cs b/src/Dapplo.Windows.Imaging/Interop/IWICBitmapFrameDecode.cs
new file mode 100644
index 00000000..53c4a153
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/IWICBitmapFrameDecode.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// A decoded frame, inherits IWICBitmapSource: those methods are repeated first, in the order of wincodec.idl.
+/// See IWICBitmapFrameDecode interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("3B16811B-6A43-4ec9-A813-3D930C13B940")]
+internal interface IWICBitmapFrameDecode
+{
+ // IWICBitmapSource
+ [PreserveSig] HResult GetSize(out uint width, out uint height);
+ [PreserveSig] HResult GetPixelFormat(out Guid pixelFormat);
+ [PreserveSig] HResult GetResolution(out double dpiX, out double dpiY);
+ [PreserveSig] HResult CopyPalette(nint palette);
+ [PreserveSig] HResult CopyPixels(nint rectangle, uint stride, uint bufferSize, nint buffer);
+
+ // IWICBitmapFrameDecode, ppIColorContexts points to an array of IWICColorContext pointers created by the caller
+ [PreserveSig] HResult GetMetadataQueryReader(out IWICMetadataQueryReader ppIMetadataQueryReader);
+ [PreserveSig] HResult GetColorContexts(uint cCount, nint ppIColorContexts, out uint pcActualCount);
+ [PreserveSig] HResult GetThumbnail(out nint ppIThumbnail);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/IWICBitmapFrameEncode.cs b/src/Dapplo.Windows.Imaging/Interop/IWICBitmapFrameEncode.cs
new file mode 100644
index 00000000..554651f0
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/IWICBitmapFrameEncode.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// A frame of a WIC encoder, methods in the order of wincodec.idl.
+/// See IWICBitmapFrameEncode interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("00000105-a8f2-4877-ba0a-fd2b6645fb94")]
+internal interface IWICBitmapFrameEncode
+{
+ [PreserveSig] HResult Initialize(IPropertyBag2 pIEncoderOptions);
+ [PreserveSig] HResult SetSize(uint uiWidth, uint uiHeight);
+ [PreserveSig] HResult SetResolution(double dpiX, double dpiY);
+ [PreserveSig] HResult SetPixelFormat([In, Out] ref Guid pPixelFormat);
+ ///
+ /// ppIColorContext points to an array of IWICColorContext pointers
+ ///
+ [PreserveSig] HResult SetColorContexts(uint cCount, nint ppIColorContext);
+ [PreserveSig] HResult SetPalette(IWICPalette pIPalette);
+ [PreserveSig] HResult SetThumbnail(nint pIThumbnail);
+ [PreserveSig] HResult WritePixels(uint lineCount, uint cbStride, uint cbBufferSize, nint pbPixels);
+ [PreserveSig] HResult WriteSource(IWICBitmapSource pIBitmapSource, nint prc);
+ [PreserveSig] HResult Commit();
+ [PreserveSig] HResult GetMetadataQueryWriter(out IWICMetadataQueryWriter ppIMetadataQueryWriter);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/IWICBitmapSource.cs b/src/Dapplo.Windows.Imaging/Interop/IWICBitmapSource.cs
new file mode 100644
index 00000000..fd68bc89
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/IWICBitmapSource.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// A source of pixels for WIC. It is called on WIC objects (decoded frames, format converters) and implemented by Dapplo.Windows.Imaging
+/// to hand pixels from memory or from a row callback to an encoder without a copy, so it is public and COM visible.
+/// See IWICBitmapSource interface
+///
+[ComImport]
+[ComVisible(true)]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("00000120-a8f2-4877-ba0a-fd2b6645fb94")]
+public interface IWICBitmapSource
+{
+ ///
+ /// The size of the bitmap in pixels
+ ///
+ [PreserveSig]
+ HResult GetSize(out uint width, out uint height);
+
+ ///
+ /// The pixel format GUID, see
+ ///
+ [PreserveSig]
+ HResult GetPixelFormat(out Guid pixelFormat);
+
+ ///
+ /// The resolution in dots per inch
+ ///
+ [PreserveSig]
+ HResult GetResolution(out double dpiX, out double dpiY);
+
+ ///
+ /// Copy the palette into the IWICPalette (pointer), for indexed formats
+ ///
+ [PreserveSig]
+ HResult CopyPalette(nint palette);
+
+ ///
+ /// Copy the pixels of a rectangle (pointer to a WICRect, null for the whole bitmap) into the buffer
+ ///
+ /// pointer to a WICRect (X, Y, Width, Height as 32 bit integers), or 0 for the whole bitmap
+ /// bytes per row in the buffer
+ /// size of the buffer in bytes
+ /// pointer to the buffer
+ [PreserveSig]
+ HResult CopyPixels(nint rectangle, uint stride, uint bufferSize, nint buffer);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/IWICColorContext.cs b/src/Dapplo.Windows.Imaging/Interop/IWICColorContext.cs
new file mode 100644
index 00000000..4cb7ff56
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/IWICColorContext.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Runtime.InteropServices;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// A color context (ICC profile or EXIF color space), methods in the order of wincodec.idl.
+/// See IWICColorContext interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("3C613A02-34B2-44ea-9A7C-45AEA9C6FD6D")]
+internal interface IWICColorContext
+{
+ [PreserveSig] HResult InitializeFromFilename([MarshalAs(UnmanagedType.LPWStr)] string wzFilename);
+ [PreserveSig] HResult InitializeFromMemory(nint pbBuffer, uint cbBufferSize);
+ [PreserveSig] HResult InitializeFromExifColorSpace(uint value);
+
+ ///
+ /// WICColorContextType: 0 = uninitialized, 1 = profile, 2 = EXIF color space
+ ///
+ [PreserveSig] HResult GetType(out uint pType);
+ [PreserveSig] HResult GetProfileBytes(uint cbBuffer, nint pbBuffer, out uint pcbActual);
+ [PreserveSig] HResult GetExifColorSpace(out uint pValue);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/IWICFormatConverter.cs b/src/Dapplo.Windows.Imaging/Interop/IWICFormatConverter.cs
new file mode 100644
index 00000000..27d706c3
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/IWICFormatConverter.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// Converts the pixel format of a source, inherits IWICBitmapSource: those methods are repeated first, in the order of wincodec.idl.
+/// See IWICFormatConverter interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("00000301-a8f2-4877-ba0a-fd2b6645fb94")]
+internal interface IWICFormatConverter
+{
+ // IWICBitmapSource
+ [PreserveSig] HResult GetSize(out uint width, out uint height);
+ [PreserveSig] HResult GetPixelFormat(out Guid pixelFormat);
+ [PreserveSig] HResult GetResolution(out double dpiX, out double dpiY);
+ [PreserveSig] HResult CopyPalette(nint palette);
+ [PreserveSig] HResult CopyPixels(nint rectangle, uint stride, uint bufferSize, nint buffer);
+
+ // IWICFormatConverter
+ [PreserveSig]
+ HResult Initialize(IWICBitmapSource pISource, [In] ref Guid dstFormat, WicBitmapDitherType dither, IWICPalette pIPalette, double alphaThresholdPercent, WicBitmapPaletteType paletteTranslate);
+
+ [PreserveSig]
+ HResult CanConvert([In] ref Guid srcPixelFormat, [In] ref Guid dstPixelFormat, out int pfCanConvert);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/IWICImagingFactory.cs b/src/Dapplo.Windows.Imaging/Interop/IWICImagingFactory.cs
new file mode 100644
index 00000000..d4bb120d
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/IWICImagingFactory.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// The WIC imaging factory.
+/// Every method up to CreateComponentEnumerator is declared, in the order of wincodec.idl, so the vtable slots are right.
+/// The methods which aren't used have untyped (nint) parameters, they must not be called.
+/// The methods after CreateComponentEnumerator (CreateFastMetadataEncoderFromDecoder etc.) are left out, which is allowed for the end of a vtable.
+/// See IWICImagingFactory interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("EC5EC8A9-C395-4314-9C77-54D7A935FF70")]
+internal interface IWICImagingFactory
+{
+ [PreserveSig] HResult CreateDecoderFromFilename(nint wzFilename, nint pguidVendor, uint dwDesiredAccess, int metadataOptions, out nint ppIDecoder);
+ [PreserveSig] HResult CreateDecoderFromStream(IStream pIStream, nint pguidVendor, WicDecodeOptions metadataOptions, out IWICBitmapDecoder ppIDecoder);
+ [PreserveSig] HResult CreateDecoderFromFileHandle(nint hFile, nint pguidVendor, int metadataOptions, out nint ppIDecoder);
+ [PreserveSig] HResult CreateComponentInfo([In] ref Guid clsidComponent, [MarshalAs(UnmanagedType.IUnknown)] out object ppIInfo);
+ [PreserveSig] HResult CreateDecoder(nint guidContainerFormat, nint pguidVendor, out nint ppIDecoder);
+ [PreserveSig] HResult CreateEncoder([In] ref Guid guidContainerFormat, nint pguidVendor, out IWICBitmapEncoder ppIEncoder);
+ [PreserveSig] HResult CreatePalette(out IWICPalette ppIPalette);
+ [PreserveSig] HResult CreateFormatConverter(out IWICFormatConverter ppIFormatConverter);
+ [PreserveSig] HResult CreateBitmapScaler(out nint ppIBitmapScaler);
+ [PreserveSig] HResult CreateBitmapClipper(out nint ppIBitmapClipper);
+ [PreserveSig] HResult CreateBitmapFlipRotator(out IWICBitmapFlipRotator ppIBitmapFlipRotator);
+ [PreserveSig] HResult CreateStream(out nint ppIWICStream);
+ [PreserveSig] HResult CreateColorContext(out IWICColorContext ppIWICColorContext);
+ [PreserveSig] HResult CreateColorTransformer(out nint ppIWICColorTransform);
+ [PreserveSig] HResult CreateBitmap(uint uiWidth, uint uiHeight, nint pixelFormat, int option, out nint ppIBitmap);
+ [PreserveSig] HResult CreateBitmapFromSource(nint piBitmapSource, int option, out nint ppIBitmap);
+ [PreserveSig] HResult CreateBitmapFromSourceRect(nint piBitmapSource, uint x, uint y, uint width, uint height, out nint ppIBitmap);
+ [PreserveSig] HResult CreateBitmapFromMemory(uint uiWidth, uint uiHeight, nint pixelFormat, uint cbStride, uint cbBufferSize, nint pbBuffer, out nint ppIBitmap);
+ [PreserveSig] HResult CreateBitmapFromHBITMAP(nint hBitmap, nint hPalette, int options, out nint ppIBitmap);
+ [PreserveSig] HResult CreateBitmapFromHICON(nint hIcon, out nint ppIBitmap);
+
+ ///
+ /// Creates an enumerator of the components of the specified types
+ ///
+ /// WICComponentType flags, e.g. WICDecoder (0x1) or WICEncoder (0x2)
+ /// WICComponentEnumerateOptions, e.g. WICComponentEnumerateDefault (0x0) or WICComponentEnumerateRefresh (0x1)
+ /// IEnumUnknown with the components
+ [PreserveSig]
+ HResult CreateComponentEnumerator(WicComponentType componentTypes, WicComponentEnumerateOptions options, out IEnumUnknown ppIEnumUnknown);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/IWICMetadataQueryReader.cs b/src/Dapplo.Windows.Imaging/Interop/IWICMetadataQueryReader.cs
new file mode 100644
index 00000000..445b5d2c
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/IWICMetadataQueryReader.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// Reads metadata with the WIC metadata query language, methods in the order of wincodec.idl.
+/// See IWICMetadataQueryReader interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("30989668-E1C9-4597-B395-458EEDB808DF")]
+internal interface IWICMetadataQueryReader
+{
+ [PreserveSig] HResult GetContainerFormat(out Guid pguidContainerFormat);
+ [PreserveSig] HResult GetLocation(uint cchMaxLength, nint wzNamespace, out uint pcchActualLength);
+
+ ///
+ /// Read a value, pvarValue points to a PROPVARIANT which the caller clears with PropVariantClear
+ ///
+ [PreserveSig] HResult GetMetadataByName([MarshalAs(UnmanagedType.LPWStr)] string wzName, nint pvarValue);
+ [PreserveSig] HResult GetEnumerator(out IEnumString ppIEnumString);
+}
+
+///
+/// Writes metadata with the WIC metadata query language, inherits IWICMetadataQueryReader: those methods are repeated first.
+/// See IWICMetadataQueryWriter interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("A721791A-0DEF-4d06-BD91-2118BF1DB10B")]
+internal interface IWICMetadataQueryWriter
+{
+ // IWICMetadataQueryReader
+ [PreserveSig] HResult GetContainerFormat(out Guid pguidContainerFormat);
+ [PreserveSig] HResult GetLocation(uint cchMaxLength, nint wzNamespace, out uint pcchActualLength);
+ [PreserveSig] HResult GetMetadataByName([MarshalAs(UnmanagedType.LPWStr)] string wzName, nint pvarValue);
+ [PreserveSig] HResult GetEnumerator(out IEnumString ppIEnumString);
+
+ // IWICMetadataQueryWriter
+ [PreserveSig] HResult SetMetadataByName([MarshalAs(UnmanagedType.LPWStr)] string wzName, nint pvarValue);
+ [PreserveSig] HResult RemoveMetadataByName([MarshalAs(UnmanagedType.LPWStr)] string wzName);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/IWICPalette.cs b/src/Dapplo.Windows.Imaging/Interop/IWICPalette.cs
new file mode 100644
index 00000000..37515a83
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/IWICPalette.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Runtime.InteropServices;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// A color palette for indexed pixel formats (e.g. GIF), methods in the order of wincodec.idl.
+/// See IWICPalette interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("00000040-a8f2-4877-ba0a-fd2b6645fb94")]
+internal interface IWICPalette
+{
+ [PreserveSig] HResult InitializePredefined(WicBitmapPaletteType ePaletteType, int fAddTransparentColor);
+ [PreserveSig] HResult InitializeCustom(nint pColors, uint colorCount);
+ [PreserveSig] HResult InitializeFromBitmap(IWICBitmapSource pISurface, uint colorCount, int fAddTransparentColor);
+ [PreserveSig] HResult InitializeFromPalette(IWICPalette pIPalette);
+ [PreserveSig] HResult GetType(out WicBitmapPaletteType pePaletteType);
+ [PreserveSig] HResult GetColorCount(out uint pcCount);
+ [PreserveSig] HResult GetColors(uint colorCount, nint pColors, out uint pcActualColors);
+ [PreserveSig] HResult IsBlackWhite(out int pfIsBlackWhite);
+ [PreserveSig] HResult IsGrayscale(out int pfIsGrayscale);
+ [PreserveSig] HResult HasAlpha(out int pfHasAlpha);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/IWICPixelFormatInfo.cs b/src/Dapplo.Windows.Imaging/Interop/IWICPixelFormatInfo.cs
new file mode 100644
index 00000000..fe8fc797
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/IWICPixelFormatInfo.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// Information on a pixel format, inherits IWICComponentInfo: those methods are repeated first, in the order of wincodec.idl.
+/// Only GetBitsPerPixel is used.
+/// See IWICPixelFormatInfo interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("E8EDA601-3D48-431A-AB44-69059BE88BBE")]
+internal interface IWICPixelFormatInfo
+{
+ // IWICComponentInfo
+ [PreserveSig] HResult GetComponentType(out WicComponentType pType);
+ [PreserveSig] HResult GetCLSID(out Guid pclsid);
+ [PreserveSig] HResult GetSigningStatus(out uint pStatus);
+ [PreserveSig] HResult GetAuthor(uint cchAuthor, nint wzAuthor, out uint pcchActual);
+ [PreserveSig] HResult GetVendorGUID(out Guid pguidVendor);
+ [PreserveSig] HResult GetVersion(uint cchVersion, nint wzVersion, out uint pcchActual);
+ [PreserveSig] HResult GetSpecVersion(uint cchSpecVersion, nint wzSpecVersion, out uint pcchActual);
+ [PreserveSig] HResult GetFriendlyName(uint cchFriendlyName, nint wzFriendlyName, out uint pcchActual);
+
+ // IWICPixelFormatInfo
+ [PreserveSig] HResult GetFormatGUID(out Guid pFormat);
+ [PreserveSig] HResult GetColorContext(out nint ppIColorContext);
+ [PreserveSig] HResult GetBitsPerPixel(out uint puiBitsPerPixel);
+ [PreserveSig] HResult GetChannelCount(out uint puiChannelCount);
+ [PreserveSig] HResult GetChannelMask(uint uiChannelIndex, uint cbMaskBuffer, nint pbMaskBuffer, out uint pcbActual);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/PropVariant.cs b/src/Dapplo.Windows.Imaging/Interop/PropVariant.cs
new file mode 100644
index 00000000..27146e98
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/PropVariant.cs
@@ -0,0 +1,376 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// Marshals PROPVARIANT values of the WIC metadata to and from .NET.
+/// A PROPVARIANT is a VARTYPE (ushort) and three reserved ushorts, followed by the value at offset 8.
+/// Blobs and vectors (counted arrays) are a 32 bit count at offset 8 and a pointer after it (offset 8 + pointer size, because of the alignment).
+/// Everything allocated for a PROPVARIANT uses CoTaskMemAlloc, so PropVariantClear frees it.
+///
+internal static class PropVariant
+{
+ // VARENUM values
+ private const ushort VtEmpty = 0;
+ private const ushort VtI2 = 2;
+ private const ushort VtI4 = 3;
+ private const ushort VtR4 = 4;
+ private const ushort VtR8 = 5;
+ private const ushort VtDate = 7;
+ private const ushort VtBstr = 8;
+ private const ushort VtBool = 11;
+ private const ushort VtUnknown = 13;
+ private const ushort VtI1 = 16;
+ private const ushort VtUi1 = 17;
+ private const ushort VtUi2 = 18;
+ private const ushort VtUi4 = 19;
+ private const ushort VtI8 = 20;
+ private const ushort VtUi8 = 21;
+ private const ushort VtInt = 22;
+ private const ushort VtUint = 23;
+ private const ushort VtLpstr = 30;
+ private const ushort VtLpwstr = 31;
+ private const ushort VtFiletime = 64;
+ private const ushort VtBlob = 65;
+ private const ushort VtClsid = 72;
+ private const ushort VtVector = 0x1000;
+
+ ///
+ /// Bytes to allocate for a PROPVARIANT, 24 on 64 bit (16 on 32 bit)
+ ///
+ private const int Size = 24;
+
+ private const int ValueOffset = 8;
+ private static int PointerOffset => ValueOffset + IntPtr.Size;
+
+ ///
+ /// Allocate an empty (VT_EMPTY) PROPVARIANT, free it with
+ ///
+ public static nint Allocate()
+ {
+ var propVariant = Marshal.AllocCoTaskMem(Size);
+ for (int i = 0; i < Size; i += 4)
+ {
+ Marshal.WriteInt32(propVariant, i, 0);
+ }
+ return propVariant;
+ }
+
+ ///
+ /// Clear (PropVariantClear) and free the PROPVARIANT
+ ///
+ public static void Free(nint propVariant)
+ {
+ if (propVariant == IntPtr.Zero)
+ {
+ return;
+ }
+ PropVariantClear(propVariant);
+ Marshal.FreeCoTaskMem(propVariant);
+ }
+
+ ///
+ /// The VARTYPE of the PROPVARIANT
+ ///
+ public static ushort GetVarType(nint propVariant) => unchecked((ushort)Marshal.ReadInt16(propVariant));
+
+ ///
+ /// Convert the value of a PROPVARIANT to .NET.
+ /// VT_UNKNOWN gives the object (runtime callable wrapper), the caller must release it.
+ ///
+ /// pointer to the PROPVARIANT
+ /// the value, null for VT_EMPTY
+ /// false when the type is not supported
+ public static bool TryRead(nint propVariant, out object value)
+ {
+ var varType = GetVarType(propVariant);
+ var data = propVariant + ValueOffset;
+ value = null;
+ switch (varType)
+ {
+ case VtEmpty:
+ return true;
+ case VtI1:
+ value = unchecked((sbyte)Marshal.ReadByte(data));
+ return true;
+ case VtUi1:
+ value = Marshal.ReadByte(data);
+ return true;
+ case VtI2:
+ value = Marshal.ReadInt16(data);
+ return true;
+ case VtUi2:
+ value = unchecked((ushort)Marshal.ReadInt16(data));
+ return true;
+ case VtI4:
+ case VtInt:
+ value = Marshal.ReadInt32(data);
+ return true;
+ case VtUi4:
+ case VtUint:
+ value = unchecked((uint)Marshal.ReadInt32(data));
+ return true;
+ case VtI8:
+ value = Marshal.ReadInt64(data);
+ return true;
+ case VtUi8:
+ value = unchecked((ulong)Marshal.ReadInt64(data));
+ return true;
+ case VtR4:
+ value = BitConverter.ToSingle(BitConverter.GetBytes(Marshal.ReadInt32(data)), 0);
+ return true;
+ case VtR8:
+ value = BitConverter.Int64BitsToDouble(Marshal.ReadInt64(data));
+ return true;
+ case VtDate:
+ value = DateTime.FromOADate(BitConverter.Int64BitsToDouble(Marshal.ReadInt64(data)));
+ return true;
+ case VtBool:
+ value = Marshal.ReadInt16(data) != 0;
+ return true;
+ case VtLpstr:
+ value = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(data));
+ return true;
+ case VtLpwstr:
+ value = Marshal.PtrToStringUni(Marshal.ReadIntPtr(data));
+ return true;
+ case VtBstr:
+ value = Marshal.PtrToStringBSTR(Marshal.ReadIntPtr(data));
+ return true;
+ case VtFiletime:
+ value = DateTime.FromFileTimeUtc(Marshal.ReadInt64(data));
+ return true;
+ case VtClsid:
+ value = Marshal.PtrToStructure(Marshal.ReadIntPtr(data));
+ return true;
+ case VtUnknown:
+ var unknown = Marshal.ReadIntPtr(data);
+ value = unknown == IntPtr.Zero ? null : Marshal.GetObjectForIUnknown(unknown);
+ return true;
+ case VtBlob:
+ case VtVector | VtUi1:
+ value = ReadArray(propVariant, 1);
+ return true;
+ case VtVector | VtI1:
+ value = ReadArray(propVariant, 1);
+ return true;
+ case VtVector | VtUi2:
+ value = ReadArray(propVariant, 2);
+ return true;
+ case VtVector | VtI2:
+ value = ReadArray(propVariant, 2);
+ return true;
+ case VtVector | VtUi4:
+ value = ReadArray(propVariant, 4);
+ return true;
+ case VtVector | VtI4:
+ value = ReadArray(propVariant, 4);
+ return true;
+ case VtVector | VtUi8:
+ value = ReadArray(propVariant, 8);
+ return true;
+ case VtVector | VtI8:
+ value = ReadArray(propVariant, 8);
+ return true;
+ case VtVector | VtR4:
+ value = ReadArray(propVariant, 4);
+ return true;
+ case VtVector | VtR8:
+ value = ReadArray(propVariant, 8);
+ return true;
+ case VtVector | VtLpstr:
+ case VtVector | VtLpwstr:
+ var count = Marshal.ReadInt32(propVariant, ValueOffset);
+ var elements = Marshal.ReadIntPtr(propVariant, PointerOffset);
+ var strings = new string[count];
+ for (int i = 0; i < count; i++)
+ {
+ var element = Marshal.ReadIntPtr(elements, i * IntPtr.Size);
+ strings[i] = varType == (VtVector | VtLpstr) ? Marshal.PtrToStringAnsi(element) : Marshal.PtrToStringUni(element);
+ }
+ value = strings;
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private static unsafe T[] ReadArray(nint propVariant, int elementSize) where T : unmanaged
+ {
+ var count = Marshal.ReadInt32(propVariant, ValueOffset);
+ var elements = Marshal.ReadIntPtr(propVariant, PointerOffset);
+ var result = new T[count];
+ if (count > 0 && elements != IntPtr.Zero)
+ {
+ fixed (T* destination = result)
+ {
+ Buffer.MemoryCopy((void*)elements, destination, (long)count * elementSize, (long)count * elementSize);
+ }
+ }
+ return result;
+ }
+
+ ///
+ /// Store a .NET value in an empty PROPVARIANT (from )
+ ///
+ /// pointer to the PROPVARIANT
+ /// the value, see for the supported types
+ /// when the type of the value is not supported
+ public static void Write(nint propVariant, object value)
+ {
+ var data = propVariant + ValueOffset;
+ switch (value)
+ {
+ case null:
+ SetVarType(propVariant, VtEmpty);
+ break;
+ case WicMetadataValue typed:
+ Write(propVariant, typed);
+ break;
+ case string text:
+ SetVarType(propVariant, VtLpwstr);
+ Marshal.WriteIntPtr(data, Marshal.StringToCoTaskMemUni(text));
+ break;
+ case bool flag:
+ SetVarType(propVariant, VtBool);
+ Marshal.WriteInt16(data, flag ? (short)-1 : (short)0);
+ break;
+ case sbyte number:
+ SetVarType(propVariant, VtI1);
+ Marshal.WriteByte(data, unchecked((byte)number));
+ break;
+ case byte number:
+ SetVarType(propVariant, VtUi1);
+ Marshal.WriteByte(data, number);
+ break;
+ case short number:
+ SetVarType(propVariant, VtI2);
+ Marshal.WriteInt16(data, number);
+ break;
+ case ushort number:
+ SetVarType(propVariant, VtUi2);
+ Marshal.WriteInt16(data, unchecked((short)number));
+ break;
+ case int number:
+ SetVarType(propVariant, VtI4);
+ Marshal.WriteInt32(data, number);
+ break;
+ case uint number:
+ SetVarType(propVariant, VtUi4);
+ Marshal.WriteInt32(data, unchecked((int)number));
+ break;
+ case long number:
+ SetVarType(propVariant, VtI8);
+ Marshal.WriteInt64(data, number);
+ break;
+ case ulong number:
+ SetVarType(propVariant, VtUi8);
+ Marshal.WriteInt64(data, unchecked((long)number));
+ break;
+ case float number:
+ SetVarType(propVariant, VtR4);
+ Marshal.WriteInt32(data, BitConverter.ToInt32(BitConverter.GetBytes(number), 0));
+ break;
+ case double number:
+ SetVarType(propVariant, VtR8);
+ Marshal.WriteInt64(data, BitConverter.DoubleToInt64Bits(number));
+ break;
+ case WicRational rational:
+ SetVarType(propVariant, VtUi8);
+ Marshal.WriteInt64(data, unchecked((long)rational.ToPacked()));
+ break;
+ case WicSignedRational rational:
+ SetVarType(propVariant, VtI8);
+ Marshal.WriteInt64(data, rational.ToPacked());
+ break;
+ case DateTime dateTime:
+ SetVarType(propVariant, VtFiletime);
+ Marshal.WriteInt64(data, dateTime.ToFileTimeUtc());
+ break;
+ case DateTimeOffset dateTimeOffset:
+ SetVarType(propVariant, VtFiletime);
+ Marshal.WriteInt64(data, dateTimeOffset.ToFileTime());
+ break;
+ case Guid guid:
+ SetVarType(propVariant, VtClsid);
+ var guidMemory = Marshal.AllocCoTaskMem(16);
+ Marshal.StructureToPtr(guid, guidMemory, false);
+ Marshal.WriteIntPtr(data, guidMemory);
+ break;
+ case byte[] bytes:
+ WriteArray(propVariant, VtBlob, bytes, 1);
+ break;
+ case ushort[] numbers:
+ WriteArray(propVariant, VtVector | VtUi2, numbers, 2);
+ break;
+ case uint[] numbers:
+ WriteArray(propVariant, VtVector | VtUi4, numbers, 4);
+ break;
+ case int[] numbers:
+ WriteArray(propVariant, VtVector | VtI4, numbers, 4);
+ break;
+ case string[] texts:
+ SetVarType(propVariant, VtVector | VtLpwstr);
+ var array = Marshal.AllocCoTaskMem(Math.Max(1, texts.Length) * IntPtr.Size);
+ for (int i = 0; i < texts.Length; i++)
+ {
+ Marshal.WriteIntPtr(array, i * IntPtr.Size, Marshal.StringToCoTaskMemUni(texts[i] ?? string.Empty));
+ }
+ Marshal.WriteInt32(propVariant, ValueOffset, texts.Length);
+ Marshal.WriteIntPtr(propVariant, PointerOffset, array);
+ break;
+ default:
+ throw new ArgumentException($"Metadata values of type {value.GetType()} are not supported", nameof(value));
+ }
+ }
+
+ private static void Write(nint propVariant, WicMetadataValue value)
+ {
+ var data = propVariant + ValueOffset;
+ switch (value.Kind)
+ {
+ case WicMetadataValueKind.AnsiString:
+ SetVarType(propVariant, VtLpstr);
+ Marshal.WriteIntPtr(data, Marshal.StringToCoTaskMemAnsi((string)value.Value ?? string.Empty));
+ break;
+ case WicMetadataValueKind.ByteVector:
+ WriteArray(propVariant, VtVector | VtUi1, (byte[])value.Value ?? Array.Empty(), 1);
+ break;
+ default:
+ Write(propVariant, value.Value);
+ break;
+ }
+ }
+
+ private static unsafe void WriteArray(nint propVariant, ushort varType, T[] values, int elementSize) where T : unmanaged
+ {
+ SetVarType(propVariant, varType);
+ var byteCount = (long)values.Length * elementSize;
+ var memory = Marshal.AllocCoTaskMem((int)Math.Max(1, byteCount));
+ fixed (T* source = values)
+ {
+ Buffer.MemoryCopy(source, (void*)memory, byteCount, byteCount);
+ }
+ Marshal.WriteInt32(propVariant, ValueOffset, values.Length);
+ Marshal.WriteIntPtr(propVariant, PointerOffset, memory);
+ }
+
+ ///
+ /// Store a COM object as VT_UNKNOWN, the PROPVARIANT holds a reference which PropVariantClear releases
+ ///
+ public static void WriteUnknown(nint propVariant, object comObject, Type interfaceType)
+ {
+ SetVarType(propVariant, VtUnknown);
+ Marshal.WriteIntPtr(propVariant + ValueOffset, Marshal.GetComInterfaceForObject(comObject, interfaceType));
+ }
+
+ private static void SetVarType(nint propVariant, ushort varType) => Marshal.WriteInt16(propVariant, unchecked((short)varType));
+
+ [DllImport("ole32.dll", ExactSpelling = true)]
+ private static extern HResult PropVariantClear(nint pvar);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/WicComponentEnumerateOptions.cs b/src/Dapplo.Windows.Imaging/Interop/WicComponentEnumerateOptions.cs
new file mode 100644
index 00000000..f4f32f9e
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/WicComponentEnumerateOptions.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// WICComponentEnumerateOptions from wincodec.idl
+///
+[Flags]
+internal enum WicComponentEnumerateOptions : uint
+{
+ Default = 0x00000000,
+ Refresh = 0x00000001,
+ BuiltInOnly = 0x20000000,
+ Unsigned = 0x40000000,
+ Disabled = 0x80000000
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/WicComponentType.cs b/src/Dapplo.Windows.Imaging/Interop/WicComponentType.cs
new file mode 100644
index 00000000..06bdf718
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/WicComponentType.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// WICComponentType from wincodec.idl
+///
+[Flags]
+internal enum WicComponentType : uint
+{
+ Decoder = 0x00000001,
+ Encoder = 0x00000002,
+ PixelFormatConverter = 0x00000004,
+ MetadataReader = 0x00000008,
+ MetadataWriter = 0x00000010,
+ PixelFormat = 0x00000020,
+ AllComponents = 0x0000003F
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/WicEnums.cs b/src/Dapplo.Windows.Imaging/Interop/WicEnums.cs
new file mode 100644
index 00000000..f5fc1001
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/WicEnums.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// WICDecodeOptions from wincodec.idl
+///
+internal enum WicDecodeOptions : uint
+{
+ MetadataCacheOnDemand = 0x00000000,
+ MetadataCacheOnLoad = 0x00000001
+}
+
+///
+/// WICBitmapEncoderCacheOption from wincodec.idl
+///
+internal enum WicBitmapEncoderCacheOption : uint
+{
+ InMemory = 0x00000000,
+ TempFile = 0x00000001,
+ NoCache = 0x00000002
+}
+
+///
+/// WICBitmapDitherType from wincodec.idl (only the values which are used)
+///
+internal enum WicBitmapDitherType : uint
+{
+ None = 0x00000000
+}
+
+///
+/// WICBitmapPaletteType from wincodec.idl (only the values which are used)
+///
+internal enum WicBitmapPaletteType : uint
+{
+ Custom = 0x00000000
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/WicErrors.cs b/src/Dapplo.Windows.Imaging/Interop/WicErrors.cs
new file mode 100644
index 00000000..cbb2487e
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/WicErrors.cs
@@ -0,0 +1,32 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// WINCODEC_ERR_* values from winerror.h which are handled
+///
+internal static class WicErrors
+{
+ ///
+ /// WINCODEC_ERR_PROPERTYNOTFOUND
+ ///
+ public const HResult PropertyNotFound = unchecked((HResult)0x88982F40);
+
+ ///
+ /// WINCODEC_ERR_PALETTEUNAVAILABLE
+ ///
+ public const HResult PaletteUnavailable = unchecked((HResult)0x88982F45);
+
+ ///
+ /// WINCODEC_ERR_COMPONENTNOTFOUND: no codec for the container format or the stream
+ ///
+ public const HResult ComponentNotFound = unchecked((HResult)0x88982F50);
+
+ ///
+ /// WINCODEC_ERR_COMPONENTINITIALIZEFAILURE: the codec is registered but couldn't be initialized, e.g. the HEIF encoder without the HEVC video extension
+ ///
+ public const HResult ComponentInitializeFailure = unchecked((HResult)0x88982F8B);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/WicMetadataBlocks.cs b/src/Dapplo.Windows.Imaging/Interop/WicMetadataBlocks.cs
new file mode 100644
index 00000000..0e1798bf
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/WicMetadataBlocks.cs
@@ -0,0 +1,146 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.ComTypes;
+using Dapplo.Windows.Common.Enums;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// The top-level metadata blocks of a decoded frame (QueryInterface on IWICBitmapFrameDecode), methods in the order of wincodecsdk.idl.
+/// See IWICMetadataBlockReader interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("FEAA2A8D-B3F3-43E4-B25C-D1DE990A1AE1")]
+internal interface IWICMetadataBlockReader
+{
+ [PreserveSig] HResult GetContainerFormat(out Guid pguidContainerFormat);
+ [PreserveSig] HResult GetCount(out uint pcCount);
+ [PreserveSig] HResult GetReaderByIndex(uint nIndex, out IWICMetadataReader ppIMetadataReader);
+ [PreserveSig] HResult GetEnumerator(out IEnumUnknown ppIEnumMetadata);
+}
+
+///
+/// The top-level metadata blocks of a frame which is encoded (QueryInterface on IWICBitmapFrameEncode), inherits IWICMetadataBlockReader.
+/// See IWICMetadataBlockWriter interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("08FB9676-B444-41E8-8DBE-6A53A542BFF1")]
+internal interface IWICMetadataBlockWriter
+{
+ // IWICMetadataBlockReader
+ [PreserveSig] HResult GetContainerFormat(out Guid pguidContainerFormat);
+ [PreserveSig] HResult GetCount(out uint pcCount);
+ [PreserveSig] HResult GetReaderByIndex(uint nIndex, out IWICMetadataReader ppIMetadataReader);
+ [PreserveSig] HResult GetEnumerator(out IEnumUnknown ppIEnumMetadata);
+
+ // IWICMetadataBlockWriter
+ [PreserveSig] HResult InitializeFromBlockReader(IWICMetadataBlockReader pIMDBlockReader);
+ [PreserveSig] HResult GetWriterByIndex(uint nIndex, out IWICMetadataWriter ppIMetadataWriter);
+ [PreserveSig] HResult AddWriter(IWICMetadataWriter pIMetadataWriter);
+ [PreserveSig] HResult SetWriterByIndex(uint nIndex, IWICMetadataWriter pIMetadataWriter);
+ [PreserveSig] HResult RemoveWriterByIndex(uint nIndex);
+}
+
+///
+/// A metadata reader (one block), methods in the order of wincodecsdk.idl.
+/// The PROPVARIANT parameters of GetValueByIndex are [in, out, unique]: 0 when not needed, the caller clears them.
+/// See IWICMetadataReader interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("9204FE99-D8FC-4FD5-A001-9536B067A899")]
+internal interface IWICMetadataReader
+{
+ [PreserveSig] HResult GetMetadataFormat(out Guid pguidMetadataFormat);
+ [PreserveSig] HResult GetMetadataHandlerInfo(out nint ppIHandler);
+ [PreserveSig] HResult GetCount(out uint pcCount);
+ [PreserveSig] HResult GetValueByIndex(uint nIndex, nint pvarSchema, nint pvarId, nint pvarValue);
+ [PreserveSig] HResult GetValue(nint pvarSchema, nint pvarId, nint pvarValue);
+ [PreserveSig] HResult GetEnumerator(out nint ppIEnumMetadata);
+}
+
+///
+/// A metadata writer (one block), only the first method is declared, which is allowed for a prefix of the vtable.
+/// See IWICMetadataWriter interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("F7836E16-3BE0-470B-86BB-160D0AECD7DE")]
+internal interface IWICMetadataWriter
+{
+ [PreserveSig] HResult GetMetadataFormat(out Guid pguidMetadataFormat);
+}
+
+///
+/// Persists a metadata block to or from a stream, IPersist and IPersistStream methods first, in the order of the SDK.
+/// See IWICPersistStream interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("00675040-6908-45F8-86A3-49C7DFD6D9AD")]
+internal interface IWICPersistStream
+{
+ // IPersist
+ [PreserveSig] HResult GetClassID(out Guid pClassID);
+
+ // IPersistStream
+ [PreserveSig] HResult IsDirty();
+ [PreserveSig] HResult Load(IStream pStm);
+ [PreserveSig] HResult Save(IStream pStm, int fClearDirty);
+ [PreserveSig] HResult GetSizeMax(out ulong pcbSize);
+
+ // IWICPersistStream
+ [PreserveSig] HResult LoadEx(IStream pIStream, nint pguidPreferredVendor, uint dwPersistOptions);
+ [PreserveSig] HResult SaveEx(IStream pIStream, uint dwPersistOptions, int fClearDirty);
+}
+
+///
+/// The component factory, the same object as the imaging factory (QueryInterface), it inherits IWICImagingFactory: all of its methods are repeated first.
+/// See IWICComponentFactory interface
+///
+[ComImport]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+[Guid("412D0C3A-9650-44FA-AF5B-DD2A06C8E8FB")]
+internal interface IWICComponentFactory
+{
+ // IWICImagingFactory, 25 methods which aren't called through this interface
+ [PreserveSig] HResult CreateDecoderFromFilename(nint a, nint b, uint c, int d, out nint e);
+ [PreserveSig] HResult CreateDecoderFromStream(nint a, nint b, int c, out nint d);
+ [PreserveSig] HResult CreateDecoderFromFileHandle(nint a, nint b, int c, out nint d);
+ [PreserveSig] HResult CreateComponentInfo(nint a, out nint b);
+ [PreserveSig] HResult CreateDecoder(nint a, nint b, out nint c);
+ [PreserveSig] HResult CreateEncoder(nint a, nint b, out nint c);
+ [PreserveSig] HResult CreatePalette(out nint a);
+ [PreserveSig] HResult CreateFormatConverter(out nint a);
+ [PreserveSig] HResult CreateBitmapScaler(out nint a);
+ [PreserveSig] HResult CreateBitmapClipper(out nint a);
+ [PreserveSig] HResult CreateBitmapFlipRotator(out nint a);
+ [PreserveSig] HResult CreateStream(out nint a);
+ [PreserveSig] HResult CreateColorContext(out nint a);
+ [PreserveSig] HResult CreateColorTransformer(out nint a);
+ [PreserveSig] HResult CreateBitmap(uint a, uint b, nint c, int d, out nint e);
+ [PreserveSig] HResult CreateBitmapFromSource(nint a, int b, out nint c);
+ [PreserveSig] HResult CreateBitmapFromSourceRect(nint a, uint b, uint c, uint d, uint e, out nint f);
+ [PreserveSig] HResult CreateBitmapFromMemory(uint a, uint b, nint c, uint d, uint e, nint f, out nint g);
+ [PreserveSig] HResult CreateBitmapFromHBITMAP(nint a, nint b, int c, out nint d);
+ [PreserveSig] HResult CreateBitmapFromHICON(nint a, out nint b);
+ [PreserveSig] HResult CreateComponentEnumerator(uint a, uint b, out nint c);
+ [PreserveSig] HResult CreateFastMetadataEncoderFromDecoder(nint a, out nint b);
+ [PreserveSig] HResult CreateFastMetadataEncoderFromFrameDecode(nint a, out nint b);
+ [PreserveSig] HResult CreateQueryWriter(nint a, nint b, out nint c);
+ [PreserveSig] HResult CreateQueryWriterFromReader(nint a, nint b, out nint c);
+
+ // IWICComponentFactory
+ [PreserveSig] HResult CreateMetadataReader([In] ref Guid guidMetadataFormat, nint pguidVendor, uint dwOptions, IStream pIStream, out IWICMetadataReader ppIReader);
+ [PreserveSig] HResult CreateMetadataReaderFromContainer(nint guidContainerFormat, nint pguidVendor, uint dwOptions, nint pIStream, out nint ppIReader);
+ [PreserveSig] HResult CreateMetadataWriter(nint guidMetadataFormat, nint pguidVendor, uint dwMetadataOptions, out nint ppIWriter);
+ [PreserveSig] HResult CreateMetadataWriterFromReader(IWICMetadataReader pIReader, nint pguidVendor, out IWICMetadataWriter ppIWriter);
+ [PreserveSig] HResult CreateQueryReaderFromBlockReader(nint pIBlockReader, out nint ppIQueryReader);
+ [PreserveSig] HResult CreateQueryWriterFromBlockWriter(nint pIBlockWriter, out nint ppIQueryWriter);
+ [PreserveSig] HResult CreateEncoderPropertyBag(nint ppropOptions, uint cCount, out nint ppIPropertyBag);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/WicNativeMethods.cs b/src/Dapplo.Windows.Imaging/Interop/WicNativeMethods.cs
new file mode 100644
index 00000000..4edc6c99
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/WicNativeMethods.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using Dapplo.Windows.Common.Enums;
+using Dapplo.Windows.Common.Extensions;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// Creation of the WIC imaging factory
+///
+internal static class WicNativeMethods
+{
+ ///
+ /// CLSID_WICImagingFactory, the version 1 factory which is available on every supported Windows version
+ ///
+ internal static readonly Guid ClsidWicImagingFactory = new Guid("CACAF262-9370-4615-A13B-9F5539DA4C0A");
+
+ private const uint ClsCtxInprocServer = 0x1;
+
+ ///
+ /// CO_E_NOTINITIALIZED: CoInitialize has not been called on the current thread
+ ///
+ private const HResult CoENotInitialized = unchecked((HResult)0x800401F0);
+
+ ///
+ /// Create the WIC imaging factory in-process. The factory is free-threaded, so this works from STA and MTA threads.
+ ///
+ /// IWICImagingFactory, the caller must release it
+ /// when the factory can't be created
+ internal static IWICImagingFactory CreateImagingFactory()
+ {
+ var clsid = ClsidWicImagingFactory;
+ var iid = typeof(IWICImagingFactory).GUID;
+ var hResult = CoCreateInstance(ref clsid, IntPtr.Zero, ClsCtxInprocServer, ref iid, out var factory);
+ if (hResult == CoENotInitialized)
+ {
+ // COM isn't initialized on this (native) thread yet, the runtime's COM activation initializes it (implicit MTA) and creates the factory
+ factory = Activator.CreateInstance(Type.GetTypeFromCLSID(ClsidWicImagingFactory, true));
+ return (IWICImagingFactory)factory;
+ }
+ if (!hResult.Succeeded() || factory == null)
+ {
+ throw new COMException("Couldn't create the WIC imaging factory", (int)hResult);
+ }
+ return (IWICImagingFactory)factory;
+ }
+
+ ///
+ /// See CoCreateInstance
+ ///
+ [DllImport("ole32.dll", ExactSpelling = true)]
+ private static extern HResult CoCreateInstance([In] ref Guid rclsid, IntPtr pUnkOuter, uint dwClsContext, [In] ref Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppv);
+}
diff --git a/src/Dapplo.Windows.Imaging/Interop/WicRect.cs b/src/Dapplo.Windows.Imaging/Interop/WicRect.cs
new file mode 100644
index 00000000..a6872f7e
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/Interop/WicRect.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Runtime.InteropServices;
+
+namespace Dapplo.Windows.Imaging.Interop;
+
+///
+/// WICRect from wincodec.idl
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct WicRect
+{
+ public int X;
+ public int Y;
+ public int Width;
+ public int Height;
+
+ public WicRect(int x, int y, int width, int height)
+ {
+ X = x;
+ Y = y;
+ Width = width;
+ Height = height;
+ }
+}
diff --git a/src/Dapplo.Windows.Imaging/WicCodecInfo.cs b/src/Dapplo.Windows.Imaging/WicCodecInfo.cs
new file mode 100644
index 00000000..2e44055c
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicCodecInfo.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// Immutable description of one WIC codec as reported by WIC, see
+///
+public sealed class WicCodecInfo
+{
+ internal WicCodecInfo(WicCodecKind kind, Guid clsid, Guid containerFormat, Guid vendorGuid, string friendlyName, string author, string version,
+ IReadOnlyList fileExtensions, IReadOnlyList mimeTypes, WicSigningStatus signingStatus)
+ {
+ Kind = kind;
+ Clsid = clsid;
+ ContainerFormat = containerFormat;
+ VendorGuid = vendorGuid;
+ FriendlyName = friendlyName ?? string.Empty;
+ Author = author ?? string.Empty;
+ Version = version ?? string.Empty;
+ FileExtensions = fileExtensions ?? Array.Empty();
+ MimeTypes = mimeTypes ?? Array.Empty();
+ SigningStatus = signingStatus;
+ }
+
+ ///
+ /// Decoder or encoder
+ ///
+ public WicCodecKind Kind { get; }
+
+ ///
+ /// The CLSID of the codec
+ ///
+ public Guid Clsid { get; }
+
+ ///
+ /// The container format GUID, e.g. GUID_ContainerFormatPng (1B7CFAF4-713F-473C-BBCD-6137425FAEAF)
+ ///
+ public Guid ContainerFormat { get; }
+
+ ///
+ /// The vendor GUID, see
+ ///
+ public Guid VendorGuid { get; }
+
+ ///
+ /// The friendly name, e.g. "PNG Decoder" or "Microsoft HEIF Decoder"; empty when the codec doesn't report one
+ ///
+ public string FriendlyName { get; }
+
+ ///
+ /// The author; empty when the codec doesn't report one
+ ///
+ public string Author { get; }
+
+ ///
+ /// The version of the codec; empty when the codec doesn't report one
+ ///
+ public string Version { get; }
+
+ ///
+ /// The file extensions, lower case with the dot, e.g. ".heic", without duplicates
+ ///
+ public IReadOnlyList FileExtensions { get; }
+
+ ///
+ /// The MIME types, lower case, e.g. "image/heic", without duplicates
+ ///
+ public IReadOnlyList MimeTypes { get; }
+
+ ///
+ /// The signing status flags
+ ///
+ public WicSigningStatus SigningStatus { get; }
+
+ ///
+ /// True when the codec has the Microsoft vendor GUID ().
+ /// Note: this is true for the codecs which come with Windows and also for the codecs of Microsoft from the Store (HEIF, WebP, Raw, JPEG XL).
+ ///
+ public bool IsFromMicrosoft => VendorGuid == WicVendors.Microsoft;
+
+ ///
+ public override string ToString() => $"{FriendlyName} ({Kind}, {string.Join(" ", FileExtensions)})";
+}
diff --git a/src/Dapplo.Windows.Imaging/WicCodecKind.cs b/src/Dapplo.Windows.Imaging/WicCodecKind.cs
new file mode 100644
index 00000000..e6cc0a47
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicCodecKind.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// The kind of a WIC codec
+///
+public enum WicCodecKind
+{
+ ///
+ /// The codec reads (decodes) images, WICDecoder
+ ///
+ Decoder,
+
+ ///
+ /// The codec writes (encodes) images, WICEncoder
+ ///
+ Encoder
+}
diff --git a/src/Dapplo.Windows.Imaging/WicCodecNotFoundException.cs b/src/Dapplo.Windows.Imaging/WicCodecNotFoundException.cs
new file mode 100644
index 00000000..40efda63
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicCodecNotFoundException.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Runtime.InteropServices;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// WIC has no codec for the container format or the stream (WINCODEC_ERR_COMPONENTNOTFOUND, 0x88982F50),
+/// or the codec is registered but can't be initialized (WINCODEC_ERR_COMPONENTINITIALIZEFAILURE, 0x88982F8B, e.g. the HEIF encoder without the HEVC video extension).
+/// This is a normal case, e.g. when a Store codec like JPEG XL or HEIF isn't installed, so callers can fall back. tells which.
+///
+public sealed class WicCodecNotFoundException : COMException
+{
+ ///
+ /// Create the exception
+ ///
+ /// string
+ /// the HRESULT
+ public WicCodecNotFoundException(string message, int errorCode) : base(message, errorCode)
+ {
+ }
+}
diff --git a/src/Dapplo.Windows.Imaging/WicCodecs.cs b/src/Dapplo.Windows.Imaging/WicCodecs.cs
new file mode 100644
index 00000000..5404ebe1
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicCodecs.cs
@@ -0,0 +1,190 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Runtime.InteropServices;
+using Dapplo.Log;
+using Dapplo.Windows.Common.Enums;
+using Dapplo.Windows.Common.Extensions;
+using Dapplo.Windows.Imaging.Interop;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// Discovery of the codecs of the Windows Imaging Component (WIC).
+/// Unlike the registry (HKCR\CLSID\{7ED96837-96F0-4812-B211-F13C24117ED3}\Instance), which doesn't list the built-in codecs on newer Windows versions
+/// and never lists packaged (Store) codecs like HEIF/AVIF and WebP, this asks WIC itself, which reports all of them.
+///
+public static class WicCodecs
+{
+ private static readonly LogSource Log = new LogSource();
+
+ ///
+ /// All decoders WIC reports, including packaged (Store) codecs
+ ///
+ /// true to let WIC refresh its list of components (WICComponentEnumerateRefresh), e.g. after a codec was installed
+ /// IReadOnlyList of WicCodecInfo, in the order WIC reports them
+ /// when WIC isn't available or can't enumerate the codecs
+ public static IReadOnlyList GetDecoders(bool refresh = false) => GetCodecs(WicCodecKind.Decoder, refresh);
+
+ ///
+ /// All encoders WIC reports, including packaged (Store) codecs
+ ///
+ /// true to let WIC refresh its list of components (WICComponentEnumerateRefresh), e.g. after a codec was installed
+ /// IReadOnlyList of WicCodecInfo, in the order WIC reports them
+ /// when WIC isn't available or can't enumerate the codecs
+ public static IReadOnlyList GetEncoders(bool refresh = false) => GetCodecs(WicCodecKind.Encoder, refresh);
+
+ private static IReadOnlyList GetCodecs(WicCodecKind kind, bool refresh)
+ {
+ var componentType = kind == WicCodecKind.Decoder ? WicComponentType.Decoder : WicComponentType.Encoder;
+ var options = refresh ? WicComponentEnumerateOptions.Refresh : WicComponentEnumerateOptions.Default;
+ var codecs = new List();
+
+ IWICImagingFactory factory = null;
+ IEnumUnknown enumerator = null;
+ try
+ {
+ factory = WicNativeMethods.CreateImagingFactory();
+ var hResult = factory.CreateComponentEnumerator(componentType, options, out enumerator);
+ if (!hResult.Succeeded() || enumerator == null)
+ {
+ throw new COMException($"Couldn't enumerate the WIC {kind.ToString().ToLowerInvariant()}s", (int)hResult);
+ }
+
+ while (true)
+ {
+ object item = null;
+ try
+ {
+ hResult = enumerator.Next(1, out item, out var fetched);
+ if (!hResult.Succeeded())
+ {
+ // Can't continue the enumeration, keep what was found so far
+ Log.Warn().WriteLine("Enumerating the WIC {0}s stopped after {1} entries: {2}", kind, codecs.Count, hResult);
+ break;
+ }
+ if (hResult != HResult.S_OK || fetched == 0 || item == null)
+ {
+ break;
+ }
+
+ // The cast does the QueryInterface, it gives the same runtime callable wrapper, which is released once in finally
+ if (item is not IWICBitmapCodecInfo codecInfo)
+ {
+ Log.Warn().WriteLine("A WIC {0} doesn't implement IWICBitmapCodecInfo, skipping it", kind);
+ continue;
+ }
+ codecs.Add(ReadCodecInfo(kind, codecInfo));
+ }
+ catch (COMException ex)
+ {
+ // One broken codec must not break the list
+ Log.Warn().WriteLine(ex, "Skipping a WIC {0} which couldn't be read: 0x{1:X8}", kind, ex.ErrorCode);
+ }
+ finally
+ {
+ Release(item);
+ }
+ }
+ }
+ finally
+ {
+ Release(enumerator);
+ Release(factory);
+ }
+ return new ReadOnlyCollection(codecs);
+ }
+
+ private static WicCodecInfo ReadCodecInfo(WicCodecKind kind, IWICBitmapCodecInfo codecInfo)
+ {
+ Check(codecInfo.GetCLSID(out var clsid), "GetCLSID");
+ Check(codecInfo.GetContainerFormat(out var containerFormat), "GetContainerFormat");
+ Check(codecInfo.GetVendorGUID(out var vendorGuid), "GetVendorGUID");
+ Check(codecInfo.GetSigningStatus(out var signingStatus), "GetSigningStatus");
+ var friendlyName = ReadString(codecInfo.GetFriendlyName, nameof(codecInfo.GetFriendlyName));
+ var author = ReadString(codecInfo.GetAuthor, nameof(codecInfo.GetAuthor));
+ var version = ReadString(codecInfo.GetVersion, nameof(codecInfo.GetVersion));
+ var fileExtensions = SplitList(ReadString(codecInfo.GetFileExtensions, nameof(codecInfo.GetFileExtensions)), true);
+ var mimeTypes = SplitList(ReadString(codecInfo.GetMimeTypes, nameof(codecInfo.GetMimeTypes)), false);
+ return new WicCodecInfo(kind, clsid, containerFormat, vendorGuid, friendlyName, author, version, fileExtensions, mimeTypes, (WicSigningStatus)signingStatus);
+ }
+
+ ///
+ /// The signature of the WIC string methods: buffer size in characters, buffer, actual size in characters (including the terminating 0)
+ ///
+ private delegate HResult WicStringGetter(uint cch, nint buffer, out uint cchActual);
+
+ ///
+ /// Read a string the WIC way: call once with cch = 0 and a null buffer to get the length, then allocate and call again
+ ///
+ private static unsafe string ReadString(WicStringGetter getter, string methodName)
+ {
+ Check(getter(0, IntPtr.Zero, out var length), methodName);
+ if (length == 0)
+ {
+ return string.Empty;
+ }
+ var buffer = new char[length];
+ uint actual;
+ fixed (char* bufferPointer = buffer)
+ {
+ Check(getter(length, (nint)bufferPointer, out actual), methodName);
+ }
+ var count = (int)Math.Min(actual, length);
+ var end = Array.IndexOf(buffer, '\0', 0, count);
+ return new string(buffer, 0, end < 0 ? count : end);
+ }
+
+ ///
+ /// Split the comma separated list WIC returns, trim and lower case the entries, drop empty entries and duplicates
+ ///
+ /// string with e.g. ".heic,.heif" or "image/heic, image/heif"
+ /// true to make sure every entry starts with a dot
+ /// IReadOnlyList of string
+ private static IReadOnlyList SplitList(string value, bool isExtension)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return Array.Empty();
+ }
+
+ var seen = new HashSet(StringComparer.Ordinal);
+ var result = new List();
+ foreach (var part in value.Split(','))
+ {
+ var entry = part.Trim().ToLowerInvariant();
+ if (entry.Length == 0)
+ {
+ continue;
+ }
+ if (isExtension && entry[0] != '.')
+ {
+ entry = "." + entry;
+ }
+ if (entry.Length > (isExtension ? 1 : 0) && seen.Add(entry))
+ {
+ result.Add(entry);
+ }
+ }
+ return new ReadOnlyCollection(result);
+ }
+
+ private static void Check(HResult hResult, string methodName)
+ {
+ if (!hResult.Succeeded())
+ {
+ throw new COMException($"IWICBitmapCodecInfo.{methodName} failed", (int)hResult);
+ }
+ }
+
+ private static void Release(object comObject)
+ {
+ if (comObject != null && Marshal.IsComObject(comObject))
+ {
+ Marshal.ReleaseComObject(comObject);
+ }
+ }
+}
diff --git a/src/Dapplo.Windows.Imaging/WicContainerFormats.cs b/src/Dapplo.Windows.Imaging/WicContainerFormats.cs
new file mode 100644
index 00000000..f2a91869
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicContainerFormats.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// The WIC container format GUIDs (GUID_ContainerFormat* of wincodec.h), compare with or use for .
+/// The codecs for HEIF (also AVIF), WebP, Raw and JPEG XL are Store extensions, which might not be installed: check with .
+///
+public static class WicContainerFormats
+{
+ /// GUID_ContainerFormatBmp
+ public static readonly Guid Bmp = new Guid("0AF1D87E-FCFE-4188-BDEB-A7906471CBE3");
+
+ /// GUID_ContainerFormatPng
+ public static readonly Guid Png = new Guid("1B7CFAF4-713F-473C-BBCD-6137425FAEAF");
+
+ /// GUID_ContainerFormatIco
+ public static readonly Guid Ico = new Guid("A3A860C4-338F-4C17-919A-FBA4B5628F21");
+
+ /// GUID_ContainerFormatJpeg
+ public static readonly Guid Jpeg = new Guid("19E4A5AA-5662-4FC5-A0C0-1758028E1057");
+
+ /// GUID_ContainerFormatTiff
+ public static readonly Guid Tiff = new Guid("163BCC30-E2E9-4F0B-961D-A3E9FDB788A3");
+
+ /// GUID_ContainerFormatGif
+ public static readonly Guid Gif = new Guid("1F8A5601-7D4D-4CBD-9C82-1BC8D4EEB9A5");
+
+ /// GUID_ContainerFormatWmp: JPEG XR (HD Photo, Windows Media Photo)
+ public static readonly Guid Wmp = new Guid("57A37CAA-367A-4540-916B-F183C5093A4B");
+
+ /// GUID_ContainerFormatDds
+ public static readonly Guid Dds = new Guid("9967CB95-2E85-4AC8-8CA2-83D7CCD425C9");
+
+ /// GUID_ContainerFormatAdng: DNG
+ public static readonly Guid Adng = new Guid("F3FF6D0D-38C0-41C4-B1FE-1F3824F17B84");
+
+ /// GUID_ContainerFormatHeif: HEIF, also AVIF (Store extension)
+ public static readonly Guid Heif = new Guid("E1E62521-6787-405B-A339-500715B5763F");
+
+ /// GUID_ContainerFormatWebp (Store extension)
+ public static readonly Guid Webp = new Guid("E094B0E2-67F2-45B3-B0EA-115337CA7CF3");
+
+ /// GUID_ContainerFormatRaw (Store extension)
+ public static readonly Guid Raw = new Guid("FE99CE60-F19C-433C-A3AE-00ACEFA9CA21");
+
+ /// GUID_ContainerFormatJpegXL (Store extension)
+ public static readonly Guid JpegXl = new Guid("FEC14E3F-427A-4736-AAE6-27ED84F69322");
+}
diff --git a/src/Dapplo.Windows.Imaging/WicDelegates.cs b/src/Dapplo.Windows.Imaging/WicDelegates.cs
new file mode 100644
index 00000000..73caad88
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicDelegates.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// Supplies rows of pixels for an encoder: copy the rows [top, top + count) into destination, row i starts at i * destinationStride.
+/// Each row has width * bits per pixel / 8 bytes (rounded up), the bytes between rows (stride padding) don't matter.
+/// The span ends after the last row, so it can be shorter than count * destinationStride.
+///
+/// the first row
+/// the number of rows
+/// where the rows go
+/// bytes from the start of one row to the start of the next row
+public delegate void WicRowProvider(int top, int count, Span destination, int destinationStride);
+
+///
+/// Receives decoded rows of pixels: the rows [top, top + count), row i starts at i * sourceStride
+///
+/// the first row
+/// the number of rows
+/// the pixels, only valid during the call
+/// bytes from the start of one row to the start of the next row
+public delegate void WicRowConsumer(int top, int count, ReadOnlySpan source, int sourceStride);
+
+///
+/// Supplies the memory for a decoded image once its size is known, e.g. the Scan0 of a locked GDI+ Bitmap
+///
+/// the size, resolution and metadata of the image which will be decoded
+/// bytes per row of the memory, at least width * bits per pixel / 8
+/// pointer to stride * height bytes
+public delegate IntPtr WicPixelBufferProvider(WicFrameInfo frameInfo, out int stride);
diff --git a/src/Dapplo.Windows.Imaging/WicEncodeOptions.cs b/src/Dapplo.Windows.Imaging/WicEncodeOptions.cs
new file mode 100644
index 00000000..ac9a8ba6
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicEncodeOptions.cs
@@ -0,0 +1,104 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// The options for
+///
+public sealed class WicEncodeOptions
+{
+ ///
+ /// The container format, e.g. , , or
+ ///
+ public Guid ContainerFormat { get; set; }
+
+ ///
+ /// 0..1, the encoder option "ImageQuality", when the encoder supports it
+ ///
+ public float? ImageQuality { get; set; }
+
+ ///
+ /// The encoder option "Lossless", when the encoder supports it
+ ///
+ public bool? Lossless { get; set; }
+
+ ///
+ /// Any other encoder option by name, e.g. "CompressionQuality", "TiffCompressionMethod" or "FilterOption".
+ /// The value is converted to the type the encoder reports; an option the encoder doesn't have is skipped.
+ /// See for the options of an encoder.
+ ///
+ public IDictionary EncoderOptions { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// The horizontal resolution in dots per inch
+ ///
+ public double DpiX { get; set; } = 96;
+
+ ///
+ /// The vertical resolution in dots per inch
+ ///
+ public double DpiY { get; set; } = 96;
+
+ ///
+ /// Called after the frame is initialized and before the pixels are written, to write metadata and a color profile.
+ /// What the encoder doesn't support is skipped and reported in , it never fails the encoding.
+ ///
+ public Action Metadata { get; set; }
+}
+
+///
+/// The result of an encoding
+///
+public sealed class WicEncodeResult
+{
+ internal WicEncodeResult(Guid pixelFormat, IReadOnlyList appliedOptions, IReadOnlyList metadataIssues)
+ {
+ PixelFormat = pixelFormat;
+ AppliedOptions = appliedOptions;
+ MetadataIssues = metadataIssues;
+ }
+
+ ///
+ /// The pixel format the encoder wrote, the pixels were converted when it differs from the format of the source
+ ///
+ public Guid PixelFormat { get; }
+
+ ///
+ /// The names of the encoder options which were set
+ ///
+ public IReadOnlyList AppliedOptions { get; }
+
+ ///
+ /// The metadata which the encoder didn't accept
+ ///
+ public IReadOnlyList MetadataIssues { get; }
+}
+
+///
+/// An option of an encoder, see
+///
+public sealed class WicEncoderOption
+{
+ internal WicEncoderOption(string name, ushort varType)
+ {
+ Name = name;
+ VarType = varType;
+ }
+
+ ///
+ /// The name, e.g. "ImageQuality"
+ ///
+ public string Name { get; }
+
+ ///
+ /// The VARTYPE of the value, e.g. 4 (VT_R4) for a float or 11 (VT_BOOL) for a bool
+ ///
+ public ushort VarType { get; }
+
+ ///
+ public override string ToString() => $"{Name} (VARTYPE {VarType})";
+}
diff --git a/src/Dapplo.Windows.Imaging/WicFrameInfo.cs b/src/Dapplo.Windows.Imaging/WicFrameInfo.cs
new file mode 100644
index 00000000..fcc425da
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicFrameInfo.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// The first frame of a decoded image
+///
+public sealed class WicFrameInfo
+{
+ internal WicFrameInfo(int width, int height, double dpiX, double dpiY, Guid pixelFormat, Guid containerFormat, int frameCount, WicMetadata metadata, bool orientationApplied)
+ {
+ Width = width;
+ Height = height;
+ DpiX = dpiX;
+ DpiY = dpiY;
+ PixelFormat = pixelFormat;
+ ContainerFormat = containerFormat;
+ FrameCount = frameCount;
+ Metadata = metadata ?? WicMetadata.Empty;
+ OrientationApplied = orientationApplied;
+ }
+
+ ///
+ /// The width in pixels, after the orientation when it is applied
+ ///
+ public int Width { get; }
+
+ ///
+ /// The height in pixels, after the orientation when it is applied
+ ///
+ public int Height { get; }
+
+ ///
+ /// The horizontal resolution in dots per inch, after the orientation when it is applied
+ ///
+ public double DpiX { get; }
+
+ ///
+ /// The vertical resolution in dots per inch, after the orientation when it is applied
+ ///
+ public double DpiY { get; }
+
+ ///
+ /// The pixel format of the frame as stored, see
+ ///
+ public Guid PixelFormat { get; }
+
+ ///
+ /// The container format, see
+ ///
+ public Guid ContainerFormat { get; }
+
+ ///
+ /// The number of frames, only the first is decoded
+ ///
+ public int FrameCount { get; }
+
+ ///
+ /// The metadata of the first frame, never null
+ ///
+ public WicMetadata Metadata { get; }
+
+ ///
+ /// True when the EXIF orientation was applied, so Width and Height are those of the rotated image.
+ /// keeps the value of the file.
+ ///
+ public bool OrientationApplied { get; }
+}
diff --git a/src/Dapplo.Windows.Imaging/WicImage.cs b/src/Dapplo.Windows.Imaging/WicImage.cs
new file mode 100644
index 00000000..f5055e2e
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicImage.cs
@@ -0,0 +1,639 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Globalization;
+using System.IO;
+using System.Runtime.InteropServices;
+using Dapplo.Log;
+using Dapplo.Windows.Common.Enums;
+using Dapplo.Windows.Common.Extensions;
+using Dapplo.Windows.Imaging.Internal;
+using Dapplo.Windows.Imaging.Interop;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// Encode and decode images with the Windows Imaging Component (WIC), without WPF, System.Drawing or other pixel libraries:
+/// pixels are passed as memory (pointer and stride) or row by row through a callback, streams are .NET streams.
+/// Every COM object is created per call and released before the call returns, the methods work from STA and MTA threads.
+/// When WIC has no codec for the container format or the stream, a is thrown.
+///
+public static class WicImage
+{
+ private static readonly LogSource Log = new LogSource();
+
+ ///
+ /// Rows per CopyPixels call when decoding through a are chosen to stay around this size
+ ///
+ private const int BandBytes = 4 * 1024 * 1024;
+
+ #region Encoding
+
+ ///
+ /// Encode pixels in memory, e.g. GDI+ BitmapData.Scan0 or a contiguous ImageSharp buffer. The pixels are read without a copy of the image.
+ ///
+ /// the stream to write to, from its current position; a stream which can't seek is written through a MemoryStream
+ /// the container format and the encoder options
+ /// pointer to the first row
+ /// width in pixels
+ /// height in pixels
+ /// bytes from the start of one row to the next, at least width * bits per pixel / 8 (negative strides are not supported)
+ /// the pixel format of the memory, see
+ /// WicEncodeResult with the pixel format the encoder used and the skipped metadata
+ public static WicEncodeResult Encode(Stream destination, WicEncodeOptions options, IntPtr pixels, int width, int height, int stride, Guid pixelFormat)
+ {
+ if (pixels == IntPtr.Zero)
+ {
+ throw new ArgumentNullException(nameof(pixels));
+ }
+ ValidateEncode(destination, options, width, height);
+ return Encode(destination, options, factory =>
+ {
+ var bitsPerPixel = Wic.GetBitsPerPixel(factory, pixelFormat);
+ if (stride < Wic.RowBytes(width, bitsPerPixel))
+ {
+ throw new ArgumentOutOfRangeException(nameof(stride), stride, "The stride is smaller than a row");
+ }
+ return new WicMemoryPixelSource(pixels, width, height, stride, pixelFormat, bitsPerPixel, options.DpiX, options.DpiY);
+ });
+ }
+
+ ///
+ /// Encode pixels which are supplied row by row, e.g. from ImageSharp ProcessPixelRows when the image isn't contiguous.
+ /// The encoder pulls the rows it needs, no copy of the image is made.
+ ///
+ /// the stream to write to, from its current position; a stream which can't seek is written through a MemoryStream
+ /// the container format and the encoder options
+ /// width in pixels
+ /// height in pixels
+ /// the pixel format of the rows, see
+ /// supplies the rows
+ /// WicEncodeResult with the pixel format the encoder used and the skipped metadata
+ public static WicEncodeResult Encode(Stream destination, WicEncodeOptions options, int width, int height, Guid pixelFormat, WicRowProvider rows)
+ {
+ if (rows == null)
+ {
+ throw new ArgumentNullException(nameof(rows));
+ }
+ ValidateEncode(destination, options, width, height);
+ return Encode(destination, options, factory => new WicRowPixelSource(rows, width, height, pixelFormat, Wic.GetBitsPerPixel(factory, pixelFormat), options.DpiX, options.DpiY));
+ }
+
+ ///
+ /// The options of an encoder, e.g. "ImageQuality" or "Lossless", for
+ ///
+ /// see
+ /// IReadOnlyList of WicEncoderOption
+ /// when there is no encoder for the container format
+ public static IReadOnlyList GetEncoderOptions(Guid containerFormat)
+ {
+ IWICImagingFactory factory = null;
+ IWICBitmapEncoder encoder = null;
+ IWICBitmapFrameEncode frame = null;
+ IPropertyBag2 propertyBag = null;
+ try
+ {
+ factory = WicNativeMethods.CreateImagingFactory();
+ var container = containerFormat;
+ Wic.Check(factory.CreateEncoder(ref container, IntPtr.Zero, out encoder), $"Creating the encoder for {containerFormat}");
+ using var memoryStream = new MemoryStream();
+ var stream = new ManagedIStream(memoryStream);
+ Wic.Check(encoder.Initialize(stream, WicBitmapEncoderCacheOption.NoCache), "IWICBitmapEncoder.Initialize", stream);
+ Wic.Check(encoder.CreateNewFrame(out frame, out propertyBag), "IWICBitmapEncoder.CreateNewFrame", stream);
+ var result = new List();
+ if (propertyBag != null)
+ {
+ foreach (var (name, varType) in ReadPropertyInfos(propertyBag))
+ {
+ result.Add(new WicEncoderOption(name, varType));
+ }
+ }
+ return new ReadOnlyCollection(result);
+ }
+ finally
+ {
+ Wic.Release(propertyBag);
+ Wic.Release(frame);
+ Wic.Release(encoder);
+ Wic.Release(factory);
+ }
+ }
+
+ private static void ValidateEncode(Stream destination, WicEncodeOptions options, int width, int height)
+ {
+ if (destination == null)
+ {
+ throw new ArgumentNullException(nameof(destination));
+ }
+ if (!destination.CanWrite)
+ {
+ throw new ArgumentException("The stream must be writable", nameof(destination));
+ }
+ if (options == null)
+ {
+ throw new ArgumentNullException(nameof(options));
+ }
+ if (width <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(width), width, "The width must be positive");
+ }
+ if (height <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(height), height, "The height must be positive");
+ }
+ }
+
+ private static WicEncodeResult Encode(Stream destination, WicEncodeOptions options, Func createSource)
+ {
+ var target = destination.CanSeek ? destination : new MemoryStream();
+ IWICImagingFactory factory = null;
+ IWICBitmapEncoder encoder = null;
+ IWICBitmapFrameEncode frame = null;
+ IPropertyBag2 propertyBag = null;
+ IWICFormatConverter converter = null;
+ IWICMetadataQueryWriter queryWriter = null;
+ IWICPalette palette = null;
+ try
+ {
+ factory = WicNativeMethods.CreateImagingFactory();
+ var source = createSource(factory);
+ var stream = new ManagedIStream(target);
+
+ var container = options.ContainerFormat;
+ Wic.Check(factory.CreateEncoder(ref container, IntPtr.Zero, out encoder), $"Creating the encoder for {options.ContainerFormat}");
+ Wic.Check(encoder.Initialize(stream, WicBitmapEncoderCacheOption.NoCache), "IWICBitmapEncoder.Initialize", stream);
+ Wic.Check(encoder.CreateNewFrame(out frame, out propertyBag), "IWICBitmapEncoder.CreateNewFrame", stream);
+
+ var appliedOptions = propertyBag == null ? new List() : ApplyEncoderOptions(propertyBag, options);
+ Wic.Check(frame.Initialize(propertyBag), "IWICBitmapFrameEncode.Initialize", stream);
+ Wic.Check(frame.SetSize((uint)source.Width, (uint)source.Height), "IWICBitmapFrameEncode.SetSize", stream);
+ Wic.Check(frame.SetResolution(options.DpiX, options.DpiY), "IWICBitmapFrameEncode.SetResolution", stream);
+ var pixelFormat = source.PixelFormat;
+ Wic.Check(frame.SetPixelFormat(ref pixelFormat), "IWICBitmapFrameEncode.SetPixelFormat", stream);
+
+ IReadOnlyList metadataIssues = Array.Empty();
+ if (options.Metadata != null)
+ {
+ var queryWriterResult = frame.GetMetadataQueryWriter(out queryWriter);
+ var metadataWriter = new WicMetadataWriter(factory, frame, queryWriterResult.Succeeded() ? queryWriter : null, options.ContainerFormat, queryWriterResult);
+ try
+ {
+ options.Metadata(metadataWriter);
+ }
+ finally
+ {
+ metadataWriter.Close();
+ }
+ metadataIssues = metadataWriter.Issues;
+ }
+
+ IWICBitmapSource pixels = source;
+ if (pixelFormat != source.PixelFormat)
+ {
+ // The encoder wants another pixel format, WIC converts while it pulls the rows
+ Log.Debug().WriteLine("The encoder for {0} converts {1} to {2}", options.ContainerFormat, source.PixelFormat, pixelFormat);
+ var indexedBits = IndexedBitsPerPixel(pixelFormat);
+ if (indexedBits > 0)
+ {
+ // An indexed format (e.g. GIF) needs a palette, made from the pixels, with a transparent color when the source has alpha
+ Wic.Check(factory.CreatePalette(out palette), "IWICImagingFactory.CreatePalette");
+ Wic.Check(palette.InitializeFromBitmap(source, 1u << indexedBits, HasAlpha(source.PixelFormat) ? 1 : 0), "IWICPalette.InitializeFromBitmap", source);
+ Wic.Check(frame.SetPalette(palette), "IWICBitmapFrameEncode.SetPalette", stream);
+ }
+ Wic.Check(factory.CreateFormatConverter(out converter), "IWICImagingFactory.CreateFormatConverter");
+ Wic.Check(converter.Initialize(source, ref pixelFormat, WicBitmapDitherType.None, palette, indexedBits > 0 ? 50 : 0, WicBitmapPaletteType.Custom), "IWICFormatConverter.Initialize", source);
+ pixels = (IWICBitmapSource)converter;
+ }
+ Wic.Check(frame.WriteSource(pixels, IntPtr.Zero), "IWICBitmapFrameEncode.WriteSource", source, stream);
+ Wic.Check(frame.Commit(), "IWICBitmapFrameEncode.Commit", stream);
+ Wic.Check(encoder.Commit(), "IWICBitmapEncoder.Commit", stream);
+ stream.SeekToEndOfWritten();
+
+ if (!ReferenceEquals(target, destination))
+ {
+ target.Position = 0;
+ target.CopyTo(destination);
+ }
+ return new WicEncodeResult(pixelFormat, new ReadOnlyCollection(appliedOptions), metadataIssues);
+ }
+ finally
+ {
+ Wic.Release(queryWriter);
+ Wic.Release(palette);
+ Wic.Release(converter);
+ Wic.Release(propertyBag);
+ Wic.Release(frame);
+ Wic.Release(encoder);
+ Wic.Release(factory);
+ if (!ReferenceEquals(target, destination))
+ {
+ target.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// The bits per pixel of the indexed WIC pixel formats (GUID_WICPixelFormat1bppIndexed .. 8bppIndexed), 0 for the others
+ ///
+ private static int IndexedBitsPerPixel(Guid pixelFormat)
+ {
+ var bytes = pixelFormat.ToByteArray();
+ var indexedBase = new Guid("6FDDC324-4E03-4BFE-B185-3D77768DC900").ToByteArray();
+ for (int i = 0; i < 15; i++)
+ {
+ if (bytes[i] != indexedBase[i])
+ {
+ return 0;
+ }
+ }
+ return bytes[15] switch
+ {
+ 0x01 => 1,
+ 0x02 => 2,
+ 0x03 => 4,
+ 0x04 => 8,
+ _ => 0
+ };
+ }
+
+ private static bool HasAlpha(Guid pixelFormat) =>
+ pixelFormat == WicPixelFormats.Format32bppBgra || pixelFormat == WicPixelFormats.Format32bppPbgra || pixelFormat == WicPixelFormats.Format32bppRgba ||
+ pixelFormat == WicPixelFormats.Format64bppRgba || pixelFormat == WicPixelFormats.Format64bppRgbaHalf || pixelFormat == WicPixelFormats.Format128bppRgbaFloat;
+
+ private static List<(string Name, ushort VarType)> ReadPropertyInfos(IPropertyBag2 propertyBag)
+ {
+ var result = new List<(string, ushort)>();
+ if (!propertyBag.CountProperties(out var count).Succeeded())
+ {
+ return result;
+ }
+ for (uint i = 0; i < count; i++)
+ {
+ if (!propertyBag.GetPropertyInfo(i, 1, out var info, out var fetched).Succeeded() || fetched == 0)
+ {
+ continue;
+ }
+ try
+ {
+ var name = Marshal.PtrToStringUni(info.Name);
+ if (!string.IsNullOrEmpty(name))
+ {
+ result.Add((name, info.VarType));
+ }
+ }
+ finally
+ {
+ if (info.Name != IntPtr.Zero)
+ {
+ Marshal.FreeCoTaskMem(info.Name);
+ }
+ }
+ }
+ return result;
+ }
+
+ ///
+ /// Set the options the encoder supports, skip the others
+ ///
+ private static List ApplyEncoderOptions(IPropertyBag2 propertyBag, WicEncodeOptions options)
+ {
+ var applied = new List();
+ var wanted = new Dictionary(options.EncoderOptions, StringComparer.OrdinalIgnoreCase);
+ if (options.ImageQuality.HasValue)
+ {
+ wanted["ImageQuality"] = options.ImageQuality.Value;
+ }
+ if (options.Lossless.HasValue)
+ {
+ wanted["Lossless"] = options.Lossless.Value;
+ }
+ if (wanted.Count == 0)
+ {
+ return applied;
+ }
+
+ var supported = ReadPropertyInfos(propertyBag);
+ foreach (var option in wanted)
+ {
+ var match = supported.FindIndex(s => string.Equals(s.Name, option.Key, StringComparison.OrdinalIgnoreCase));
+ if (match < 0)
+ {
+ Log.Info().WriteLine("The encoder doesn't have the option {0}, skipping it", option.Key);
+ continue;
+ }
+ var (name, varType) = supported[match];
+ object value;
+ try
+ {
+ value = ConvertToVarType(option.Value, varType);
+ }
+ catch (Exception ex) when (ex is InvalidCastException || ex is FormatException || ex is OverflowException)
+ {
+ Log.Warn().WriteLine("The value {0} of the encoder option {1} can't be converted to VARTYPE {2}, skipping it", option.Value, name, varType);
+ continue;
+ }
+ var bag = new PropBag2 { Name = Marshal.StringToCoTaskMemUni(name) };
+ try
+ {
+ var hResult = propertyBag.Write(1, ref bag, ref value);
+ if (hResult.Succeeded())
+ {
+ applied.Add(name);
+ }
+ else
+ {
+ Log.Warn().WriteLine("The encoder didn't accept the option {0} = {1}: 0x{2:X8}", name, option.Value, (int)hResult);
+ }
+ }
+ finally
+ {
+ Marshal.FreeCoTaskMem(bag.Name);
+ }
+ }
+ return applied;
+ }
+
+ private static object ConvertToVarType(object value, ushort varType)
+ {
+ var culture = CultureInfo.InvariantCulture;
+ return varType switch
+ {
+ 2 => Convert.ToInt16(value, culture),
+ 3 => Convert.ToInt32(value, culture),
+ 4 => Convert.ToSingle(value, culture),
+ 5 => Convert.ToDouble(value, culture),
+ 11 => Convert.ToBoolean(value, culture),
+ 16 => Convert.ToSByte(value, culture),
+ 17 => Convert.ToByte(value, culture),
+ 18 => Convert.ToUInt16(value, culture),
+ 19 => Convert.ToUInt32(value, culture),
+ 20 => Convert.ToInt64(value, culture),
+ 21 => Convert.ToUInt64(value, culture),
+ 22 => Convert.ToInt32(value, culture),
+ 23 => Convert.ToUInt32(value, culture),
+ _ => value
+ };
+ }
+
+ #endregion
+
+ #region Decoding
+
+ ///
+ /// Read the size, resolution, pixel format and metadata of the first frame, without decoding the pixels.
+ /// A seekable stream is positioned back where it was, so can follow.
+ ///
+ /// the stream with the image, from its current position
+ /// true to report the size after applying the EXIF orientation
+ /// WicFrameInfo
+ /// when no decoder recognizes the stream
+ public static WicFrameInfo GetFrameInfo(Stream source, bool applyOrientation = false)
+ {
+ var position = source != null && source.CanSeek ? source.Position : -1;
+ try
+ {
+ return WithFrame(source, applyOrientation, (factory, frame, info) => info);
+ }
+ finally
+ {
+ if (position >= 0)
+ {
+ source.Position = position;
+ }
+ }
+ }
+
+ ///
+ /// Decode the first frame into memory the caller allocated, e.g. the locked buffer of a Bitmap of the size from
+ ///
+ /// the stream with the image, from its current position
+ /// the pixel format of the memory, see
+ /// pointer to stride * height bytes
+ /// bytes per row of the memory
+ /// true to apply the EXIF orientation (rotate and flip), the memory then has the size of the rotated image
+ /// WicFrameInfo with the size and the metadata
+ /// when no decoder recognizes the stream
+ public static WicFrameInfo Decode(Stream source, Guid targetPixelFormat, IntPtr pixels, int stride, bool applyOrientation = false)
+ {
+ if (pixels == IntPtr.Zero)
+ {
+ throw new ArgumentNullException(nameof(pixels));
+ }
+ return Decode(source, targetPixelFormat, (WicFrameInfo info, out int bufferStride) =>
+ {
+ bufferStride = stride;
+ return pixels;
+ }, applyOrientation);
+ }
+
+ ///
+ /// Decode the first frame into memory which is supplied once the size is known, e.g. the locked buffer of a new Bitmap. The stream is read once.
+ ///
+ /// the stream with the image, from its current position
+ /// the pixel format of the memory, see
+ /// supplies the memory and its stride
+ /// true to apply the EXIF orientation (rotate and flip)
+ /// WicFrameInfo with the size and the metadata
+ /// when no decoder recognizes the stream
+ public static WicFrameInfo Decode(Stream source, Guid targetPixelFormat, WicPixelBufferProvider bufferProvider, bool applyOrientation = false)
+ {
+ if (bufferProvider == null)
+ {
+ throw new ArgumentNullException(nameof(bufferProvider));
+ }
+ return WithFrame(source, applyOrientation, (factory, frame, info) =>
+ {
+ var bitsPerPixel = Wic.GetBitsPerPixel(factory, targetPixelFormat);
+ var rowBytes = Wic.RowBytes(info.Width, bitsPerPixel);
+ var pixels = bufferProvider(info, out var stride);
+ if (pixels == IntPtr.Zero)
+ {
+ throw new InvalidOperationException("The buffer provider returned no memory");
+ }
+ if (stride < rowBytes)
+ {
+ throw new ArgumentOutOfRangeException(nameof(stride), stride, $"The stride must be at least {rowBytes}");
+ }
+ var bufferSize = (long)stride * (info.Height - 1) + rowBytes;
+ if (bufferSize > uint.MaxValue)
+ {
+ throw new NotSupportedException("The image is too large for one CopyPixels call, use the WicRowConsumer overload");
+ }
+ WithConvertedSource(factory, frame, targetPixelFormat, converted =>
+ Wic.Check(converted.CopyPixels(IntPtr.Zero, (uint)stride, (uint)bufferSize, pixels), "IWICBitmapSource.CopyPixels"));
+ return info;
+ });
+ }
+
+ ///
+ /// Decode the first frame and hand the rows to a consumer, in bands of rows from a pooled buffer
+ ///
+ /// the stream with the image, from its current position
+ /// the pixel format of the rows, see
+ /// receives the rows
+ /// true to apply the EXIF orientation (rotate and flip)
+ /// WicFrameInfo with the size and the metadata
+ /// when no decoder recognizes the stream
+ public static WicFrameInfo Decode(Stream source, Guid targetPixelFormat, WicRowConsumer rows, bool applyOrientation = false)
+ {
+ if (rows == null)
+ {
+ throw new ArgumentNullException(nameof(rows));
+ }
+ return WithFrame(source, applyOrientation, (factory, frame, info) =>
+ {
+ var rowBytes = Wic.RowBytes(info.Width, Wic.GetBitsPerPixel(factory, targetPixelFormat));
+ var bandRows = Math.Max(1, Math.Min(info.Height, BandBytes / Math.Max(1, rowBytes)));
+ var buffer = ArrayPool.Shared.Rent(checked(bandRows * rowBytes));
+ try
+ {
+ WithConvertedSource(factory, frame, targetPixelFormat, converted =>
+ {
+ for (int top = 0; top < info.Height; top += bandRows)
+ {
+ var count = Math.Min(bandRows, info.Height - top);
+ var rect = new WicRect(0, top, info.Width, count);
+ var rectHandle = GCHandle.Alloc(rect, GCHandleType.Pinned);
+ var bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
+ try
+ {
+ Wic.Check(converted.CopyPixels(rectHandle.AddrOfPinnedObject(), (uint)rowBytes, (uint)(count * rowBytes), bufferHandle.AddrOfPinnedObject()), "IWICBitmapSource.CopyPixels");
+ }
+ finally
+ {
+ bufferHandle.Free();
+ rectHandle.Free();
+ }
+ rows(top, count, new ReadOnlySpan(buffer, 0, count * rowBytes), rowBytes);
+ }
+ });
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(buffer);
+ }
+ return info;
+ });
+ }
+
+ ///
+ /// Open the stream, get the first frame (with the orientation applied when requested) and its information
+ ///
+ private static T WithFrame(Stream source, bool applyOrientation, Func action)
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+ if (!source.CanRead)
+ {
+ throw new ArgumentException("The stream must be readable", nameof(source));
+ }
+ var input = source;
+ if (!source.CanSeek)
+ {
+ // Decoders seek, read a stream which can't seek into memory
+ var memoryStream = new MemoryStream();
+ source.CopyTo(memoryStream);
+ memoryStream.Position = 0;
+ input = memoryStream;
+ }
+
+ IWICImagingFactory factory = null;
+ IWICBitmapDecoder decoder = null;
+ IWICBitmapFrameDecode frame = null;
+ IWICBitmapFlipRotator flipRotator = null;
+ try
+ {
+ factory = WicNativeMethods.CreateImagingFactory();
+ var stream = new ManagedIStream(input);
+ Wic.Check(factory.CreateDecoderFromStream(stream, IntPtr.Zero, WicDecodeOptions.MetadataCacheOnDemand, out decoder), "Creating a decoder for the stream", stream);
+ Wic.Check(decoder.GetContainerFormat(out var containerFormat), "IWICBitmapDecoder.GetContainerFormat", stream);
+ Wic.Check(decoder.GetFrameCount(out var frameCount), "IWICBitmapDecoder.GetFrameCount", stream);
+ Wic.Check(decoder.GetFrame(0, out frame), "IWICBitmapDecoder.GetFrame", stream);
+ Wic.Check(frame.GetSize(out var width, out var height), "IWICBitmapFrameDecode.GetSize", stream);
+ Wic.Check(frame.GetPixelFormat(out var pixelFormat), "IWICBitmapFrameDecode.GetPixelFormat", stream);
+ if (!frame.GetResolution(out var dpiX, out var dpiY).Succeeded())
+ {
+ dpiX = dpiY = 96;
+ }
+ var metadata = WicMetadataReading.Read(factory, frame, containerFormat);
+
+ IWICBitmapSource pixels = (IWICBitmapSource)frame;
+ var orientationApplied = false;
+ if (applyOrientation && metadata.Orientation is int orientation && orientation > 1)
+ {
+ Wic.Check(factory.CreateBitmapFlipRotator(out flipRotator), "IWICImagingFactory.CreateBitmapFlipRotator");
+ Wic.Check(flipRotator.Initialize(pixels, OrientationToTransform(orientation)), "IWICBitmapFlipRotator.Initialize", stream);
+ Wic.Check(flipRotator.GetSize(out width, out height), "IWICBitmapFlipRotator.GetSize", stream);
+ if (orientation >= 5)
+ {
+ (dpiX, dpiY) = (dpiY, dpiX);
+ }
+ pixels = (IWICBitmapSource)flipRotator;
+ orientationApplied = true;
+ }
+
+ var info = new WicFrameInfo((int)width, (int)height, dpiX, dpiY, pixelFormat, containerFormat, (int)frameCount, metadata, orientationApplied);
+ try
+ {
+ return action(factory, pixels, info);
+ }
+ catch (COMException ex) when (stream.LastException != null && ex is not WicCodecNotFoundException)
+ {
+ // The stream failed while the pixels were decoded
+ System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(stream.LastException).Throw();
+ throw;
+ }
+ }
+ finally
+ {
+ Wic.Release(flipRotator);
+ Wic.Release(frame);
+ Wic.Release(decoder);
+ Wic.Release(factory);
+ if (!ReferenceEquals(input, source))
+ {
+ input.Dispose();
+ }
+ }
+ }
+
+ private static void WithConvertedSource(IWICImagingFactory factory, IWICBitmapSource source, Guid targetPixelFormat, Action action)
+ {
+ IWICFormatConverter converter = null;
+ try
+ {
+ Wic.Check(factory.CreateFormatConverter(out converter), "IWICImagingFactory.CreateFormatConverter");
+ var target = targetPixelFormat;
+ Wic.Check(converter.Initialize(source, ref target, WicBitmapDitherType.None, null, 0, WicBitmapPaletteType.Custom), $"Converting to the pixel format {targetPixelFormat}");
+ action((IWICBitmapSource)converter);
+ }
+ finally
+ {
+ Wic.Release(converter);
+ }
+ }
+
+ ///
+ /// The transformation which shows an image with the EXIF orientation upright.
+ /// WIC rotates clockwise; the combinations for 5 (transpose) and 7 (transverse) are verified with a test, see WicMetadataTests.
+ ///
+ internal static WicBitmapTransformOptions OrientationToTransform(int orientation) => orientation switch
+ {
+ 2 => WicBitmapTransformOptions.FlipHorizontal,
+ 3 => WicBitmapTransformOptions.Rotate180,
+ 4 => WicBitmapTransformOptions.FlipVertical,
+ 5 => WicBitmapTransformOptions.Rotate270 | WicBitmapTransformOptions.FlipHorizontal,
+ 6 => WicBitmapTransformOptions.Rotate90,
+ 7 => WicBitmapTransformOptions.Rotate90 | WicBitmapTransformOptions.FlipHorizontal,
+ 8 => WicBitmapTransformOptions.Rotate270,
+ _ => WicBitmapTransformOptions.Rotate0
+ };
+
+ #endregion
+}
diff --git a/src/Dapplo.Windows.Imaging/WicMetadata.cs b/src/Dapplo.Windows.Imaging/WicMetadata.cs
new file mode 100644
index 00000000..e73c0a02
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicMetadata.cs
@@ -0,0 +1,193 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// The metadata of a decoded frame, read once while decoding (a snapshot, no COM object is kept).
+/// The paths are those of the WIC metadata query language with the names of the blocks, e.g. "/app1/ifd/{ushort=274}" for the orientation in a JPEG
+/// and "/ifd/xmp/dc:title" for the XMP title in a TIFF. The paths with the tags of the blocks, as WIC enumerates them (e.g. "/app1/{ushort=0}/{ushort=274}"), work for too.
+/// See the metadata query language and .
+///
+public sealed class WicMetadata
+{
+ internal static readonly WicMetadata Empty = new WicMetadata(Array.Empty>(), Array.Empty>(), null, null, Array.Empty());
+
+ private readonly Dictionary _lookup;
+
+ internal WicMetadata(IList> items, IList> aliases, int? orientation, byte[] colorProfile, IList rawBlocks)
+ {
+ Items = new ReadOnlyCollection>(items);
+ Orientation = orientation;
+ ColorProfile = colorProfile;
+ RawBlocks = new ReadOnlyCollection(rawBlocks);
+ _lookup = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ foreach (var item in items.Concat(aliases))
+ {
+ if (!_lookup.ContainsKey(item.Key))
+ {
+ _lookup.Add(item.Key, item.Value);
+ }
+ }
+ }
+
+ ///
+ /// Every readable item of the frame, walked recursively through the nested readers: (query path, value).
+ /// A nested reader itself is listed as a , followed by its items.
+ /// The values are string, string[], the integer types, float, double, bool, DateTime (UTC, from a FILETIME), Guid, byte[] (blob or vector of bytes)
+ /// and arrays of numbers. EXIF rationals are ulong (RATIONAL) or long (SRATIONAL), see . Values of other types are left out.
+ ///
+ public IReadOnlyList> Items { get; }
+
+ ///
+ /// The EXIF orientation 1..8 (1 is normal, 6 is rotated 90 degrees clockwise to view), read with the path of the container or System.Photo.Orientation, or null
+ ///
+ public int? Orientation { get; }
+
+ ///
+ /// The ICC profile bytes of the first color context of the frame, or null
+ ///
+ public byte[] ColorProfile { get; }
+
+ ///
+ /// The EXIF, XMP and IPTC blocks as raw bytes (IWICPersistStream), to carry them over unchanged with :
+ /// the top-level APP1 (EXIF), APP13, XMP and IPTC blocks, and the XMP and IPTC blocks nested in a top-level block (e.g. "/ifd/xmp" in a TIFF).
+ ///
+ public IReadOnlyList RawBlocks { get; }
+
+ ///
+ /// Get the value of a path as it is listed in , e.g. "/app1/ifd/{ushort=274}", or with the tags of the blocks, e.g. "/app1/{ushort=0}/{ushort=274}" (case insensitive).
+ /// Policy names like "System.Title" are not resolved, use the paths of .
+ ///
+ /// the path
+ /// the value, see for the types
+ /// true when the frame has the path
+ public bool TryGet(string query, out object value)
+ {
+ if (query == null)
+ {
+ value = null;
+ return false;
+ }
+ return _lookup.TryGetValue(query, out value);
+ }
+
+ ///
+ /// Get an EXIF rational (stored by WIC as VT_UI8 or VT_I8 with the numerator in the low 32 bits)
+ ///
+ public bool TryGetRational(string query, out WicRational value)
+ {
+ if (TryGet(query, out var raw))
+ {
+ switch (raw)
+ {
+ case ulong packed:
+ value = WicRational.FromPacked(packed);
+ return true;
+ case long signedPacked:
+ value = WicRational.FromPacked(unchecked((ulong)signedPacked));
+ return true;
+ }
+ }
+ value = default;
+ return false;
+ }
+}
+
+///
+/// A nested metadata reader (a block like "/app1" or "/app1/ifd/exif"), as a value in
+///
+public sealed class WicMetadataBlock
+{
+ internal WicMetadataBlock(string path, Guid format)
+ {
+ Path = path;
+ Format = format;
+ }
+
+ ///
+ /// The path of the block
+ ///
+ public string Path { get; }
+
+ ///
+ /// The metadata format of the block, see
+ ///
+ public Guid Format { get; }
+
+ ///
+ public override string ToString() => $"Block {Path} ({Format})";
+}
+
+///
+/// The raw bytes of a metadata block
+///
+public sealed class WicMetadataRawBlock
+{
+ ///
+ /// Create a raw block, e.g. to add it with
+ ///
+ /// the query path of the block, e.g. "/app1" or "/xmp", null when unknown
+ /// the metadata format, see
+ /// the bytes as the metadata handler persists them
+ public WicMetadataRawBlock(string path, Guid format, byte[] data)
+ {
+ Path = path;
+ Format = format;
+ Data = data ?? throw new ArgumentNullException(nameof(data));
+ }
+
+ ///
+ /// The query path of the block, e.g. "/app1" or "/ifd/xmp"
+ ///
+ public string Path { get; }
+
+ ///
+ /// The metadata format, see
+ ///
+ public Guid Format { get; }
+
+ ///
+ /// The bytes as the metadata handler persists them (IWICPersistStream.SaveEx)
+ ///
+ public byte[] Data { get; }
+
+ ///
+ public override string ToString() => $"{Path} ({Format}): {Data.Length} bytes";
+}
+
+///
+/// Metadata which an encoder didn't accept, see
+///
+public sealed class WicMetadataIssue
+{
+ internal WicMetadataIssue(string query, int errorCode, string message)
+ {
+ Query = query;
+ ErrorCode = errorCode;
+ Message = message;
+ }
+
+ ///
+ /// The query path, or a description like "color profile" or "raw block /xmp"
+ ///
+ public string Query { get; }
+
+ ///
+ /// The HRESULT
+ ///
+ public int ErrorCode { get; }
+
+ ///
+ /// What failed
+ ///
+ public string Message { get; }
+
+ ///
+ public override string ToString() => $"{Query}: {Message} (0x{ErrorCode:X8})";
+}
diff --git a/src/Dapplo.Windows.Imaging/WicMetadataFormats.cs b/src/Dapplo.Windows.Imaging/WicMetadataFormats.cs
new file mode 100644
index 00000000..5243701e
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicMetadataFormats.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// The WIC metadata format GUIDs (GUID_MetadataFormat* of wincodecsdk.h) of the blocks which are used most, see .
+///
+public static class WicMetadataFormats
+{
+ /// GUID_MetadataFormatApp1: the JPEG APP1 segment with the EXIF data
+ public static readonly Guid App1 = new Guid("8FD3DFC3-F951-492B-817F-69C2E6D9A5B0");
+
+ /// GUID_MetadataFormatIfd: a TIFF image file directory
+ public static readonly Guid Ifd = new Guid("537396C6-2D8A-4BB6-9BF8-2F0A8E2A3ADF");
+
+ /// GUID_MetadataFormatExif: the EXIF sub IFD
+ public static readonly Guid Exif = new Guid("1C3C4F9D-B84A-467D-9493-36CFBD59EA57");
+
+ /// GUID_MetadataFormatGps: the GPS sub IFD
+ public static readonly Guid Gps = new Guid("7134AB8A-9351-44AD-AF62-448DB6B502EC");
+
+ /// GUID_MetadataFormatXMP: an XMP packet
+ public static readonly Guid Xmp = new Guid("BB5ACC38-F216-4CEC-A6C5-5F6E739763A9");
+
+ /// GUID_MetadataFormatIPTC
+ public static readonly Guid Iptc = new Guid("4FAB0914-E129-4087-A1D1-BC812D45A7B5");
+
+ /// GUID_MetadataFormatApp13: the JPEG APP13 segment (Photoshop, IPTC)
+ public static readonly Guid App13 = new Guid("326556A2-F502-4354-9CC0-8E3F48EAF6B5");
+
+ /// GUID_MetadataFormatChunktEXt: a PNG tEXt chunk
+ public static readonly Guid PngText = new Guid("568D8936-C0A9-4923-905D-DF2B38238FBC");
+
+ /// GUID_MetadataFormatChunkiTXt: a PNG iTXt chunk
+ public static readonly Guid PngInternationalText = new Guid("C2BEC729-0B68-4B77-AA0E-6295A6AC1814");
+
+ /// GUID_MetadataFormatChunktIME: the PNG tIME chunk
+ public static readonly Guid PngTime = new Guid("6B00AE2D-E24B-460A-98B6-878BD03072FD");
+
+ /// GUID_MetadataFormatHeif: the HEIF properties
+ public static readonly Guid Heif = new Guid("817EF3E1-1288-45F4-A852-260D9E7CCE83");
+
+ /// GUID_MetadataFormatUnknown: a block WIC has no reader for, the bytes are kept as they are
+ public static readonly Guid Unknown = new Guid("A45E592F-9078-4A7C-ADB5-4EDC4FD61B1F");
+}
diff --git a/src/Dapplo.Windows.Imaging/WicMetadataPaths.cs b/src/Dapplo.Windows.Imaging/WicMetadataPaths.cs
new file mode 100644
index 00000000..2e26dfc6
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicMetadataPaths.cs
@@ -0,0 +1,137 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// The metadata query paths per container: the same EXIF tag has a different path in each container,
+/// e.g. Software is "/app1/ifd/{ushort=305}" in a JPEG and "/ifd/{ushort=305}" in a TIFF or JPEG XR.
+/// Every method returns null when the container has no such path (or it isn't known to work).
+/// See doc/articles/imaging.md for which paths the codecs of Windows accept.
+///
+public static class WicMetadataPaths
+{
+ /// EXIF tag ImageDescription
+ public const ushort TagImageDescription = 270;
+
+ /// EXIF tag Orientation
+ public const ushort TagOrientation = 274;
+
+ /// EXIF tag Software
+ public const ushort TagSoftware = 305;
+
+ /// EXIF tag DateTime (when the file was changed, "YYYY:MM:DD HH:MM:SS")
+ public const ushort TagDateTime = 306;
+
+ /// EXIF tag Artist
+ public const ushort TagArtist = 315;
+
+ /// EXIF sub IFD tag DateTimeOriginal (when the image was captured, "YYYY:MM:DD HH:MM:SS")
+ public const ushort TagDateTimeOriginal = 36867;
+
+ /// The photo metadata policy for the orientation, which WIC maps to the path of each container it knows
+ public const string PolicyOrientation = "System.Photo.Orientation";
+
+ ///
+ /// The path of the main (0th) IFD with the EXIF tags: "/app1/ifd" for JPEG, "/ifd" for TIFF, JPEG XR and HEIF
+ ///
+ public static string Ifd(Guid containerFormat)
+ {
+ if (containerFormat == WicContainerFormats.Jpeg)
+ {
+ return "/app1/ifd";
+ }
+ if (containerFormat == WicContainerFormats.Tiff || containerFormat == WicContainerFormats.Wmp || containerFormat == WicContainerFormats.Heif)
+ {
+ return "/ifd";
+ }
+ return null;
+ }
+
+ ///
+ /// The path of a tag of the main IFD, e.g. ExifTag(Jpeg, 305) is "/app1/ifd/{ushort=305}"
+ ///
+ public static string ExifTag(Guid containerFormat, ushort tag)
+ {
+ var ifd = Ifd(containerFormat);
+ return ifd == null ? null : $"{ifd}/{{ushort={tag}}}";
+ }
+
+ ///
+ /// The path of a tag of the EXIF sub IFD, e.g. ExifSubTag(Jpeg, 36867) is "/app1/ifd/exif/{ushort=36867}"
+ ///
+ public static string ExifSubTag(Guid containerFormat, ushort tag)
+ {
+ var ifd = Ifd(containerFormat);
+ return ifd == null ? null : $"{ifd}/exif/{{ushort={tag}}}";
+ }
+
+ ///
+ /// The path of the EXIF orientation
+ ///
+ public static string Orientation(Guid containerFormat) => ExifTag(containerFormat, TagOrientation);
+
+ ///
+ /// The path of the software which wrote the file: the EXIF tag Software, for PNG the tEXt keyword "Software"
+ ///
+ public static string Software(Guid containerFormat) => containerFormat == WicContainerFormats.Png ? PngText("Software") : ExifTag(containerFormat, TagSoftware);
+
+ ///
+ /// The path of the description: the EXIF tag ImageDescription; null for PNG, use or with an index
+ ///
+ public static string Description(Guid containerFormat) => ExifTag(containerFormat, TagImageDescription);
+
+ ///
+ /// The path of the EXIF DateTimeOriginal (capture time), the value is a string "YYYY:MM:DD HH:MM:SS"
+ ///
+ public static string DateTimeOriginal(Guid containerFormat) => ExifSubTag(containerFormat, TagDateTimeOriginal);
+
+ ///
+ /// The path of a PNG tEXt chunk with the keyword, e.g. "/tEXt/{str=Software}"; keywords like Title, Author, Description, Copyright, Creation Time, Software, Source and Comment are registered.
+ /// Every tEXt chunk has one keyword, a second keyword in the same chunk replaces the first: give each keyword its own chunk index, e.g. "/[1]tEXt/{str=Description}".
+ ///
+ /// the keyword
+ /// the index of the tEXt chunk, 0 for the first
+ public static string PngText(string keyword, int index = 0) => index == 0 ? $"/tEXt/{{str={keyword}}}" : $"/[{index}]tEXt/{{str={keyword}}}";
+
+ ///
+ /// The path of the XMP block: "/xmp" for JPEG and PNG (an iTXt chunk), "/ifd/xmp" for TIFF and JPEG XR
+ ///
+ public static string Xmp(Guid containerFormat)
+ {
+ if (containerFormat == WicContainerFormats.Jpeg || containerFormat == WicContainerFormats.Png)
+ {
+ return "/xmp";
+ }
+ if (containerFormat == WicContainerFormats.Tiff || containerFormat == WicContainerFormats.Wmp)
+ {
+ return "/ifd/xmp";
+ }
+ return null;
+ }
+
+ ///
+ /// The path of an XMP property, e.g. XmpProperty(Jpeg, "dc:title") is "/xmp/dc:title"
+ ///
+ public static string XmpProperty(Guid containerFormat, string property)
+ {
+ var xmp = Xmp(containerFormat);
+ return xmp == null ? null : $"{xmp}/{property}";
+ }
+
+ ///
+ /// The queries tried for the orientation: the path of the container, then the policy
+ ///
+ internal static IEnumerable OrientationQueries(Guid containerFormat)
+ {
+ var path = Orientation(containerFormat);
+ if (path != null)
+ {
+ yield return path;
+ }
+ yield return PolicyOrientation;
+ }
+}
diff --git a/src/Dapplo.Windows.Imaging/WicMetadataValue.cs b/src/Dapplo.Windows.Imaging/WicMetadataValue.cs
new file mode 100644
index 00000000..805d47a2
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicMetadataValue.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// The PROPVARIANT type to use for a value, where the .NET type alone doesn't decide it
+///
+public enum WicMetadataValueKind
+{
+ /// The type follows from the .NET type of the value
+ Default,
+
+ /// VT_LPSTR, e.g. for the EXIF ASCII tags (Software, Make, Model, DateTime) in an IFD
+ AnsiString,
+
+ /// VT_VECTOR | VT_UI1 instead of VT_BLOB for a byte[]
+ ByteVector
+}
+
+///
+/// A value with an explicit PROPVARIANT type, for
+///
+public sealed class WicMetadataValue
+{
+ private WicMetadataValue(WicMetadataValueKind kind, object value)
+ {
+ Kind = kind;
+ Value = value;
+ }
+
+ /// The type to use
+ public WicMetadataValueKind Kind { get; }
+
+ /// The value
+ public object Value { get; }
+
+ /// A VT_LPSTR string, the EXIF ASCII tags in an IFD need this
+ public static WicMetadataValue AnsiString(string value) => new WicMetadataValue(WicMetadataValueKind.AnsiString, value);
+
+ /// A VT_VECTOR | VT_UI1 byte array
+ public static WicMetadataValue ByteVector(byte[] value) => new WicMetadataValue(WicMetadataValueKind.ByteVector, value);
+
+ ///
+ public override string ToString() => $"{Kind}: {Value}";
+}
diff --git a/src/Dapplo.Windows.Imaging/WicMetadataWriter.cs b/src/Dapplo.Windows.Imaging/WicMetadataWriter.cs
new file mode 100644
index 00000000..0bd12a70
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicMetadataWriter.cs
@@ -0,0 +1,289 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using Dapplo.Log;
+using Dapplo.Windows.Common.Enums;
+using Dapplo.Windows.Common.Extensions;
+using Dapplo.Windows.Imaging.Internal;
+using Dapplo.Windows.Imaging.Interop;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// Writes the metadata of the frame which is encoded, see .
+/// Everything the encoder doesn't accept is skipped and reported in , it never fails the encoding.
+/// Only valid during the callback.
+///
+public sealed class WicMetadataWriter
+{
+ private static readonly LogSource Log = new LogSource();
+
+ private readonly IWICImagingFactory _factory;
+ private readonly IWICBitmapFrameEncode _frame;
+ private readonly IWICMetadataQueryWriter _queryWriter;
+ private readonly List _issues = new List();
+ private bool _closed;
+
+ internal WicMetadataWriter(IWICImagingFactory factory, IWICBitmapFrameEncode frame, IWICMetadataQueryWriter queryWriter, Guid containerFormat, HResult queryWriterResult)
+ {
+ _factory = factory;
+ _frame = frame;
+ _queryWriter = queryWriter;
+ ContainerFormat = containerFormat;
+ QueryWriterResult = queryWriterResult;
+ }
+
+ ///
+ /// The container format which is written, to pick the paths with
+ ///
+ public Guid ContainerFormat { get; }
+
+ ///
+ /// True when the encoder has a metadata query writer, without one always fails
+ ///
+ public bool SupportsQueries => _queryWriter != null;
+
+ private HResult QueryWriterResult { get; }
+
+ ///
+ /// What was skipped so far
+ ///
+ public IReadOnlyList Issues => _issues;
+
+ internal void Close() => _closed = true;
+
+ private void EnsureOpen()
+ {
+ if (_closed)
+ {
+ throw new InvalidOperationException("The metadata writer can only be used in the WicEncodeOptions.Metadata callback");
+ }
+ }
+
+ private void AddIssue(string query, HResult hResult, string message)
+ {
+ Log.Info().WriteLine("Metadata {0} was not written: {1} (0x{2:X8})", query, message, (int)hResult);
+ _issues.Add(new WicMetadataIssue(query, (int)hResult, message));
+ }
+
+ ///
+ /// Set a value with the metadata query language, e.g. Set(WicMetadataPaths.Software(format), WicMetadataValue.AnsiString("Greenshot")) or Set("/xmp/dc:title", "Title").
+ /// Supported values: string (VT_LPWSTR), (VT_LPSTR or a vector of bytes), bool, the integer types, float, double,
+ /// , , DateTime and DateTimeOffset (FILETIME), Guid, byte[] (VT_BLOB), ushort[], uint[], int[] and string[].
+ /// Parent blocks which don't exist yet are created by WIC.
+ ///
+ /// the path, see
+ /// the value
+ /// true when the encoder accepted it, otherwise it is in
+ /// when the type of the value is not supported
+ public bool Set(string query, object value)
+ {
+ EnsureOpen();
+ if (string.IsNullOrEmpty(query))
+ {
+ throw new ArgumentNullException(nameof(query));
+ }
+ if (_queryWriter == null)
+ {
+ AddIssue(query, QueryWriterResult, "The encoder has no metadata writer");
+ return false;
+ }
+ var propVariant = PropVariant.Allocate();
+ try
+ {
+ PropVariant.Write(propVariant, value);
+ var hResult = _queryWriter.SetMetadataByName(query, propVariant);
+ if (!hResult.Succeeded())
+ {
+ AddIssue(query, hResult, "The encoder didn't accept the value");
+ return false;
+ }
+ return true;
+ }
+ finally
+ {
+ PropVariant.Free(propVariant);
+ }
+ }
+
+ ///
+ /// Remove a value which the encoder would write, e.g. one it adds itself
+ ///
+ /// true when it was removed
+ public bool Remove(string query)
+ {
+ EnsureOpen();
+ if (_queryWriter == null)
+ {
+ AddIssue(query, QueryWriterResult, "The encoder has no metadata writer");
+ return false;
+ }
+ var hResult = _queryWriter.RemoveMetadataByName(query);
+ if (!hResult.Succeeded())
+ {
+ AddIssue(query, hResult, "The value couldn't be removed");
+ return false;
+ }
+ return true;
+ }
+
+ ///
+ /// Write an ICC profile as color context of the frame
+ ///
+ /// the bytes of the ICC profile
+ /// true when the encoder accepted it
+ public bool SetColorProfile(byte[] icc)
+ {
+ EnsureOpen();
+ if (icc == null || icc.Length == 0)
+ {
+ throw new ArgumentNullException(nameof(icc));
+ }
+ IWICColorContext colorContext = null;
+ var pointer = IntPtr.Zero;
+ var handle = GCHandle.Alloc(icc, GCHandleType.Pinned);
+ try
+ {
+ var hResult = _factory.CreateColorContext(out colorContext);
+ if (hResult.Succeeded())
+ {
+ hResult = colorContext.InitializeFromMemory(handle.AddrOfPinnedObject(), (uint)icc.Length);
+ }
+ if (hResult.Succeeded())
+ {
+ pointer = Marshal.GetComInterfaceForObject(colorContext, typeof(IWICColorContext));
+ var pointers = new[] { pointer };
+ var pointersHandle = GCHandle.Alloc(pointers, GCHandleType.Pinned);
+ try
+ {
+ hResult = _frame.SetColorContexts(1, pointersHandle.AddrOfPinnedObject());
+ }
+ finally
+ {
+ pointersHandle.Free();
+ }
+ }
+ if (!hResult.Succeeded())
+ {
+ AddIssue("color profile", hResult, "The encoder didn't accept the color profile");
+ return false;
+ }
+ return true;
+ }
+ finally
+ {
+ handle.Free();
+ if (pointer != IntPtr.Zero)
+ {
+ Marshal.Release(pointer);
+ }
+ Wic.Release(colorContext);
+ }
+ }
+
+ ///
+ /// Write a metadata block from its raw bytes, e.g. a of a decoded image, to carry it over unchanged.
+ /// With a path (e.g. "/xmp" or "/ifd/xmp") the block is set at that path, without a path it is added as top-level block of the frame.
+ /// Verified with the codecs of Windows: "/app1" and "/xmp" for JPEG, "/xmp" for PNG, "/ifd/xmp" for TIFF and JPEG XR (written as tag 700).
+ ///
+ /// the raw block
+ /// true when the encoder accepted it
+ public bool AddRawBlock(WicMetadataRawBlock block)
+ {
+ EnsureOpen();
+ if (block == null)
+ {
+ throw new ArgumentNullException(nameof(block));
+ }
+ var description = $"raw block {block.Path ?? block.Format.ToString()}";
+ IWICComponentFactory componentFactory = _factory as IWICComponentFactory;
+ IWICMetadataReader reader = null;
+ IWICMetadataWriter writer = null;
+ try
+ {
+ if (componentFactory == null)
+ {
+ AddIssue(description, HResult.E_NOINTERFACE, "The factory has no IWICComponentFactory");
+ return false;
+ }
+ // Not disposed: the metadata reader reads from the stream until the encoder has written the block
+ var memoryStream = new MemoryStream(block.Data, false);
+ var format = block.Format;
+ var hResult = componentFactory.CreateMetadataReader(ref format, IntPtr.Zero, 0, new ManagedIStream(memoryStream), out reader);
+ if (hResult.Succeeded())
+ {
+ hResult = componentFactory.CreateMetadataWriterFromReader(reader, IntPtr.Zero, out writer);
+ }
+ if (!hResult.Succeeded())
+ {
+ AddIssue(description, hResult, "The block couldn't be read");
+ return false;
+ }
+
+ if (block.Path != null && _queryWriter != null)
+ {
+ var propVariant = PropVariant.Allocate();
+ try
+ {
+ PropVariant.WriteUnknown(propVariant, writer, typeof(IWICMetadataWriter));
+ hResult = _queryWriter.SetMetadataByName(block.Path, propVariant);
+ }
+ finally
+ {
+ PropVariant.Free(propVariant);
+ }
+ if (hResult.Succeeded())
+ {
+ return true;
+ }
+ // The XMP of an IFD (TIFF, JPEG XR) is the BYTE tag 700, its bytes are the XMP packet
+ if (block.Format == WicMetadataFormats.Xmp && block.Path.EndsWith("/ifd/xmp", StringComparison.OrdinalIgnoreCase))
+ {
+ var tagPath = block.Path.Substring(0, block.Path.Length - "xmp".Length) + "{ushort=700}";
+ var tagValue = PropVariant.Allocate();
+ try
+ {
+ PropVariant.Write(tagValue, block.Data);
+ hResult = _queryWriter.SetMetadataByName(tagPath, tagValue);
+ }
+ finally
+ {
+ PropVariant.Free(tagValue);
+ }
+ if (hResult.Succeeded())
+ {
+ return true;
+ }
+ }
+ // A top level path can also be added as block
+ if (block.Path.LastIndexOf('/') > 0)
+ {
+ AddIssue(description, hResult, "The encoder didn't accept the block at this path");
+ return false;
+ }
+ }
+
+ if (_frame is not IWICMetadataBlockWriter blockWriter)
+ {
+ AddIssue(description, HResult.E_NOINTERFACE, "The encoder has no metadata block writer");
+ return false;
+ }
+ hResult = blockWriter.AddWriter(writer);
+ if (!hResult.Succeeded())
+ {
+ AddIssue(description, hResult, "The encoder didn't accept the block");
+ return false;
+ }
+ return true;
+ }
+ finally
+ {
+ Wic.Release(writer);
+ Wic.Release(reader);
+ }
+ }
+}
diff --git a/src/Dapplo.Windows.Imaging/WicPixelFormats.cs b/src/Dapplo.Windows.Imaging/WicPixelFormats.cs
new file mode 100644
index 00000000..4af83f70
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicPixelFormats.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// The WIC pixel format GUIDs (GUID_WICPixelFormat* of wincodec.h) which are used most.
+/// Any other WIC pixel format GUID can be used too.
+///
+public static class WicPixelFormats
+{
+ ///
+ /// GUID_WICPixelFormat32bppBGRA: B, G, R, A bytes, straight alpha. The same layout as GDI+ Format32bppArgb and ImageSharp Bgra32.
+ ///
+ public static readonly Guid Format32bppBgra = new Guid("6FDDC324-4E03-4BFE-B185-3D77768DC90F");
+
+ ///
+ /// GUID_WICPixelFormat32bppPBGRA: B, G, R, A bytes, premultiplied alpha. The same layout as GDI+ Format32bppPArgb.
+ ///
+ public static readonly Guid Format32bppPbgra = new Guid("6FDDC324-4E03-4BFE-B185-3D77768DC910");
+
+ ///
+ /// GUID_WICPixelFormat32bppBGR: B, G, R bytes and an unused byte. The same layout as GDI+ Format32bppRgb.
+ ///
+ public static readonly Guid Format32bppBgr = new Guid("6FDDC324-4E03-4BFE-B185-3D77768DC90E");
+
+ ///
+ /// GUID_WICPixelFormat24bppBGR: B, G, R bytes. The same layout as GDI+ Format24bppRgb and ImageSharp Bgr24.
+ ///
+ public static readonly Guid Format24bppBgr = new Guid("6FDDC324-4E03-4BFE-B185-3D77768DC90C");
+
+ ///
+ /// GUID_WICPixelFormat24bppRGB: R, G, B bytes. The same layout as ImageSharp Rgb24.
+ ///
+ public static readonly Guid Format24bppRgb = new Guid("6FDDC324-4E03-4BFE-B185-3D77768DC90D");
+
+ ///
+ /// GUID_WICPixelFormat32bppRGBA: R, G, B, A bytes, straight alpha. The same layout as ImageSharp Rgba32.
+ ///
+ public static readonly Guid Format32bppRgba = new Guid("F5C7AD2D-6A8D-43DD-A7A8-A29935261AE9");
+
+ ///
+ /// GUID_WICPixelFormat64bppRGBA: 16 bit R, G, B, A, straight alpha.
+ ///
+ public static readonly Guid Format64bppRgba = new Guid("6FDDC324-4E03-4BFE-B185-3D77768DC916");
+
+ ///
+ /// GUID_WICPixelFormat64bppRGBAHalf: 16 bit floating point (half) R, G, B, A, for HDR.
+ ///
+ public static readonly Guid Format64bppRgbaHalf = new Guid("6FDDC324-4E03-4BFE-B185-3D77768DC93A");
+
+ ///
+ /// GUID_WICPixelFormat128bppRGBAFloat: 32 bit floating point R, G, B, A, for HDR.
+ ///
+ public static readonly Guid Format128bppRgbaFloat = new Guid("6FDDC324-4E03-4BFE-B185-3D77768DC919");
+}
diff --git a/src/Dapplo.Windows.Imaging/WicRational.cs b/src/Dapplo.Windows.Imaging/WicRational.cs
new file mode 100644
index 00000000..e05441ce
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicRational.cs
@@ -0,0 +1,92 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// An unsigned rational, e.g. an EXIF RATIONAL like XResolution or ExposureTime.
+/// WIC stores it as VT_UI8 with the numerator in the low and the denominator in the high 32 bits.
+///
+public readonly struct WicRational : IEquatable
+{
+ ///
+ /// Create a rational
+ ///
+ public WicRational(uint numerator, uint denominator)
+ {
+ Numerator = numerator;
+ Denominator = denominator;
+ }
+
+ /// The numerator
+ public uint Numerator { get; }
+
+ /// The denominator
+ public uint Denominator { get; }
+
+ /// The value as double, NaN when the denominator is 0
+ public double ToDouble() => Denominator == 0 ? double.NaN : (double)Numerator / Denominator;
+
+ /// The VT_UI8 form WIC uses
+ public ulong ToPacked() => Numerator | ((ulong)Denominator << 32);
+
+ /// Create from the VT_UI8 form WIC uses
+ public static WicRational FromPacked(ulong packed) => new WicRational(unchecked((uint)packed), (uint)(packed >> 32));
+
+ ///
+ public bool Equals(WicRational other) => Numerator == other.Numerator && Denominator == other.Denominator;
+
+ ///
+ public override bool Equals(object obj) => obj is WicRational other && Equals(other);
+
+ ///
+ public override int GetHashCode() => ToPacked().GetHashCode();
+
+ ///
+ public override string ToString() => $"{Numerator}/{Denominator}";
+}
+
+///
+/// A signed rational, e.g. an EXIF SRATIONAL like ExposureBiasValue.
+/// WIC stores it as VT_I8 with the numerator in the low and the denominator in the high 32 bits.
+///
+public readonly struct WicSignedRational : IEquatable
+{
+ ///
+ /// Create a signed rational
+ ///
+ public WicSignedRational(int numerator, int denominator)
+ {
+ Numerator = numerator;
+ Denominator = denominator;
+ }
+
+ /// The numerator
+ public int Numerator { get; }
+
+ /// The denominator
+ public int Denominator { get; }
+
+ /// The value as double, NaN when the denominator is 0
+ public double ToDouble() => Denominator == 0 ? double.NaN : (double)Numerator / Denominator;
+
+ /// The VT_I8 form WIC uses
+ public long ToPacked() => unchecked((long)((uint)Numerator | ((ulong)(uint)Denominator << 32)));
+
+ /// Create from the VT_I8 form WIC uses
+ public static WicSignedRational FromPacked(long packed) => new WicSignedRational(unchecked((int)packed), unchecked((int)(packed >> 32)));
+
+ ///
+ public bool Equals(WicSignedRational other) => Numerator == other.Numerator && Denominator == other.Denominator;
+
+ ///
+ public override bool Equals(object obj) => obj is WicSignedRational other && Equals(other);
+
+ ///
+ public override int GetHashCode() => ToPacked().GetHashCode();
+
+ ///
+ public override string ToString() => $"{Numerator}/{Denominator}";
+}
diff --git a/src/Dapplo.Windows.Imaging/WicSigningStatus.cs b/src/Dapplo.Windows.Imaging/WicSigningStatus.cs
new file mode 100644
index 00000000..e6a57f56
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicSigningStatus.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// The signing status of a WIC component, WICComponentSigning from wincodec.idl.
+/// See WICComponentSigning
+///
+[Flags]
+public enum WicSigningStatus : uint
+{
+ ///
+ /// No flags reported
+ ///
+ None = 0,
+
+ ///
+ /// WICComponentSigned: the component is signed
+ ///
+ Signed = 0x00000001,
+
+ ///
+ /// WICComponentUnsigned: the component is not signed
+ ///
+ Unsigned = 0x00000002,
+
+ ///
+ /// WICComponentSafe: the component is safe (allowed) to use, e.g. for an unsigned component which the user or administrator allowed
+ ///
+ Safe = 0x00000004,
+
+ ///
+ /// WICComponentDisabled: the component is disabled
+ ///
+ Disabled = 0x80000000
+}
diff --git a/src/Dapplo.Windows.Imaging/WicVendors.cs b/src/Dapplo.Windows.Imaging/WicVendors.cs
new file mode 100644
index 00000000..6c786b66
--- /dev/null
+++ b/src/Dapplo.Windows.Imaging/WicVendors.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Dapplo.Windows.Imaging;
+
+///
+/// The WIC vendor GUIDs of wincodec.h, compare them with
+///
+public static class WicVendors
+{
+ ///
+ /// GUID_VendorMicrosoft: the codecs of Microsoft, those which come with Windows (BMP, PNG, JPEG ...) and those of Microsoft from the Store (HEIF, WebP, Raw, JPEG XL)
+ ///
+ public static readonly Guid Microsoft = new Guid("F0E749CA-EDEF-4589-A73A-EE0E626A2A2B");
+
+ ///
+ /// GUID_VendorMicrosoftBuiltIn: used to ask for the built-in Microsoft implementation, e.g. with CreateDecoder; the codecs themselves report
+ ///
+ public static readonly Guid MicrosoftBuiltIn = new Guid("257A30FD-06B6-462B-AEA4-63F70B86E533");
+}
diff --git a/src/Dapplo.Windows.Tests/Dapplo.Windows.Tests.csproj b/src/Dapplo.Windows.Tests/Dapplo.Windows.Tests.csproj
index 7edc5bde..526b10cc 100644
--- a/src/Dapplo.Windows.Tests/Dapplo.Windows.Tests.csproj
+++ b/src/Dapplo.Windows.Tests/Dapplo.Windows.Tests.csproj
@@ -32,6 +32,7 @@
+
diff --git a/src/Dapplo.Windows.Tests/WicCodecsTests.cs b/src/Dapplo.Windows.Tests/WicCodecsTests.cs
new file mode 100644
index 00000000..b1860f0d
--- /dev/null
+++ b/src/Dapplo.Windows.Tests/WicCodecsTests.cs
@@ -0,0 +1,142 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using Dapplo.Log;
+using Dapplo.Log.XUnit;
+using Dapplo.Windows.Imaging;
+using Xunit;
+
+namespace Dapplo.Windows.Tests;
+
+///
+/// Tests for the WIC codec discovery, these only read and work on every Windows 10 1809+ machine (also the CI runner)
+///
+public class WicCodecsTests
+{
+ private static readonly LogSource Log = new LogSource();
+
+ public WicCodecsTests(ITestOutputHelper testOutputHelper)
+ {
+ LogSettings.RegisterDefaultLogger(LogLevels.Verbose, testOutputHelper);
+ }
+
+ private static void LogCodecs(IEnumerable codecs)
+ {
+ foreach (var codec in codecs)
+ {
+ Log.Info().WriteLine("{0}: {1} by {2} v{3}, container {4}, vendor {5}, {6}, extensions {7}, mime types {8}",
+ codec.Kind, codec.FriendlyName, codec.Author, codec.Version, codec.ContainerFormat, codec.VendorGuid, codec.SigningStatus,
+ string.Join(" ", codec.FileExtensions), string.Join(" ", codec.MimeTypes));
+ }
+ }
+
+ private static void AssertContains(IReadOnlyList codecs, string extension, string mimeType)
+ {
+ Assert.Contains(codecs, c => c.FileExtensions.Contains(extension) && c.MimeTypes.Contains(mimeType));
+ }
+
+ private static void AssertWellFormed(IReadOnlyList codecs, WicCodecKind kind)
+ {
+ Assert.NotEmpty(codecs);
+ foreach (var codec in codecs)
+ {
+ Assert.Equal(kind, codec.Kind);
+ Assert.NotEqual(Guid.Empty, codec.Clsid);
+ Assert.NotNull(codec.FriendlyName);
+ Assert.All(codec.FileExtensions, extension =>
+ {
+ Assert.StartsWith(".", extension);
+ Assert.True(extension.Length > 1);
+ Assert.Equal(extension.Trim().ToLowerInvariant(), extension);
+ });
+ Assert.All(codec.MimeTypes, mimeType =>
+ {
+ Assert.NotEmpty(mimeType);
+ Assert.Equal(mimeType.Trim().ToLowerInvariant(), mimeType);
+ });
+ Assert.Equal(codec.FileExtensions.Count, codec.FileExtensions.Distinct().Count());
+ Assert.Equal(codec.MimeTypes.Count, codec.MimeTypes.Distinct().Count());
+ }
+ }
+
+ [Fact]
+ public void GetDecoders_ContainsBuiltInDecoders()
+ {
+ var decoders = WicCodecs.GetDecoders();
+ LogCodecs(decoders);
+ AssertWellFormed(decoders, WicCodecKind.Decoder);
+ AssertContains(decoders, ".png", "image/png");
+ AssertContains(decoders, ".jpg", "image/jpeg");
+ AssertContains(decoders, ".gif", "image/gif");
+ AssertContains(decoders, ".bmp", "image/bmp");
+ AssertContains(decoders, ".tiff", "image/tiff");
+ var png = decoders.First(c => c.FileExtensions.Contains(".png"));
+ Assert.True(png.IsFromMicrosoft);
+ Assert.True(png.SigningStatus.HasFlag(WicSigningStatus.Signed));
+ }
+
+ [Fact]
+ public void GetEncoders_ContainsBuiltInEncoders()
+ {
+ var encoders = WicCodecs.GetEncoders();
+ LogCodecs(encoders);
+ AssertWellFormed(encoders, WicCodecKind.Encoder);
+ AssertContains(encoders, ".png", "image/png");
+ AssertContains(encoders, ".jpg", "image/jpeg");
+ }
+
+ [Fact]
+ public void GetDecoders_Refresh()
+ {
+ var decoders = WicCodecs.GetDecoders();
+ var refreshed = WicCodecs.GetDecoders(true);
+ AssertWellFormed(refreshed, WicCodecKind.Decoder);
+ Assert.Equal(decoders.Select(c => c.Clsid).OrderBy(g => g), refreshed.Select(c => c.Clsid).OrderBy(g => g));
+ }
+
+ [StaFact]
+ public void GetDecoders_FromSta()
+ {
+ Assert.Equal(ApartmentState.STA, Thread.CurrentThread.GetApartmentState());
+ AssertContains(WicCodecs.GetDecoders(), ".png", "image/png");
+ }
+
+ [Fact]
+ public void GetDecoders_FromMta()
+ {
+ IReadOnlyList decoders = null;
+ Exception error = null;
+ var thread = new Thread(() =>
+ {
+ try
+ {
+ decoders = WicCodecs.GetDecoders();
+ }
+ catch (Exception ex)
+ {
+ error = ex;
+ }
+ });
+ thread.SetApartmentState(ApartmentState.MTA);
+ thread.Start();
+ thread.Join();
+ Assert.Null(error);
+ AssertContains(decoders, ".png", "image/png");
+ }
+
+ ///
+ /// The HEIF Image Extensions from the Store add the decoder for HEIF and AVIF, skipped when it isn't installed
+ ///
+ [Fact]
+ public void GetDecoders_HeifExtension()
+ {
+ var heif = WicCodecs.GetDecoders().FirstOrDefault(c => c.FileExtensions.Contains(".heic"));
+ Assert.SkipWhen(heif == null, "The HEIF Image Extensions are not installed");
+ Log.Info().WriteLine("HEIF decoder: {0}", heif);
+ Assert.Contains(".heic", heif.FileExtensions);
+ Assert.Contains(".avif", heif.FileExtensions);
+ }
+}
diff --git a/src/Dapplo.Windows.Tests/WicImageTests.cs b/src/Dapplo.Windows.Tests/WicImageTests.cs
new file mode 100644
index 00000000..9f3eaddd
--- /dev/null
+++ b/src/Dapplo.Windows.Tests/WicImageTests.cs
@@ -0,0 +1,381 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using System;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Dapplo.Log;
+using Dapplo.Log.XUnit;
+using Dapplo.Windows.Imaging;
+using Xunit;
+
+namespace Dapplo.Windows.Tests;
+
+///
+/// Round trips through the WIC encoders and decoders, the Store codecs (JPEG XL, HEIF) are skipped when they aren't installed
+///
+public class WicImageTests
+{
+ private static readonly LogSource Log = new LogSource();
+
+ public WicImageTests(ITestOutputHelper testOutputHelper)
+ {
+ LogSettings.RegisterDefaultLogger(LogLevels.Verbose, testOutputHelper);
+ }
+
+ ///
+ /// A test image with every byte of a row different and varying alpha, the bytes between rows (stride padding) are 0xCD
+ ///
+ private static byte[] CreatePixels(int width, int height, int bytesPerPixel, int stride, bool opaque = false)
+ {
+ var pixels = Enumerable.Repeat((byte)0xCD, stride * height).ToArray();
+ var random = new Random(width * 31 + height);
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width * bytesPerPixel; x++)
+ {
+ pixels[y * stride + x] = (byte)random.Next(256);
+ }
+ if (opaque && bytesPerPixel == 4)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ pixels[y * stride + x * 4 + 3] = 255;
+ }
+ }
+ }
+ return pixels;
+ }
+
+ private static void AssertSamePixels(byte[] expected, int expectedStride, byte[] actual, int actualStride, int rowBytes, int height)
+ {
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < rowBytes; x++)
+ {
+ if (expected[y * expectedStride + x] != actual[y * actualStride + x])
+ {
+ Assert.Fail($"Pixel byte {x} of row {y} differs: expected {expected[y * expectedStride + x]}, actual {actual[y * actualStride + x]}");
+ }
+ }
+ }
+ }
+
+ private static byte[] EncodeFromMemory(WicEncodeOptions options, byte[] pixels, int width, int height, int stride, Guid pixelFormat, out WicEncodeResult result)
+ {
+ using var output = new MemoryStream();
+ var handle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
+ try
+ {
+ result = WicImage.Encode(output, options, handle.AddrOfPinnedObject(), width, height, stride, pixelFormat);
+ }
+ finally
+ {
+ handle.Free();
+ }
+ return output.ToArray();
+ }
+
+ private static byte[] EncodeFromRows(WicEncodeOptions options, byte[] pixels, int width, int height, int stride, Guid pixelFormat, int bytesPerPixel)
+ {
+ using var output = new MemoryStream();
+ WicImage.Encode(output, options, width, height, pixelFormat, (top, count, destination, destinationStride) =>
+ {
+ for (int row = 0; row < count; row++)
+ {
+ new ReadOnlySpan(pixels, (top + row) * stride, width * bytesPerPixel).CopyTo(destination.Slice(row * destinationStride));
+ }
+ });
+ return output.ToArray();
+ }
+
+ private static byte[] DecodeToMemory(byte[] encoded, Guid pixelFormat, int stride, out WicFrameInfo info, bool applyOrientation = false)
+ {
+ info = WicImage.GetFrameInfo(new MemoryStream(encoded), applyOrientation);
+ var pixels = new byte[stride * info.Height];
+ var handle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
+ try
+ {
+ info = WicImage.Decode(new MemoryStream(encoded), pixelFormat, handle.AddrOfPinnedObject(), stride, applyOrientation);
+ }
+ finally
+ {
+ handle.Free();
+ }
+ return pixels;
+ }
+
+ private static bool HasEncoder(Guid containerFormat) => WicCodecs.GetEncoders().Any(e => e.ContainerFormat == containerFormat);
+ private static bool HasDecoder(Guid containerFormat) => WicCodecs.GetDecoders().Any(e => e.ContainerFormat == containerFormat);
+
+ ///
+ /// A Store encoder can be registered but fail to initialize (WINCODEC_ERR_COMPONENTINITIALIZEFAILURE), e.g. on the CI runner or HEIF without the HEVC extension
+ ///
+ private static bool CanCreateEncoder(Guid containerFormat, out string reason)
+ {
+ reason = null;
+ if (!HasEncoder(containerFormat))
+ {
+ reason = "The encoder is not installed";
+ return false;
+ }
+ try
+ {
+ WicImage.GetEncoderOptions(containerFormat);
+ return true;
+ }
+ catch (WicCodecNotFoundException ex)
+ {
+ reason = $"The encoder can't be used: {ex.Message}";
+ return false;
+ }
+ }
+
+ [Theory]
+ [InlineData("png")]
+ [InlineData("tiff")]
+ public void RoundTrip_Bgra32_AllOverloads(string containerName)
+ {
+ var container = containerName == "png" ? WicContainerFormats.Png : WicContainerFormats.Tiff;
+ const int width = 37, height = 23, stride = width * 4;
+ var pixels = CreatePixels(width, height, 4, stride);
+ var options = new WicEncodeOptions { ContainerFormat = container };
+
+ var fromMemory = EncodeFromMemory(options, pixels, width, height, stride, WicPixelFormats.Format32bppBgra, out var result);
+ Assert.Equal(WicPixelFormats.Format32bppBgra, result.PixelFormat);
+ var fromRows = EncodeFromRows(options, pixels, width, height, stride, WicPixelFormats.Format32bppBgra, 4);
+
+ foreach (var encoded in new[] { fromMemory, fromRows })
+ {
+ var decoded = DecodeToMemory(encoded, WicPixelFormats.Format32bppBgra, stride, out var info);
+ Assert.Equal(width, info.Width);
+ Assert.Equal(height, info.Height);
+ Assert.Equal(container, info.ContainerFormat);
+ AssertSamePixels(pixels, stride, decoded, stride, width * 4, height);
+
+ DecodeToRowsAndCompare(encoded, pixels, stride, width, height);
+ }
+ }
+
+ private static void DecodeToRowsAndCompare(byte[] encoded, byte[] pixels, int stride, int width, int height)
+ {
+ var info = WicImage.GetFrameInfo(new MemoryStream(encoded));
+ var decoded = new byte[width * 4 * info.Height];
+ WicImage.Decode(new MemoryStream(encoded), WicPixelFormats.Format32bppBgra, (top, count, source, sourceStride) =>
+ {
+ for (int row = 0; row < count; row++)
+ {
+ source.Slice(row * sourceStride, width * 4).CopyTo(new Span(decoded, (top + row) * width * 4, width * 4));
+ }
+ });
+ AssertSamePixels(pixels, stride, decoded, width * 4, width * 4, height);
+ }
+
+ ///
+ /// 24bpp with a width of 63 (189 bytes per row) and a stride larger than a row, like GDI+ padding
+ ///
+ [Fact]
+ public void RoundTrip_Bgr24_OddWidthAndPaddedStride()
+ {
+ const int width = 63, height = 17, rowBytes = width * 3, stride = 200;
+ var pixels = CreatePixels(width, height, 3, stride);
+ var options = new WicEncodeOptions { ContainerFormat = WicContainerFormats.Png };
+
+ var fromMemory = EncodeFromMemory(options, pixels, width, height, stride, WicPixelFormats.Format24bppBgr, out _);
+ var fromRows = EncodeFromRows(options, pixels, width, height, stride, WicPixelFormats.Format24bppBgr, 3);
+ foreach (var encoded in new[] { fromMemory, fromRows })
+ {
+ // Decode with the GDI+ stride of 24bpp: rounded up to 4 bytes
+ const int gdiStride = (rowBytes + 3) & ~3;
+ var decoded = DecodeToMemory(encoded, WicPixelFormats.Format24bppBgr, gdiStride, out var info);
+ Assert.Equal(width, info.Width);
+ AssertSamePixels(pixels, stride, decoded, gdiStride, rowBytes, height);
+ }
+ }
+
+ [Fact]
+ public void RoundTrip_JpegXr_Lossless()
+ {
+ const int width = 33, height = 19, stride = width * 4;
+ var pixels = CreatePixels(width, height, 4, stride);
+ var encoded = EncodeFromMemory(new WicEncodeOptions { ContainerFormat = WicContainerFormats.Wmp, Lossless = true }, pixels, width, height, stride, WicPixelFormats.Format32bppBgra, out var result);
+ Assert.Contains("Lossless", result.AppliedOptions);
+ var decoded = DecodeToMemory(encoded, WicPixelFormats.Format32bppBgra, stride, out var info);
+ Assert.Equal(WicContainerFormats.Wmp, info.ContainerFormat);
+ AssertSamePixels(pixels, stride, decoded, stride, width * 4, height);
+ }
+
+ [Fact]
+ public void Encode_Jpeg_Quality_IsApplied()
+ {
+ const int width = 64, height = 64, stride = width * 4;
+ var pixels = CreatePixels(width, height, 4, stride, true);
+ var low = EncodeFromMemory(new WicEncodeOptions { ContainerFormat = WicContainerFormats.Jpeg, ImageQuality = 0.1f }, pixels, width, height, stride, WicPixelFormats.Format32bppBgra, out var lowResult);
+ var high = EncodeFromMemory(new WicEncodeOptions { ContainerFormat = WicContainerFormats.Jpeg, ImageQuality = 1f }, pixels, width, height, stride, WicPixelFormats.Format32bppBgra, out _);
+ Assert.Contains("ImageQuality", lowResult.AppliedOptions);
+ // JPEG has no alpha, WIC converted the pixels
+ Assert.NotEqual(WicPixelFormats.Format32bppBgra, lowResult.PixelFormat);
+ Assert.True(low.Length < high.Length, $"quality 0.1 gave {low.Length} bytes, quality 1 gave {high.Length}");
+ }
+
+ [Fact]
+ public void GetEncoderOptions_Jpeg()
+ {
+ var options = WicImage.GetEncoderOptions(WicContainerFormats.Jpeg);
+ Log.Info().WriteLine("JPEG encoder options: {0}", string.Join(", ", options));
+ Assert.Contains(options, o => o.Name == "ImageQuality");
+ }
+
+ [Fact]
+ public void JpegXl_LosslessRoundTrip()
+ {
+ Assert.SkipUnless(HasDecoder(WicContainerFormats.JpegXl), "The JPEG XL decoder is not installed");
+ Assert.SkipUnless(CanCreateEncoder(WicContainerFormats.JpegXl, out var reason), reason ?? string.Empty);
+ var encoderOptions = WicImage.GetEncoderOptions(WicContainerFormats.JpegXl);
+ Log.Info().WriteLine("JPEG XL encoder options: {0}", string.Join(", ", encoderOptions));
+ Assert.SkipUnless(encoderOptions.Any(o => o.Name == "Lossless"), "The JPEG XL encoder has no Lossless option");
+ const int width = 41, height = 29, stride = width * 4;
+ var pixels = CreatePixels(width, height, 4, stride);
+ var encoded = EncodeFromMemory(new WicEncodeOptions { ContainerFormat = WicContainerFormats.JpegXl, Lossless = true }, pixels, width, height, stride, WicPixelFormats.Format32bppBgra, out _);
+ var decoded = DecodeToMemory(encoded, WicPixelFormats.Format32bppBgra, stride, out var info);
+ Assert.Equal(WicContainerFormats.JpegXl, info.ContainerFormat);
+ AssertSamePixels(pixels, stride, decoded, stride, width * 4, height);
+ }
+
+ [Fact]
+ public void Heif_EncodeDecode()
+ {
+ Assert.SkipUnless(HasDecoder(WicContainerFormats.Heif), "The HEIF decoder is not installed");
+ Assert.SkipUnless(CanCreateEncoder(WicContainerFormats.Heif, out var reason), reason ?? string.Empty);
+ const int width = 64, height = 48, stride = width * 4;
+ var pixels = CreatePixels(width, height, 4, stride, true);
+ byte[] encoded;
+ try
+ {
+ encoded = EncodeFromMemory(new WicEncodeOptions { ContainerFormat = WicContainerFormats.Heif, ImageQuality = 0.9f }, pixels, width, height, stride, WicPixelFormats.Format32bppBgra, out _);
+ }
+ catch (COMException ex)
+ {
+ // The HEIF encoder needs the HEVC video extension
+ Assert.Skip($"The HEIF encoder failed: {ex.Message}");
+ return;
+ }
+ var decoded = DecodeToMemory(encoded, WicPixelFormats.Format32bppBgra, stride, out var info);
+ Assert.Equal(WicContainerFormats.Heif, info.ContainerFormat);
+ Assert.Equal(width, info.Width);
+ Assert.Equal(height, info.Height);
+ Assert.Contains(decoded, b => b != 0);
+ }
+
+ [Fact]
+ public void Encode_UnknownContainer_ThrowsCodecNotFound()
+ {
+ var pixels = new byte[4 * 4 * 4];
+ var exception = Assert.Throws(() =>
+ EncodeFromMemory(new WicEncodeOptions { ContainerFormat = Guid.NewGuid() }, pixels, 4, 4, 16, WicPixelFormats.Format32bppBgra, out _));
+ Log.Info().WriteLine("Expected: {0}", exception.Message);
+ Assert.Throws(() => WicImage.GetEncoderOptions(Guid.NewGuid()));
+ }
+
+ [Fact]
+ public void Decode_UnknownData_ThrowsCodecNotFound()
+ {
+ var garbage = Enumerable.Range(0, 1000).Select(i => (byte)(i * 37)).ToArray();
+ Assert.Throws(() => WicImage.GetFrameInfo(new MemoryStream(garbage)));
+ }
+
+ [Fact]
+ public void Encode_RowCallbackException_ReachesCaller()
+ {
+ var exception = Assert.Throws(() =>
+ WicImage.Encode(new MemoryStream(), new WicEncodeOptions { ContainerFormat = WicContainerFormats.Png }, 8, 8, WicPixelFormats.Format32bppBgra,
+ (top, count, destination, stride) => throw new InvalidOperationException("from the callback")));
+ Assert.Equal("from the callback", exception.Message);
+ }
+
+ ///
+ /// Streams which can't seek, and a decode from the middle of a larger stream
+ ///
+ [Fact]
+ public void Streams_NonSeekableAndOffset()
+ {
+ const int width = 9, height = 7, stride = width * 4;
+ var pixels = CreatePixels(width, height, 4, stride);
+ var encoded = EncodeFromMemory(new WicEncodeOptions { ContainerFormat = WicContainerFormats.Png }, pixels, width, height, stride, WicPixelFormats.Format32bppBgra, out _);
+
+ // Encode into a stream which can't seek
+ using var forwardOnly = new ForwardOnlyStream(new MemoryStream());
+ var handle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
+ try
+ {
+ WicImage.Encode(forwardOnly, new WicEncodeOptions { ContainerFormat = WicContainerFormats.Png }, handle.AddrOfPinnedObject(), width, height, stride, WicPixelFormats.Format32bppBgra);
+ }
+ finally
+ {
+ handle.Free();
+ }
+ Assert.Equal(encoded, forwardOnly.Inner.ToArray());
+
+ // Decode from a stream which can't seek
+ var info = WicImage.GetFrameInfo(new ForwardOnlyStream(new MemoryStream(encoded)));
+ Assert.Equal(width, info.Width);
+
+ // Decode from an offset in a larger stream, the position is restored by GetFrameInfo
+ var container = new MemoryStream();
+ container.Write(new byte[123], 0, 123);
+ container.Write(encoded, 0, encoded.Length);
+ container.Position = 123;
+ info = WicImage.GetFrameInfo(container);
+ Assert.Equal(123, container.Position);
+ Assert.Equal(height, info.Height);
+
+ // Encode at an offset, the stream ends after the image
+ var output = new MemoryStream();
+ output.Write(new byte[10], 0, 10);
+ EncodeAt(output, pixels, width, height, stride);
+ Assert.Equal(10 + encoded.Length, output.Position);
+ Assert.Equal(encoded, output.ToArray().Skip(10).ToArray());
+ }
+
+ private static void EncodeAt(Stream output, byte[] pixels, int width, int height, int stride)
+ {
+ var handle = GCHandle.Alloc(pixels, GCHandleType.Pinned);
+ try
+ {
+ WicImage.Encode(output, new WicEncodeOptions { ContainerFormat = WicContainerFormats.Png }, handle.AddrOfPinnedObject(), width, height, stride, WicPixelFormats.Format32bppBgra);
+ }
+ finally
+ {
+ handle.Free();
+ }
+ }
+
+ [StaFact]
+ public void RoundTrip_FromSta()
+ {
+ const int width = 5, height = 5, stride = width * 4;
+ var pixels = CreatePixels(width, height, 4, stride);
+ var encoded = EncodeFromRows(new WicEncodeOptions { ContainerFormat = WicContainerFormats.Png }, pixels, width, height, stride, WicPixelFormats.Format32bppBgra, 4);
+ var decoded = DecodeToMemory(encoded, WicPixelFormats.Format32bppBgra, stride, out _);
+ AssertSamePixels(pixels, stride, decoded, stride, width * 4, height);
+ }
+
+ ///
+ /// A stream which can't seek or tell its length
+ ///
+ private sealed class ForwardOnlyStream : Stream
+ {
+ public ForwardOnlyStream(MemoryStream inner) => Inner = inner;
+ public MemoryStream Inner { get; }
+ public override bool CanRead => true;
+ public override bool CanSeek => false;
+ public override bool CanWrite => true;
+ public override long Length => throw new NotSupportedException();
+ public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
+ public override void Flush() => Inner.Flush();
+ public override int Read(byte[] buffer, int offset, int count) => Inner.Read(buffer, offset, count);
+ public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
+ public override void SetLength(long value) => throw new NotSupportedException();
+ public override void Write(byte[] buffer, int offset, int count) => Inner.Write(buffer, offset, count);
+ }
+}
diff --git a/src/Dapplo.Windows.Tests/WicMetadataTests.cs b/src/Dapplo.Windows.Tests/WicMetadataTests.cs
new file mode 100644
index 00000000..fe5802d1
--- /dev/null
+++ b/src/Dapplo.Windows.Tests/WicMetadataTests.cs
@@ -0,0 +1,341 @@
+// Copyright (c) Dapplo and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using Dapplo.Log;
+using Dapplo.Log.XUnit;
+using Dapplo.Windows.Imaging;
+using Xunit;
+
+namespace Dapplo.Windows.Tests;
+
+///
+/// Metadata through the WIC metadata query language, with the built-in codecs (the paths are documented in doc/articles/imaging.md)
+///
+public class WicMetadataTests
+{
+ private static readonly LogSource Log = new LogSource();
+
+ public WicMetadataTests(ITestOutputHelper testOutputHelper)
+ {
+ LogSettings.RegisterDefaultLogger(LogLevels.Verbose, testOutputHelper);
+ }
+
+ public static IEnumerable