Skip to content

Commit bb9ed65

Browse files
committed
add jpeg com marker support
1 parent efd0c8e commit bb9ed65

File tree

10 files changed

+179
-2
lines changed

10 files changed

+179
-2
lines changed

ImageSharp.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issues", "issues", "{5C9B68
237237
tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg = tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg
238238
tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg = tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg
239239
tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg = tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg
240+
tests\Images\Input\Jpg\issues\issue-2067-comment.jpg = tests\Images\Input\Jpg\issues\issue-2067-comment.jpg
240241
EndProjectSection
241242
EndProject
242243
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fuzz", "fuzz", "{516A3532-6AC2-417B-AD79-9BD5D0D378A0}"

src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Buffers.Binary;
77
using System.Runtime.CompilerServices;
88
using System.Runtime.InteropServices;
9+
using System.Text;
910
using SixLabors.ImageSharp.Common.Helpers;
1011
using SixLabors.ImageSharp.Formats.Jpeg.Components;
1112
using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder;
@@ -481,7 +482,7 @@ internal void ParseStream(BufferedReadStream stream, SpectralConverter spectralC
481482

482483
case JpegConstants.Markers.APP15:
483484
case JpegConstants.Markers.COM:
484-
stream.Skip(markerContentByteSize);
485+
this.ProcessComMarker(stream, markerContentByteSize);
485486
break;
486487

487488
case JpegConstants.Markers.DAC:
@@ -515,6 +516,23 @@ public void Dispose()
515516
this.scanDecoder = null;
516517
}
517518

519+
/// <summary>
520+
/// Assigns COM marker bytes to comment property
521+
/// </summary>
522+
/// <param name="stream">The input stream.</param>
523+
/// <param name="markerContentByteSize">The remaining bytes in the segment block.</param>
524+
private void ProcessComMarker(BufferedReadStream stream, int markerContentByteSize)
525+
{
526+
Span<byte> temp = stackalloc byte[markerContentByteSize];
527+
char[] chars = new char[markerContentByteSize];
528+
JpegMetadata metadata = this.Metadata.GetFormatMetadata(JpegFormat.Instance);
529+
530+
stream.Read(temp);
531+
Encoding.ASCII.GetChars(temp, chars);
532+
533+
metadata.Comments.Add(chars);
534+
}
535+
518536
/// <summary>
519537
/// Returns encoded colorspace based on the adobe APP14 marker.
520538
/// </summary>

src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#nullable disable
44

55
using System.Buffers.Binary;
6+
using System.Text;
67
using SixLabors.ImageSharp.Common.Helpers;
78
using SixLabors.ImageSharp.Formats.Jpeg.Components;
89
using SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder;
@@ -89,6 +90,9 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
8990
// Write Exif, XMP, ICC and IPTC profiles
9091
this.WriteProfiles(metadata, buffer);
9192

93+
// Write comments
94+
this.WriteComment(jpegMetadata);
95+
9296
// Write the image dimensions.
9397
this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer);
9498

@@ -167,6 +171,47 @@ private void WriteJfifApplicationHeader(ImageMetadata meta, Span<byte> buffer)
167171
this.outputStream.Write(buffer, 0, 18);
168172
}
169173

174+
/// <summary>
175+
/// Writes comment
176+
/// </summary>
177+
/// <param name="metadata">The image metadata.</param>
178+
private void WriteComment(JpegMetadata metadata)
179+
{
180+
if (metadata.Comments is { Count: 0 })
181+
{
182+
return;
183+
}
184+
185+
// Length (comment strings lengths) + (comments markers with payload sizes)
186+
int commentsBytes = metadata.Comments.Sum(x => x.Length) + (metadata.Comments.Count * 4);
187+
int commentStart = 0;
188+
Span<byte> commentBuffer = stackalloc byte[commentsBytes];
189+
190+
foreach (Memory<char> comment in metadata.Comments)
191+
{
192+
int totalComLength = comment.Length + 4;
193+
194+
Span<byte> commentData = commentBuffer.Slice(commentStart, totalComLength);
195+
Span<byte> markers = commentData.Slice(0, 2);
196+
Span<byte> payloadSize = commentData.Slice(2, 2);
197+
Span<byte> payload = commentData.Slice(4, comment.Length);
198+
199+
// Beginning of comment ff fe
200+
markers[0] = JpegConstants.Markers.XFF;
201+
markers[1] = JpegConstants.Markers.COM;
202+
203+
// Write payload size
204+
BinaryPrimitives.WriteInt16BigEndian(payloadSize, (short)(commentData.Length - 2));
205+
206+
Encoding.ASCII.GetBytes(comment.Span, payload);
207+
208+
// Indicate begin of next comment in buffer
209+
commentStart += totalComLength;
210+
}
211+
212+
this.outputStream.Write(commentBuffer, 0, commentBuffer.Length);
213+
}
214+
170215
/// <summary>
171216
/// Writes the Define Huffman Table marker and tables.
172217
/// </summary>

