Browse Source

Remove allocations and cleanup.

pull/2641/head
James Jackson-South 2 years ago
parent
commit
d8484da739
  1. 24
      src/ImageSharp/Formats/Jpeg/JpegComData.cs
  2. 9
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  3. 77
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  4. 104
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs

24
src/ImageSharp/Formats/Jpeg/JpegComData.cs

@ -1,32 +1,32 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
namespace SixLabors.ImageSharp.Formats.Jpeg;
/// <summary>
/// Contains JPEG comment
/// Represents a JPEG comment
/// </summary>
public readonly struct JpegComData
{
/// <summary>
/// Converts string to <see cref="JpegComData"/>
/// </summary>
/// <param name="value">The comment string.</param>
/// <returns>The <see cref="JpegComData"/></returns>
public static JpegComData FromString(string value) => new(value.AsMemory());
/// <summary>
/// Initializes a new instance of the <see cref="JpegComData"/> struct.
/// </summary>
/// <param name="value">The comment ReadOnlyMemory of chars.</param>
/// <param name="value">The comment buffer.</param>
public JpegComData(ReadOnlyMemory<char> value)
=> this.Value = value;
/// <summary>
/// Gets the value.
/// </summary>
public ReadOnlyMemory<char> Value { get; }
/// <summary>
/// Converts Value to string
/// Converts string to <see cref="JpegComData"/>
/// </summary>
/// <returns>The comment string.</returns>
/// <param name="value">The comment string.</param>
/// <returns>The <see cref="JpegComData"/></returns>
public static JpegComData FromString(string value) => new(value.AsMemory());
/// <inheritdoc/>
public override string ToString() => this.Value.ToString();
}

9
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -6,7 +6,6 @@ using System.Buffers;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
@ -481,6 +480,8 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
break;
case JpegConstants.Markers.APP15:
stream.Skip(markerContentByteSize);
break;
case JpegConstants.Markers.COM:
this.ProcessComMarker(stream, markerContentByteSize);
break;
@ -523,16 +524,16 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
/// <param name="markerContentByteSize">The remaining bytes in the segment block.</param>
private void ProcessComMarker(BufferedReadStream stream, int markerContentByteSize)
{
char[] temp = new char[markerContentByteSize];
char[] chars = new char[markerContentByteSize];
JpegMetadata metadata = this.Metadata.GetFormatMetadata(JpegFormat.Instance);
for (int i = 0; i < markerContentByteSize; i++)
{
int read = stream.ReadByte();
temp[i] = (char)read;
chars[i] = (char)read;
}
metadata.Comments.Add(new JpegComData(temp));
metadata.Comments.Add(new JpegComData(chars));
}
/// <summary>

