@@ -44,14 +44,49 @@ public QoiDecoderCore(DecoderOptions options)
4444
4545 public Size Dimensions { get ; }
4646
47+ /// <inheritdoc />
4748 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+ }
4969
70+ /// <inheritdoc />
5071 public ImageInfo Identify ( BufferedReadStream stream , CancellationToken cancellationToken )
5172 {
5273 ImageMetadata metadata = new ( ) ;
53- QoiMetadata qoiMetadata = metadata . GetQoiMetadata ( ) ;
5474
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+ {
5590 Span < byte > magicBytes = stackalloc byte [ 4 ] ;
5691 Span < byte > widthBytes = stackalloc byte [ 4 ] ;
5792 Span < byte > heightBytes = stackalloc byte [ 4 ] ;
@@ -85,32 +120,138 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat
85120 $ "The image has an invalid size: width = { width } , height = { height } ") ;
86121 }
87122
88- qoiMetadata . Width = width ;
89- qoiMetadata . Height = height ;
90-
91- Size size = new ( ( int ) width , ( int ) height ) ;
92-
93123 int channels = stream . ReadByte ( ) ;
94124 if ( channels is - 1 or ( not 3 and not 4 ) )
95125 {
96126 ThrowInvalidImageContentException ( ) ;
97127 }
98128
99129 PixelTypeInfo pixelType = new ( 8 * channels ) ;
100- qoiMetadata . Channels = ( QoiChannels ) channels ;
101130
102131 int colorSpace = stream . ReadByte ( ) ;
103132 if ( colorSpace is - 1 or ( not 0 and not 1 ) )
104133 {
105134 ThrowInvalidImageContentException ( ) ;
106135 }
107136
108- qoiMetadata . ColorSpace = ( QoiColorSpace ) colorSpace ;
109-
110- return new ImageInfo ( pixelType , size , metadata ) ;
137+ this . header = new QoiHeader ( width , height , ( QoiChannels ) channels , ( QoiColorSpace ) colorSpace ) ;
111138 }
112139
113140 [ DoesNotReturn ]
114141 private static void ThrowInvalidImageContentException ( )
115142 => 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 ;
116257}
0 commit comments