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. 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) 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());
} }

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

@ -221,14 +221,21 @@ 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)
{ {
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) 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); 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)
{ {
using Image<Rgba32> input = new(8, 8); ExifProfile source = new();
ExifProfile exif = new(); source.SetValue(ExifTag.Make, "POC");
exif.SetValue(ExifTag.Make, "ImageSharp"); source.SetValue(ExifTag.GPSLatitudeRef, "N");
exif.SetValue(ExifTag.GPSLatitudeRef, "N"); ExifProfile filteredLazy = new(source.ToByteArray())
input.Metadata.ExifProfile = exif; {
Parts = ExifParts.IfdTags | ExifParts.ExifTags
using MemoryStream jpegStream = new(); };
input.Save(jpegStream, new JpegEncoder());
jpegStream.Position = 0; using Image<Rgba32> image = new(1, 1);
using MemoryStream output = reentrant
using Image source = Image.Load(jpegStream); ? new SwapOnCanSeekStream(() => image.Metadata.ExifProfile = filteredLazy)
source.Metadata.ExifProfile!.Parts = ExifParts.IfdTags | ExifParts.ExifTags; : new MemoryStream();
using MemoryStream webpStream = new(); if (!reentrant)
source.Save(webpStream, new WebpEncoder()); {
webpStream.Position = 0; image.Metadata.ExifProfile = filteredLazy;
}
using Image result = Image.Load(webpStream);
Assert.True(result.Metadata.ExifProfile!.TryGetValue(ExifTag.Make, out _)); image.SaveAsWebp(output);
Assert.False(result.Metadata.ExifProfile.TryGetValue(ExifTag.GPSLatitudeRef, out _)); 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] [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