Skip to content

Commit

Permalink
Merge pull request #2641 from RobertMut/main
Browse files Browse the repository at this point in the history
Add JPEG COM marker support
  • Loading branch information
JimBobSquarePants authored Mar 13, 2024
2 parents 7e7c795 + d8484da commit c8e7775
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 51 deletions.
1 change: 1 addition & 0 deletions ImageSharp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,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}"
Expand Down
32 changes: 32 additions & 0 deletions src/ImageSharp/Formats/Jpeg/JpegComData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

namespace SixLabors.ImageSharp.Formats.Jpeg;

/// <summary>
/// Represents a JPEG comment
/// </summary>
public readonly struct JpegComData
{
/// <summary>
/// Initializes a new instance of the <see cref="JpegComData"/> struct.
/// </summary>
/// <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 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());

/// <inheritdoc/>
public override string ToString() => this.Value.ToString();
}
23 changes: 22 additions & 1 deletion src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -480,9 +480,11 @@ internal void ParseStream(BufferedReadStream stream, SpectralConverter spectralC
break;

case JpegConstants.Markers.APP15:
case JpegConstants.Markers.COM:
stream.Skip(markerContentByteSize);
break;
case JpegConstants.Markers.COM:
this.ProcessComMarker(stream, markerContentByteSize);
break;

case JpegConstants.Markers.DAC:
if (metadataOnly)
Expand Down Expand Up @@ -515,6 +517,25 @@ public void Dispose()
this.scanDecoder = null;
}

/// <summary>
/// Assigns COM marker bytes to comment property
/// </summary>
/// <param name="stream">The input stream.</param>
/// <param name="markerContentByteSize">The remaining bytes in the segment block.</param>
private void ProcessComMarker(BufferedReadStream stream, int markerContentByteSize)
{
char[] chars = new char[markerContentByteSize];
JpegMetadata metadata = this.Metadata.GetFormatMetadata(JpegFormat.Instance);

for (int i = 0; i < markerContentByteSize; i++)
{
int read = stream.ReadByte();
chars[i] = (char)read;
}

metadata.Comments.Add(new JpegComData(chars));
}

/// <summary>
/// Returns encoded colorspace based on the adobe APP14 marker.
/// </summary>
Expand Down
52 changes: 52 additions & 0 deletions src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
#nullable disable

using System.Buffers;
using System.Buffers.Binary;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Jpeg.Components;
Expand All @@ -25,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>
Expand Down Expand Up @@ -89,6 +93,9 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
// Write Exif, XMP, ICC and IPTC profiles
this.WriteProfiles(metadata, buffer);

// Write comments
this.WriteComments(image.Configuration, jpegMetadata);

// Write the image dimensions.
this.WriteStartOfFrame(image.Width, image.Height, frameConfig, buffer);

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

/// <summary>
/// Writes the COM tags.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="metadata">The image metadata.</param>
private void WriteComments(Configuration configuration, JpegMetadata metadata)
{
if (metadata.Comments.Count == 0)
{
return;
}

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)
{
int totalLength = comment.Value.Length;
if (totalLength == 0)
{
continue;
}

// 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);

// Write the marker header.
this.WriteMarkerHeader(JpegConstants.Markers.COM, currentLength + 2, buffer);

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];
}

// Write the comment.
this.outputStream.Write(buffer, 0, currentLength);
totalLength -= currentLength;
}
}
}

/// <summary>
/// Writes the Define Huffman Table marker and tables.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions src/ImageSharp/Formats/Jpeg/JpegMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class JpegMetadata : IDeepCloneable
/// </summary>
public JpegMetadata()
{
this.Comments = new List<JpegComData>();
}

/// <summary>
Expand All @@ -25,6 +26,7 @@ private JpegMetadata(JpegMetadata other)
{
this.ColorType = other.ColorType;

this.Comments = other.Comments;
this.LuminanceQuality = other.LuminanceQuality;
this.ChrominanceQuality = other.ChrominanceQuality;
}
Expand Down Expand Up @@ -101,6 +103,11 @@ public int Quality
/// </remarks>
public bool? Progressive { get; internal set; }

/// <summary>
/// Gets the comments.
/// </summary>
public IList<JpegComData> Comments { get; }

/// <inheritdoc/>
public IDeepCloneable DeepClone() => new JpegMetadata(this);
}
1 change: 1 addition & 0 deletions src/ImageSharp/Formats/Jpeg/MetadataExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
15 changes: 15 additions & 0 deletions tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,21 @@ public void EncodedStringTags_Read()
VerifyEncodedStrings(exif);
}

[Theory]
[WithFile(TestImages.Jpeg.Issues.Issue2067_CommentMarker, PixelTypes.Rgba32)]
public void JpegDecoder_DecodeMetadataComment<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
string expectedComment = "TEST COMMENT";
using Image<TPixel> image = provider.GetImage(JpegDecoder.Instance);
JpegMetadata metadata = image.Metadata.GetJpegMetadata();

Assert.Equal(1, metadata.Comments.Count);
Assert.Equal(expectedComment, metadata.Comments.ElementAtOrDefault(0).ToString());
image.DebugSave(provider);
image.CompareToOriginal(provider);
}

private static void VerifyEncodedStrings(ExifProfile exif)
{
Assert.NotNull(exif);
Expand Down
Loading

0 comments on commit c8e7775

Please sign in to comment.