Skip to content

Commit c8e7775

Browse files
Merge pull request #2641 from RobertMut/main
Add JPEG COM marker support
2 parents 7e7c795 + d8484da commit c8e7775

File tree

11 files changed

+259
-51
lines changed

11 files changed

+259
-51
lines changed

ImageSharp.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issues", "issues", "{5C9B68
238238
tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg = tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg
239239
tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg = tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg
240240
tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg = tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg
241+
tests\Images\Input\Jpg\issues\issue-2067-comment.jpg = tests\Images\Input\Jpg\issues\issue-2067-comment.jpg
241242
EndProjectSection
242243
EndProject
243244
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fuzz", "fuzz", "{516A3532-6AC2-417B-AD79-9BD5D0D378A0}"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.ImageSharp.Formats.Jpeg;
5+
6+
/// <summary>
7+
/// Represents a JPEG comment
8+
/// </summary>
9+
public readonly struct JpegComData
10+
{
11+
/// <summary>
12+
/// Initializes a new instance of the <see cref="JpegComData"/> struct.
13+
/// </summary>
14+
/// <param name="value">The comment buffer.</param>
15+
public JpegComData(ReadOnlyMemory<char> value)
16+
=> this.Value = value;
17+
18+
/// <summary>
19+
/// Gets the value.
20+
/// </summary>
21+
public ReadOnlyMemory<char> Value { get; }
22+
23+
/// <summary>
24+
/// Converts string to <see cref="JpegComData"/>
25+
/// </summary>
26+
/// <param name="value">The comment string.</param>
27+
/// <returns>The <see cref="JpegComData"/></returns>
28+
public static JpegComData FromString(string value) => new(value.AsMemory());
29+
30+
/// <inheritdoc/>
31+
public override string ToString() => this.Value.ToString();
32+
}

src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,9 +480,11 @@ internal void ParseStream(BufferedReadStream stream, SpectralConverter spectralC
480480
break;
481481

482482
case JpegConstants.Markers.APP15:
483-
case JpegConstants.Markers.COM:
484483
stream.Skip(markerContentByteSize);
485484
break;
485+
case JpegConstants.Markers.COM:
486+
this.ProcessComMarker(stream, markerContentByteSize);
487+
break;
486488

487489
case JpegConstants.Markers.DAC:
488490
if (metadataOnly)
@@ -515,6 +517,25 @@ public void Dispose()
515517
this.scanDecoder = null;
516518
}
517519

520+
/// <summary>
521+
/// Assigns COM marker bytes to comment property
522+
/// </summary>
523+
/// <param name="stream">The input stream.</param>
524+
/// <param name="markerContentByteSize">The remaining bytes in the segment block.</param>
525+
private void ProcessComMarker(BufferedReadStream stream, int markerContentByteSize)
526+
{
527+
char[] chars = new char[markerContentByteSize];
528+
JpegMetadata metadata = this.Metadata.GetFormatMetadata(JpegFormat.Instance);
529+
530+
for (int i = 0; i < markerContentByteSize; i++)
531+
{
532+
int read = stream.ReadByte();
533+
chars[i] = (char)read;
534+
}
535+
536+
metadata.Comments.Add(new JpegComData(chars));
537+
}
538+
518539
/// <summary>
519540
/// Returns encoded colorspace based on the adobe APP14 marker.
520541
/// </summary>

src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Six Labors Split License.
33
#nullable disable
44

5+
using System.Buffers;
56
using System.Buffers.Binary;
67
using SixLabors.ImageSharp.Common.Helpers;
78
using SixLabors.ImageSharp.Formats.Jpeg.Components;
@@ -25,6 +26,9 @@ internal sealed unsafe partial class JpegEncoderCore : IImageEncoderInternals
2526
/// </summary>
2627
private static readonly JpegFrameConfig[] FrameConfigs = CreateFrameConfigs();
2728

29+
/// <summary>
30+
/// The current calling encoder.
31+
/// </summary>
2832
private readonly JpegEncoder encoder;
2933

3034
/// <summary>
@@ -89,6 +93,9 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
8993
// Write Exif, XMP, ICC and IPTC profiles
9094
this.WriteProfiles(metadata, buffer);
9195

96+
// Write comments
97+
this.WriteComments(image.Configuration, jpegMetadata);
98+
9299
// Write the image dimensions.
93100
this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer);
94101