src/ImageSharp/Formats/Jpeg/JpegMetadata.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public class JpegMetadata : IDeepCloneable
1515
/// </summary>
1616
public JpegMetadata()
1717
{
18+
this.Comments = new List<Memory<char>>();
1819
}
1920

2021
/// <summary>
@@ -25,6 +26,7 @@ private JpegMetadata(JpegMetadata other)
2526
{
2627
this.ColorType = other.ColorType;
2728

29+
this.Comments = other.Comments;
2830
this.LuminanceQuality = other.LuminanceQuality;
2931
this.ChrominanceQuality = other.ChrominanceQuality;
3032
}
@@ -101,6 +103,11 @@ public int Quality
101103
/// </remarks>
102104
public bool? Progressive { get; internal set; }
103105

106+
/// <summary>
107+
/// Gets the comments.
108+
/// </summary>
109+
public ICollection<Memory<char>>? Comments { get; }
110+
104111
/// <inheritdoc/>
105112
public IDeepCloneable DeepClone() => new JpegMetadata(this);
106113
}

src/ImageSharp/Formats/Jpeg/MetadataExtensions.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4+
using System.Text;
45
using SixLabors.ImageSharp.Formats.Jpeg;
56
using SixLabors.ImageSharp.Metadata;
67

@@ -17,4 +18,24 @@ public static partial class MetadataExtensions
1718
/// <param name="metadata">The metadata this method extends.</param>
1819
/// <returns>The <see cref="JpegMetadata"/>.</returns>
1920
public static JpegMetadata GetJpegMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(JpegFormat.Instance);
21+
22+
/// <summary>
23+
/// Saves the comment into <see cref="JpegMetadata"/>
24+
/// </summary>
25+
/// <param name="metadata">The metadata this method extends.</param>
26+
/// <param name="comment">The comment string.</param>
27+
public static void SaveComment(this JpegMetadata metadata, string comment)
28+
{
29+
ASCIIEncoding encoding = new();
30+
31+
byte[] bytes = encoding.GetBytes(comment);
32+
metadata.Comments?.Add(encoding.GetChars(bytes));
33+
}
34+
35+
/// <summary>
36+
/// Gets the comments from <see cref="JpegMetadata"/>
37+
/// </summary>
38+
/// <param name="metadata">The metadata this method extends.</param>
39+
/// <returns>The IEnumerable string of comments.</returns>
40+
public static IEnumerable<string>? GetComments(this JpegMetadata metadata) => metadata.Comments?.Select(x => x.ToString());
2041
}

tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,4 +364,19 @@ public void Issue2517_DecodeWorks<TPixel>(TestImageProvider<TPixel> provider)
364364
image.DebugSave(provider);
365365
image.CompareToOriginal(provider);
366366
}
367+
368+
[Theory]
369+
[WithFile(TestImages.Jpeg.Issues.Issue2067_CommentMarker, PixelTypes.Rgba32)]
370+
public void JpegDecoder_DecodeMetadataComment<TPixel>(TestImageProvider<TPixel> provider)
371+
where TPixel : unmanaged, IPixel<TPixel>
372+
{
373+
string expectedComment = "TEST COMMENT";
374+
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
375+
JpegMetadata metadata = image.Metadata.GetJpegMetadata();
376+
377+
Assert.Equal(1, metadata.Comments?.Count);
378+
Assert.Equal(expectedComment, metadata.GetComments()?.FirstOrDefault());
379+
image.DebugSave(provider);
380+
image.CompareToOriginal(provider);
381+
}
367382
}

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,50 @@ public void Encode_PreservesQuality(string imagePath, int quality)
154154
}
155155
}
156156

