Skip to content

Commit 757f1b5

Browse files
committed
Add initial support for subsampeling in interleave mode none
Add as first step a 3 component unit test in interleave mode none that can be encoded and decoded.
1 parent 01de853 commit 757f1b5

File tree

8 files changed

+224
-33
lines changed

8 files changed

+224
-33
lines changed

src/ErrorCode.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ public enum ErrorCode
202202
/// </summary>
203203
InvalidParameterMappingTableContinuation = 38,
204204

205+
/// <summary>
206+
/// This error is returned when the stream contains an invalid sampling factor.
207+
/// </summary>
208+
InvalidParameterSamplingFactor = 39,
209+
205210
// Logic errors:
206211

207212
/// <summary>

src/JpegLSEncoder.cs

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ public sealed class JpegLSEncoder
2727
private JpegLSPresetCodingParameters? _userPresetCodingParameters = new();
2828
private State _state = State.Initial;
2929
private int _encodedComponentCount;
30+
private byte[]? _samplingFactors;
31+
private int _horizontalSamplingMax;
32+
private int _verticalSamplingMax;
3033

3134
/// <summary>
3235
/// Initializes a new instance of the <see cref="JpegLSEncoder"/> class.
@@ -247,6 +250,23 @@ public Memory<byte> Destination
247250

248251
private bool IsFrameInfoConfigured => FrameInfo.Height != 0;
249252

