@@ -44,14 +44,49 @@ public QoiDecoderCore(DecoderOptions options)
44
44
45
45
public Size Dimensions { get ; }
46
46
47
+ /// <inheritdoc />
47
48
public Image < TPixel > Decode < TPixel > ( BufferedReadStream stream , CancellationToken cancellationToken )
48
- where TPixel : unmanaged, IPixel < TPixel > => throw new NotImplementedException ( ) ;
49
+ where TPixel : unmanaged, IPixel < TPixel >
50
+ {
51
+ // Process the header to get metadata
52
+ this . ProcessHeader ( stream ) ;
53
+
54
+ // Create Image object
55
+ ImageMetadata metadata = new ( )
56
+ {
57
+ DecodedImageFormat = QoiFormat . Instance ,
58
+ HorizontalResolution = this . header . Width ,
59
+ VerticalResolution = this . header . Height ,
60
+ ResolutionUnits = PixelResolutionUnit . AspectRatio
61
+ } ;
62
+ Image < TPixel > image = new ( this . configuration , ( int ) this . header . Width , ( int ) this . header . Height , metadata ) ;
63
+ Buffer2D < TPixel > pixels = image . GetRootFramePixelBuffer ( ) ;
64
+
65
+ this . ProcessPixels ( stream , pixels ) ;
66
+
67
+ return image ;
68
+ }
49
69
70
+ /// <inheritdoc />
50
71
public ImageInfo Identify ( BufferedReadStream stream , CancellationToken cancellationToken )
51
72
{
52
73
ImageMetadata metadata = new ( ) ;
53
- QoiMetadata qoiMetadata = metadata . GetQoiMetadata ( ) ;
54
74
75
+ this . ProcessHeader ( stream ) ;
76
+ PixelTypeInfo pixelType = new ( 8 * ( int ) this . header . Channels ) ;
77
+ Size size = new ( ( int ) this . header . Width , ( int ) this . header . Height ) ;
78
+
79
+ return new ImageInfo ( pixelType , size , metadata ) ;
80
+ }
81
+
82
+ /// <summary>
83
+ /// Processes the 14-byte header to validate the image and save the metadata
84
+ /// in <see cref="header"/>
85
+ /// </summary>
86
+ /// <param name="stream">The stream where the bytes are being read</param>
87
+ /// <exception cref="InvalidImageContentException">If the stream doesn't store a qoi image</exception>
88
+ private void ProcessHeader ( Stream stream )
89
+ {
55
90
Span < byte > magicBytes = stackalloc byte [ 4 ] ;
56
91
Span < byte > widthBytes = stackalloc byte [ 4 ] ;
57
92
Span < byte > heightBytes = stackalloc byte [ 4 ] ;
@@ -85,32 +120,138 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
85
120
$ "The image has an invalid size: width = { width } , height = { height } ") ;
86
121
}
87
122
88
- qoiMetadata . Width = width ;
89
- qoiMetadata . Height = height ;
90
-
91
- Size size = new ( ( int ) width , ( int ) height ) ;
92
-
93
123
int channels = stream . ReadByte ( ) ;
94
124
if ( channels is - 1 or ( not 3 and not 4 ) )
95
125
{
96
126
ThrowInvalidImageContentException ( ) ;
97
127
}
98
128
99
129
PixelTypeInfo pixelType = new ( 8 * channels ) ;
100
- qoiMetadata . Channels = ( QoiChannels ) channels ;
101
130
102
131
int colorSpace = stream . ReadByte ( ) ;
103
132
if ( colorSpace is - 1 or ( not 0 and not 1 ) )
104
133
{
105
134
ThrowInvalidImageContentException ( ) ;
106
135
}
107
136
108
- qoiMetadata . ColorSpace = ( QoiColorSpace ) colorSpace ;
109
-
110
- return new ImageInfo ( pixelType , size , metadata ) ;
137
+ this . header = new QoiHeader ( width , height , ( QoiChannels ) channels , ( QoiColorSpace ) colorSpace ) ;
111
138
}
112
139
113
140
[ DoesNotReturn ]
114
141
private static void ThrowInvalidImageContentException ( )
115
142
=> throw new InvalidImageContentException ( "The image is not a valid QOI image." ) ;
143
+
144
+ private void ProcessPixels < TPixel > ( BufferedReadStream stream , Buffer2D < TPixel > pixels )
145
+ where TPixel : unmanaged, IPixel < TPixel >
146
+ {
147
+ Rgba32 [ ] previouslySeenPixels = new Rgba32 [ 64 ] ;
148
+ Rgba32 previousPixel = new ( 0 , 0 , 0 , 255 ) ;
149
+ for ( int i = 0 ; i < this . header . Height ; i ++ )
150
+ {
151
+ for ( int j = 0 ; j < this . header . Width ; j ++ )
152
+ {
153
+ byte operationByte = ( byte ) stream . ReadByte ( ) ;
154
+ byte [ ] pixelBytes ;
155
+ Rgba32 readPixel ;
156
+ TPixel pixel = new ( ) ;
157
+ int pixelArrayPosition ;
158
+ switch ( ( QoiChunkEnum ) operationByte )
159
+ {
160
+ case QoiChunkEnum . QOI_OP_RGB :
161
+ pixelBytes = new byte [ 3 ] ;
162
+ if ( stream . Read ( pixelBytes ) < 3 )
163
+ {
164
+ ThrowInvalidImageContentException ( ) ;
165
+ }
166
+
167
+ readPixel = previousPixel with { R = pixelBytes [ 0 ] , G = pixelBytes [ 1 ] , B = pixelBytes [ 2 ] } ;
168
+ pixel . FromRgba32 ( readPixel ) ;
169
+ pixelArrayPosition = this . GetArrayPosition ( readPixel ) ;
170
+ previouslySeenPixels [ pixelArrayPosition ] = readPixel ;
171
+ break ;
172
+
173
+ case QoiChunkEnum . QOI_OP_RGBA :
174
+ pixelBytes = new byte [ 4 ] ;
175
+ if ( stream . Read ( pixelBytes ) < 4 )
176
+ {
177
+ ThrowInvalidImageContentException ( ) ;
178
+ }
179
+
180
+ readPixel = new Rgba32 ( pixelBytes [ 0 ] , pixelBytes [ 1 ] , pixelBytes [ 2 ] , pixelBytes [ 3 ] ) ;
181
+ pixel . FromRgba32 ( readPixel ) ;
182
+ pixelArrayPosition = this . GetArrayPosition ( readPixel ) ;
183
+ previouslySeenPixels [ pixelArrayPosition ] = readPixel ;
184
+ break ;
185
+
186
+ default :
187
+ switch ( ( QoiChunkEnum ) ( operationByte & 0b11000000 ) )
188
+ {
189
+ case QoiChunkEnum . QOI_OP_INDEX :
190
+ readPixel = previouslySeenPixels [ operationByte ] ;
191
+ pixel . FromRgba32 ( readPixel ) ;
192
+ break ;
193
+ case QoiChunkEnum . QOI_OP_DIFF :
194
+ // Get each value
195
+ byte redDifference = ( byte ) ( ( operationByte & 0b00110000 ) >> 4 ) ,
196
+ greenDifference = ( byte ) ( ( operationByte & 0b00001100 ) >> 2 ) ,
197
+ blueDifference = ( byte ) ( operationByte & 0b00000011 ) ;
198
+ readPixel = previousPixel with
199
+ {
200
+ R = ( byte ) ( ( previousPixel . R + ( redDifference - 2 ) ) % 256 ) ,
201
+ G = ( byte ) ( ( previousPixel . G + ( greenDifference - 2 ) ) % 256 ) ,
202
+ B = ( byte ) ( ( previousPixel . B + ( blueDifference - 2 ) ) % 256 )
203
+ } ;
204
+ pixel . FromRgba32 ( readPixel ) ;
205
+ pixelArrayPosition = this . GetArrayPosition ( readPixel ) ;
206
+ previouslySeenPixels [ pixelArrayPosition ] = readPixel ;
207
+ break ;
208
+ case QoiChunkEnum . QOI_OP_LUMA :
209
+ // Get difference green channel
210
+ byte diffGreen = ( byte ) ( operationByte & 0b00111111 ) ,
211
+ currentGreen = ( byte ) ( ( previousPixel . G + ( diffGreen - 32 ) ) % 256 ) ,
212
+ nextByte = ( byte ) stream . ReadByte ( ) ,
213
+ diffRedDG = ( byte ) ( nextByte >> 4 ) ,
214
+ diffBlueDG = ( byte ) ( nextByte & 0b00001111 ) ,
215
+ currentRed = ( byte ) ( ( diffRedDG - 8 + ( diffGreen - 32 ) + previousPixel . R ) % 256 ) ,
216
+ currentBlue = ( byte ) ( ( diffBlueDG - 8 + ( diffGreen - 32 ) + previousPixel . B ) % 256 ) ;
217
+ readPixel = previousPixel with { R = currentRed , B = currentBlue , G = currentGreen } ;
218
+ pixel . FromRgba32 ( readPixel ) ;
219
+ pixelArrayPosition = this . GetArrayPosition ( readPixel ) ;
220
+ previouslySeenPixels [ pixelArrayPosition ] = readPixel ;
221
+ break ;
222
+ case QoiChunkEnum . QOI_OP_RUN :
223
+ byte repetitions = ( byte ) ( operationByte & 0b00111111 ) ;
224
+ if ( repetitions is 62 or 63 )
225
+ {
226
+ ThrowInvalidImageContentException ( ) ;
227
+ }
228
+
229
+ readPixel = previousPixel ;
230
+ pixel . FromRgba32 ( readPixel ) ;
231
+ for ( int k = - 1 ; k < repetitions ; k ++ , j ++ )
232
+ {
233
+ if ( j == this . header . Width )
234
+ {
235
+ j = 0 ;
236
+ i ++ ;
237
+ }
238
+ pixels [ j , i ] = pixel ;
239
+ }
240
+
241
+ j -- ;
242
+ continue ;
243
+
244
+ default :
245
+ ThrowInvalidImageContentException ( ) ;
246
+ return ;
247
+ }
248
+ break ;
249
+ }
250
+ pixels [ j , i ] = pixel ;
251
+ previousPixel = readPixel ;
252
+ }
253
+ }
254
+ }
255
+
256
+ private int GetArrayPosition ( Rgba32 pixel ) => ( ( pixel . R * 3 ) + ( pixel . G * 5 ) + ( pixel . B * 7 ) + ( pixel . A * 11 ) ) % 64 ;
116
257
}
0 commit comments