Skip to content

Commit

Permalink
Improve Image save performance (dotnet#10784)
Browse files Browse the repository at this point in the history
Getting the array of encoder ImageCodeInfo is expensive and unnecessary to do repeatedly. Cache the guids for internal use without converting to managed classes and jagged arrays.
  • Loading branch information
JeremyKuhne authored Feb 1, 2024
1 parent 869e40e commit 27713b2
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 108 deletions.
71 changes: 36 additions & 35 deletions src/System.Drawing.Common/src/System/Drawing/Image.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
using System.Runtime.Serialization;
using Windows.Win32.System.Com;

#if DEBUG
#endif

namespace System.Drawing;

/// <summary>
Expand Down Expand Up @@ -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);
}

/// <summary>
/// Saves this <see cref='Image'/> to the specified file in the specified format and with the specified encoder parameters.
/// </summary>
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;
Expand All @@ -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
Expand All @@ -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);
}

/// <summary>
Expand All @@ -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;
Expand Down Expand Up @@ -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
{
Expand Down
33 changes: 10 additions & 23 deletions src/System.Drawing.Common/src/System/Drawing/ImageConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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))
{
Expand All @@ -63,37 +62,25 @@ 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();
}
}

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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Get the encoder guid for the given image format guid.
/// </summary>
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<byte> buffer = new((int)size);

fixed (byte* b = buffer)
{
PInvoke.GdipGetImageEncoders(numEncoders, size, (GdiPlus.ImageCodecInfo*)b).ThrowIfFailed();
ReadOnlySpan<GdiPlus.ImageCodecInfo> 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;
Expand Down
83 changes: 33 additions & 50 deletions src/System.Drawing.Common/src/System/Drawing/Imaging/ImageFormat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -93,16 +93,16 @@ public sealed class ImageFormat
/// Specifies the High Efficiency Image Format (HEIF).
/// </summary>
/// <remarks>
/// This format is supported since Windows 10 1809.
/// <para>This format is supported since Windows 10 1809.</para>
/// </remarks>
[SupportedOSPlatform("windows10.0.17763.0")]
public static ImageFormat Heif => s_heif;

/// <summary>
/// Specifies the WebP image format.
/// Specifies the WebP image format.
/// </summary>
/// <remarks>
/// This format is supported since Windows 10 1809.
/// <para>This format is supported since Windows 10 1809.</para>
/// </remarks>
[SupportedOSPlatform("windows10.0.17763.0")]
public static ImageFormat Webp => s_webp;
Expand All @@ -111,52 +111,35 @@ public sealed class ImageFormat
/// Returns a value indicating whether the specified object is an <see cref='ImageFormat'/> equivalent to this
/// <see cref='ImageFormat'/>.
/// </summary>
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;

/// <summary>
/// Returns a hash code.
/// </summary>
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;
}
/// <summary>
/// The encoder that supports this format, if any.
/// </summary>
internal Guid Encoder => ImageCodecInfo.GetEncoderClsid(_guid);

/// <summary>
/// Converts this <see cref='ImageFormat'/> to a human-readable string.
/// </summary>
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}]";
}
}
1 change: 1 addition & 0 deletions src/System.Private.Windows.Core/src/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ HWND_*
IDI_*
IEnumUnknown
IGlobalInterfaceTable
ImageFormat*
INPLACE_E_NOTOOLSPACE
IntersectClipRect
IStream
Expand Down

0 comments on commit 27713b2

Please sign in to comment.