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) | [![NuGet](https://img.shields.io/nuget/v/Dapplo.Windows.InstallerManager.svg)](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) | [![NuGet](https://img.shields.io/nuget/v/Dapplo.Windows.Dialogs.svg)](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) | [![NuGet](https://img.shields.io/nuget/v/Dapplo.Windows.Icons.svg)](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) | [![NuGet](https://img.shields.io/nuget/v/Dapplo.Windows.Imaging.svg)](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) | [![NuGet](https://img.shields.io/nuget/v/Dapplo.Windows.Devices.svg)](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) | [![NuGet](https://img.shields.io/nuget/v/Dapplo.Windows.DesktopWindowsManager.svg)](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) | [![NuGet](https://img.shields.io/nuget/v/Dapplo.Windows.Citrix.svg)](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 MetadataContainers => new[] + { + new object[] { "jpeg" }, new object[] { "tiff" }, new object[] { "png" }, new object[] { "wmp" } + }; + + private static Guid Container(string name) => name switch + { + "jpeg" => WicContainerFormats.Jpeg, + "tiff" => WicContainerFormats.Tiff, + "png" => WicContainerFormats.Png, + "wmp" => WicContainerFormats.Wmp, + "gif" => WicContainerFormats.Gif, + "bmp" => WicContainerFormats.Bmp, + _ => throw new ArgumentOutOfRangeException(nameof(name)) + }; + + /// + /// A 3x2 image, the blue channel of pixel i is i * 40 so the position of each pixel can be found after a rotation + /// + private static byte[] CreatePixels() + { + var pixels = new byte[3 * 2 * 4]; + for (int i = 0; i < 6; i++) + { + pixels[i * 4] = (byte)(i * 40); + pixels[i * 4 + 1] = 100; + pixels[i * 4 + 2] = 200; + pixels[i * 4 + 3] = 255; + } + return pixels; + } + + private static byte[] Encode(Guid container, Action metadata, out WicEncodeResult result, byte[] pixels = null, int width = 3, int height = 2) + { + pixels ??= CreatePixels(); + using var output = new MemoryStream(); + var handle = GCHandle.Alloc(pixels, GCHandleType.Pinned); + try + { + result = WicImage.Encode(output, new WicEncodeOptions { ContainerFormat = container, Metadata = metadata }, handle.AddrOfPinnedObject(), width, height, width * 4, WicPixelFormats.Format32bppBgra); + } + finally + { + handle.Free(); + } + foreach (var issue in result.MetadataIssues) + { + Log.Info().WriteLine("Metadata issue: {0}", issue); + } + return output.ToArray(); + } + + private static void AssertValue(WicMetadata metadata, string query, object expected) + { + Assert.True(metadata.TryGet(query, out var value), $"{query} is missing, the frame has: {string.Join(", ", metadata.Items.Select(i => i.Key))}"); + Assert.Equal(expected, value); + } + + [Theory] + [MemberData(nameof(MetadataContainers))] + public void Metadata_Paths_RoundTrip(string containerName) + { + var container = Container(containerName); + var isPng = container == WicContainerFormats.Png; + var encoded = Encode(container, writer => + { + Assert.True(writer.SupportsQueries); + Assert.Equal(container, writer.ContainerFormat); + // The EXIF ASCII tags are VT_LPSTR, PNG text is a VT_LPWSTR + Assert.True(writer.Set(WicMetadataPaths.Software(container), isPng ? "Greenshot" : WicMetadataValue.AnsiString("Greenshot"))); + Assert.True(writer.Set(isPng ? WicMetadataPaths.PngText("Description", 1) : WicMetadataPaths.Description(container), isPng ? "A capture" : WicMetadataValue.AnsiString("A capture"))); + Assert.True(writer.Set(WicMetadataPaths.XmpProperty(container, "dc:title"), "XMP title")); + if (!isPng) + { + Assert.True(writer.Set(WicMetadataPaths.DateTimeOriginal(container), WicMetadataValue.AnsiString("2026:11:02 12:34:56"))); + Assert.True(writer.Set(WicMetadataPaths.ExifSubTag(container, 33434), new WicRational(1, 250))); + } + }, out var result); + Assert.Empty(result.MetadataIssues); + + var metadata = WicImage.GetFrameInfo(new MemoryStream(encoded)).Metadata; + foreach (var item in metadata.Items) + { + Log.Debug().WriteLine("{0} = {1}", item.Key, item.Value); + } + AssertValue(metadata, WicMetadataPaths.Software(container), "Greenshot"); + AssertValue(metadata, isPng ? WicMetadataPaths.PngText("Description", 1) : WicMetadataPaths.Description(container), "A capture"); + // A plain string makes a simple XMP property, the policy System.Title writes the language alternative (see Metadata_Policies) + AssertValue(metadata, WicMetadataPaths.XmpProperty(container, "dc:title"), "XMP title"); + if (!isPng) + { + AssertValue(metadata, WicMetadataPaths.DateTimeOriginal(container), "2026:11:02 12:34:56"); + Assert.True(metadata.TryGetRational(WicMetadataPaths.ExifSubTag(container, 33434), out var exposure)); + Assert.Equal(new WicRational(1, 250), exposure); + } + } + + [Fact] + public void Metadata_TagPathsAreAliases() + { + var encoded = Encode(WicContainerFormats.Jpeg, writer => writer.Set("/app1/ifd/exif/{ushort=36867}", WicMetadataValue.AnsiString("2026:01:01 00:00:00")), out _); + var metadata = WicImage.GetFrameInfo(new MemoryStream(encoded)).Metadata; + // WIC enumerates the blocks by their tags, the friendly names work the same + AssertValue(metadata, "/app1/{ushort=0}/{ushort=34665}/{ushort=36867}", "2026:01:01 00:00:00"); + AssertValue(metadata, "/APP1/IFD/EXIF/{ushort=36867}", "2026:01:01 00:00:00"); + Assert.Contains(metadata.Items, i => i.Key == "/app1/ifd/exif" && i.Value is WicMetadataBlock block && block.Format == WicMetadataFormats.Exif); + } + + /// + /// The photo metadata policies write every location the container has, e.g. EXIF and XMP + /// + [Theory] + [InlineData("jpeg")] + [InlineData("tiff")] + [InlineData("wmp")] + public void Metadata_Policies(string containerName) + { + var container = Container(containerName); + var encoded = Encode(container, writer => + { + Assert.True(writer.Set("System.ApplicationName", "Greenshot")); + Assert.True(writer.Set("System.Title", "Title")); + }, out _); + var metadata = WicImage.GetFrameInfo(new MemoryStream(encoded)).Metadata; + AssertValue(metadata, WicMetadataPaths.Software(container), "Greenshot"); + AssertValue(metadata, WicMetadataPaths.XmpProperty(container, "xmp:CreatorTool"), "Greenshot"); + AssertValue(metadata, WicMetadataPaths.Description(container), "Title"); + AssertValue(metadata, WicMetadataPaths.XmpProperty(container, "dc:title/x-default"), "Title"); + } + + [Theory] + [MemberData(nameof(MetadataContainers))] + public void ColorProfile_RoundTrip(string containerName) + { + var iccPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "spool", "drivers", "color", "sRGB Color Space Profile.icm"); + Assert.SkipUnless(File.Exists(iccPath), "The sRGB profile of Windows is missing"); + var icc = File.ReadAllBytes(iccPath); + var encoded = Encode(Container(containerName), writer => Assert.True(writer.SetColorProfile(icc)), out var result); + Assert.Empty(result.MetadataIssues); + Assert.Equal(icc, WicImage.GetFrameInfo(new MemoryStream(encoded)).Metadata.ColorProfile); + } + + [Fact] + public void ColorProfile_NoneIsNull() + { + var encoded = Encode(WicContainerFormats.Png, null, out _); + Assert.Null(WicImage.GetFrameInfo(new MemoryStream(encoded)).Metadata.ColorProfile); + } + + /// + /// Every EXIF orientation, decoded with applyOrientation: the result must be the upright image. + /// The 3x2 source has the pixels 0 1 2 / 3 4 5, the expected rows are the source indices of the upright image. + /// + [Theory] + [InlineData(1, 3, "0,1,2,3,4,5")] + [InlineData(2, 3, "2,1,0,5,4,3")] + [InlineData(3, 3, "5,4,3,2,1,0")] + [InlineData(4, 3, "3,4,5,0,1,2")] + [InlineData(5, 2, "0,3,1,4,2,5")] + [InlineData(6, 2, "3,0,4,1,5,2")] + [InlineData(7, 2, "5,2,4,1,3,0")] + [InlineData(8, 2, "2,5,1,4,0,3")] + public void Orientation_Applied(int orientation, int expectedWidth, string expectedIndices) + { + var encoded = Encode(WicContainerFormats.Tiff, writer => Assert.True(writer.Set(WicMetadataPaths.Orientation(WicContainerFormats.Tiff), (ushort)orientation)), out _); + + var plain = WicImage.GetFrameInfo(new MemoryStream(encoded)); + Assert.Equal(orientation, plain.Metadata.Orientation); + Assert.Equal(3, plain.Width); + Assert.False(plain.OrientationApplied); + + var info = WicImage.GetFrameInfo(new MemoryStream(encoded), true); + Assert.Equal(expectedWidth, info.Width); + Assert.Equal(6 / expectedWidth, info.Height); + Assert.Equal(orientation > 1, info.OrientationApplied); + Assert.Equal(orientation, info.Metadata.Orientation); + + var pixels = new byte[6 * 4]; + var handle = GCHandle.Alloc(pixels, GCHandleType.Pinned); + try + { + WicImage.Decode(new MemoryStream(encoded), WicPixelFormats.Format32bppBgra, handle.AddrOfPinnedObject(), info.Width * 4, true); + } + finally + { + handle.Free(); + } + Assert.Equal(expectedIndices, string.Join(",", Enumerable.Range(0, 6).Select(p => pixels[p * 4] / 40))); + } + + [Fact] + public void Orientation_Jpeg_FromPolicy() + { + var encoded = Encode(WicContainerFormats.Jpeg, writer => Assert.True(writer.Set(WicMetadataPaths.PolicyOrientation, (ushort)6)), out _); + var info = WicImage.GetFrameInfo(new MemoryStream(encoded), true); + Assert.Equal(6, info.Metadata.Orientation); + Assert.Equal(2, info.Width); + Assert.Equal(3, info.Height); + } + + /// + /// What an encoder doesn't support is reported, it doesn't fail the encoding + /// + [Fact] + public void Metadata_Unsupported_IsReported() + { + var encoded = Encode(WicContainerFormats.Png, writer => + { + Assert.False(writer.Set("/ifd/{ushort=305}", "not in a PNG")); + Assert.True(writer.Set(WicMetadataPaths.Software(WicContainerFormats.Png), "Greenshot")); + }, out var result); + var issue = Assert.Single(result.MetadataIssues); + Assert.Equal("/ifd/{ushort=305}", issue.Query); + Assert.NotEqual(0, issue.ErrorCode); + AssertValue(WicImage.GetFrameInfo(new MemoryStream(encoded)).Metadata, "/tEXt/{str=Software}", "Greenshot"); + + // BMP has no metadata at all + Encode(WicContainerFormats.Bmp, writer => + { + Assert.False(writer.SupportsQueries); + Assert.False(writer.Set("System.ApplicationName", "Greenshot")); + Assert.False(writer.SetColorProfile(new byte[] { 1, 2, 3 })); + }, out result); + Assert.Equal(2, result.MetadataIssues.Count); + } + + [Fact] + public void MetadataWriter_InvalidUse() + { + WicMetadataWriter kept = null; + Encode(WicContainerFormats.Png, writer => + { + kept = writer; + Assert.Throws(() => writer.Set("/tEXt/{str=Software}", new object())); + }, out _); + Assert.Throws(() => kept.Set("/tEXt/{str=Software}", "too late")); + } + + /// + /// The XMP block of a decoded image, carried over to a new image as raw bytes + /// + [Theory] + [MemberData(nameof(MetadataContainers))] + public void RawBlock_Xmp_CarriedOver(string containerName) + { + var container = Container(containerName); + var original = Encode(container, writer => Assert.True(writer.Set(WicMetadataPaths.XmpProperty(container, "dc:title"), "Raw title")), out _); + var rawBlocks = WicImage.GetFrameInfo(new MemoryStream(original)).Metadata.RawBlocks; + var xmp = Assert.Single(rawBlocks, b => b.Format == WicMetadataFormats.Xmp); + Assert.Equal(WicMetadataPaths.Xmp(container), xmp.Path); + Assert.Contains("Raw title", System.Text.Encoding.UTF8.GetString(xmp.Data)); + + var copy = Encode(container, writer => Assert.True(writer.AddRawBlock(xmp)), out var result); + Assert.Empty(result.MetadataIssues); + AssertValue(WicImage.GetFrameInfo(new MemoryStream(copy)).Metadata, WicMetadataPaths.XmpProperty(container, "dc:title"), "Raw title"); + } + + /// + /// The APP1 (EXIF) block of a JPEG, carried over to a new JPEG as raw bytes + /// + [Fact] + public void RawBlock_JpegApp1_CarriedOver() + { + var software = WicMetadataPaths.Software(WicContainerFormats.Jpeg); + var original = Encode(WicContainerFormats.Jpeg, writer => Assert.True(writer.Set(software, WicMetadataValue.AnsiString("Raw software"))), out _); + var app1 = Assert.Single(WicImage.GetFrameInfo(new MemoryStream(original)).Metadata.RawBlocks, b => b.Format == WicMetadataFormats.App1); + Assert.Equal("/app1", app1.Path); + + var copy = Encode(WicContainerFormats.Jpeg, writer => Assert.True(writer.AddRawBlock(app1)), out var result); + Assert.Empty(result.MetadataIssues); + AssertValue(WicImage.GetFrameInfo(new MemoryStream(copy)).Metadata, software, "Raw software"); + } + + /// + /// GIF needs a palette, the transparent pixel stays transparent + /// + [Fact] + public void Gif_WithTransparency() + { + var pixels = CreatePixels(); + pixels[3] = 0; // first pixel transparent + var encoded = Encode(WicContainerFormats.Gif, null, out var result, pixels); + Assert.NotEqual(WicPixelFormats.Format32bppBgra, result.PixelFormat); + var decoded = new byte[pixels.Length]; + var handle = GCHandle.Alloc(decoded, GCHandleType.Pinned); + try + { + WicImage.Decode(new MemoryStream(encoded), WicPixelFormats.Format32bppBgra, handle.AddrOfPinnedObject(), 12); + } + finally + { + handle.Free(); + } + Assert.Equal(0, decoded[3]); + for (int i = 1; i < 6; i++) + { + Assert.Equal(255, decoded[i * 4 + 3]); + Assert.Equal(pixels[i * 4], decoded[i * 4]); + } + } + + [Fact] + public void Rational_Packing() + { + var rational = new WicRational(1, 250); + Assert.Equal(1UL | (250UL << 32), rational.ToPacked()); + Assert.Equal(rational, WicRational.FromPacked(rational.ToPacked())); + Assert.Equal(0.004, rational.ToDouble(), 6); + Assert.True(double.IsNaN(new WicRational(1, 0).ToDouble())); + + var signed = new WicSignedRational(-1, 3); + Assert.Equal(signed, WicSignedRational.FromPacked(signed.ToPacked())); + Assert.Equal(-1.0 / 3, signed.ToDouble(), 6); + } +} diff --git a/src/Dapplo.Windows.sln b/src/Dapplo.Windows.sln index 4137f0c9..bf69c99a 100644 --- a/src/Dapplo.Windows.sln +++ b/src/Dapplo.Windows.sln @@ -67,6 +67,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapplo.Windows.Example.DocS EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapplo.Windows.Automation", "Dapplo.Windows.Automation\Dapplo.Windows.Automation.csproj", "{5B0D3E2A-8C41-4F6B-9A7E-1D2C3B4A5F60}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapplo.Windows.Imaging", "Dapplo.Windows.Imaging\Dapplo.Windows.Imaging.csproj", "{3E7A9C14-6B2D-4F85-A0C3-9D1E5B7F2A48}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -201,6 +203,10 @@ Global {5B0D3E2A-8C41-4F6B-9A7E-1D2C3B4A5F60}.Debug|Any CPU.Build.0 = Debug|Any CPU {5B0D3E2A-8C41-4F6B-9A7E-1D2C3B4A5F60}.Release|Any CPU.ActiveCfg = Release|Any CPU {5B0D3E2A-8C41-4F6B-9A7E-1D2C3B4A5F60}.Release|Any CPU.Build.0 = Release|Any CPU + {3E7A9C14-6B2D-4F85-A0C3-9D1E5B7F2A48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3E7A9C14-6B2D-4F85-A0C3-9D1E5B7F2A48}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3E7A9C14-6B2D-4F85-A0C3-9D1E5B7F2A48}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3E7A9C14-6B2D-4F85-A0C3-9D1E5B7F2A48}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/version.json b/src/version.json index 5057c5c7..08735f16 100644 --- a/src/version.json +++ b/src/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "3.17", + "version": "3.18", "publicReleaseRefSpec": [ "^refs/heads/master$" ],