253+
/// <summary>
254+
/// Configures the sampling factor when encoding a component.
255+
/// </summary>
256+
/// <param name="componentIndex">The index of the component to set the mapping table ID for.</param>
257+
/// <param name="horizontalFactor">The horizontal subsampling factor.</param>
258+
/// <param name="verticalFactor">The vertical subsampling factor.</param>
259+
public void SetSamplingFactor(int componentIndex, int horizontalFactor, int verticalFactor)
260+
{
261+
ThrowHelper.ThrowIfOutsideRange(Constants.MinimumComponentIndex, Constants.MaximumComponentIndex, componentIndex);
262+
ThrowHelper.ThrowIfOutsideRange(0, 4, horizontalFactor);
263+
ThrowHelper.ThrowIfOutsideRange(0, 4, verticalFactor);
264+
265+
// Usage of sampling factors is rare: use lazy initialization.
266+
_samplingFactors ??= new byte[Constants.MaximumComponentCount];
267+
_samplingFactors[componentIndex] = (byte)((horizontalFactor << 4) | verticalFactor);
268+
}
269+
250270
/// <summary>
251271
/// Configures the mapping table ID the encoder should reference when encoding a component.
252272
/// The referenced mapping table can be included in the stream or provided in another JPEG-LS abbreviated format stream.
@@ -298,14 +318,14 @@ public void EncodeComponents(ReadOnlySpan<byte> source, int sourceComponentCount
298318
ThrowHelper.ThrowInvalidOperationIfFalse(IsFrameInfoConfigured);
299319
ThrowHelper.ThrowArgumentExceptionIfFalse(sourceComponentCount <= FrameInfo.ComponentCount - _encodedComponentCount, nameof(sourceComponentCount));
300320
CheckInterleaveModeAgainstComponentCount(sourceComponentCount);
301-
int scanStride = CheckStrideAndSourceLength(source.Length, stride, sourceComponentCount);
302321

303322
int maximumSampleValue = CalculateMaximumSampleValue(FrameInfo.BitsPerSample);
304323
if (!_userPresetCodingParameters!.TryMakeExplicit(maximumSampleValue, NearLossless, out var explicitCodingParameters))
305324
throw ThrowHelper.CreateArgumentException(ErrorCode.InvalidArgumentPresetCodingParameters);
306325

307326
if (_encodedComponentCount == 0)
308327
{
328+
DetermineMaxSamplingFactors();
309329
TransitionToTablesAndMiscellaneousState();
310330
WriteColorTransformSegment();
311331
WriteStartOfFrameSegment();
@@ -315,24 +335,28 @@ public void EncodeComponents(ReadOnlySpan<byte> source, int sourceComponentCount
315335

316336
if (InterleaveMode == InterleaveMode.None)
317337
{
318-
int byteCountComponent = scanStride * FrameInfo.Height;
319338
for (int component = 0; ;)
320339
{
340+
int scanWidth = GetScanWidth(_encodedComponentCount + component);
341+
int scanHeight = GetScanHeight(_encodedComponentCount + component);
342+
int scanStride = CheckStrideAndSourceLengthInterleaveModeNone(source.Length, stride, scanWidth, scanHeight);
321343
_writer.WriteStartOfScanSegment(1, NearLossless, InterleaveMode);
322-
EncodeScan(source, scanStride, 1, explicitCodingParameters);
344+
EncodeScan(source, scanStride, scanWidth, scanHeight, 1, explicitCodingParameters);
323345

324346
++component;
325347
if (component == sourceComponentCount)
326348
break;
327349

328350
// Synchronize the source stream (EncodeScan works on a local copy)
351+
int byteCountComponent = scanStride * scanHeight;
329352
source = source[byteCountComponent..];
330353
}
331354
}
332355
else
333356
{
357+
int scanStride = CheckStrideAndSourceLength(source.Length, stride, sourceComponentCount);
334358
_writer.WriteStartOfScanSegment(sourceComponentCount, NearLossless, InterleaveMode);
335-
EncodeScan(source, scanStride, sourceComponentCount, explicitCodingParameters);
359+
EncodeScan(source, scanStride, FrameInfo.Width, FrameInfo.Height, sourceComponentCount, explicitCodingParameters);
336360
}
337361

338362
_encodedComponentCount += sourceComponentCount;
@@ -526,10 +550,10 @@ public static Memory<byte> Encode(
526550
return encoder.EncodedData;
527551
}
528552

529-
private void EncodeScan(ReadOnlySpan<byte> source, int stride, int componentCount, JpegLSPresetCodingParameters codingParameters)
553+
private void EncodeScan(ReadOnlySpan<byte> source, int stride, int scanWidth, int scanHeight, int componentCount, JpegLSPresetCodingParameters codingParameters)
530554
{
531555
_scanEncoder = new ScanEncoder(
532-
new FrameInfo(FrameInfo.Width, FrameInfo.Height, FrameInfo.BitsPerSample, componentCount),
556+
new FrameInfo(scanWidth, scanHeight, FrameInfo.BitsPerSample, componentCount),
533557
codingParameters,
534558
new CodingParameters
535559
{
@@ -592,7 +616,7 @@ private void WriteColorTransformSegment()
592616

593617
private void WriteStartOfFrameSegment()
594618
{
595-
if (_writer.WriteStartOfFrameSegment(FrameInfo))
619+
if (_writer.WriteStartOfFrameSegment(FrameInfo, _samplingFactors))
596620
{
597621
// Image dimensions are oversized and need to be written to a JPEG-LS preset parameters (LSE) segment.
598622
_writer.WriteJpegLSPresetParametersSegment(FrameInfo.Height, FrameInfo.Width);
@@ -652,6 +676,28 @@ private int CheckStrideAndSourceLength(int sourceLength, int stride, int sourceC
652676
return stride;
653677
}
654678

679+
private int CheckStrideAndSourceLengthInterleaveModeNone(int sourceLength, int stride, int scanWidth, int scanHeight)
680+
{
681+
int minimumStride = scanWidth * BitToByteCount(FrameInfo.BitsPerSample);
682+
if (stride == AutoCalculateStride)
683+
{
684+
stride = minimumStride;
685+
}
686+
else
687+
{
688+
if (stride < minimumStride)
689+
ThrowHelper.ThrowArgumentException(ErrorCode.InvalidArgumentStride);
690+
}
691+
692+
int notUsedBytesAtEnd = stride - minimumStride;
693+
int minimumSourceLength = (stride * scanHeight) - notUsedBytesAtEnd;
694+
695+
if (sourceLength < minimumSourceLength)
696+
ThrowHelper.ThrowArgumentException(ErrorCode.InvalidArgumentSize);
697+
698+
return stride;
699+
}
700+
655701
private int CalculateMinimumStride(int sourceComponentCount)
656702
{
657703
int stride = FrameInfo.Width * BitToByteCount(FrameInfo.BitsPerSample);
@@ -661,6 +707,54 @@ private int CalculateMinimumStride(int sourceComponentCount)
661707
return stride * sourceComponentCount;
662708
}
663709

710+
private void DetermineMaxSamplingFactors()
711+
{
712+
if (_samplingFactors == null)
713+
return;
714+
715+
_horizontalSamplingMax = 1;
716+
_verticalSamplingMax = 1;
717+
for (int i = 0; i < FrameInfo.ComponentCount; ++i)
718+
{
719+
_horizontalSamplingMax = Math.Max(_horizontalSamplingMax, GetHorizontalSamplingFactor(i));
720+
_verticalSamplingMax = Math.Max(_verticalSamplingMax, GetVerticalSamplingFactor(i));
721+
}
722+
}
723+
724+
private int GetScanWidth(int componentIndex)
725+
{
726+
if (_samplingFactors == null)
727+
return FrameInfo.Width;
728+
729+
return FrameInfo.Width * GetHorizontalSamplingFactor(componentIndex) / _horizontalSamplingMax;
730+
}
731+
732+
private int GetScanHeight(int componentIndex)
733+
{
734+
if (_samplingFactors == null)
735+
return FrameInfo.Height;
736+
737+
return FrameInfo.Height * GetVerticalSamplingFactor(componentIndex) / _verticalSamplingMax;
738+
}
739+
740+
private int GetHorizontalSamplingFactor(int componentIndex)
741+
{
742+
byte samplingFactor = _samplingFactors![componentIndex];
743+
if (samplingFactor == 0)
744+
return 1;
745+
746+
return samplingFactor >> 4;
747+
}
748+
749+
private int GetVerticalSamplingFactor(int componentIndex)
750+
{
751+
byte samplingFactor = _samplingFactors![componentIndex];
752+
if (samplingFactor == 0)
753+
return 1;
754+
755+
return samplingFactor & 0xF;
756+
}
757+
664758
private static ReadOnlySpan<byte> ToUtf8(string text)
665759
{
666760
if (string.IsNullOrEmpty(text))

src/JpegStreamReader.cs

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ internal struct JpegStreamReader
1616
private readonly object _eventSender;
1717
private readonly List<ComponentInfo> _componentInfos;
1818
private readonly List<MappingTableEntry> _mappingTables = [];
19+
1920
private State _state;
20-
private bool _dnlMarkerExpected;
2121
private int _nearLossless;
2222
private int _segmentDataSize;
2323
private int _segmentStartPosition;
@@ -26,6 +26,9 @@ internal struct JpegStreamReader
2626
private int _bitsPerSample;
2727
private int _componentCount;
2828
private int _readComponentCount;
29+
private int _horizontalSamplingMax = 1;
30+
private int _verticalSamplingMax = 1;
31+
private bool _dnlMarkerExpected;
2932
private bool _componentWithMappingTableExists;
3033

3134
public JpegStreamReader()
@@ -72,7 +75,8 @@ private enum State
7275

7376
internal int ScanComponentCount { get; private set; }
7477

75-
internal readonly FrameInfo ScanFrameInfo => new(_width, _height, _bitsPerSample, ScanComponentCount);
78+
internal readonly FrameInfo ScanFrameInfo =>
79+
new(GetScanWidth(_readComponentCount - ScanComponentCount), GetScanHeight(_readComponentCount - ScanComponentCount), _bitsPerSample, ScanComponentCount);
7680

7781
internal CompressedDataFormat CompressedDataFormat { get; private set; }
7882

@@ -483,10 +487,9 @@ private void ReadStartOfFrameSegment()
483487
for (int i = 0; i != _componentCount; i++)
484488
{
485489
// Component specification parameters
486-
AddComponent(ReadByte()); // Ci = Component identifier
490+
byte componentIdentifier = ReadByte(); // Ci = Component identifier
487491
byte horizontalVerticalSamplingFactor = ReadByte(); // Hi + Vi = Horizontal sampling factor + Vertical sampling factor
488-
if (horizontalVerticalSamplingFactor != 0x11)
489-
ThrowHelper.ThrowInvalidDataException(ErrorCode.ParameterValueNotSupported);
492+
AddComponent(componentIdentifier, horizontalVerticalSamplingFactor);
490493

491494
SkipByte(); // Tqi = Quantization table destination selector (reserved for JPEG-LS, should be set to 0)
492495
}
@@ -820,12 +823,27 @@ private readonly void CheckSegmentSize(int expectedSize)
820823
ThrowHelper.ThrowInvalidDataException(ErrorCode.InvalidMarkerSegmentSize);
821824
}
822825

823-
private readonly void AddComponent(byte componentId)
826+
private void AddComponent(byte componentId, byte horizontalVerticalSamplingFactor)
824827
{
825828
if (_componentInfos.Exists(scan => scan.Id == componentId))
826829
ThrowHelper.ThrowInvalidDataException(ErrorCode.DuplicateComponentIdInStartOfFrameSegment);
827830

828-
_componentInfos.Add(new ComponentInfo(componentId));
831+
byte horizontalSamplingFactor = (byte)(horizontalVerticalSamplingFactor >> 4);
832+
byte verticalSamplingFactor = (byte)(horizontalVerticalSamplingFactor & 0xF);
833+
if (horizontalSamplingFactor < 1 || horizontalSamplingFactor > 4 || verticalSamplingFactor < 1 || verticalSamplingFactor > 4)
834+
ThrowHelper.ThrowInvalidDataException(ErrorCode.InvalidParameterSamplingFactor);
835+
836+
_componentInfos.Add(new ComponentInfo(componentId, horizontalSamplingFactor, verticalSamplingFactor));
837+
838+
if (horizontalSamplingFactor > _horizontalSamplingMax)
839+
{
840+
_horizontalSamplingMax = horizontalSamplingFactor;
841+
}
842+
843+
if (verticalSamplingFactor > _verticalSamplingMax)
844+
{
845+
_verticalSamplingMax = verticalSamplingFactor;
846+
}
829847
}
830848

831849
private readonly void CheckWidth()
@@ -862,6 +880,24 @@ private void SetHeight(int value, bool finalUpdate)
862880
_height = value;
863881
}
864882

883+
private readonly int GetScanWidth(int componentIndex)
884+
{
885+
int horizontalSamplingFactor = _componentInfos[componentIndex].HorizontalSamplingFactor;
886+
if (_horizontalSamplingMax == horizontalSamplingFactor)
887+
return _width;
888+
889+
return _width * horizontalSamplingFactor / _horizontalSamplingMax;
890+
}
891+
892+
private readonly int GetScanHeight(int componentIndex)
893+
{
894+
int verticalSamplingFactor = _componentInfos[componentIndex].VerticalSamplingFactor;
895+
if (_verticalSamplingMax == verticalSamplingFactor)
896+
return _height;
897+
898+
return _height * verticalSamplingFactor / _verticalSamplingMax;
899+
}
900+
865901
private void StoreComponentInfo(byte componentId, byte tableId, int nearLossless, InterleaveMode interleaveMode)
866902
{
867903
// Ignore when info is default, prevent search and ID mismatch issues.
@@ -877,7 +913,7 @@ private void StoreComponentInfo(byte componentId, byte tableId, int nearLossless
877913
_componentWithMappingTableExists = true;
878914
}
879915

880-
_componentInfos[index] = new ComponentInfo(componentId, tableId, nearLossless, interleaveMode);
916+
_componentInfos[index] = new ComponentInfo(_componentInfos[index], tableId, nearLossless, interleaveMode);
881917
}
882918

883919
private static void CheckInterleaveMode(InterleaveMode mode, int componentCountInScan)
@@ -966,14 +1002,25 @@ or sofDifferentialProgressive or sofDifferentialLossless or sofExtendedArithmeti
9661002

9671003
private readonly struct ComponentInfo
9681004
{
969-
internal readonly int NearLossless;
970-
internal readonly InterleaveMode InterleaveMode;
9711005
internal readonly byte Id;
1006+
internal readonly byte HorizontalSamplingFactor;
1007+
internal readonly byte VerticalSamplingFactor;
9721008
internal readonly byte MappingTableId;
1009+
internal readonly int NearLossless;
1010+
internal readonly InterleaveMode InterleaveMode;
9731011

974-
internal ComponentInfo(byte id, byte mappingTableId = 0, int nearLossless = 0, InterleaveMode interleaveMode = InterleaveMode.None)
1012+
internal ComponentInfo(byte id, byte horizontalSamplingFactor, byte verticalSamplingFactor)
9751013
{
9761014
Id = id;
1015+
HorizontalSamplingFactor = horizontalSamplingFactor;
1016+
VerticalSamplingFactor = verticalSamplingFactor;
1017+
}
1018+
1019+
internal ComponentInfo(ComponentInfo componentInfo, byte mappingTableId, int nearLossless, InterleaveMode interleaveMode)
1020+
{
1021+
Id = componentInfo.Id;
1022+
HorizontalSamplingFactor = componentInfo.HorizontalSamplingFactor;
1023+
VerticalSamplingFactor = componentInfo.VerticalSamplingFactor;
9771024
MappingTableId = mappingTableId;
9781025
NearLossless = nearLossless;
9791026
InterleaveMode = interleaveMode;

src/JpegStreamWriter.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ internal void WriteJpegLSPresetParametersSegment(int height, int width)
129129
WriteUint32(width); // Xe: number of columns in the image.
130130
}
131131

132-
internal bool WriteStartOfFrameSegment(FrameInfo frameInfo)
132+
internal bool WriteStartOfFrameSegment(FrameInfo frameInfo, byte[]? samplingFactors)
133133
{
134134
// Create a Frame Header as defined in ISO/IEC 14495-1, C.2.2 and T.81, B.2.2
135135
int dataSize = 6 + (frameInfo.ComponentCount * 3);
@@ -145,12 +145,12 @@ internal bool WriteStartOfFrameSegment(FrameInfo frameInfo)
145145

146146
// Use by default 1 as the start component identifier to remain compatible with the
147147
// code sample of ISO/IEC 14495-1, H.4 and the JPEG-LS ISO conformance sample files.
148-
for (int componentId = 1; componentId <= frameInfo.ComponentCount; ++componentId)
148+
for (int i = 0; i < frameInfo.ComponentCount; i++)
149149
{
150150
// Component Specification parameters
151-
WriteByte((byte)componentId); // Ci = Component identifier
152-
WriteByte(0x11); // Hi + Vi = Horizontal sampling factor + Vertical sampling factor
153-
WriteByte(0); // Tqi = Quantization table destination selector (reserved for JPEG-LS, should be set to 0)
151+
WriteByte((byte)(i + 1)); // Ci = Component identifier
152+
WriteByte(GetSamplingFactor(samplingFactors, i)); // Hi + Vi = Horizontal sampling factor + Vertical sampling factor
153+
WriteByte(0); // Tqi = Quantization table destination selector (reserved for JPEG-LS, should be set to 0)
154154
}
155155

156156
return oversizedImage;
@@ -295,4 +295,15 @@ private void WriteUint32(int value)
295295
BinaryPrimitives.WriteUInt32BigEndian(Destination.Span[_position..], (uint)value);
296296
_position += sizeof(uint);
297297
}
298+
299+
private static byte GetSamplingFactor(byte[]? samplingFactors, int componentIndex)
300+
{
301+
const byte defaultNoSamplingFactor = 0x11;
302+
303+
if (samplingFactors == null)
304+
return defaultNoSamplingFactor;
305+
306+
byte samplingFactor = samplingFactors[componentIndex];
307+
return samplingFactor == 0 ? defaultNoSamplingFactor : samplingFactor;
308+
}
298309
}

src/ThrowHelper.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ private static string GetErrorMessage(ErrorCode errorCode)
136136
ErrorCode.InvalidParameterColorTransformation => "Invalid JPEG-LS stream: Color transformation segment contains invalid values or frame info mismatch",
137137
ErrorCode.InvalidParameterMappingTableId => "Invalid JPEG-LS stream: mapping table ID outside valid range or duplicate",
138138
ErrorCode.InvalidParameterMappingTableContinuation => "Invalid JPEG-LS stream: mapping table continuation without matching mapping table specification",
139+
ErrorCode.InvalidParameterSamplingFactor => "Invalid JPEG-LS stream: sampling factor outside range [1, 4] or overall sum(h*v) > 10",
139140
ErrorCode.InvalidOperation => "Method call is invalid for the current state",
140141
ErrorCode.InvalidArgument => "Invalid argument",
141142
ErrorCode.InvalidArgumentHeight => "The height argument is outside the supported range [1, 2147483647]",

0 commit comments

Comments
 (0)