157+
[Theory]
158+
[WithFile(TestImages.Jpeg.Issues.Issue2067_CommentMarker, PixelTypes.Rgba32)]
159+
public void Encode_PreservesComments<TPixel>(TestImageProvider<TPixel> provider)
160+
where TPixel : unmanaged, IPixel<TPixel>
161+
{
162+
// arrange
163+
using var input = provider.GetImage(JpegDecoder.Instance);
164+
using var memStream = new MemoryStream();
165+
166+
// act
167+
input.Save(memStream, JpegEncoder);
168+
169+
// assert
170+
memStream.Position = 0;
171+
using var output = Image.Load<Rgba32>(memStream);
172+
JpegMetadata actual = output.Metadata.GetJpegMetadata();
173+
Assert.NotEmpty(actual.Comments);
174+
Assert.Equal(1, actual.Comments.Count);
175+
Assert.Equal("TEST COMMENT", actual.Comments.ElementAt(0).ToString());
176+
}
177+
178+
[Fact]
179+
public void Encode_SavesMultipleComments()
180+
{
181+
// arrange
182+
using var input = new Image<Rgba32>(1, 1);
183+
JpegMetadata meta = input.Metadata.GetJpegMetadata();
184+
using var memStream = new MemoryStream();
185+
186+
// act
187+
meta.SaveComment("First comment");
188+
meta.SaveComment("Second Comment");
189+
input.Save(memStream, JpegEncoder);
190+
191+
// assert
192+
memStream.Position = 0;
193+
using var output = Image.Load<Rgba32>(memStream);
194+
JpegMetadata actual = output.Metadata.GetJpegMetadata();
195+
Assert.NotEmpty(actual.Comments);
196+
Assert.Equal(2, actual.Comments.Count);
197+
Assert.Equal(meta.Comments.ElementAt(0).ToString(), actual.Comments.ElementAt(0).ToString());
198+
Assert.Equal(meta.Comments.ElementAt(1).ToString(), actual.Comments.ElementAt(1).ToString());
199+
}
200+
157201
[Theory]
158202
[WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgb24, JpegEncodingColor.Luminance)]
159203
[WithFile(TestImages.Jpeg.Baseline.Jpeg444, PixelTypes.Rgb24, JpegEncodingColor.YCbCrRatio444)]

tests/ImageSharp.Tests/Formats/Jpg/JpegMetadataTests.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4+
using System.Collections.ObjectModel;
45
using SixLabors.ImageSharp.Formats.Jpeg;
56

67
namespace SixLabors.ImageSharp.Tests.Formats.Jpg;
@@ -55,6 +56,27 @@ public void Quality_ReturnsMaxQuality()
5556

5657
var meta = new JpegMetadata { LuminanceQuality = qualityLuma, ChrominanceQuality = qualityChroma };
5758

58-
Assert.Equal(meta.Quality, qualityLuma);
59+
Assert.Equal(meta.Quality, qualityLuma);
60+
}
61+
62+
[Fact]
63+
public void Comment_EmptyComment()
64+
{
65+
var meta = new JpegMetadata();
66+
67+
Assert.True(Array.Empty<Memory<char>>().SequenceEqual(meta.Comments));
68+
}
69+
70+
[Fact]
71+
public void Comment_OnlyComment()
72+
{
73+
string comment = "test comment";
74+
var expectedCollection = new Collection<Memory<char>> { new(comment.ToCharArray()) };
75+
76+
var meta = new JpegMetadata();
77+
meta.Comments?.Add(comment.ToCharArray());
78+
79+
Assert.Equal(1, meta.Comments?.Count);
80+
Assert.True(expectedCollection.FirstOrDefault().ToString() == meta.Comments?.FirstOrDefault().ToString());
5981
}
6082
}

tests/ImageSharp.Tests/TestImages.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ public static class Issues
309309
public const string Issue2564 = "Jpg/issues/issue-2564.jpg";
310310
public const string HangBadScan = "Jpg/issues/Hang_C438A851.jpg";
311311
public const string Issue2517 = "Jpg/issues/issue2517-bad-d7.jpg";
312+
public const string Issue2067_CommentMarker = "Jpg/issues/issue-2067-comment.jpg";
312313

313314
public static class Fuzz
314315
{
Lines changed: 3 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)