@@ -24,6 +24,14 @@ public class GifDecoderTests
2424 TestImages . Gif . Giphy , TestImages . Gif . Kumin
2525 ] ;
2626
27+ /// <summary>
28+ /// The four palette colors in the stored frame geometry fixtures.
29+ /// </summary>
30+ private static readonly Rgba32 [ ] GeometryTestColorTable =
31+ [
32+ new ( 255 , 0 , 0 ) , new ( 0 , 255 , 0 ) , new ( 0 , 0 , 255 ) , new ( 255 , 255 , 255 )
33+ ] ;
34+
2735 [ Theory ]
2836 [ WithFileCollection ( nameof ( MultiFrameTestFiles ) , PixelTypes . Rgba32 ) ]
2937 public void Decode_VerifyAllFrames < TPixel > ( TestImageProvider < TPixel > provider )
@@ -442,6 +450,127 @@ public void Decode_MalformedApplicationExtension_ThrowsWithStrict()
442450 } ) ;
443451 }
444452
453+ /// <summary>
454+ /// Invalid LZW table codes fail unless image data errors are explicitly ignored.
455+ /// Recovery retains completed rows and continues to the following animation frame.
456+ /// </summary>
457+ /// <param name="path">The stored GIF with an invalid first code or an invalid code after a clear.</param>
458+ /// <param name="completedRows">The number of valid rows before the invalid code.</param>
459+ /// <param name="frameCount">The number of stored animation frames.</param>
460+ [ Theory ]
461+ [ InlineData ( TestImages . Gif . LzwCodeOutsideTable , 0 , 1 ) ]
462+ [ InlineData ( TestImages . Gif . LzwCodeOutsideTableHiddenRow , 1 , 2 ) ]
463+ public void Decode_LzwCodePastCodeTables_RespectsIntegrityHandling ( string path , int completedRows , int frameCount )
464+ {
465+ byte [ ] data = TestFile . Create ( path ) . Bytes ;
466+
467+ foreach ( SegmentIntegrityHandling integrity in Enum . GetValues < SegmentIntegrityHandling > ( ) )
468+ {
469+ DecoderOptions options = new ( ) { SegmentIntegrityHandling = integrity } ;
470+ if ( integrity is not SegmentIntegrityHandling . IgnoreImageData )
471+ {
472+ Assert . Throws < InvalidImageContentException > ( ( ) =>
473+ {
474+ using Image < Rgba32 > image = Image . Load < Rgba32 > ( options , data ) ;
475+ } ) ;
476+
477+ continue ;
478+ }
479+
480+ using Image < Rgba32 > actual = Image . Load < Rgba32 > ( options , data ) ;
481+ Assert . Equal ( new Size ( 4 , frameCount == 1 ? 4 : 2 ) , actual . Size ) ;
482+ Assert . Equal ( frameCount , actual . Frames . Count ) ;
483+
484+ for ( int y = 0 ; y < actual . Height ; y ++ )
485+ {
486+ for ( int x = 0 ; x < actual . Width ; x ++ )
487+ {
488+ // A completed row is red. An undecoded row keeps the frame's transparent background.
489+ Assert . Equal ( y < completedRows ? new Rgba32 ( 255 , 0 , 0 ) : default , actual [ x , y ] ) ;
490+
491+ if ( frameCount == 2 )
492+ {
493+ // The complete green frame after the corrupt frame proves that sub-block recovery resumed correctly.
494+ Assert . Equal ( new Rgba32 ( 0 , 255 , 0 ) , actual . Frames [ 1 ] [ x , y ] ) ;
495+ }
496+ }
497+ }
498+ }
499+ }
500+
501+ /// <summary>
502+ /// Identify uses LZW index skipping and must apply the same image data policy as full decoding.
503+ /// </summary>
504+ /// <param name="path">The stored GIF with an invalid table code.</param>
505+ /// <param name="frameCount">The number of stored animation frames.</param>
506+ [ Theory ]
507+ [ InlineData ( TestImages . Gif . LzwCodeOutsideTable , 1 ) ]
508+ [ InlineData ( TestImages . Gif . LzwCodeOutsideTableHiddenRow , 2 ) ]
509+ public void Identify_LzwCodePastCodeTables_RespectsIntegrityHandling ( string path , int frameCount )
510+ {
511+ byte [ ] data = TestFile . Create ( path ) . Bytes ;
512+
513+ foreach ( SegmentIntegrityHandling integrity in Enum . GetValues < SegmentIntegrityHandling > ( ) )
514+ {
515+ DecoderOptions options = new ( ) { SegmentIntegrityHandling = integrity } ;
516+ if ( integrity is not SegmentIntegrityHandling . IgnoreImageData )
517+ {
518+ Assert . Throws < InvalidImageContentException > ( ( ) => Image . Identify ( options , data ) ) ;
519+ continue ;
520+ }
521+
522+ ImageInfo info = Image . Identify ( options , data ) ;
523+ Assert . Equal ( new Size ( 4 , frameCount == 1 ? 4 : 2 ) , info . Size ) ;
524+ Assert . Equal ( frameCount , info . FrameMetadataCollection . Count ) ;
525+ }
526+ }
527+
528+ /// <summary>
529+ /// Clips stored frame rows to the logical screen without changing interlace order or later frame parsing.
530+ /// </summary>
531+ /// <param name="path">The stored GIF with a four-color row pattern and a second frame containing one white pixel.</param>
532+ /// <param name="width">The first frame's stored width.</param>
533+ /// <param name="height">The first frame's stored height.</param>
534+ /// <param name="left">The first frame's left offset.</param>
535+ /// <param name="top">The first frame's top offset.</param>
536+ [ Theory ]
537+ [ InlineData ( TestImages . Gif . Frame20x20At0x0Interlaced , 20 , 20 , 0 , 0 ) ]
538+ [ InlineData ( TestImages . Gif . Frame20x20At0x0Sequential , 20 , 20 , 0 , 0 ) ]
539+ [ InlineData ( TestImages . Gif . Frame10x10At0x5Interlaced , 10 , 10 , 0 , 5 ) ]
540+ [ InlineData ( TestImages . Gif . Frame10x10At0x5Sequential , 10 , 10 , 0 , 5 ) ]
541+ [ InlineData ( TestImages . Gif . Frame10x10At5x3Interlaced , 10 , 10 , 5 , 3 ) ]
542+ [ InlineData ( TestImages . Gif . Frame10x10At0x10Interlaced , 10 , 10 , 0 , 10 ) ]
543+ [ InlineData ( TestImages . Gif . Frame10x10At0x10Sequential , 10 , 10 , 0 , 10 ) ]
544+ [ InlineData ( TestImages . Gif . Frame10x20At0x8Interlaced , 10 , 20 , 0 , 8 ) ]
545+ [ InlineData ( TestImages . Gif . Frame10x20At0x9Interlaced , 10 , 20 , 0 , 9 ) ]
546+ [ InlineData ( TestImages . Gif . Frame10x1At0x0Interlaced , 10 , 1 , 0 , 0 ) ]
547+ [ InlineData ( TestImages . Gif . Frame10x2At0x0Interlaced , 10 , 2 , 0 , 0 ) ]
548+ [ InlineData ( TestImages . Gif . Frame10x3At0x7Interlaced , 10 , 3 , 0 , 7 ) ]
549+ [ InlineData ( TestImages . Gif . Frame10x4At0x0Interlaced , 10 , 4 , 0 , 0 ) ]
550+ [ InlineData ( TestImages . Gif . Frame4x4At12x0Interlaced , 4 , 4 , 12 , 0 ) ]
551+ [ InlineData ( TestImages . Gif . Frame4x4At12x0Sequential , 4 , 4 , 12 , 0 ) ]
552+ public void Decode_FrameExceedingLogicalScreen_WritesOnlyRowsInsideCanvas ( string path , int width , int height , int left , int top )
553+ {
554+ using Image < Rgba32 > image = Image . Load < Rgba32 > ( TestFile . Create ( path ) . Bytes ) ;
555+
556+ Assert . Equal ( new Size ( 10 , 10 ) , image . Size ) ;
557+ Assert . Equal ( 2 , image . Frames . Count ) ;
558+
559+ for ( int y = 0 ; y < image . Height ; y ++ )
560+ {
561+ for ( int x = 0 ; x < image . Width ; x ++ )
562+ {
563+ // Frame row n contains palette index n modulo four. Pixels outside its rectangle stay transparent.
564+ bool inFrame = x >= left && x < left + width && y >= top && y < top + height ;
565+ Rgba32 expected = inFrame ? GeometryTestColorTable [ ( y - top ) % 4 ] : default ;
566+ Assert . Equal ( expected , image [ x , y ] ) ;
567+ }
568+ }
569+
570+ // Even when the first frame is clipped or fully hidden, its data must not consume the next image descriptor.
571+ Assert . Equal ( new Rgba32 ( 255 , 255 , 255 ) , image . Frames [ 1 ] [ 0 , 0 ] ) ;
572+ }
573+
445574 private static byte [ ] CreateGifWithMalformedApplicationExtension ( )
446575 {
447576 // The application extension declares a 5-byte application block (0x05), but GIF application
0 commit comments