diff --git a/ImageSharp.sln b/ImageSharp.sln
index 2967acb8f..82eeefcde 100644
--- a/ImageSharp.sln
+++ b/ImageSharp.sln
@@ -237,6 +237,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issues", "issues", "{5C9B68
tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg = tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg
tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg = tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg
tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg = tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg
+ tests\Images\Input\Jpg\issues\issue-2067-comment.jpg = tests\Images\Input\Jpg\issues\issue-2067-comment.jpg
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fuzz", "fuzz", "{516A3532-6AC2-417B-AD79-9BD5D0D378A0}"
diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
index ccace190f..e11923ac8 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
@@ -6,6 +6,7 @@ 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,7 +482,7 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
case JpegConstants.Markers.APP15:
case JpegConstants.Markers.COM:
- stream.Skip(markerContentByteSize);
+ this.ProcessComMarker(stream, markerContentByteSize);
break;
case JpegConstants.Markers.DAC:
@@ -515,6 +516,23 @@ internal sealed class JpegDecoderCore : IRawJpegData, IImageDecoderInternals
this.scanDecoder = null;
}
+ ///
+ /// Assigns COM marker bytes to comment property
+ ///
+ /// The input stream.
+ /// The remaining bytes in the segment block.
+ private void ProcessComMarker(BufferedReadStream stream, int markerContentByteSize)
+ {
+ Span temp = stackalloc byte[markerContentByteSize];
+ char[] chars = new char[markerContentByteSize];
+ JpegMetadata metadata = this.Metadata.GetFormatMetadata(JpegFormat.Instance);
+
+ stream.Read(temp);
+ Encoding.ASCII.GetChars(temp, chars);
+
+ metadata.Comments.Add(chars);
+ }
+
///
/// Returns encoded colorspace based on the adobe APP14 marker.
///
diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
index 7fc2a1f45..cc6042b6e 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
@@ -3,6 +3,7 @@
#nullable disable
using System.Buffers.Binary;
+using System.Text;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
using SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder;
@@ -89,6 +90,9 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
// Write Exif, XMP, ICC and IPTC profiles
this.WriteProfiles(metadata, buffer);
+ // Write comments
+ this.WriteComment(jpegMetadata);
+
// Write the image dimensions.
this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer);
@@ -167,6 +171,47 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
this.outputStream.Write(buffer, 0, 18);
}
+ ///
+ /// Writes comment
+ ///
+ /// The image metadata.
+ private void WriteComment(JpegMetadata metadata)
+ {
+ if (metadata.Comments is { Count: 0 })
+ {
+ return;
+ }
+
+ // Length (comment strings lengths) + (comments markers with payload sizes)
+ int commentsBytes = metadata.Comments.Sum(x => x.Length) + (metadata.Comments.Count * 4);
+ int commentStart = 0;
+ Span commentBuffer = stackalloc byte[commentsBytes];
+
+ foreach (Memory comment in metadata.Comments)
+ {
+ int totalComLength = comment.Length + 4;
+
+ Span commentData = commentBuffer.Slice(commentStart, totalComLength);
+ Span markers = commentData.Slice(0, 2);
+ Span payloadSize = commentData.Slice(2, 2);
+ Span payload = commentData.Slice(4, comment.Length);
+
+ // Beginning of comment ff fe
+ markers[0] = JpegConstants.Markers.XFF;
+ markers[1] = JpegConstants.Markers.COM;
+
+ // Write payload size
+ BinaryPrimitives.WriteInt16BigEndian(payloadSize, (short)(commentData.Length - 2));
+
+ Encoding.ASCII.GetBytes(comment.Span, payload);
+
+ // Indicate begin of next comment in buffer
+ commentStart += totalComLength;
+ }
+
+ this.outputStream.Write(commentBuffer, 0, commentBuffer.Length);
+ }
+
///
/// Writes the Define Huffman Table marker and tables.
///
diff --git a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
index 59fc2f9cb..61fe3b214 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
@@ -15,6 +15,7 @@ public class JpegMetadata : IDeepCloneable
///
public JpegMetadata()
{
+ this.Comments = new List>();
}
///
@@ -25,6 +26,7 @@ public class JpegMetadata : IDeepCloneable
{
this.ColorType = other.ColorType;
+ this.Comments = other.Comments;
this.LuminanceQuality = other.LuminanceQuality;
this.ChrominanceQuality = other.ChrominanceQuality;
}
@@ -101,6 +103,11 @@ public class JpegMetadata : IDeepCloneable
///
public bool? Progressive { get; internal set; }
+ ///
+ /// Gets the comments.
+ ///
+ public ICollection>? Comments { get; }
+
///
public IDeepCloneable DeepClone() => new JpegMetadata(this);
}
diff --git a/src/ImageSharp/Formats/Jpeg/MetadataExtensions.cs b/src/ImageSharp/Formats/Jpeg/MetadataExtensions.cs
index 753dfdb60..53efc7d0c 100644
--- a/src/ImageSharp/Formats/Jpeg/MetadataExtensions.cs
+++ b/src/ImageSharp/Formats/Jpeg/MetadataExtensions.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Text;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Metadata;
@@ -17,4 +18,24 @@ public static partial class MetadataExtensions
/// The metadata this method extends.
/// The .
public static JpegMetadata GetJpegMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(JpegFormat.Instance);
+
+ ///
+ /// Saves the comment into
+ ///
+ /// The metadata this method extends.
+ /// The comment string.
+ public static void SaveComment(this JpegMetadata metadata, string comment)
+ {
+ ASCIIEncoding encoding = new();
+
+ byte[] bytes = encoding.GetBytes(comment);
+ metadata.Comments?.Add(encoding.GetChars(bytes));
+ }
+
+ ///
+ /// Gets the comments from
+ ///
+ /// The metadata this method extends.
+ /// The IEnumerable string of comments.
+ public static IEnumerable? GetComments(this JpegMetadata metadata) => metadata.Comments?.Select(x => x.ToString());
}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
index c8d93f6e9..b219e715f 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
@@ -364,4 +364,19 @@ public partial class JpegDecoderTests
image.DebugSave(provider);
image.CompareToOriginal(provider);
}
+
+ [Theory]
+ [WithFile(TestImages.Jpeg.Issues.Issue2067_CommentMarker, PixelTypes.Rgba32)]
+ public void JpegDecoder_DecodeMetadataComment(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ string expectedComment = "TEST COMMENT";
+ using Image image = provider.GetImage(JpegDecoder.Instance);
+ JpegMetadata metadata = image.Metadata.GetJpegMetadata();
+
+ Assert.Equal(1, metadata.Comments?.Count);
+ Assert.Equal(expectedComment, metadata.GetComments()?.FirstOrDefault());
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider);
+ }
}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs
index 2b721b9b5..50f47a134 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.Metadata.cs
@@ -154,6 +154,50 @@ public partial class JpegEncoderTests
}
}
+ [Theory]
+ [WithFile(TestImages.Jpeg.Issues.Issue2067_CommentMarker, PixelTypes.Rgba32)]
+ public void Encode_PreservesComments(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ // arrange
+ using var input = provider.GetImage(JpegDecoder.Instance);
+ using var memStream = new MemoryStream();
+
+ // act
+ input.Save(memStream, JpegEncoder);
+
+ // assert
+ memStream.Position = 0;
+ using var output = Image.Load(memStream);
+ JpegMetadata actual = output.Metadata.GetJpegMetadata();
+ Assert.NotEmpty(actual.Comments);
+ Assert.Equal(1, actual.Comments.Count);
+ Assert.Equal("TEST COMMENT", actual.Comments.ElementAt(0).ToString());
+ }
+
+ [Fact]
+ public void Encode_SavesMultipleComments()
+ {
+ // arrange
+ using var input = new Image(1, 1);
+ JpegMetadata meta = input.Metadata.GetJpegMetadata();
+ using var memStream = new MemoryStream();
+
+ // act
+ meta.SaveComment("First comment");
+ meta.SaveComment("Second Comment");
+ input.Save(memStream, JpegEncoder);
+
+ // assert
+ memStream.Position = 0;
+ using var output = Image.Load(memStream);
+ JpegMetadata actual = output.Metadata.GetJpegMetadata();
+ Assert.NotEmpty(actual.Comments);
+ Assert.Equal(2, actual.Comments.Count);
+ Assert.Equal(meta.Comments.ElementAt(0).ToString(), actual.Comments.ElementAt(0).ToString());
+ Assert.Equal(meta.Comments.ElementAt(1).ToString(), actual.Comments.ElementAt(1).ToString());
+ }
+
[Theory]
[WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgb24, JpegEncodingColor.Luminance)]
[WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio444)]
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs
index 05f22667d..901bb4619 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs
@@ -1,6 +1,7 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
+using System.Collections.ObjectModel;
using SixLabors.ImageSharp.Formats.Jpeg;
namespace SixLabors.ImageSharp.Tests.Formats.Jpg;
@@ -55,6 +56,27 @@ public class JpegMetadataTests
var meta = new JpegMetadata { LuminanceQuality = qualityLuma, ChrominanceQuality = qualityChroma };
- Assert.Equal(meta.Quality, qualityLuma);
+ Assert.Equal(meta.Quality, qualityLuma);
+ }
+
+ [Fact]
+ public void Comment_EmptyComment()
+ {
+ var meta = new JpegMetadata();
+
+ Assert.True(Array.Empty>().SequenceEqual(meta.Comments));
+ }
+
+ [Fact]
+ public void Comment_OnlyComment()
+ {
+ string comment = "test comment";
+ var expectedCollection = new Collection> { new(comment.ToCharArray()) };
+
+ var meta = new JpegMetadata();
+ meta.Comments?.Add(comment.ToCharArray());
+
+ Assert.Equal(1, meta.Comments?.Count);
+ Assert.True(expectedCollection.FirstOrDefault().ToString() == meta.Comments?.FirstOrDefault().ToString());
}
}
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 8aa95d349..0dab4ff87 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -309,6 +309,7 @@ public static class TestImages
public const string Issue2564 = "Jpg/issues/issue-2564.jpg";
public const string HangBadScan = "Jpg/issues/Hang_C438A851.jpg";
public const string Issue2517 = "Jpg/issues/issue2517-bad-d7.jpg";
+ public const string Issue2067_CommentMarker = "Jpg/issues/issue-2067-comment.jpg";
public static class Fuzz
{
diff --git a/tests/Images/Input/Jpg/issues/issue-2067-comment.jpg b/tests/Images/Input/Jpg/issues/issue-2067-comment.jpg
new file mode 100644
index 000000000..18dc6f2e3
--- /dev/null
+++ b/tests/Images/Input/Jpg/issues/issue-2067-comment.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d87b5429adeffcfac535aa8af2ec9801bf6c965a2e6751cfec4f8534195ba8f4
+size 21082