• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

team-charls / charls-dotnet / 20407544346

21 Dec 2025 09:04AM UTC coverage: 94.507% (-0.1%) from 94.645%
20407544346

Pull #61

github

web-flow
Merge 9de9b5c65 into 4c54859bf
Pull Request #61: Add support for subsampling

1411 of 1546 branches covered (91.27%)

Branch coverage included in aggregate %.

133 of 138 new or added lines in 7 files covered. (96.38%)

1 existing line in 1 file now uncovered.

3441 of 3588 relevant lines covered (95.9%)

520290.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.71
/src/JpegLSEncoder.cs
1
// Copyright (c) Team CharLS.
2
// SPDX-License-Identifier: BSD-3-Clause
3

4
using System.Reflection;
5
using System.Text;
6

7
using static CharLS.Managed.Algorithm;
8

9
namespace CharLS.Managed;
10

11
/// <summary>
12
/// JPEG-LS encoder that provided the functionality to encode JPEG-LS images.
13
/// </summary>
14
public sealed class JpegLSEncoder
15
{
16
    /// <summary>
17
    /// Special value to indicate that encoder needs to calculate the required stride.
18
    /// </summary>
19
    public const int AutoCalculateStride = Constants.AutoCalculateStride;
20

21
    private JpegStreamWriter _writer;
22
    private ScanEncoder _scanEncoder;
23
    private State _state = State.Initial;
24
    private int _encodedComponentCount;
25
    private byte[]? _samplingFactors;
26
    private int _horizontalSamplingMax;
27
    private int _verticalSamplingMax;
28

29
    /// <summary>
30
    /// Initializes a new instance of the <see cref="JpegLSEncoder"/> class.
31
    /// </summary>
32
    public JpegLSEncoder()
254✔
33
    {
34
    }
254✔
35

36
    /// <summary>
37
    /// Initializes a new instance of the <see cref="JpegLSEncoder"/> class.
38
    /// </summary>
39
    /// <param name="width">The width of the image to encode.</param>
40
    /// <param name="height">The height of the image to encode.</param>
41
    /// <param name="bitsPerSample">The bits per sample of the image to encode.</param>
42
    /// <param name="componentCount">The component count of the image to encode.</param>
43
    /// <param name="interleaveMode">The interleave mode of the image to encode (default None).</param>
44
    /// <param name="allocateDestination">Flag to control if destination buffer should be allocated or not (default true).</param>
45
    /// <param name="extraBytes">Number of extra destination bytes. Comments and tables are not included in the standard estimate (default 0).</param>
46
    /// <exception cref="ArgumentOutOfRangeException">Thrown when one of the arguments is invalid.</exception>
47
    /// <exception cref="OutOfMemoryException">Thrown when memory allocation for the destination buffer fails.</exception>
48
    public JpegLSEncoder(int width, int height, int bitsPerSample, int componentCount, InterleaveMode interleaveMode = InterleaveMode.None, bool allocateDestination = true, int extraBytes = 0)
49
        : this(new FrameInfo(width, height, bitsPerSample, componentCount), interleaveMode, allocateDestination, extraBytes)
86✔
50
    {
51
    }
74✔
52

53
    /// <summary>
54
    /// Initializes a new instance of the <see cref="JpegLSEncoder"/> class.
55
    /// </summary>
56
    /// <param name="frameInfo">The frameInfo of the image to encode.</param>
57
    /// <param name="interleaveMode">The interleave mode of the image to encode (default None).</param>
58
    /// <param name="allocateDestination">Flag to control if destination buffer should be allocated or not (default true).</param>
59
    /// <param name="extraBytes">Number of extra destination bytes. Comments and tables are not included in the standard estimate (default 0).</param>
60
    /// <exception cref="ArgumentOutOfRangeException">Thrown when one of the arguments is invalid.</exception>
61
    /// <exception cref="OutOfMemoryException">Thrown when memory allocation for the destination buffer fails.</exception>
62
    public JpegLSEncoder(FrameInfo frameInfo, InterleaveMode interleaveMode = InterleaveMode.None, bool allocateDestination = true, int extraBytes = 0)
82✔
63
    {
64
        ArgumentOutOfRangeException.ThrowIfNegative(extraBytes);
82✔
65

66
        FrameInfo = frameInfo;
82✔
67
        InterleaveMode = interleaveMode;
82✔
68

69
        if (allocateDestination)
82✔
70
        {
71
            Destination = new byte[EstimatedDestinationSize + extraBytes];
80✔
72
        }
73
    }
82✔
74

75
    private enum State
76
    {
77
        Initial,
78
        DestinationSet,
79
        SpiffHeader,
80
        TablesAndMiscellaneous,
81
        Completed
82
    }
83

84
    /// <summary>
85
    /// Gets or sets the frame information of the image.
86
    /// </summary>
87
    /// <value>
88
    /// The frame information of the image.
89
    /// </value>
90
    /// <exception cref="ArgumentException">Thrown when the passed FrameInfo is invalid.</exception>
91
    /// <exception cref="ArgumentNullException">Thrown when the passed FrameInfo instance is null.</exception>
92
    public FrameInfo FrameInfo { get; set; }
4,112✔
93

94
    /// <summary>
95
    /// Gets or sets the near lossless parameter to be used to encode the JPEG-LS stream.
96
    /// </summary>
97
    /// <value>
98
    /// The near lossless parameter value, 0 means lossless.
99
    /// </value>
100
    /// <exception cref="ArgumentException">Thrown when the passed value is invalid.</exception>
101
    public int NearLossless
102
    {
103
        get;
988✔
104
        set
105
        {
106
            ThrowHelper.ThrowIfOutsideRange(
34✔
107
                Constants.MinimumNearLossless, Constants.MaximumNearLossless, value, ErrorCode.InvalidArgumentNearLossless);
34✔
108
            field = value;
30✔
109
        }
30✔
110
    }
111

112
    /// <summary>
113
    /// Gets or sets the interleave mode.
114
    /// </summary>
115
    /// <value>
116
    /// The interleave mode that should be used to encode the image. Default is None.
117
    /// </value>
118
    /// <exception cref="ArgumentException">Thrown when the passed value is invalid for the defined image.</exception>
119
    public InterleaveMode InterleaveMode
120
    {
121
        get;
1,162✔
122
        set
123
        {
124
            ThrowHelper.ThrowArgumentOutOfRangeExceptionIfFalse(value.IsValid(), ErrorCode.InvalidArgumentInterleaveMode, nameof(value));
242✔
125
            field = value;
238✔
126
        }
238✔
127
    }
128

129
    /// <summary>
130
    /// Configures the encoding options the encoder should use.
131
    /// </summary>
132
    /// <value>
133
    /// Options to use. Options can be combined. Default is None.
134
    /// </value>
135
    /// <exception cref="ArgumentOutOfRangeException">Thrown when the passed enum value is invalid.</exception>
136
    public EncodingOptions EncodingOptions
137
    {
138
        get;
640✔
139
        set
140
        {
141
            ThrowHelper.ThrowArgumentOutOfRangeExceptionIfFalse(value.IsValid(), ErrorCode.InvalidArgumentEncodingOptions, nameof(value));
14✔
142
            field = value;
12✔
143
        }
12✔
144
    }
145

146
    /// <summary>
147
    /// Gets or sets the JPEG-LS preset coding parameters.
148
    /// </summary>
149
    /// <value>
150
    /// The JPEG-LS preset coding parameters that should be used to encode the image.
151
    /// </value>
152
    /// <exception cref="ArgumentNullException">value.</exception>
153
    public JpegLSPresetCodingParameters? PresetCodingParameters
154
    {
155
        get;
456✔
156
        set
157
        {
158
            ArgumentNullException.ThrowIfNull(value);
48✔
159
            field = value;
48✔
160
        }
48✔
161
    }
162

163
    = new();
336✔
164

165
    /// <summary>
166
    /// Gets or sets the HP color transformation the encoder should use
167
    /// If not set the encoder will use no color transformation.
168
    /// Color transformations are an HP extension and not defined by the JPEG-LS standard and can only be set for 3 component.
169
    /// </summary>
170
    /// <value>
171
    /// The color transformation that should be used to encode the image.
172
    /// </value>
173
    /// <exception cref="ArgumentOutOfRangeException">value.</exception>
174
    public ColorTransformation ColorTransformation
175
    {
176
        get;
502✔
177
        set
178
        {
179
            ThrowHelper.ThrowArgumentOutOfRangeExceptionIfFalse(
80✔
180
                value.IsValid(), ErrorCode.InvalidArgumentColorTransformation);
80✔
181
            field = value;
78✔
182
        }
78✔
183
    }
184

185
    /// <summary>
186
    /// Gets the estimated size in bytes of the memory buffer that should be used as output destination.
187
    /// </summary>
188
    /// <value>
189
    /// The size in bytes of the memory buffer.
190
    /// </value>
191
    /// <exception cref="OverflowException">When the required size doesn't fit in an int.</exception>
192
    public int EstimatedDestinationSize
193
    {
194
        get
195
        {
196
            ThrowHelper.ThrowInvalidOperationIfFalse(IsFrameInfoConfigured);
226✔
197

198
            checked
199
            {
200
                return (FrameInfo.Width * FrameInfo.Height * FrameInfo.ComponentCount *
224✔
201
                           BitToByteCount(FrameInfo.BitsPerSample)) + 1024 + Constants.SpiffHeaderSizeInBytes;
224✔
202
            }
203
        }
204
    }
205

206
    /// <summary>
207
    /// Gets or sets the memory region that will be the destination for the encoded JPEG-LS data.
208
    /// </summary>
209
    /// <value>
210
    /// The memory buffer to be used as the destination.
211
    /// </value>
212
    /// <exception cref="ArgumentException">Thrown when the passed value is an empty buffer.</exception>
213
    public Memory<byte> Destination
214
    {
215
        get => _writer.Destination;
110✔
216

217
        set
218
        {
219
            ThrowHelper.ThrowInvalidOperationIfFalse(_state == State.Initial);
298✔
220
            _writer.Destination = value;
296✔
221
            _state = State.DestinationSet;
296✔
222
        }
296✔
223
    }
224

225
    /// <summary>
226
    /// Gets the memory region with the encoded JPEG-LS data.
227
    /// </summary>
228
    /// <value>
229
    /// The memory region with the encoded data.
230
    /// </value>
231
    public Memory<byte> EncodedData => Destination[..BytesWritten];
106✔
232

233
    /// <summary>
234
    /// Gets the bytes written to the destination buffer.
235
    /// </summary>
236
    /// <value>
237
    /// The bytes written to the destination buffer.
238
    /// </value>
239
    public int BytesWritten => _writer.BytesWritten;
324✔
240

241
    private bool IsFrameInfoConfigured => FrameInfo.Height != 0;
494✔
242

243
    /// <summary>
244
    /// Configures the sampling factor when encoding a component.
245
    /// </summary>
246
    /// <param name="componentIndex">The index of the component to set the mapping table ID for.</param>
247
    /// <param name="horizontalFactor">The horizontal subsampling factor.</param>
248
    /// <param name="verticalFactor">The vertical subsampling factor.</param>
249
    public void SetSamplingFactor(int componentIndex, int horizontalFactor, int verticalFactor)
250
    {
251
        ThrowHelper.ThrowIfOutsideRange(Constants.MinimumComponentIndex, Constants.MaximumComponentIndex, componentIndex);
12✔
252
        ThrowHelper.ThrowIfOutsideRange(0, 4, horizontalFactor);
12✔
253
        ThrowHelper.ThrowIfOutsideRange(0, 4, verticalFactor);
12✔
254

255
        // Usage of sampling factors is rare: use lazy initialization.
256
        _samplingFactors ??= new byte[Constants.MaximumComponentCount];
12✔
257
        _samplingFactors[componentIndex] = (byte)((horizontalFactor << 4) | verticalFactor);
12✔
258
    }
12✔
259

260
    /// <summary>
261
    /// Configures the mapping table ID the encoder should reference when encoding a component.
262
    /// The referenced mapping table can be included in the stream or provided in another JPEG-LS abbreviated format stream.
263
    /// </summary>
264
    /// <param name="componentIndex">The index of the component to set the mapping table ID for.</param>
265
    /// <param name="mappingTableId">The table ID the component should reference.</param>
266
    public void SetMappingTableId(int componentIndex, int mappingTableId)
267
    {
268
        ThrowHelper.ThrowIfOutsideRange(Constants.MinimumComponentIndex, Constants.MaximumComponentIndex, componentIndex);
10✔
269
        ThrowHelper.ThrowIfOutsideRange(0, Constants.MaximumMappingTableId, mappingTableId);
8✔
270

271
        _writer.SetTableId(componentIndex, mappingTableId);
6✔
272
    }
6✔
273

274
    /// <summary>
275
    /// Resets the write position of the destination buffer to the beginning.
276
    /// </summary>
277
    public void Rewind()
278
    {
279
        if (_state == State.Initial)
4✔
280
            return; // Nothing to do, stay in the same state.
2✔
281

282
        _writer.Rewind();
2✔
283
        _state = State.DestinationSet;
2✔
284
        _encodedComponentCount = 0;
2✔
285
    }
2✔
286

287
    /// <summary>
288
    /// Encodes the passed image data into encoded JPEG-LS data.
289
    /// </summary>
290
    /// <param name="source">The memory region that is the source input to the encoding process.</param>
291
    /// <param name="stride">The stride of the image pixel of the source input.</param>
292
    public void Encode(ReadOnlySpan<byte> source, int stride = Constants.AutoCalculateStride)
293
    {
294
        EncodeComponents(source, FrameInfo.ComponentCount, stride);
212✔
295
    }
186✔
296

297
    /// <summary>
298
    /// Encodes the passed image data into encoded JPEG-LS data.
299
    /// This is an advanced method that provides more control how image data is encoded in JPEG-LS scans.
300
    /// It should be called until all components are encoded.
301
    /// </summary>
302
    /// <param name="source">The memory region that is the source input to the encoding process.</param>
303
    /// <param name="sourceComponentCount">The number of components present in the input source.</param>
304
    /// <param name="stride">The stride of the image pixel of the source input.</param>
305
    public void EncodeComponents(ReadOnlySpan<byte> source, int sourceComponentCount, int stride = Constants.AutoCalculateStride)
306
    {
307
        CheckStateCanWrite();
244✔
308
        ThrowHelper.ThrowInvalidOperationIfFalse(IsFrameInfoConfigured);
242✔
309
        ThrowHelper.ThrowArgumentExceptionIfFalse(sourceComponentCount <= FrameInfo.ComponentCount - _encodedComponentCount, nameof(sourceComponentCount));
240✔
310
        CheckInterleaveModeAgainstComponentCount(sourceComponentCount);
238✔
311

312
        int maximumSampleValue = CalculateMaximumSampleValue(FrameInfo.BitsPerSample);
232✔
313
        if (!PresetCodingParameters!.TryMakeExplicit(maximumSampleValue, NearLossless, out var explicitCodingParameters))
232✔
314
            throw ThrowHelper.CreateArgumentException(ErrorCode.InvalidArgumentPresetCodingParameters);
2✔
315

316
        if (_encodedComponentCount == 0)
230✔
317
        {
318
            DetermineMaxSamplingFactors();
212✔
319
            TransitionToTablesAndMiscellaneousState();
212✔
320
            WriteColorTransformSegment();
212✔
321
            WriteStartOfFrameSegment();
206✔
322
        }
323

324
        WriteJpegLSPresetParametersSegment(maximumSampleValue, explicitCodingParameters);
224✔
325

326
        if (InterleaveMode == InterleaveMode.None)
224✔
327
        {
328
            for (int component = 0; ;)
138✔
329
            {
330
                int scanWidth = GetScanWidth(_encodedComponentCount + component);
186✔
331
                int scanHeight = GetScanHeight(_encodedComponentCount + component);
186✔
332
                int scanStride = CheckStrideAndSourceLengthInterleaveModeNone(source.Length, stride, scanWidth, scanHeight);
186✔
333
                _writer.WriteStartOfScanSegment(1, NearLossless, InterleaveMode);
182✔
334
                EncodeScan(source, scanStride, scanWidth, scanHeight, 1, explicitCodingParameters);
182✔
335

336
                ++component;
182✔
337
                if (component == sourceComponentCount)
182✔
338
                    break;
339

340
                // Synchronize the source stream (EncodeScan works on a local copy)
341
                int byteCountComponent = scanStride * scanHeight;
48✔
342
                source = source[byteCountComponent..];
48✔
343
            }
344
        }
345
        else
346
        {
347
            int scanWidth = GetScanWidth(_encodedComponentCount);
86✔
348
            int scanHeight = GetScanHeight(_encodedComponentCount);
86✔
349
            int scanStride = CheckStrideAndSourceLength(source.Length, stride, scanWidth, scanHeight, sourceComponentCount);
86✔
350
            _writer.WriteStartOfScanSegment(sourceComponentCount, NearLossless, InterleaveMode);
82✔
351
            EncodeScan(source, scanStride, scanWidth, scanHeight, sourceComponentCount, explicitCodingParameters);
82✔
352
        }
353

354
        _encodedComponentCount += sourceComponentCount;
216✔
355
        if (_encodedComponentCount == FrameInfo.ComponentCount)
216✔
356
        {
357
            WriteEndOfImage();
198✔
358
        }
359
    }
216✔
360

361
    /// <summary>
362
    /// Writes a SPIFF header to the destination memory buffer.
363
    /// A SPIFF header is optional, but recommended for standalone JPEG-LS files.
364
    /// It should not be used when embedding a JPEG-LS image in a DICOM file.
365
    /// </summary>
366
    /// <param name="spiffHeader">Reference to a SPIFF header that will be written to the destination buffer.</param>
367
    public void WriteSpiffHeader(SpiffHeader spiffHeader)
368
    {
369
        ArgumentNullException.ThrowIfNull(spiffHeader);
32✔
370

371
        ThrowHelper.ThrowIfOutsideRange(1, int.MaxValue, spiffHeader.Height, ErrorCode.InvalidArgumentHeight);
30✔
372
        ThrowHelper.ThrowIfOutsideRange(1, int.MaxValue, spiffHeader.Width, ErrorCode.InvalidArgumentWidth);
28✔
373
        ThrowHelper.ThrowInvalidOperationIfFalse(_state == State.DestinationSet);
26✔
374

375
        _writer.WriteStartOfImage();
22✔
376
        _writer.WriteSpiffHeaderSegment(spiffHeader);
22✔
377
        _state = State.SpiffHeader;
22✔
378
    }
22✔
379

380
    /// <summary>
381
    /// Writes a standard SPIFF header to the destination. The additional values are computed from the current encoder settings.
382
    /// A SPIFF header is optional, but recommended for standalone JPEG-LS files.
383
    /// It should not be used when embedding a JPEG-LS image in a DICOM file.
384
    /// </summary>
385
    /// <param name="colorSpace">The color space of the image.</param>
386
    /// <param name="resolutionUnit">The resolution units of the next 2 parameters.</param>
387
    /// <param name="verticalResolution">The vertical resolution.</param>
388
    /// <param name="horizontalResolution">The horizontal resolution.</param>
389
    public void WriteStandardSpiffHeader(
390
        SpiffColorSpace colorSpace,
391
        SpiffResolutionUnit resolutionUnit = SpiffResolutionUnit.AspectRatio,
392
        int verticalResolution = 1,
393
        int horizontalResolution = 1)
394
    {
395
        ThrowHelper.ThrowInvalidOperationIfFalse(IsFrameInfoConfigured);
26✔
396

397
        var spiffHeader = new SpiffHeader
24✔
398
        {
24✔
399
            ColorSpace = colorSpace,
24✔
400
            Height = FrameInfo.Height,
24✔
401
            Width = FrameInfo.Width,
24✔
402
            BitsPerSample = FrameInfo.BitsPerSample,
24✔
403
            ComponentCount = FrameInfo.ComponentCount,
24✔
404
            ResolutionUnit = resolutionUnit,
24✔
405
            VerticalResolution = verticalResolution,
24✔
406
            HorizontalResolution = horizontalResolution
24✔
407
        };
24✔
408
        WriteSpiffHeader(spiffHeader);
24✔
409
    }
20✔
410

411
    /// <summary>
412
    /// Writes a SPIFF directory entry to the destination.
413
    /// </summary>
414
    /// <param name="entryTag">The entry tag of the directory entry.</param>
415
    /// <param name="entryData">The data of the directory entry.</param>
416
    public void WriteSpiffEntry(SpiffEntryTag entryTag, ReadOnlySpan<byte> entryData)
417
    {
418
        WriteSpiffEntry((int)entryTag, entryData);
12✔
419
    }
8✔
420

421
    /// <summary>
422
    /// Writes a SPIFF directory entry to the destination.
423
    /// </summary>
424
    /// <param name="entryTag">The entry tag of the directory entry.</param>
425
    /// <param name="entryData">The data of the directory entry.</param>
426
    public void WriteSpiffEntry(int entryTag, ReadOnlySpan<byte> entryData)
427
    {
428
        ThrowHelper.ThrowArgumentExceptionIfFalse(entryTag != Constants.SpiffEndOfDirectoryEntryType, nameof(entryTag));
14✔
429
        ThrowHelper.ThrowArgumentExceptionIfFalse(entryData.Length <= 65528, nameof(entryData), ErrorCode.InvalidArgumentSize);
12✔
430
        ThrowHelper.ThrowInvalidOperationIfFalse(_state == State.SpiffHeader);
10✔
431

432
        _writer.WriteSpiffDirectoryEntry(entryTag, entryData);
8✔
433
    }
8✔
434

435
    /// <summary>
436
    /// Writes a SPIFF end of directory entry to the destination.
437
    /// The encoder will normally do this automatically. It is made available
438
    /// for the scenario to create SPIFF headers in front of existing JPEG-LS streams.
439
    /// </summary>
440
    /// <remarks>
441
    /// The end of directory also includes a SOI marker. This marker should be skipped from the JPEG-LS stream.
442
    /// </remarks>
443
    public void WriteSpiffEndOfDirectoryEntry()
444
    {
445
        ThrowHelper.ThrowInvalidOperationIfFalse(_state == State.SpiffHeader);
8✔
446
        TransitionToTablesAndMiscellaneousState();
4✔
447
    }
4✔
448

449
    /// <summary>
450
    /// Writes a comment (COM) segment to the destination.
451
    /// </summary>
452
    /// <remarks>
453
    /// Function should be called before encoding the image data.
454
    /// </remarks>
455
    /// <param name="comment">The 'comment' bytes. Application specific, usually human-readable UTF-8 string.</param>
456
    public void WriteComment(ReadOnlySpan<byte> comment)
457
    {
458
        ThrowHelper.ThrowArgumentExceptionIfFalse(comment.Length <= Constants.SegmentMaxDataSize, nameof(comment));
22✔
459
        CheckStateCanWrite();
20✔
460

461
        TransitionToTablesAndMiscellaneousState();
16✔
462
        _writer.WriteCommentSegment(comment);
16✔
463
    }
16✔
464

465
    /// <summary>
466
    /// Writes a comment (COM) segment to the destination.
467
    /// </summary>
468
    /// <remarks>
469
    /// Function should be called before encoding the image data.
470
    /// </remarks>
471
    /// <param name="comment">Application specific value, usually human-readable UTF-8 string.</param>
472
    public void WriteComment(string comment)
473
    {
474
        WriteComment(ToUtf8(comment));
16✔
475
    }
12✔
476

477
    /// <summary>
478
    /// Writes an application data (APPn) segment to the destination.
479
    /// </summary>
480
    /// <remarks>
481
    /// Function should be called before encoding the image data.
482
    /// </remarks>
483
    /// <param name="applicationDataId">The ID of the application data segment in the range [0..15].</param>
484
    /// <param name="applicationData">The 'application data' bytes. Application specific.</param>
485
    public void WriteApplicationData(int applicationDataId, ReadOnlySpan<byte> applicationData)
486
    {
487
        ThrowHelper.ThrowIfOutsideRange(Constants.MinimumApplicationDataId, Constants.MaximumApplicationDataId, applicationDataId);
22✔
488
        ThrowHelper.ThrowArgumentExceptionIfFalse(applicationData.Length <= Constants.SegmentMaxDataSize, nameof(applicationData));
18✔
489
        CheckStateCanWrite();
16✔
490

491
        TransitionToTablesAndMiscellaneousState();
14✔
492
        _writer.WriteApplicationDataSegment(applicationDataId, applicationData);
14✔
493
    }
14✔
494

495
    /// <summary>
496
    /// Writes a mapping table segment to the destination.
497
    /// </summary>
498
    /// <remarks>
499
    /// No validation is performed if the table ID is unique and if the table size matches the required size.
500
    /// </remarks>
501
    /// <param name="tableId">Table ID. Unique identifier of the mapping table in the range [1..255].</param>
502
    /// <param name="entrySize">Size in bytes of a single table entry.</param>
503
    /// <param name="tableData">Buffer that holds the mapping table.</param>
504
    public void WriteMappingTable(int tableId, int entrySize, ReadOnlySpan<byte> tableData)
505
    {
506
        ThrowHelper.ThrowIfOutsideRange(Constants.MinimumMappingTableId, Constants.MaximumMappingTableId, tableId);
18✔
507
        ThrowHelper.ThrowIfOutsideRange(Constants.MinimumMappingEntrySize, Constants.MaximumMappingEntrySize, entrySize);
14✔
508
        ThrowHelper.ThrowArgumentExceptionIfFalse(tableData.Length >= entrySize, nameof(tableData), ErrorCode.InvalidArgumentSize);
10✔
509
        CheckStateCanWrite();
8✔
510

511
        TransitionToTablesAndMiscellaneousState();
6✔
512
        _writer.WriteJpegLSPresetParametersSegment(tableId, entrySize, tableData);
6✔
513
    }
6✔
514

515
    /// <summary>
516
    /// Creates a JPEG-LS stream in abbreviated format that only contain mapping tables (See JPEG-LS standard, C.4).
517
    /// These tables should have been written to the stream first with the method write_mapping_table.
518
    /// </summary>
519
    public void CreateAbbreviatedFormat()
520
    {
521
        ThrowHelper.ThrowInvalidOperationIfFalse(_state == State.TablesAndMiscellaneous);
4✔
522
        WriteEndOfImage();
2✔
523
    }
2✔
524

525
    /// <summary>
526
    /// Encodes the passed image data into encoded JPEG-LS data.
527
    /// </summary>
528
    /// <param name="source">Source pixel data that needs to be encoded.</param>
529
    /// <param name="frameInfo">Frame info object that describes the pixel data.</param>
530
    /// <param name="interleaveMode">Defines how the pixel data should be encoded.</param>
531
    /// <param name="encodingOptions">Defines several options how to encode the pixel data.</param>
532
    /// <param name="stride">The stride to use; byte count to the next pixel row. Pass 0 (AutoCalculateStride) for the default.</param>
533
    public static Memory<byte> Encode(
534
        ReadOnlySpan<byte> source,
535
        FrameInfo frameInfo,
536
        InterleaveMode interleaveMode = InterleaveMode.None,
537
        EncodingOptions encodingOptions = EncodingOptions.None,
538
        int stride = AutoCalculateStride)
539
    {
540
        JpegLSEncoder encoder = new(frameInfo) { InterleaveMode = interleaveMode, EncodingOptions = encodingOptions };
6✔
541
        encoder.Encode(source, stride);
6✔
542
        return encoder.EncodedData;
2✔
543
    }
544

545
    private void EncodeScan(ReadOnlySpan<byte> source, int stride, int scanWidth, int scanHeight, int componentCount, JpegLSPresetCodingParameters codingParameters)
546
    {
547
        _scanEncoder = new ScanEncoder(
264✔
548
            new FrameInfo(scanWidth, scanHeight, FrameInfo.BitsPerSample, componentCount),
264✔
549
            codingParameters,
264✔
550
            new CodingParameters
264✔
551
            {
264✔
552
                InterleaveMode = InterleaveMode,
264✔
553
                NearLossless = NearLossless,
264✔
554
                RestartInterval = 0,
264✔
555
                ColorTransformation = ColorTransformation
264✔
556
            });
264✔
557

558
        int bytesWritten = _scanEncoder.EncodeScan(source, _writer.GetRemainingDestination(), stride);
264✔
559

560
        // Synchronize the destination encapsulated in the writer (encode_scan works on a local copy)
561
        _writer.AdvancePosition(bytesWritten);
264✔
562
    }
264✔
563

564
    private void TransitionToTablesAndMiscellaneousState()
565
    {
566
        switch (_state)
252✔
567
        {
568
            case State.TablesAndMiscellaneous:
569
                return;
12✔
570
            case State.SpiffHeader:
571
                _writer.WriteSpiffEndOfDirectoryEntry();
6✔
572
                break;
6✔
573
            default:
574
                Debug.Assert(_state == State.DestinationSet);
575
                _writer.WriteStartOfImage();
234✔
576
                break;
577
        }
578

579
        if (EncodingOptions.HasFlag(EncodingOptions.IncludeVersionNumber))
240✔
580
        {
581
            string informationVersion = RemoveGitHash(Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion!);
2!
582

583
            byte[] utfBytes = Encoding.UTF8.GetBytes(informationVersion);
2✔
584

585
            var versionNumber = "charls-dotnet "u8.ToArray().Concat(utfBytes).ToArray();
2✔
586
            _writer.WriteCommentSegment(versionNumber);
2✔
587
        }
588

589
        _state = State.TablesAndMiscellaneous;
240✔
590
        return;
240✔
591

592
        static string RemoveGitHash(string version)
593
        {
594
            int plusIndex = version.IndexOf('+', StringComparison.InvariantCulture);
2✔
595
            return plusIndex != -1 ? version[..plusIndex] : version;
2!
596
        }
597
    }
598

599
    private void WriteColorTransformSegment()
600
    {
601
        if (ColorTransformation == ColorTransformation.None)
212✔
602
            return;
180✔
603

604
        ThrowHelper.ThrowArgumentExceptionIfFalse(ColorTransformations.IsPossible(FrameInfo), null, ErrorCode.InvalidArgumentColorTransformation);
32✔
605

606
        _writer.WriteColorTransformSegment(ColorTransformation);
26✔
607
    }
26✔
608

609
    private void WriteStartOfFrameSegment()
610
    {
611
        if (_writer.WriteStartOfFrameSegment(FrameInfo, _samplingFactors))
206✔
612
        {
613
            // Image dimensions are oversized and need to be written to a JPEG-LS preset parameters (LSE) segment.
614
            _writer.WriteJpegLSPresetParametersSegment(FrameInfo.Height, FrameInfo.Width);
4✔
615
        }
616
    }
206✔
617

618
    private void WriteJpegLSPresetParametersSegment(int maximumSampleValue, JpegLSPresetCodingParameters presetCodingParameters)
619
    {
620
        if (!PresetCodingParameters!.IsDefault(maximumSampleValue, NearLossless) ||
224✔
621
            (EncodingOptions.HasFlag(EncodingOptions.IncludePCParametersJai) && FrameInfo.BitsPerSample > 12))
224✔
622
        {
623
            // Write the actual used values to the stream, not zero's.
624
            // Explicit values reduces the risk for decoding by other implementations.
625
            _writer.WriteJpegLSPresetParametersSegment(presetCodingParameters);
26✔
626
        }
627
    }
224✔
628

629
    private void WriteEndOfImage()
630
    {
631
        _writer.WriteEndOfImage(EncodingOptions.HasFlag(EncodingOptions.EvenDestinationSize));
200✔
632
        _state = State.Completed;
200✔
633
    }
200✔
634

635
    private void CheckStateCanWrite()
636
    {
637
        ThrowHelper.ThrowInvalidOperationIfFalse(_state is >= State.DestinationSet and < State.Completed);
288✔
638
    }
278✔
639

640
    private void CheckInterleaveModeAgainstComponentCount(int componentCount)
641
    {
642
        if (InterleaveMode != InterleaveMode.None && componentCount is 1 or > Constants.MaximumComponentCountInScan)
238✔
643
            ThrowHelper.ThrowArgumentException(ErrorCode.InvalidArgumentInterleaveMode);
6✔
644
    }
232✔
645

646
    private int CheckStrideAndSourceLength(int sourceLength, int stride, int scanWidth, int scanHeight, int sourceComponentCount)
647
    {
648
        int minimumStride = CalculateMinimumStride(scanWidth, sourceComponentCount);
86✔
649

650
        if (stride == AutoCalculateStride)
86✔
651
        {
652
            stride = minimumStride;
78✔
653
        }
654
        else
655
        {
656
            if (stride < minimumStride)
8✔
657
                ThrowHelper.ThrowArgumentException(ErrorCode.InvalidArgumentStride);
4✔
658
        }
659

660
        int notUsedBytesAtEnd = stride - minimumStride;
82✔
661
        int minimumSourceLength = InterleaveMode == InterleaveMode.None
82!
662
            ? (stride * sourceComponentCount * scanHeight) - notUsedBytesAtEnd
82✔
663
            : (stride * scanHeight) - notUsedBytesAtEnd;
82✔
664

665
        if (sourceLength < minimumSourceLength)
82!
UNCOV
666
            ThrowHelper.ThrowArgumentException(ErrorCode.InvalidArgumentSize);
×
667

668
        return stride;
82✔
669
    }
670

671
    private int CheckStrideAndSourceLengthInterleaveModeNone(int sourceLength, int stride, int scanWidth, int scanHeight)
672
    {
673
        int minimumStride = scanWidth * BitToByteCount(FrameInfo.BitsPerSample);
186✔
674
        if (stride == AutoCalculateStride)
186✔
675
        {
676
            stride = minimumStride;
164✔
677
        }
678
        else
679
        {
680
            if (stride < minimumStride)
22✔
681
                ThrowHelper.ThrowArgumentException(ErrorCode.InvalidArgumentStride);
2✔
682
        }
683

684
        int notUsedBytesAtEnd = stride - minimumStride;
184✔
685
        int minimumSourceLength = (stride * scanHeight) - notUsedBytesAtEnd;
184✔
686

687
        if (sourceLength < minimumSourceLength)
184✔
688
            ThrowHelper.ThrowArgumentException(ErrorCode.InvalidArgumentSize);
2✔
689

690
        return stride;
182✔
691
    }
692

693
    private int CalculateMinimumStride(int scanWidth, int sourceComponentCount)
694
    {
695
        switch (InterleaveMode)
86!
696
        {
697
            case InterleaveMode.None:
NEW
698
                return FrameInfo.Width * BitToByteCount(FrameInfo.BitsPerSample);
×
699

700
            case InterleaveMode.Line:
701
                return FrameInfo.Width * BitToByteCount(FrameInfo.BitsPerSample) * sourceComponentCount;
36✔
702

703
            default:
704
                Debug.Assert(InterleaveMode == InterleaveMode.Sample);
705
                return scanWidth * BitToByteCount(FrameInfo.BitsPerSample) * sourceComponentCount;
50✔
706
        }
707
    }
708

709
    private void DetermineMaxSamplingFactors()
710
    {
711
        if (_samplingFactors == null)
212✔
712
            return;
208✔
713

714
        _horizontalSamplingMax = 1;
4✔
715
        _verticalSamplingMax = 1;
4✔
716
        for (int i = 0; i < FrameInfo.ComponentCount; ++i)
32✔
717
        {
718
            _horizontalSamplingMax = Math.Max(_horizontalSamplingMax, GetHorizontalSamplingFactor(i));
12✔
719
            _verticalSamplingMax = Math.Max(_verticalSamplingMax, GetVerticalSamplingFactor(i));
12✔
720
        }
721
    }
4✔
722

723
    private int GetScanWidth(int componentIndex)
724
    {
725
        if (_samplingFactors == null)
272✔
726
            return FrameInfo.Width;
262✔
727

728
        return FrameInfo.Width * GetHorizontalSamplingFactor(componentIndex) / _horizontalSamplingMax;
10✔
729
    }
730

731
    private int GetScanHeight(int componentIndex)
732
    {
733
        if (_samplingFactors == null)
272✔
734
            return FrameInfo.Height;
262✔
735

736
        return FrameInfo.Height * GetVerticalSamplingFactor(componentIndex) / _verticalSamplingMax;
10✔
737
    }
738

739
    private int GetHorizontalSamplingFactor(int componentIndex)
740
    {
741
        byte samplingFactor = _samplingFactors![componentIndex];
22✔
742
        if (samplingFactor == 0)
22!
NEW
743
            return 1;
×
744

745
        return samplingFactor >> 4;
22✔
746
    }
747

748
    private int GetVerticalSamplingFactor(int componentIndex)
749
    {
750
        byte samplingFactor = _samplingFactors![componentIndex];
22✔
751
        if (samplingFactor == 0)
22!
NEW
752
            return 1;
×
753

754
        return samplingFactor & 0xF;
22✔
755
    }
756

757
    private static ReadOnlySpan<byte> ToUtf8(string text)
758
    {
759
        if (string.IsNullOrEmpty(text))
16✔
760
            return default;
6✔
761

762
        var utf8Encoded = new byte[Encoding.UTF8.GetMaxByteCount(text.Length) + 1];
10✔
763
        int bytesWritten = Encoding.UTF8.GetBytes(text, 0, text.Length, utf8Encoded, 0);
10✔
764
        utf8Encoded[bytesWritten] = 0;
10✔
765

766
        return new ReadOnlySpan<byte>(utf8Encoded, 0, bytesWritten + 1);
10✔
767
    }
768
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc