@@ -42,6 +42,8 @@ interface IDimensions {
4242const KITTY_BLACK_1X1_BASE64 = readFileSync ( './addons/addon-image/fixture/kitty/black-1x1.png' ) . toString ( 'base64' ) ;
4343const KITTY_BLACK_1X1_BYTES = Array . from ( readFileSync ( './addons/addon-image/fixture/kitty/black-1x1.png' ) ) ;
4444const KITTY_RGB_3X1_BASE64 = readFileSync ( './addons/addon-image/fixture/kitty/rgb-3x1.png' ) . toString ( 'base64' ) ;
45+ const KITTY_MULTICOLOR_200X100_BASE64 = readFileSync ( './addons/addon-image/fixture/kitty/multicolor-200x100.png' ) . toString ( 'base64' ) ;
46+ const KITTY_MULTICOLOR_200X100_BYTES = Array . from ( readFileSync ( './addons/addon-image/fixture/kitty/multicolor-200x100.png' ) ) ;
4547
4648// Raw RGB pixel data (f=24): 3 bytes per pixel, no header — requires s= and v=
4749const RAW_RGB_1X1_BLACK = Buffer . from ( [ 0 , 0 , 0 ] ) . toString ( 'base64' ) ;
@@ -566,6 +568,225 @@ test.describe('Kitty Graphics Protocol', () => {
566568 } ) ;
567569 } ) ;
568570
571+ test . describe ( 'Larger image (200x100 multicolor PNG)' , ( ) => {
572+ test . describe ( 'Basic transmission and storage' , ( ) => {
573+ test ( 'stores 200x100 PNG with a=T' , async ( ) => {
574+ await ctx . proxy . write ( `\x1b_Ga=T,f=100;${ KITTY_MULTICOLOR_200X100_BASE64 } \x1b\\` ) ;
575+ await timeout ( 200 ) ;
576+ strictEqual ( await getImageStorageLength ( ) , 1 ) ;
577+ deepStrictEqual ( await getOrigSize ( 1 ) , [ 200 , 100 ] ) ;
578+ } ) ;
579+
580+ test ( 'transmit only (a=t) stores 200x100 image without display' , async ( ) => {
581+ await ctx . proxy . write ( `\x1b_Ga=t,f=100;${ KITTY_MULTICOLOR_200X100_BASE64 } \x1b\\` ) ;
582+ await timeout ( 200 ) ;
583+ strictEqual ( await ctx . page . evaluate ( `window.imageAddon._handlers.get('kitty').images.size` ) , 1 ) ;
584+ } ) ;
585+
586+ test ( 'stores with specified image ID' , async ( ) => {
587+ await ctx . proxy . write ( `\x1b_Ga=t,f=100,i=400;${ KITTY_MULTICOLOR_200X100_BASE64 } \x1b\\` ) ;
588+ await timeout ( 200 ) ;
589+ strictEqual ( await ctx . page . evaluate ( `window.imageAddon._handlers.get('kitty').images.has(400)` ) , true ) ;
590+ } ) ;
591+ } ) ;
592+
593+ test . describe ( 'Chunked transmission' , ( ) => {
594+ test ( 'handles 2-chunk transmission' , async ( ) => {
595+ const half = Math . floor ( KITTY_MULTICOLOR_200X100_BASE64 . length / 2 ) ;
596+ const part1 = KITTY_MULTICOLOR_200X100_BASE64 . substring ( 0 , half ) ;
597+ const part2 = KITTY_MULTICOLOR_200X100_BASE64 . substring ( half ) ;
598+
599+ await ctx . proxy . write ( `\x1b_Ga=T,f=100,i=500,m=1;${ part1 } \x1b\\` ) ;
600+ await timeout ( 50 ) ;
601+ strictEqual ( await getImageStorageLength ( ) , 0 ) ;
602+
603+ await ctx . proxy . write ( `\x1b_Ga=T,f=100,i=500;${ part2 } \x1b\\` ) ;
604+ await timeout ( 200 ) ;
605+ strictEqual ( await getImageStorageLength ( ) , 1 ) ;
606+ deepStrictEqual ( await getOrigSize ( 1 ) , [ 200 , 100 ] ) ;
607+ } ) ;
608+
609+ test ( 'handles 3-chunk transmission' , async ( ) => {
610+ const third = Math . floor ( KITTY_MULTICOLOR_200X100_BASE64 . length / 3 ) ;
611+ const p1 = KITTY_MULTICOLOR_200X100_BASE64 . substring ( 0 , third ) ;
612+ const p2 = KITTY_MULTICOLOR_200X100_BASE64 . substring ( third , third * 2 ) ;
613+ const p3 = KITTY_MULTICOLOR_200X100_BASE64 . substring ( third * 2 ) ;
614+
615+ await ctx . proxy . write ( `\x1b_Ga=T,f=100,i=501,m=1;${ p1 } \x1b\\` ) ;
616+ await timeout ( 50 ) ;
617+ await ctx . proxy . write ( `\x1b_Ga=T,f=100,i=501,m=1;${ p2 } \x1b\\` ) ;
618+ await timeout ( 50 ) ;
619+ await ctx . proxy . write ( `\x1b_Ga=T,f=100,i=501;${ p3 } \x1b\\` ) ;
620+ await timeout ( 200 ) ;
621+ strictEqual ( await getImageStorageLength ( ) , 1 ) ;
622+ deepStrictEqual ( await getOrigSize ( 1 ) , [ 200 , 100 ] ) ;
623+ } ) ;
624+
625+ test ( 'verifies chunked data assembles correctly' , async ( ) => {
626+ const half = Math . floor ( KITTY_MULTICOLOR_200X100_BASE64 . length / 2 ) ;
627+ const part1 = KITTY_MULTICOLOR_200X100_BASE64 . substring ( 0 , half ) ;
628+ const part2 = KITTY_MULTICOLOR_200X100_BASE64 . substring ( half ) ;
629+
630+ await ctx . proxy . write ( `\x1b_Ga=t,f=100,i=502,m=1;${ part1 } \x1b\\` ) ;
631+ await ctx . proxy . write ( `\x1b_Ga=t,f=100,i=502;${ part2 } \x1b\\` ) ;
632+ await timeout ( 200 ) ;
633+
634+ const storedData = await ctx . page . evaluate ( async ( ) => {
635+ const blob = ( window as any ) . imageAddon . _handlers . get ( 'kitty' ) . images . get ( 502 ) . data ;
636+ const buffer = await blob . arrayBuffer ( ) ;
637+ return Array . from ( new Uint8Array ( buffer ) ) ;
638+ } ) ;
639+ deepStrictEqual ( storedData , KITTY_MULTICOLOR_200X100_BYTES ) ;
640+ } ) ;
641+ } ) ;
642+
643+ test . describe ( 'Cursor positioning' , ( ) => {
644+ test ( 'cursor advances past multi-cell image' , async ( ) => {
645+ const dim = await getDimensions ( ) ;
646+ await ctx . proxy . write ( `\x1b_Ga=T,f=100;${ KITTY_MULTICOLOR_200X100_BASE64 } \x1b\\` ) ;
647+ await timeout ( 200 ) ;
648+
649+ const expectedCols = Math . ceil ( 200 / dim . cellWidth ) ;
650+ const expectedRows = Math . ceil ( 100 / dim . cellHeight ) - 1 ;
651+ const cursor = await getCursor ( ) ;
652+ strictEqual ( cursor [ 0 ] , expectedCols , 'cursor should advance by image columns' ) ;
653+ strictEqual ( cursor [ 1 ] , expectedRows , 'cursor should be on last row of image' ) ;
654+ } ) ;
655+
656+ test ( 'cursor does not move with C=1' , async ( ) => {
657+ await ctx . proxy . write ( `\x1b_Ga=T,f=100,C=1;${ KITTY_MULTICOLOR_200X100_BASE64 } \x1b\\` ) ;
658+ await timeout ( 200 ) ;
659+ deepStrictEqual ( await getCursor ( ) , [ 0 , 0 ] ) ;
660+ } ) ;
661+
662+ test ( 'cursor uses explicit c and r over image dimensions' , async ( ) => {
663+ await ctx . proxy . write ( `\x1b_Ga=T,f=100,c=10,r=5;${ KITTY_MULTICOLOR_200X100_BASE64 } \x1b\\` ) ;
664+ await timeout ( 200 ) ;
665+ deepStrictEqual ( await getCursor ( ) , [ 10 , 4 ] ) ;
666+ } ) ;
667+ } ) ;
668+
669+ test . describe ( 'Pixel verification' , ( ) => {
670+ // The 200x100 image has 20 colored rectangles in a 10x2 grid.
671+ // Each rectangle is 20px wide x 50px tall.
672+ // Top row (y=0..49): Red, Orange, Yellow, Lime, Green, Cyan, SkyBlue, Blue, Purple, Magenta
673+ // Bottom row (y=50..99): Pink, Brown, Maroon, Olive, Teal, Navy, Gray, DarkGray, LightGray, White
674+
675+ test ( 'renders red rectangle at top-left origin (0,0)' , async ( ) => {
676+ await ctx . proxy . write ( `\x1b_Ga=T,f=100;${ KITTY_MULTICOLOR_200X100_BASE64 } \x1b\\` ) ;
677+ await timeout ( 200 ) ;
678+ // Pixel (0,0) is in the first rectangle: Red
679+ deepStrictEqual ( await getPixel ( 0 , 0 , 0 , 0 ) , [ 255 , 0 , 0 , 255 ] ) ;
680+ } ) ;
681+
682+ test ( 'renders top row colors at rectangle centers' , async ( ) => {
683+ await ctx . proxy . write ( `\x1b_Ga=T,f=100;${ KITTY_MULTICOLOR_200X100_BASE64 } \x1b\\` ) ;
684+ await timeout ( 200 ) ;
685+
686+ // Sample center of each top-row rectangle (y=25, x=10,30,50,...,190)
687+ // All within the first cell row, so we read from the canvas at cell (0,0)
688+ // Red at x=10
689+ deepStrictEqual ( await getPixel ( 0 , 0 , 10 , 25 ) , [ 255 , 0 , 0 , 255 ] ) ;
690+ // Orange at x=30
691+ deepStrictEqual ( await getPixel ( 0 , 0 , 30 , 25 ) , [ 255 , 128 , 0 , 255 ] ) ;
692+ // Yellow at x=50
693+ deepStrictEqual ( await getPixel ( 0 , 0 , 50 , 25 ) , [ 255 , 255 , 0 , 255 ] ) ;
694+ // Lime at x=70
695+ deepStrictEqual ( await getPixel ( 0 , 0 , 70 , 25 ) , [ 0 , 255 , 0 , 255 ] ) ;
696+ // Green at x=90
697+ deepStrictEqual ( await getPixel ( 0 , 0 , 90 , 25 ) , [ 0 , 128 , 0 , 255 ] ) ;
698+ } ) ;
699+
700+ test ( 'renders bottom row colors at rectangle centers' , async ( ) => {
701+ await ctx . proxy . write ( `\x1b_Ga=T,f=100;${ KITTY_MULTICOLOR_200X100_BASE64 } \x1b\\` ) ;
702+ await timeout ( 200 ) ;
703+
704+ // Bottom row starts at y=50. Center at y=75.
705+ // Pink at x=10
706+ deepStrictEqual ( await getPixel ( 0 , 0 , 10 , 75 ) , [ 255 , 192 , 203 , 255 ] ) ;
707+ // Brown at x=30
708+ deepStrictEqual ( await getPixel ( 0 , 0 , 30 , 75 ) , [ 165 , 42 , 42 , 255 ] ) ;
709+ // Maroon at x=50
710+ deepStrictEqual ( await getPixel ( 0 , 0 , 50 , 75 ) , [ 128 , 0 , 0 , 255 ] ) ;
711+ // Olive at x=70
712+ deepStrictEqual ( await getPixel ( 0 , 0 , 70 , 75 ) , [ 128 , 128 , 0 , 255 ] ) ;
713+ // Teal at x=90
714+ deepStrictEqual ( await getPixel ( 0 , 0 , 90 , 75 ) , [ 0 , 128 , 128 , 255 ] ) ;
715+ } ) ;
716+
717+ test ( 'renders correct colors at rectangle boundaries' , async ( ) => {
718+ await ctx . proxy . write ( `\x1b_Ga=T,f=100;${ KITTY_MULTICOLOR_200X100_BASE64 } \x1b\\` ) ;
719+ await timeout ( 200 ) ;
720+
721+ // Last pixel of first rectangle (x=19, y=0): still Red
722+ deepStrictEqual ( await getPixel ( 0 , 0 , 19 , 0 ) , [ 255 , 0 , 0 , 255 ] ) ;
723+ // First pixel of second rectangle (x=20, y=0): Orange
724+ deepStrictEqual ( await getPixel ( 0 , 0 , 20 , 0 ) , [ 255 , 128 , 0 , 255 ] ) ;
725+ // Last pixel of top row (x=199, y=49): Magenta
726+ deepStrictEqual ( await getPixel ( 0 , 0 , 199 , 49 ) , [ 255 , 0 , 255 , 255 ] ) ;
727+ // First pixel of bottom row (x=0, y=50): Pink
728+ deepStrictEqual ( await getPixel ( 0 , 0 , 0 , 50 ) , [ 255 , 192 , 203 , 255 ] ) ;
729+ } ) ;
730+
731+ test ( 'renders correct color at bottom-right corner' , async ( ) => {
732+ await ctx . proxy . write ( `\x1b_Ga=T,f=100;${ KITTY_MULTICOLOR_200X100_BASE64 } \x1b\\` ) ;
733+ await timeout ( 200 ) ;
734+
735+ // Bottom-right corner (x=199, y=99): White
736+ deepStrictEqual ( await getPixel ( 0 , 0 , 199 , 99 ) , [ 255 , 255 , 255 , 255 ] ) ;
737+ } ) ;
738+
739+ test ( 'renders a strip of top-row pixels via getPixels' , async ( ) => {
740+ await ctx . proxy . write ( `\x1b_Ga=T,f=100;${ KITTY_MULTICOLOR_200X100_BASE64 } \x1b\\` ) ;
741+ await timeout ( 200 ) ;
742+
743+ // Read 3 pixels starting at x=18 y=0, spanning the Red/Orange boundary
744+ const pixels = await getPixels ( 0 , 0 , 18 , 0 , 3 , 1 ) ;
745+ // x=18,19 -> Red; x=20 -> Orange
746+ deepStrictEqual ( pixels ?. slice ( 0 , 4 ) , [ 255 , 0 , 0 , 255 ] ) ; // x=18: Red
747+ deepStrictEqual ( pixels ?. slice ( 4 , 8 ) , [ 255 , 0 , 0 , 255 ] ) ; // x=19: Red
748+ deepStrictEqual ( pixels ?. slice ( 8 , 12 ) , [ 255 , 128 , 0 , 255 ] ) ; // x=20: Orange
749+ } ) ;
750+ } ) ;
751+
752+ test . describe ( 'Query support' , ( ) => {
753+ test ( 'responds with OK for valid 200x100 PNG query' , async ( ) => {
754+ await ctx . page . evaluate ( ( ) => {
755+ ( window as any ) . kittyResponse = '' ;
756+ ( window as any ) . term . onData ( ( data : string ) => { ( window as any ) . kittyResponse = data ; } ) ;
757+ } ) ;
758+
759+ await ctx . proxy . write ( `\x1b_Gi=600,a=q,f=100;${ KITTY_MULTICOLOR_200X100_BASE64 } \x1b\\` ) ;
760+ await timeout ( 200 ) ;
761+
762+ const response = await ctx . page . evaluate ( 'window.kittyResponse' ) ;
763+ strictEqual ( response , '\x1b_Gi=600;OK\x1b\\' ) ;
764+ } ) ;
765+
766+ test ( 'query does not store the 200x100 image' , async ( ) => {
767+ await ctx . page . evaluate ( ( ) => {
768+ ( window as any ) . term . onData ( ( ) => { /* consume response */ } ) ;
769+ } ) ;
770+
771+ await ctx . proxy . write ( `\x1b_Gi=601,a=q,f=100;${ KITTY_MULTICOLOR_200X100_BASE64 } \x1b\\` ) ;
772+ await timeout ( 200 ) ;
773+ strictEqual ( await ctx . page . evaluate ( `window.imageAddon._handlers.get('kitty').images.has(601)` ) , false ) ;
774+ } ) ;
775+ } ) ;
776+
777+ test . describe ( 'Delete commands' , ( ) => {
778+ test ( 'delete removes 200x100 image by id' , async ( ) => {
779+ await ctx . proxy . write ( `\x1b_Ga=t,f=100,i=700;${ KITTY_MULTICOLOR_200X100_BASE64 } \x1b\\` ) ;
780+ await timeout ( 200 ) ;
781+ strictEqual ( await ctx . page . evaluate ( `window.imageAddon._handlers.get('kitty').images.has(700)` ) , true ) ;
782+
783+ await ctx . proxy . write ( `\x1b_Ga=d,i=700\x1b\\` ) ;
784+ await timeout ( 50 ) ;
785+ strictEqual ( await ctx . page . evaluate ( `window.imageAddon._handlers.get('kitty').images.has(700)` ) , false ) ;
786+ } ) ;
787+ } ) ;
788+ } ) ;
789+
569790 test . describe ( 'Raw RGB pixel format (f=24)' , ( ) => {
570791 test . describe ( 'Pixel verification' , ( ) => {
571792 test ( 'renders 1x1 black pixel with alpha set to 255' , async ( ) => {
0 commit comments