Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic support for SPIFF header #23

Merged
merged 3 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,8 @@ internal static class Constants
// The size of a SPIFF header when serialized to a JPEG byte stream.
internal const int SpiffHeaderSizeInBytes = 34;

// The maximum size of the data bytes that fit in a segment.
internal const int SegmentMaxDataSize = ushort.MaxValue - SegmentLengthSize;

internal const int Int32BitCount = 32;
}
54 changes: 19 additions & 35 deletions src/JpegLSDecoder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ namespace CharLS.JpegLS;
public sealed class JpegLSDecoder
{
private FrameInfo? _frameInfo;
//private JpegLSInterleaveMode? _interleaveMode;
private readonly JpegStreamReader _reader = new();

private enum State
Expand Down Expand Up @@ -87,7 +86,7 @@ public ReadOnlyMemory<byte> Source
/// <value>
/// The frame information of the parsed JPEG-LS image.
/// </value>
/// <exception cref="InvalidOperationException">Thrown when this property is used before <see cref="ReadHeader(bool)"/>.</exception>
/// <exception cref="InvalidOperationException">Thrown when this property is used before <see cref="ReadHeader()"/>.</exception>
public FrameInfo FrameInfo => _reader.FrameInfo ?? throw new InvalidOperationException("Incorrect state. ReadHeader has not called.");

/// <summary>
Expand All @@ -99,7 +98,7 @@ public ReadOnlyMemory<byte> Source
/// <value>
/// The near lossless parameter. A value of 0 means that the image is lossless encoded.
/// </value>
/// <exception cref="InvalidOperationException">Thrown when this property is used before <see cref="ReadHeader(bool)"/>.</exception>
/// <exception cref="InvalidOperationException">Thrown when this property is used before <see cref="ReadHeader()"/>.</exception>
public int NearLossless
{
get
Expand All @@ -116,7 +115,7 @@ public int NearLossless
/// Property should be obtained after calling <see cref="ReadHeader"/>".
/// </remarks>
/// <returns>The result of the operation: success or a failure code.</returns>
/// <exception cref="InvalidOperationException">Thrown when this property is used before <see cref="ReadHeader(bool)"/>.</exception>
/// <exception cref="InvalidOperationException">Thrown when this property is used before <see cref="ReadHeader()"/>.</exception>
public InterleaveMode InterleaveMode
{
get
Expand All @@ -132,8 +131,15 @@ public InterleaveMode InterleaveMode
/// <value>
/// The preset coding parameters.
/// </value>
/// <exception cref="InvalidOperationException">Thrown when this property is used before <see cref="ReadHeader(bool)"/>.</exception>
public JpegLSPresetCodingParameters PresetCodingParameters => _reader.JpegLSPresetCodingParameters ?? new JpegLSPresetCodingParameters();
/// <exception cref="InvalidOperationException">Thrown when this property is used before <see cref="ReadHeader()"/>.</exception>
public JpegLSPresetCodingParameters PresetCodingParameters
{
get
{
CheckHeaderRead();
return _reader.JpegLSPresetCodingParameters ?? new JpegLSPresetCodingParameters();
}
}

/// <summary>
/// Returns the HP color transformation that was used to encode the scan.
Expand All @@ -152,7 +158,7 @@ public ColorTransformation ColorTransformation
/// <param name="stride">The stride to use; byte count to the next pixel row. Pass 0 for the default.</param>
/// <returns>The size of the destination buffer in bytes.</returns>
/// <exception cref="OverflowException">When the required destination size doesn't fit in an int.</exception>
/// <exception cref="InvalidOperationException">Thrown when this method is called before <see cref="ReadHeader(bool)"/>.</exception>
/// <exception cref="InvalidOperationException">Thrown when this method is called before <see cref="ReadHeader()"/>.</exception>
public int GetDestinationSize(int stride = 0)
{
if (_state < State.HeaderRead)
Expand All @@ -179,44 +185,22 @@ public int GetDestinationSize(int stride = 0)
}
}

///// <summary>
///// Reads the SPIFF (Still Picture Interchange File Format) header.
///// </summary>
///// <param name="spiffHeader">The header or null when no valid header was found.</param>
///// <returns>true if a SPIFF header was present and could be read.</returns>
///// <exception cref="ObjectDisposedException">Thrown when the instance is used after being disposed.</exception>
//public bool TryReadSpiffHeader(out SpiffHeader? spiffHeader)
//{
// bool found = headerFound != 0;
// if (found)
// {
// found = SpiffHeader.TryCreate(headerNative, out spiffHeader);
// }
// else
// {
// spiffHeader = default;
// }

// return found;
//}

/// <summary>
/// Reads the header of the JPEG-LS stream.
/// After calling this method, the informational properties can be obtained.
/// </summary>
/// <param name="tryReadSpiffHeader">if set to <c>true</c> try to read the SPIFF header first.</param>
/// <exception cref="InvalidDataException">Thrown when the JPEG-LS stream is not valid.</exception>
public void ReadHeader(bool tryReadSpiffHeader = true)
public void ReadHeader()
{
CheckOperation(_state == State.SourceSet);

_reader.ReadHeader();
_state = State.HeaderRead;

//if (tryReadSpiffHeader && TryReadSpiffHeader(out SpiffHeader? spiffHeader))
//{
// SpiffHeader = spiffHeader;
//}
if (_reader.SpiffHeader != null && _reader.SpiffHeader.IsValid(FrameInfo))
{
SpiffHeader = _reader.SpiffHeader;
}
}

/// <summary>
Expand All @@ -226,7 +210,7 @@ public void ReadHeader(bool tryReadSpiffHeader = true)
/// <returns>A byte array with the decoded JPEG-LS data.</returns>
/// <exception cref="InvalidDataException">Thrown when the JPEG-LS stream is not valid.</exception>
/// <exception cref="ObjectDisposedException">Thrown when the instance is used after being disposed.</exception>
/// <exception cref="InvalidOperationException">Thrown when this method is called before <see cref="ReadHeader(bool)"/>.</exception>
/// <exception cref="InvalidOperationException">Thrown when this method is called before <see cref="ReadHeader()"/>.</exception>
public byte[] Decode(int stride = 0)
{
var destination = new byte[GetDestinationSize()];
Expand Down
73 changes: 64 additions & 9 deletions src/JpegStreamWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,19 @@ internal class JpegStreamWriter

internal Memory<byte> Destination { get; set; }

// ReSharper disable once ConvertToAutoPropertyWhenPossible
internal int BytesWritten => _position;

internal Memory<byte> GetRemainingDestination()
{
return Destination[_position..];
}

// ReSharper disable once ConvertToAutoPropertyWhenPossible
internal int BytesWritten => _position;
internal void Rewind()
{
_position = 0;
_componentIndex = 0;
}

internal void Seek(int byteCount)
{
Expand All @@ -32,6 +38,56 @@ internal void WriteStartOfImage()
WriteSegmentWithoutData(JpegMarkerCode.StartOfImage);
}

/// <summary>
/// Write a JPEG SPIFF (APP8 + spiff) segment.
/// This segment is documented in ISO/IEC 10918-3, Annex F.
/// </summary>
internal void WriteSpiffHeaderSegment(SpiffHeader header)
{
Debug.Assert(header.Height > 0);
Debug.Assert(header.Width > 0);

Span<byte> spiffMagicId = [(byte)'S', (byte)'P', (byte)'I', (byte)'F', (byte)'F', (byte)'\0'];

// Create a JPEG APP8 segment in Still Picture Interchange File Format (SPIFF), v2.0
WriteSegmentHeader(JpegMarkerCode.ApplicationData8, 30);
WriteBytes(spiffMagicId);
WriteByte(Constants.SpiffMajorRevisionNumber);
WriteByte(Constants.SpiffMinorRevisionNumber);
WriteByte((byte)header.ProfileId);
WriteByte((byte)header.ComponentCount);
WriteUint32(header.Height);
WriteUint32(header.Width);
WriteByte((byte)header.ColorSpace);
WriteByte((byte)header.BitsPerSample);
WriteByte((byte)header.CompressionType);
WriteByte((byte)header.ResolutionUnit);
WriteUint32(header.VerticalResolution);
WriteUint32(header.HorizontalResolution);
}

internal void WriteSpiffDirectoryEntry(int entryTag, Span<byte> entryData)
{
WriteSegmentHeader(JpegMarkerCode.ApplicationData8, sizeof(int) + entryData.Length);
WriteUint32(entryTag);
WriteBytes(entryData);
}

internal void WriteSpiffEndOfDirectoryEntry()
{
// Note: ISO/IEC 10918-3, Annex F.2.2.3 documents that the EOD entry segment should have a length of 8
// but only 6 data bytes. This approach allows to wrap existing bit streams\encoders with a SPIFF header.
// In this implementation the SOI marker is added as data bytes to simplify the stream writer design.
Span<byte> spiffEndOfDirectory =
[
0, 0,
0, Constants.SpiffEndOfDirectoryEntryType,
0xFF, (byte) JpegMarkerCode.StartOfImage
];

WriteSegment(JpegMarkerCode.ApplicationData8, spiffEndOfDirectory);
}

internal void WriteColorTransformSegment(ColorTransformation colorTransformation)
{
byte[] segment = [(byte)'m', (byte)'r', (byte)'f', (byte)'x', (byte)colorTransformation];
Expand Down Expand Up @@ -128,7 +184,7 @@ internal void WriteEndOfImage(bool evenDestinationSize)
private void WriteSegmentWithoutData(JpegMarkerCode markerCode)
{
if (_position + 2 > Destination.Length)
throw Util.CreateInvalidDataException(ErrorCode.DestinationBufferTooSmall);
ThrowHelper.ThrowArgumentOutOfRangeException(ErrorCode.DestinationBufferTooSmall);

WriteByte(Constants.JpegMarkerStartByte);
WriteByte((byte)markerCode);
Expand All @@ -142,15 +198,14 @@ private void WriteSegment(JpegMarkerCode markerCode, ReadOnlySpan<byte> data)

private void WriteSegmentHeader(JpegMarkerCode markerCode, int dataSize)
{
// ASSERT(data_size <= segment_max_data_size);
Debug.Assert(dataSize <= Constants.SegmentMaxDataSize);

// Check if there is enough room in the destination to write the complete segment.
// Other methods assume that the checking in done here and don't check again.
//const int markerCodeSize = 2;
//int totalSegmentSize = markerCodeSize + Constants.SegmentLengthSize + dataSize;
//if (const size_t total_segment_size{marker_code_size + segment_length_size + data_size};
//UNLIKELY(byte_offset_ + total_segment_size > destination_.size()))
//impl::throw_jpegls_error(jpegls_errc::destination_buffer_too_small);
const int markerCodeSize = 2;
int totalSegmentSize = markerCodeSize + Constants.SegmentLengthSize + dataSize;
if (_position + totalSegmentSize > Destination.Length)
ThrowHelper.ThrowArgumentOutOfRangeException(ErrorCode.DestinationBufferTooSmall);

WriteMarker(markerCode);
WriteUint16(Constants.SegmentLengthSize + dataSize);
Expand Down
71 changes: 71 additions & 0 deletions src/SpiffHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,75 @@ public SpiffHeader()
/// The horizontal resolution.
/// </value>
public int HorizontalResolution { get; init; }

internal bool IsValid(FrameInfo frameInfo)
{
if (CompressionType != SpiffCompressionType.JpegLS)
return false;

if (ProfileId != SpiffProfileId.None)
return false;

if (!IsValidResolutionUnits(ResolutionUnit))
return false;

if (HorizontalResolution == 0 || VerticalResolution == 0)
return false;

if (ComponentCount != frameInfo.ComponentCount)
return false;

if (!IsValidColorSpace(ColorSpace, ComponentCount))
return false;

if (BitsPerSample != frameInfo.BitsPerSample)
return false;

if (Height != frameInfo.Height)
return false;

return Width == frameInfo.Width;
}

private static bool IsValidResolutionUnits(SpiffResolutionUnit resolutionUnits)
{
return resolutionUnits switch
{
SpiffResolutionUnit.AspectRatio or
SpiffResolutionUnit.DotsPerInch or
SpiffResolutionUnit.DotsPerCentimeter => true,
_ => false
};
}

private static bool IsValidColorSpace(SpiffColorSpace colorSpace, int componentCount)
{
switch (colorSpace)
{
case SpiffColorSpace.None:
return true;

case SpiffColorSpace.BiLevelBlack:
case SpiffColorSpace.BiLevelWhite:
return false; // not supported for JPEG-LS.

case SpiffColorSpace.Grayscale:
return componentCount == 1;

case SpiffColorSpace.YcbcrItuBT709Video:
case SpiffColorSpace.YcbcrItuBT6011Rgb:
case SpiffColorSpace.YcbcrItuBT6011Video:
case SpiffColorSpace.Rgb:
case SpiffColorSpace.Cmy:
case SpiffColorSpace.PhotoYcc:
case SpiffColorSpace.CieLab:
return componentCount == 3;

case SpiffColorSpace.Cmyk:
case SpiffColorSpace.Ycck:
return componentCount == 4;
}

return false;
}
}
30 changes: 30 additions & 0 deletions src/ThrowHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Team CharLS.
// SPDX-License-Identifier: BSD-3-Clause

using System.Diagnostics.CodeAnalysis;

namespace CharLS.JpegLS;

internal class ThrowHelper
{
[DoesNotReturn]
internal static void ThrowArgumentOutOfRangeException(ErrorCode errorCode)
{
throw AddErrorCode(new ArgumentOutOfRangeException(GetErrorMessage(errorCode)), errorCode);
}

private static Exception AddErrorCode(Exception exception, ErrorCode errorCode)
{
exception.Data.Add(nameof(ErrorCode), errorCode);
return exception;
}

private static string GetErrorMessage(ErrorCode errorCode)
{
return errorCode switch
{
ErrorCode.None => "",
_ => "todo",
};
}
}
Loading