Browse Source

Apply EXIF part selection at serialization boundary

pull/3187/head
James Jackson-South 3 weeks ago
parent
commit
b335b8a5f8
  1. 3
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  2. 11
      src/ImageSharp/Metadata/Profiles/Exif/ExifProfile.cs
  3. 83
      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)
{
// Materialize lazy values so Parts is applied during serialization.
_ = exifProfile.Values;
// Serialization applies Parts even when the current profile has not been initialized.
RiffHelper.WriteChunk(stream, (uint)WebpChunkType.Exif, exifProfile.ToByteArray());
}

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

@ -221,14 +221,21 @@ public sealed class ExifProfile : IDeepCloneable<ExifProfile>
=> this.SetValueInternal(tag, value);
/// <summary>
/// Converts this instance to a byte array.
/// Converts the sections selected by <see cref="Parts"/> to a byte array.
/// </summary>
/// <returns>The <see cref="T:byte[]"/></returns>
public byte[]? ToByteArray()
{
if (this.values is null)
{
return this.data;
// 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;
}
this.InitializeValues();
}
if (this.values.Count == 0)

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

@ -24,29 +24,68 @@ public class WebpEncoderTests
{
private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06);
[Fact]
public void Encode_LazyExifProfile_AppliesSelectedParts()
/// <summary>
/// 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)
{
using Image<Rgba32> input = new(8, 8);
ExifProfile exif = new();
exif.SetValue(ExifTag.Make, "ImageSharp");
exif.SetValue(ExifTag.GPSLatitudeRef, "N");
input.Metadata.ExifProfile = exif;
using MemoryStream jpegStream = new();
input.Save(jpegStream, new JpegEncoder());
jpegStream.Position = 0;
using Image source = Image.Load(jpegStream);
source.Metadata.ExifProfile!.Parts = ExifParts.IfdTags | ExifParts.ExifTags;
using MemoryStream webpStream = new();
source.Save(webpStream, new WebpEncoder());
webpStream.Position = 0;
using Image result = Image.Load(webpStream);
Assert.True(result.Metadata.ExifProfile!.TryGetValue(ExifTag.Make, out _));
Assert.False(result.Metadata.ExifProfile.TryGetValue(ExifTag.GPSLatitudeRef, out _));
ExifProfile source = new();
source.SetValue(ExifTag.Make, "POC");
source.SetValue(ExifTag.GPSLatitudeRef, "N");
ExifProfile filteredLazy = new(source.ToByteArray())
{
Parts = ExifParts.IfdTags | ExifParts.ExifTags
};
using Image<Rgba32> image = new(1, 1);
using MemoryStream output = reentrant
? new SwapOnCanSeekStream(() => image.Metadata.ExifProfile = filteredLazy)
: new MemoryStream();
if (!reentrant)
{
image.Metadata.ExifProfile = filteredLazy;
}
image.SaveAsWebp(output);
output.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 _));
}
/// <summary>
/// Replaces metadata at the stream capability check, after encoder synchronization has completed.
/// </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]

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()
{
ExifProfile profile = new();

Loading…
Cancel
Save