Browse Source

Apply EXIF part selection at serialization boundary

pull/3187/head
James Jackson-South 4 weeks ago
parent
commit
b335b8a5f8
  1. 3
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  2. 9
      src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
  3. 75
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
  4. 41
      tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs

3
src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs

@ -143,8 +143,7 @@ internal abstract class BitWriterBase
{ {
if (exifProfile != null) if (exifProfile != null)
{ {
// Materialize lazy values so Parts is applied during serialization. // Serialization applies Parts even when the current profile has not been initialized.
_ = exifProfile.Values;
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray()); RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray());
} }

9
src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs

@ -221,16 +221,23 @@ public sealed class ExifProfile : IDeepCloneable<ExifProfile>
=> this.SetValueInternal(tag, value); => this.SetValueInternal(tag, value);
/// <summary> /// <summary>
/// Converts this instance to a byte array. /// Converts the sections selected by <see cref="Parts"/> to a byte array.
/// </summary> /// </summary>
/// <returns>The <see cref="T:byte[]"/></returns> /// <returns>The <see cref="T:byte[]"/></returns>
public byte[]? ToByteArray() public byte[]? ToByteArray()
{ {
if (this.values is null) if (this.values is null)
{
// The original bytes include every section. They can only be reused when no filtering
// is requested; otherwise lazy profiles must go through the same writer as initialized ones.
if (this.Parts == ExifParts.All)
{ {
return this.data; return this.data;
} }
this.InitializeValues();
}
if (this.values.Count == 0) if (this.values.Count == 0)
{ {
return []; return [];

75
tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs

@ -24,29 +24,68 @@ public class WebpEncoderTests
{ {
private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06); private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06);
[Fact] /// <summary>
public void Encode_LazyExifProfile_AppliesSelectedParts() /// Selected EXIF parts are respected whether the lazy profile is installed before or after synchronization.
/// </summary>
/// <param name="reentrant">Whether the stream installs the profile after metadata synchronization.</param>
[Theory]
[InlineData(false)]
[InlineData(true)]
public void Encode_LazyExifProfile_AppliesSelectedParts(bool reentrant)
{
ExifProfile source = new();
source.SetValue(ExifTag.Make, "POC");
source.SetValue(ExifTag.GPSLatitudeRef, "N");
ExifProfile filteredLazy = new(source.ToByteArray())
{ {
using Image<Rgba32> input = new(8, 8); Parts = ExifParts.IfdTags | ExifParts.ExifTags
ExifProfile exif = new(); };
exif.SetValue(ExifTag.Make, "ImageSharp");
exif.SetValue(ExifTag.GPSLatitudeRef, "N");
input.Metadata.ExifProfile = exif;
using MemoryStream jpegStream = new(); using Image<Rgba32> image = new(1, 1);
input.Save(jpegStream, new JpegEncoder()); using MemoryStream output = reentrant
jpegStream.Position = 0; ? new SwapOnCanSeekStream(() => image.Metadata.ExifProfile = filteredLazy)
: new MemoryStream();
using Image source = Image.Load(jpegStream); if (!reentrant)
source.Metadata.ExifProfile!.Parts = ExifParts.IfdTags | ExifParts.ExifTags; {
image.Metadata.ExifProfile = filteredLazy;
}
using MemoryStream webpStream = new(); image.SaveAsWebp(output);
source.Save(webpStream, new WebpEncoder()); output.Position = 0;
webpStream.Position = 0; using Image decoded = Image.Load(output);
Assert.NotNull(decoded.Metadata.ExifProfile);
Assert.True(decoded.Metadata.ExifProfile.TryGetValue(ExifTag.Make, out IExifValue<string> make));
Assert.Equal("POC", make.Value);
Assert.False(decoded.Metadata.ExifProfile.TryGetValue(ExifTag.GPSLatitudeRef, out _));
}
using Image result = Image.Load(webpStream); /// <summary>
Assert.True(result.Metadata.ExifProfile!.TryGetValue(ExifTag.Make, out _)); /// Replaces metadata at the stream capability check, after encoder synchronization has completed.
Assert.False(result.Metadata.ExifProfile.TryGetValue(ExifTag.GPSLatitudeRef, out _)); /// </summary>
private sealed class SwapOnCanSeekStream : MemoryStream
{
private Action callback;
/// <summary>
/// Initializes a stream that invokes the callback on its first capability check.
/// </summary>
/// <param name="callback">The metadata replacement callback.</param>
public SwapOnCanSeekStream(Action callback) => this.callback = callback;
/// <inheritdoc/>
public override bool CanSeek
{
get
{
Action action = this.callback;
this.callback = null;
action?.Invoke();
return base.CanSeek;
}
}
} }
[Theory] [Theory]

41
tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs

@ -490,6 +490,47 @@ public class ExifProfileTests
} }
} }
/// <summary>
/// Lazy profile serialization filters selected sections without changing the all-parts passthrough.
/// </summary>
/// <param name="parts">The sections to serialize.</param>
/// <param name="keepMake">Whether the IFD tag should remain.</param>
/// <param name="keepGps">Whether the GPS tag should remain.</param>
[Theory]
[InlineData(ExifParts.All, true, true)]
[InlineData(ExifParts.IfdTags | ExifParts.ExifTags, true, false)]
[InlineData(ExifParts.GpsTags, false, true)]
[InlineData(ExifParts.None, false, false)]
public void ProfileToByteArray_AppliesPartsToLazyValues(ExifParts parts, bool keepMake, bool keepGps)
{
ExifProfile source = new();
source.SetValue(ExifTag.Make, "POC");
source.SetValue(ExifTag.GPSLatitudeRef, "N");
byte[] originalData = source.ToByteArray();
ExifProfile lazy = new(originalData) { Parts = parts };
byte[] filteredData = lazy.ToByteArray();
ExifProfile result = new(filteredData);
Assert.Equal(keepMake, result.TryGetValue(ExifTag.Make, out IExifValue<string> make));
Assert.Equal(keepGps, result.TryGetValue(ExifTag.GPSLatitudeRef, out IExifValue<string> gps));
if (keepMake)
{
Assert.Equal("POC", make.Value);
}
if (keepGps)
{
Assert.Equal("N", gps.Value);
}
if (parts is ExifParts.All)
{
Assert.Same(originalData, filteredData);
}
}
private static ExifProfile CreateExifProfile() private static ExifProfile CreateExifProfile()
{ {
ExifProfile profile = new(); ExifProfile profile = new();

Loading…
Cancel
Save