diff --git a/src/System.Drawing.Common/src/System/Drawing/Image.cs b/src/System.Drawing.Common/src/System/Drawing/Image.cs index 33bd7b69e88..f2940bc4f51 100644 --- a/src/System.Drawing.Common/src/System/Drawing/Image.cs +++ b/src/System.Drawing.Common/src/System/Drawing/Image.cs @@ -8,9 +8,6 @@ using System.Runtime.Serialization; using Windows.Win32.System.Com; -#if DEBUG -#endif - namespace System.Drawing; /// @@ -259,18 +256,29 @@ public void Save(string filename, ImageFormat format) { ArgumentNullException.ThrowIfNull(format); - ImageCodecInfo codec = format.FindEncoder() ?? ImageFormat.Png.FindEncoder()!; + Guid encoder = format.Encoder; + if (encoder == Guid.Empty) + { + encoder = ImageCodecInfo.GetEncoderClsid(PInvokeCore.ImageFormatPNG); + } - Save(filename, codec, null); + Save(filename, encoder, null); } /// /// Saves this to the specified file in the specified format and with the specified encoder parameters. /// public void Save(string filename, ImageCodecInfo encoder, Imaging.EncoderParameters? encoderParams) + => Save(filename, encoder.Clsid, encoderParams); + + private void Save(string filename, Guid encoder, Imaging.EncoderParameters? encoderParams) { ArgumentNullException.ThrowIfNull(filename); - ArgumentNullException.ThrowIfNull(encoder); + if (encoder == Guid.Empty) + { + throw new ArgumentNullException(nameof(encoder)); + } + ThrowIfDirectoryDoesntExist(filename); GdiPlus.EncoderParameters* nativeParameters = null; @@ -283,22 +291,17 @@ public void Save(string filename, ImageCodecInfo encoder, Imaging.EncoderParamet try { - Guid guid = encoder.Clsid; - bool saved = false; - - if (_animatedGifRawData is not null && RawFormat.FindEncoder() is { } rawEncoder && rawEncoder.Clsid == guid) + if (_animatedGifRawData is not null && RawFormat.Encoder == encoder) { + // Special case for animated gifs. We don't have an encoder for them, so we just write the raw data. using var fs = File.OpenWrite(filename); fs.Write(_animatedGifRawData, 0, _animatedGifRawData.Length); - saved = true; + return; } - if (!saved) + fixed (char* fn = filename) { - fixed (char* fn = filename) - { - PInvoke.GdipSaveImageToFile(_nativeImage, fn, &guid, nativeParameters).ThrowIfFailed(); - } + PInvoke.GdipSaveImageToFile(_nativeImage, fn, &encoder, nativeParameters).ThrowIfFailed(); } } finally @@ -315,15 +318,18 @@ public void Save(string filename, ImageCodecInfo encoder, Imaging.EncoderParamet private void Save(MemoryStream stream) { - // Jpeg loses data, so we don't want to use it to serialize... - ImageFormat dest = RawFormat; - if (dest.Guid == ImageFormat.Jpeg.Guid) - dest = ImageFormat.Png; + Guid format = RawFormat.Guid; + Guid encoder = ImageCodecInfo.GetEncoderClsid(format); - // If we don't find an Encoder (for things like Icon), we just switch back to PNG... - ImageCodecInfo codec = dest.FindEncoder() ?? ImageFormat.Png.FindEncoder()!; + // Jpeg loses data, so we don't want to use it to serialize. We'll use PNG instead. + // If we don't find an Encoder (for things like Icon), we just switch back to PNG. + if (format == PInvokeCore.ImageFormatJPEG || encoder == Guid.Empty) + { + format = PInvokeCore.ImageFormatPNG; + encoder = ImageCodecInfo.GetEncoderClsid(format); + } - Save(stream, codec); + Save(this, stream, encoder, format, null); } /// @@ -332,23 +338,18 @@ private void Save(MemoryStream stream) public void Save(Stream stream, ImageFormat format) { ArgumentNullException.ThrowIfNull(format); - - ImageCodecInfo codec = format.FindEncoder()!; - Save(stream, codec); + Save(this, stream, format.Encoder, format.Guid, null); } - internal void Save(Stream stream, ImageCodecInfo encoder) => - Save(stream, encoder, encoderParameters: null); - - internal void Save(Stream stream, ImageCodecInfo encoder, GdiPlus.EncoderParameters* encoderParameters) => - Save(this, stream, encoder.Clsid, encoder.FormatID, encoderParameters); - internal static void Save(IImage image, Stream stream, Guid encoder, Guid format, GdiPlus.EncoderParameters* encoderParameters) { ArgumentNullException.ThrowIfNull(stream); - ArgumentNullException.ThrowIfNull(encoder); + if (encoder == Guid.Empty) + { + throw new ArgumentNullException(nameof(encoder)); + } - if (format == ImageFormat.Gif.Guid && image.Data is { } rawData && rawData.Length > 0) + if (format == PInvokeCore.ImageFormatGIF && image.Data is { } rawData && rawData.Length > 0) { stream.Write(rawData); return; @@ -376,7 +377,7 @@ public void Save(Stream stream, ImageCodecInfo encoder, Imaging.EncoderParameter try { - Save(stream, encoder, nativeParameters); + Save(this, stream, encoder.Clsid, encoder.FormatID, nativeParameters); } finally { diff --git a/src/System.Drawing.Common/src/System/Drawing/ImageConverter.cs b/src/System.Drawing.Common/src/System/Drawing/ImageConverter.cs index eb56d7129d8..64aab9f103b 100644 --- a/src/System.Drawing.Common/src/System/Drawing/ImageConverter.cs +++ b/src/System.Drawing.Common/src/System/Drawing/ImageConverter.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; -using System.Drawing.Imaging; using System.Globalization; using System.IO; @@ -40,7 +39,7 @@ public override bool CanConvertTo(ITypeDescriptorContext? context, [NotNullWhen( } } - public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) + public unsafe override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType) { if (destinationType == typeof(string)) { @@ -63,17 +62,18 @@ public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? c { using MemoryStream ms = new(); - ImageFormat dest = image.RawFormat; - // Jpeg loses data, so we don't want to use it to serialize. - if (dest == ImageFormat.Jpeg) + Guid format = image.RawFormat.Guid; + Guid encoder = ImageCodecInfo.GetEncoderClsid(format); + + // Jpeg loses data, so we don't want to use it to serialize. We'll use PNG instead. + // If we don't find an Encoder (for things like Icon), we just switch back to PNG. + if (format == PInvokeCore.ImageFormatJPEG || encoder == Guid.Empty) { - dest = ImageFormat.Png; + format = PInvokeCore.ImageFormatPNG; + encoder = ImageCodecInfo.GetEncoderClsid(format); } - // If we don't find an Encoder (for things like Icon), we - // just switch back to PNG. - ImageCodecInfo codec = FindEncoder(dest) ?? FindEncoder(ImageFormat.Png)!; - image.Save(ms, codec); + Image.Save(image, ms, encoder, format, null); return ms.ToArray(); } } @@ -81,19 +81,6 @@ public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? c throw GetConvertFromException(value); } - // Find any random encoder which supports this format. - private static ImageCodecInfo? FindEncoder(ImageFormat imageformat) - { - ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders(); - foreach (ImageCodecInfo codec in codecs) - { - if (codec.FormatID.Equals(imageformat.Guid)) - return codec; - } - - return null; - } - [RequiresUnreferencedCode("The Type of value cannot be statically discovered. The public parameterless constructor or the 'Default' static field may be trimmed from the Attribute's Type.")] public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext? context, object? value, Attribute[]? attributes) { diff --git a/src/System.Drawing.Common/src/System/Drawing/Imaging/ImageCodecInfo.cs b/src/System.Drawing.Common/src/System/Drawing/Imaging/ImageCodecInfo.cs index cc10b23bace..be452b5e659 100644 --- a/src/System.Drawing.Common/src/System/Drawing/Imaging/ImageCodecInfo.cs +++ b/src/System.Drawing.Common/src/System/Drawing/Imaging/ImageCodecInfo.cs @@ -36,6 +36,58 @@ internal ImageCodecInfo() // Encoder/Decoder selection APIs + // Getting the entire array of ImageCodecInfo objects is expensive and not necessary when all we need are the + // encoder CLSIDs. There are only 5 encoders: PNG, JPEG, GIF, BMP, and TIFF. We'll just build the small cache + // here to avoid all of the overhead. (We could probably just hard-code the values, but on the very small chance + // that the list of encoders changes, we'll keep it dynamic.) + private static (Guid Format, Guid Encoder)[]? s_encoders; + + /// + /// Get the encoder guid for the given image format guid. + /// + internal static Guid GetEncoderClsid(Guid format) + { + foreach ((Guid Format, Guid Encoder) in Encoders) + { + if (Format == format) + { + return Encoder; + } + } + + return Guid.Empty; + } + + private static (Guid Format, Guid Encoder)[] Encoders + { + get + { + if (s_encoders is null) + { + uint numEncoders; + uint size; + + PInvoke.GdipGetImageEncodersSize(&numEncoders, &size).ThrowIfFailed(); + + using BufferScope buffer = new((int)size); + + fixed (byte* b = buffer) + { + PInvoke.GdipGetImageEncoders(numEncoders, size, (GdiPlus.ImageCodecInfo*)b).ThrowIfFailed(); + ReadOnlySpan codecInfo = new((GdiPlus.ImageCodecInfo*)b, (int)numEncoders); + s_encoders = new (Guid Format, Guid Encoder)[codecInfo.Length]; + + for (int i = 0; i < codecInfo.Length; i++) + { + s_encoders[i] = (codecInfo[i].FormatID, codecInfo[i].Clsid); + } + } + } + + return s_encoders; + } + } + public static ImageCodecInfo[] GetImageDecoders() { ImageCodecInfo[] imageCodecs; diff --git a/src/System.Drawing.Common/src/System/Drawing/Imaging/ImageFormat.cs b/src/System.Drawing.Common/src/System/Drawing/Imaging/ImageFormat.cs index 84ae8d747b3..4adb3015b6d 100644 --- a/src/System.Drawing.Common/src/System/Drawing/Imaging/ImageFormat.cs +++ b/src/System.Drawing.Common/src/System/Drawing/Imaging/ImageFormat.cs @@ -14,18 +14,18 @@ public sealed class ImageFormat { // Format IDs // private static ImageFormat undefined = new ImageFormat(new Guid("{b96b3ca9-0728-11d3-9d7b-0000f81ef32e}")); - private static readonly ImageFormat s_memoryBMP = new(new Guid("{b96b3caa-0728-11d3-9d7b-0000f81ef32e}")); - private static readonly ImageFormat s_bmp = new(new Guid("{b96b3cab-0728-11d3-9d7b-0000f81ef32e}")); - private static readonly ImageFormat s_emf = new(new Guid("{b96b3cac-0728-11d3-9d7b-0000f81ef32e}")); - private static readonly ImageFormat s_wmf = new(new Guid("{b96b3cad-0728-11d3-9d7b-0000f81ef32e}")); - private static readonly ImageFormat s_jpeg = new(new Guid("{b96b3cae-0728-11d3-9d7b-0000f81ef32e}")); - private static readonly ImageFormat s_png = new(new Guid("{b96b3caf-0728-11d3-9d7b-0000f81ef32e}")); - private static readonly ImageFormat s_gif = new(new Guid("{b96b3cb0-0728-11d3-9d7b-0000f81ef32e}")); - private static readonly ImageFormat s_tiff = new(new Guid("{b96b3cb1-0728-11d3-9d7b-0000f81ef32e}")); - private static readonly ImageFormat s_exif = new(new Guid("{b96b3cb2-0728-11d3-9d7b-0000f81ef32e}")); - private static readonly ImageFormat s_icon = new(new Guid("{b96b3cb5-0728-11d3-9d7b-0000f81ef32e}")); - private static readonly ImageFormat s_heif = new(new Guid("{b96b3cb6-0728-11d3-9d7b-0000f81ef32e}")); - private static readonly ImageFormat s_webp = new(new Guid("{b96b3cb7-0728-11d3-9d7b-0000f81ef32e}")); + private static readonly ImageFormat s_memoryBMP = new(PInvokeCore.ImageFormatMemoryBMP); + private static readonly ImageFormat s_bmp = new(PInvokeCore.ImageFormatBMP); + private static readonly ImageFormat s_emf = new(PInvokeCore.ImageFormatEMF); + private static readonly ImageFormat s_wmf = new(PInvokeCore.ImageFormatWMF); + private static readonly ImageFormat s_jpeg = new(PInvokeCore.ImageFormatJPEG); + private static readonly ImageFormat s_png = new(PInvokeCore.ImageFormatPNG); + private static readonly ImageFormat s_gif = new(PInvokeCore.ImageFormatGIF); + private static readonly ImageFormat s_tiff = new(PInvokeCore.ImageFormatTIFF); + private static readonly ImageFormat s_exif = new(PInvokeCore.ImageFormatEXIF); + private static readonly ImageFormat s_icon = new(PInvokeCore.ImageFormatIcon); + private static readonly ImageFormat s_heif = new(PInvokeCore.ImageFormatHEIF); + private static readonly ImageFormat s_webp = new(PInvokeCore.ImageFormatWEBP); private readonly Guid _guid; @@ -93,16 +93,16 @@ public sealed class ImageFormat /// Specifies the High Efficiency Image Format (HEIF). /// /// - /// This format is supported since Windows 10 1809. + /// This format is supported since Windows 10 1809. /// [SupportedOSPlatform("windows10.0.17763.0")] public static ImageFormat Heif => s_heif; /// - /// Specifies the WebP image format. + /// Specifies the WebP image format. /// /// - /// This format is supported since Windows 10 1809. + /// This format is supported since Windows 10 1809. /// [SupportedOSPlatform("windows10.0.17763.0")] public static ImageFormat Webp => s_webp; @@ -111,52 +111,35 @@ public sealed class ImageFormat /// Returns a value indicating whether the specified object is an equivalent to this /// . /// - public override bool Equals([NotNullWhen(true)] object? o) - { - ImageFormat? format = o as ImageFormat; - if (format is null) - return false; - return _guid == format._guid; - } + public override bool Equals([NotNullWhen(true)] object? o) => o is ImageFormat format && _guid == format._guid; /// /// Returns a hash code. /// - public override int GetHashCode() - { - return _guid.GetHashCode(); - } + public override int GetHashCode() => _guid.GetHashCode(); - // Find any random encoder which supports this format - internal ImageCodecInfo? FindEncoder() - { - ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders(); - foreach (ImageCodecInfo codec in codecs) - { - if (codec.FormatID.Equals(_guid)) - return codec; - } - - return null; - } + /// + /// The encoder that supports this format, if any. + /// + internal Guid Encoder => ImageCodecInfo.GetEncoderClsid(_guid); /// /// Converts this to a human-readable string. /// public override string ToString() { - if (Guid == s_memoryBMP.Guid) return "MemoryBMP"; - if (Guid == s_bmp.Guid) return "Bmp"; - if (Guid == s_emf.Guid) return "Emf"; - if (Guid == s_wmf.Guid) return "Wmf"; - if (Guid == s_gif.Guid) return "Gif"; - if (Guid == s_jpeg.Guid) return "Jpeg"; - if (Guid == s_png.Guid) return "Png"; - if (Guid == s_tiff.Guid) return "Tiff"; - if (Guid == s_exif.Guid) return "Exif"; - if (Guid == s_icon.Guid) return "Icon"; - if (Guid == s_heif.Guid) return "Heif"; - if (Guid == s_webp.Guid) return "Webp"; + if (Guid == PInvokeCore.ImageFormatMemoryBMP) return "MemoryBMP"; + if (Guid == PInvokeCore.ImageFormatBMP) return "Bmp"; + if (Guid == PInvokeCore.ImageFormatEMF) return "Emf"; + if (Guid == PInvokeCore.ImageFormatWMF) return "Wmf"; + if (Guid == PInvokeCore.ImageFormatGIF) return "Gif"; + if (Guid == PInvokeCore.ImageFormatJPEG) return "Jpeg"; + if (Guid == PInvokeCore.ImageFormatPNG) return "Png"; + if (Guid == PInvokeCore.ImageFormatTIFF) return "Tiff"; + if (Guid == PInvokeCore.ImageFormatEXIF) return "Exif"; + if (Guid == PInvokeCore.ImageFormatIcon) return "Icon"; + if (Guid == PInvokeCore.ImageFormatHEIF) return "Heif"; + if (Guid == PInvokeCore.ImageFormatWEBP) return "Webp"; return $"[ImageFormat: {_guid}]"; } } diff --git a/src/System.Private.Windows.Core/src/NativeMethods.txt b/src/System.Private.Windows.Core/src/NativeMethods.txt index 7cc54421654..650dce25845 100644 --- a/src/System.Private.Windows.Core/src/NativeMethods.txt +++ b/src/System.Private.Windows.Core/src/NativeMethods.txt @@ -106,6 +106,7 @@ HWND_* IDI_* IEnumUnknown IGlobalInterfaceTable +ImageFormat* INPLACE_E_NOTOOLSPACE IntersectClipRect IStream