@@ -526,14 +526,35 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
526526 }
527527
528528 private async _decodeAndDisplay ( image : IKittyImageData , cmd : IKittyCommand ) : Promise < void > {
529- const bitmap = await this . _createBitmap ( image ) ;
529+ let bitmap = await this . _createBitmap ( image ) ;
530+
531+ const cropX = Math . max ( 0 , cmd . x ?? 0 ) ;
532+ const cropY = Math . max ( 0 , cmd . y ?? 0 ) ;
533+ const cropW = cmd . sourceWidth ?? ( bitmap . width - cropX ) ;
534+ const cropH = cmd . sourceHeight ?? ( bitmap . height - cropY ) ;
535+
536+ const maxCropW = Math . max ( 0 , bitmap . width - cropX ) ;
537+ const maxCropH = Math . max ( 0 , bitmap . height - cropY ) ;
538+ const finalCropW = Math . max ( 0 , Math . min ( cropW , maxCropW ) ) ;
539+ const finalCropH = Math . max ( 0 , Math . min ( cropH , maxCropH ) ) ;
540+
541+ if ( finalCropW === 0 || finalCropH === 0 ) {
542+ bitmap . close ( ) ;
543+ throw new Error ( 'invalid source rectangle' ) ;
544+ }
545+
546+ if ( cropX !== 0 || cropY !== 0 || finalCropW !== bitmap . width || finalCropH !== bitmap . height ) {
547+ const cropped = await createImageBitmap ( bitmap , cropX , cropY , finalCropW , finalCropH ) ;
548+ bitmap . close ( ) ;
549+ bitmap = cropped ;
550+ }
530551
531552 const cw = this . _renderer . dimensions ?. css . cell . width || CELL_SIZE_DEFAULT . width ;
532553 const ch = this . _renderer . dimensions ?. css . cell . height || CELL_SIZE_DEFAULT . height ;
533554
534555 // Per spec: c/r default to image's natural cell dimensions
535- const imgCols = cmd . columns ?? Math . ceil ( bitmap . width / cw ) ;
536- const imgRows = cmd . rows ?? Math . ceil ( bitmap . height / ch ) ;
556+ let imgCols = cmd . columns ?? Math . ceil ( bitmap . width / cw ) ;
557+ let imgRows = cmd . rows ?? Math . ceil ( bitmap . height / ch ) ;
537558
538559 let w = bitmap . width ;
539560 let h = bitmap . height ;
@@ -545,6 +566,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
545566 }
546567
547568 if ( w * h > this . _opts . pixelLimit ) {
569+ bitmap . close ( ) ;
548570 throw new Error ( 'image exceeds pixel limit' ) ;
549571 }
550572
@@ -561,15 +583,45 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
561583 const wantsBottom = cmd . zIndex !== undefined && cmd . zIndex < 0 ;
562584 const layer : ImageLayer = wantsBottom ? 'bottom' : 'top' ;
563585
564- const zIndex = cmd . zIndex ?? 0 ;
586+ let finalBitmap = bitmap ;
565587 if ( w !== bitmap . width || h !== bitmap . height ) {
566- const resized = await createImageBitmap ( bitmap , { resizeWidth : w , resizeHeight : h } ) ;
588+ finalBitmap = await createImageBitmap ( bitmap , { resizeWidth : w , resizeHeight : h } ) ;
567589 bitmap . close ( ) ;
568- this . _kittyStorage . addImage ( image . id , resized , true , layer , zIndex ) ;
569- } else {
570- this . _kittyStorage . addImage ( image . id , bitmap , true , layer , zIndex ) ;
571590 }
572591
592+ // Per spec: X/Y are pixel offsets within the first cell, so clamp to cell dimensions
593+ const xOffset = Math . min ( Math . max ( 0 , cmd . xOffset ?? 0 ) , cw - 1 ) ;
594+ const yOffset = Math . min ( Math . max ( 0 , cmd . yOffset ?? 0 ) , ch - 1 ) ;
595+ if ( xOffset !== 0 || yOffset !== 0 ) {
596+ const offsetCanvas = ImageRenderer . createCanvas ( window . document , finalBitmap . width + xOffset , finalBitmap . height + yOffset ) ;
597+ const offsetCtx = offsetCanvas . getContext ( '2d' ) ;
598+ if ( ! offsetCtx ) {
599+ finalBitmap . close ( ) ;
600+ throw new Error ( 'Failed to create offset canvas context' ) ;
601+ }
602+ offsetCtx . drawImage ( finalBitmap , xOffset , yOffset ) ;
603+
604+ const offsetBitmap = await createImageBitmap ( offsetCanvas ) ;
605+ offsetCanvas . width = offsetCanvas . height = 0 ;
606+ finalBitmap . close ( ) ;
607+ finalBitmap = offsetBitmap ;
608+ w = finalBitmap . width ;
609+ h = finalBitmap . height ;
610+ if ( w * h > this . _opts . pixelLimit ) {
611+ finalBitmap . close ( ) ;
612+ throw new Error ( 'image exceeds pixel limit' ) ;
613+ }
614+ if ( cmd . columns === undefined ) {
615+ imgCols = Math . ceil ( finalBitmap . width / cw ) ;
616+ }
617+ if ( cmd . rows === undefined ) {
618+ imgRows = Math . ceil ( finalBitmap . height / ch ) ;
619+ }
620+ }
621+
622+ const zIndex = cmd . zIndex ?? 0 ;
623+ this . _kittyStorage . addImage ( image . id , finalBitmap , true , layer , zIndex ) ;
624+
573625 // Kitty cursor movement
574626 // Per spec: cursor placed at first column after last image column,
575627 // on the last row of the image. C=1 means don't move cursor.
0 commit comments