@@ -167,6 +174,51 @@ private void WriteJfifApplicationHeader(ImageMetadata meta, Span<byte> buffer)
167174
this.outputStream.Write(buffer, 0, 18);
168175
}
169176

177+
/// <summary>
178+
/// Writes the COM tags.
179+
/// </summary>
180+
/// <param name="configuration">The configuration.</param>
181+
/// <param name="metadata">The image metadata.</param>
182+
private void WriteComments(Configuration configuration, JpegMetadata metadata)
183+
{
184+
if (metadata.Comments.Count == 0)
185+
{
186+
return;
187+
}
188+
189+
const int maxCommentLength = 65533;
190+
using IMemoryOwner<byte> bufferOwner = configuration.MemoryAllocator.Allocate<byte>(maxCommentLength);
191+
Span<byte> buffer = bufferOwner.Memory.Span;
192+
foreach (JpegComData comment in metadata.Comments)
193+
{
194+
int totalLength = comment.Value.Length;
195+
if (totalLength == 0)
196+
{
197+
continue;
198+
}
199+
200+
// Loop through and split the comment into multiple comments if the comment length
201+
// is greater than the maximum allowed length.
202+
while (totalLength > 0)
203+
{
204+
int currentLength = Math.Min(totalLength, maxCommentLength);
205+
206+
// Write the marker header.
207+
this.WriteMarkerHeader(JpegConstants.Markers.COM, currentLength + 2, buffer);
208+
209+
ReadOnlySpan<char> commentValue = comment.Value.Span.Slice(comment.Value.Length - totalLength, currentLength);
210+
for (int i = 0; i < commentValue.Length; i++)
211+
{
212+
buffer[i] = (byte)commentValue[i];
213+
}
214+
215+
// Write the comment.
216+
this.outputStream.Write(buffer, 0, currentLength);
217+
totalLength -= currentLength;
218+
}
219+
}
220+
}
221+
170222
/// <summary>
171223
/// Writes the Define Huffman Table marker and tables.
172224
/// </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<JpegComData>();
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 IList<JpegComData> Comments { get; }
110+
104111
/// <inheritdoc/>
105112
public IDeepCloneable DeepClone() => new JpegMetadata(this);
106113
}

src/ImageSharp/Formats/Jpeg/MetadataExtensions.cs

Lines changed: 1 addition & 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

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,21 @@ public void EncodedStringTags_Read()
425425
VerifyEncodedStrings(exif);
426426
}
427427

428+
[Theory]
429+
[WithFile(TestImages.Jpeg.Issues.Issue2067_CommentMarker, PixelTypes.Rgba32)]
430+
public void JpegDecoder_DecodeMetadataComment<TPixel>(TestImageProvider<TPixel> provider)
431+
where TPixel : unmanaged, IPixel<TPixel>
432+
{
433+
string expectedComment = "TEST COMMENT";
434+
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
435+
JpegMetadata metadata = image.Metadata.GetJpegMetadata();
436+
437+
Assert.Equal(1, metadata.Comments.Count);
438+
Assert.Equal(expectedComment, metadata.Comments.ElementAtOrDefault(0).ToString());
439+
image.DebugSave(provider);
440+
image.CompareToOriginal(provider);
441+
}
442+
428443
private static void VerifyEncodedStrings(ExifProfile exif)
429444
{
430445
Assert.NotNull(exif);

0 commit comments

Comments
 (0)