77
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -2,9 +2,8 @@
// Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers;
using System.Buffers.Binary;
using System.Collections;
using System.Text;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder;
@ -27,6 +26,9 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
/// </summary>
private static readonly JpegFrameConfig[] FrameConfigs = CreateFrameConfigs();
/// <summary>
/// The current calling encoder.
/// </summary>
private readonly JpegEncoder encoder;
/// <summary>
@ -92,7 +94,7 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
this.WriteProfiles(metadata, buffer);
// Write comments
this.WriteComment(jpegMetadata);
this.WriteComments(image.Configuration, jpegMetadata);
// Write the image dimensions.
this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer);
@ -173,67 +175,48 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
}
/// <summary>
/// Writes comment
/// Writes the COM tags.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="metadata">The image metadata.</param>
private void WriteComment(JpegMetadata metadata)
private void WriteComments(Configuration configuration, JpegMetadata metadata)
{
int maxCommentLength = 65533;
if (metadata.Comments.Count == 0)
{
return;
}
// We don't want to modify original metadata
List<JpegComData> comments = new(metadata.Comments);
int totalPayloadLength = 0;
for (int i = 0; i < comments.Count; i++)
const int maxCommentLength = 65533;
using IMemoryOwner<byte> bufferOwner = configuration.MemoryAllocator.Allocate<byte>(maxCommentLength);
Span<byte> buffer = bufferOwner.Memory.Span;
foreach (JpegComData comment in metadata.Comments)
{
JpegComData comment = comments[i];
ReadOnlyMemory<char> currentComment = comment.Value;
if (comment.Value.Length > maxCommentLength)
int totalLength = comment.Value.Length;
if (totalLength == 0)
{
ReadOnlyMemory<char> splitComment =
currentComment.Slice(maxCommentLength, currentComment.Length - maxCommentLength);
comments.Insert(i + 1, new JpegComData(splitComment));
// We don't want to keep the extra bytes
comments[i] = new JpegComData(currentComment.Slice(0, maxCommentLength));
continue;
}
totalPayloadLength += comment.Value.Length + 4;
}
Span<byte> payload = new byte[totalPayloadLength];
int currentCommentStartingIndex = 0;
for (int i = 0; i < comments.Count; i++)
{
ReadOnlyMemory<char> comment = comments[i].Value;
// Loop through and split the comment into multiple comments if the comment length
// is greater than the maximum allowed length.
while (totalLength > 0)
{
int currentLength = Math.Min(totalLength, maxCommentLength);
// Beginning of comment ff fe
payload[currentCommentStartingIndex] = JpegConstants.Markers.XFF;
payload[currentCommentStartingIndex + 1] = JpegConstants.Markers.COM;
// Write the marker header.
this.WriteMarkerHeader(JpegConstants.Markers.COM, currentLength + 2, buffer);
// Write payload size
int comWithoutMarker = comment.Length + 2;
payload[currentCommentStartingIndex + 2] = (byte)((comWithoutMarker >> 8) & 0xFF);
payload[currentCommentStartingIndex + 3] = (byte)(comWithoutMarker & 0xFF);
ReadOnlySpan<char> commentValue = comment.Value.Span.Slice(comment.Value.Length - totalLength, currentLength);
for (int i = 0; i < commentValue.Length; i++)
{
buffer[i] = (byte)commentValue[i];
}
char[] commentChars = comment.ToArray();
for (int j = 0; j < commentChars.Length; j++)
{
// Initial 4 bytes are always reserved
payload[4 + currentCommentStartingIndex + j] = (byte)commentChars[j];
// Write the comment.
this.outputStream.Write(buffer, 0, currentLength);
totalLength -= currentLength;
}
currentCommentStartingIndex += comment.Length + 4;
}
this.outputStream.Write(payload, 0, payload.Length);
}
/// <summary>

104
tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs

@ -32,19 +32,19 @@ public partial class JpegEncoderTests
public void Encode_PreservesIptcProfile()
{
// arrange
using var input = new Image<Rgba32>(1, 1);
var expectedProfile = new IptcProfile();
using Image<Rgba32> input = new(1, 1);
IptcProfile expectedProfile = new();
expectedProfile.SetValue(IptcTag.Country, "ESPAÑA");
expectedProfile.SetValue(IptcTag.City, "unit-test-city");
input.Metadata.IptcProfile = expectedProfile;
// act
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
input.Save(memStream, JpegEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
IptcProfile actual = output.Metadata.IptcProfile;
Assert.NotNull(actual);
IEnumerable<IptcValue> values = expectedProfile.Values;
@ -55,17 +55,17 @@ public partial class JpegEncoderTests
public void Encode_PreservesExifProfile()
{
// arrange
using var input = new Image<Rgba32>(1, 1);
using Image<Rgba32> input = new(1, 1);
input.Metadata.ExifProfile = new ExifProfile();
input.Metadata.ExifProfile.SetValue(ExifTag.Software, "unit_test");
// act
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
input.Save(memStream, JpegEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
ExifProfile actual = output.Metadata.ExifProfile;
Assert.NotNull(actual);
IReadOnlyList<IExifValue> values = input.Metadata.ExifProfile.Values;
@ -76,16 +76,16 @@ public partial class JpegEncoderTests
public void Encode_PreservesIccProfile()
{
// arrange
using var input = new Image<Rgba32>(1, 1);
using Image<Rgba32> input = new(1, 1);
input.Metadata.IccProfile = new IccProfile(IccTestDataProfiles.Profile_Random_Array);
// act
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
input.Save(memStream, JpegEncoder);
// assert
memStream.Position = 0;
using var output = Image.Load<Rgba32>(memStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
IccProfile actual = output.Metadata.IccProfile;
Assert.NotNull(actual);
IccProfile values = input.Metadata.IccProfile;
@ -99,12 +99,10 @@ public partial class JpegEncoderTests
{
Exception ex = Record.Exception(() =>
{
var encoder = new JpegEncoder();
using (var stream = new MemoryStream())
{
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
image.Save(stream, encoder);
}
JpegEncoder encoder = new();
using MemoryStream stream = new();
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
image.Save(stream, encoder);
});
Assert.Null(ex);
@ -114,23 +112,17 @@ public partial class JpegEncoderTests
[MemberData(nameof(RatioFiles))]
public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
var testFile = TestFile.Create(imagePath);
using (Image<Rgba32> input = testFile.CreateRgba32Image())
{
using (var memStream = new MemoryStream())
{
input.Save(memStream, JpegEncoder);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
ImageMetadata meta = output.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
}
}
TestFile testFile = TestFile.Create(imagePath);
using Image<Rgba32> input = testFile.CreateRgba32Image();
using MemoryStream memStream = new();
input.Save(memStream, JpegEncoder);
memStream.Position = 0;
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
ImageMetadata meta = output.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
[Theory]
@ -138,20 +130,14 @@ public partial class JpegEncoderTests
public void Encode_PreservesQuality(string imagePath, int quality)
{
TestFile testFile = TestFile.Create(imagePath);
using (Image<Rgba32> input = testFile.CreateRgba32Image())
{
using (var memStream = new MemoryStream())
{
input.Save(memStream, JpegEncoder);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
JpegMetadata meta = output.Metadata.GetJpegMetadata();
Assert.Equal(quality, meta.Quality);
}
}
}
using Image<Rgba32> input = testFile.CreateRgba32Image();
using MemoryStream memStream = new();
input.Save(memStream, JpegEncoder);
memStream.Position = 0;
using Image<Rgba32> output = Image.Load<Rgba32>(memStream);
JpegMetadata meta = output.Metadata.GetJpegMetadata();
Assert.Equal(quality, meta.Quality);
}
[Theory]
@ -161,7 +147,7 @@ public partial class JpegEncoderTests
{
// arrange
using Image<TPixel> input = provider.GetImage(JpegDecoder.Instance);
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
input.Save(memStream, JpegEncoder);
@ -172,16 +158,16 @@ public partial class JpegEncoderTests
JpegMetadata actual = output.Metadata.GetJpegMetadata();
Assert.NotEmpty(actual.Comments);
Assert.Equal(1, actual.Comments.Count);
Assert.Equal("TEST COMMENT", actual.Comments.ElementAtOrDefault(0).ToString());
Assert.Equal("TEST COMMENT", actual.Comments[0].ToString());
}
[Fact]
public void Encode_SavesMultipleComments()
{
// arrange
using var input = new Image<Rgba32>(1, 1);
using Image<Rgba32> input = new(1, 1);
JpegMetadata meta = input.Metadata.GetJpegMetadata();
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
meta.Comments.Add(JpegComData.FromString("First comment"));
@ -194,8 +180,8 @@ public partial class JpegEncoderTests
JpegMetadata actual = output.Metadata.GetJpegMetadata();
Assert.NotEmpty(actual.Comments);
Assert.Equal(2, actual.Comments.Count);
Assert.Equal(meta.Comments.ElementAtOrDefault(0).ToString(), actual.Comments.ElementAtOrDefault(0).ToString());
Assert.Equal(meta.Comments.ElementAtOrDefault(1).ToString(), actual.Comments.ElementAtOrDefault(1).ToString());
Assert.Equal(meta.Comments[0].ToString(), actual.Comments[0].ToString());
Assert.Equal(meta.Comments[1].ToString(), actual.Comments[1].ToString());
}
[Fact]
@ -203,9 +189,9 @@ public partial class JpegEncoderTests
{
// arrange
string longString = new('c', 65534);
using var input = new Image<Rgba32>(1, 1);
using Image<Rgba32> input = new(1, 1);
JpegMetadata meta = input.Metadata.GetJpegMetadata();
using var memStream = new MemoryStream();
using MemoryStream memStream = new();
// act
meta.Comments.Add(JpegComData.FromString(longString));
@ -217,8 +203,8 @@ public partial class JpegEncoderTests
JpegMetadata actual = output.Metadata.GetJpegMetadata();
Assert.NotEmpty(actual.Comments);
Assert.Equal(2, actual.Comments.Count);
Assert.Equal(longString[..65533], actual.Comments.ElementAtOrDefault(0).ToString());
Assert.Equal("c", actual.Comments.ElementAtOrDefault(1).ToString());
Assert.Equal(longString[..65533], actual.Comments[0].ToString());
Assert.Equal("c", actual.Comments[1].ToString());
}
[Theory]
@ -231,14 +217,14 @@ public partial class JpegEncoderTests
{
// arrange
using Image<TPixel> input = provider.GetImage(JpegDecoder.Instance);
using var memoryStream = new MemoryStream();
using MemoryStream memoryStream = new();
// act
input.Save(memoryStream, JpegEncoder);
// assert
memoryStream.Position = 0;
using var output = Image.Load<Rgba32>(memoryStream);
using Image<Rgba32> output = Image.Load<Rgba32>(memoryStream);
JpegMetadata meta = output.Metadata.GetJpegMetadata();
Assert.Equal(expectedColorType, meta.ColorType);
}

Loading…
Cancel
Save