mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 19:27:56 +01:00
[ImageResizer] Fix bug "Operation caused an invalid state" (#15852)
* Add metadata check to ensure valid metadata * Thank you Mister Spellbot
This commit is contained in:
committed by
GitHub
parent
2a34cf740b
commit
8bb0772ae5
@@ -187,31 +187,6 @@ namespace ImageResizer.Extensions
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect whether the metadata object is valid and can be copied successfully
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// ImageMetadata.Clone() causes an exception if there is something wrong with the metadata object.
|
||||
/// Operation is rather expensive.
|
||||
/// </remarks>
|
||||
/// <param name="metadata">Metadata object to be checked</param>
|
||||
/// <returns>true if valid, false if invalid</returns>
|
||||
public static bool IsMetadataObjectValid(this BitmapMetadata metadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = metadata.Clone();
|
||||
|
||||
return true;
|
||||
}
|
||||
#pragma warning disable CA1031 // Do not catch general exception types
|
||||
catch (Exception)
|
||||
#pragma warning restore CA1031 // Do not catch general exception types
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints all metadata to debug console
|
||||
/// </summary>
|
||||
@@ -260,6 +235,11 @@ namespace ImageResizer.Extensions
|
||||
|
||||
void GetMetadataRecursively(BitmapMetadata metadata, string query)
|
||||
{
|
||||
if (metadata == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (string relativeQuery in metadata)
|
||||
{
|
||||
string absolutePath = query + relativeQuery;
|
||||
|
||||
@@ -52,13 +52,9 @@ namespace ImageResizer.Models
|
||||
BitmapCreateOptions.PreservePixelFormat,
|
||||
BitmapCacheOption.None);
|
||||
|
||||
var encoder = BitmapEncoder.Create(decoder.CodecInfo.ContainerFormat);
|
||||
if (!encoder.CanEncode())
|
||||
{
|
||||
encoder = BitmapEncoder.Create(_settings.FallbackEncoder);
|
||||
}
|
||||
var containerFormat = decoder.CodecInfo.ContainerFormat;
|
||||
|
||||
ConfigureEncoder(encoder);
|
||||
var encoder = CreateEncoder(containerFormat);
|
||||
|
||||
if (decoder.Metadata != null)
|
||||
{
|
||||
@@ -78,49 +74,15 @@ namespace ImageResizer.Models
|
||||
|
||||
foreach (var originalFrame in decoder.Frames)
|
||||
{
|
||||
BitmapMetadata metadata = (BitmapMetadata)originalFrame.Metadata;
|
||||
if (metadata != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var transformedBitmap = Transform(originalFrame);
|
||||
BitmapMetadata originalMetadata = (BitmapMetadata)originalFrame.Metadata;
|
||||
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"### Processing metadata of file {_file}");
|
||||
metadata.PrintsAllMetadataToDebugOutput();
|
||||
originalMetadata.PrintsAllMetadataToDebugOutput();
|
||||
#endif
|
||||
|
||||
// read all metadata and build up metadata object from the scratch. Discard invalid (unreadable/unwritable) metadata.
|
||||
var newMetadata = new BitmapMetadata(metadata.Format);
|
||||
var listOfMetadata = metadata.GetListOfMetadata();
|
||||
foreach (var (metadataPath, value) in listOfMetadata)
|
||||
{
|
||||
if (value is BitmapMetadata bitmapMetadata)
|
||||
{
|
||||
var innerMetadata = new BitmapMetadata(bitmapMetadata.Format);
|
||||
newMetadata.SetQuerySafe(metadataPath, innerMetadata);
|
||||
}
|
||||
else
|
||||
{
|
||||
newMetadata.SetQuerySafe(metadataPath, value);
|
||||
}
|
||||
}
|
||||
|
||||
if (newMetadata.IsMetadataObjectValid())
|
||||
{
|
||||
metadata = newMetadata;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Seems like we build an invalid metadata object. ImageResizer will fail when trying to write the image to disk. We discard all metadata to be able to save the image.
|
||||
metadata = null;
|
||||
}
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
metadata = null;
|
||||
|
||||
Debug.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
var metadata = GetValidMetadata(originalMetadata, transformedBitmap, containerFormat);
|
||||
|
||||
if (_settings.RemoveMetadata && metadata != null)
|
||||
{
|
||||
@@ -133,12 +95,9 @@ namespace ImageResizer.Models
|
||||
metadata = newMetadata;
|
||||
}
|
||||
|
||||
encoder.Frames.Add(
|
||||
BitmapFrame.Create(
|
||||
Transform(originalFrame),
|
||||
thumbnail: null, /* should be null, see #15413 */
|
||||
metadata,
|
||||
colorContexts: null /* should be null, see #14866 */ ));
|
||||
var frame = CreateBitmapFrame(transformedBitmap, metadata);
|
||||
|
||||
encoder.Frames.Add(frame);
|
||||
}
|
||||
|
||||
path = GetDestinationPath(encoder);
|
||||
@@ -162,7 +121,19 @@ namespace ImageResizer.Models
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigureEncoder(BitmapEncoder encoder)
|
||||
private BitmapEncoder CreateEncoder(Guid containerFormat)
|
||||
{
|
||||
var createdEncoder = BitmapEncoder.Create(containerFormat);
|
||||
if (!createdEncoder.CanEncode())
|
||||
{
|
||||
createdEncoder = BitmapEncoder.Create(_settings.FallbackEncoder);
|
||||
}
|
||||
|
||||
ConfigureEncoder(createdEncoder);
|
||||
|
||||
return createdEncoder;
|
||||
|
||||
void ConfigureEncoder(BitmapEncoder encoder)
|
||||
{
|
||||
switch (encoder)
|
||||
{
|
||||
@@ -179,6 +150,7 @@ namespace ImageResizer.Models
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BitmapSource Transform(BitmapSource source)
|
||||
{
|
||||
@@ -232,6 +204,101 @@ namespace ImageResizer.Models
|
||||
return scaledBitmap;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks original metadata by writing an image containing the given metadata into a memory stream.
|
||||
/// In case of errors, we try to rebuild the metadata object and check again.
|
||||
/// We return null if we were not able to get hold of valid metadata.
|
||||
/// </summary>
|
||||
private BitmapMetadata GetValidMetadata(BitmapMetadata originalMetadata, BitmapSource transformedBitmap, Guid containerFormat)
|
||||
{
|
||||
if (originalMetadata == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if the original metadata is valid
|
||||
var frameWithOriginalMetadata = CreateBitmapFrame(transformedBitmap, originalMetadata);
|
||||
if (EnsureFrameIsValid(frameWithOriginalMetadata))
|
||||
{
|
||||
return originalMetadata;
|
||||
}
|
||||
|
||||
// Original metadata was invalid. We try to rebuild the metadata object from the scratch and discard invalid metadata fields
|
||||
var recreatedMetadata = BuildMetadataFromTheScratch(originalMetadata);
|
||||
var frameWithRecreatedMetadata = CreateBitmapFrame(transformedBitmap, recreatedMetadata);
|
||||
if (EnsureFrameIsValid(frameWithRecreatedMetadata))
|
||||
{
|
||||
return recreatedMetadata;
|
||||
}
|
||||
|
||||
// Seems like we have an invalid metadata object. ImageResizer will fail when trying to write the image to disk. We discard all metadata to be able to save the image.
|
||||
return null;
|
||||
|
||||
// The safest way to check if the metadata object is valid is to call Save() on the encoder.
|
||||
// I tried other ways to check if metadata is valid (like calling Clone() on the metadata object) but this was not reliable resulting in a few github issues.
|
||||
bool EnsureFrameIsValid(BitmapFrame frameToBeChecked)
|
||||
{
|
||||
try
|
||||
{
|
||||
var encoder = CreateEncoder(containerFormat);
|
||||
encoder.Frames.Add(frameToBeChecked);
|
||||
using (var testStream = new MemoryStream())
|
||||
{
|
||||
encoder.Save(testStream);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
#pragma warning disable CA1031 // Do not catch general exception types
|
||||
catch (Exception)
|
||||
#pragma warning restore CA1031 // Do not catch general exception types
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read all metadata and build up metadata object from the scratch. Discard invalid (unreadable/unwritable) metadata.
|
||||
/// </summary>
|
||||
private static BitmapMetadata BuildMetadataFromTheScratch(BitmapMetadata originalMetadata)
|
||||
{
|
||||
try
|
||||
{
|
||||
var metadata = new BitmapMetadata(originalMetadata.Format);
|
||||
var listOfMetadata = originalMetadata.GetListOfMetadata();
|
||||
foreach (var (metadataPath, value) in listOfMetadata)
|
||||
{
|
||||
if (value is BitmapMetadata bitmapMetadata)
|
||||
{
|
||||
var innerMetadata = new BitmapMetadata(bitmapMetadata.Format);
|
||||
metadata.SetQuerySafe(metadataPath, innerMetadata);
|
||||
}
|
||||
else
|
||||
{
|
||||
metadata.SetQuerySafe(metadataPath, value);
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
Debug.WriteLine(ex);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static BitmapFrame CreateBitmapFrame(BitmapSource transformedBitmap, BitmapMetadata metadata)
|
||||
{
|
||||
return BitmapFrame.Create(
|
||||
transformedBitmap,
|
||||
thumbnail: null, /* should be null, see #15413 */
|
||||
metadata,
|
||||
colorContexts: null /* should be null, see #14866 */ );
|
||||
}
|
||||
|
||||
private string GetDestinationPath(BitmapEncoder encoder)
|
||||
{
|
||||
var directory = _destinationDirectory ?? _fileSystem.Path.GetDirectoryName(_file);
|
||||
|
||||
Reference in New Issue
Block a user