@@ -107,17 +107,17 @@ func (r *KittyRenderer) GetLastImageID() uint32 {
107107}
108108
109109func reserveUnicodeImageNum (opts * KittyOptions ) (uint32 , error ) {
110- const maxUnicodeImageNum = 0xFFFFFF
110+ const maxUnicodeImageNum = uint64 ( ^ uint32 ( 0 ))
111111
112112 if opts != nil && opts .ImageNum > 0 {
113- if opts .ImageNum > maxUnicodeImageNum {
114- return 0 , fmt .Errorf ("unicode placeholders require image number <= 0xFFFFFF " )
113+ if uint64 ( opts .ImageNum ) > maxUnicodeImageNum {
114+ return 0 , fmt .Errorf ("unicode placeholders require image number <= 0xFFFFFFFF " )
115115 }
116116 return uint32 (opts .ImageNum ), nil
117117 }
118118
119119 imageNum := atomic .AddUint32 (& globalKittyImageNum , 1 )
120- if imageNum > maxUnicodeImageNum {
120+ if imageNum == 0 {
121121 atomic .StoreUint32 (& globalKittyImageNum , 1 )
122122 imageNum = 1
123123 }
@@ -167,30 +167,25 @@ func (r *KittyRenderer) Render(img image.Image, opts RenderOptions) (string, err
167167
168168 kittyOpts := opts .KittyOpts
169169
170- // Check if using Unicode placeholders - this requires a different two-step approach
170+ // Check if using Unicode placeholders - this uses a three-step flow:
171+ // transmit image data, create virtual placement (U=1), then draw placeholders.
171172 useUnicode := kittyOpts != nil && kittyOpts .UseUnicode
172173
173174 if useUnicode {
174- // Two-step process for Unicode placeholders (matches old termimg behavior):
175- // 1. Transmit image data (no display) using PNG format
176- // 2. Create placement with U=1 and explicit cols/rows
177- // 3. Generate placeholder characters
178-
179175 imageNum , err := reserveUnicodeImageNum (kittyOpts )
180176 if err != nil {
181177 return "" , err
182178 }
183179
184- // Encode as PNG for transmission
180+ // Encode as PNG for transfer.
185181 var buf bytes.Buffer
186182 if err := png .Encode (& buf , processed ); err != nil {
187183 return "" , fmt .Errorf ("failed to encode png: %w" , err )
188184 }
189185 data := buf .Bytes ()
190186 encoded := Base64Encode (data )
191187
192- // Step 1: Transmit image data in chunks (no display)
193- // Format: f=100 (PNG), t=d (direct), i=<id>, q=2 (quiet)
188+ // Step 1: Transmit image data in chunks (no immediate placement).
194189 remaining := encoded
195190 first := true
196191 for len (remaining ) > 0 {
@@ -217,15 +212,14 @@ func (r *KittyRenderer) Render(img image.Image, opts RenderOptions) (string, err
217212 output .WriteString (seq )
218213 }
219214
220- // Step 2: Create virtual placement with Unicode mode
221- // Format: a=p (placement), U=1 (unicode), i=<id>, c=<cols>, r=<rows>, q=2
215+ // Step 2: Create virtual placement that placeholders will reference.
222216 placementSeq := fmt .Sprintf ("\x1b _Ga=p,U=1,i=%d,c=%d,r=%d,q=2\x1b \\ " , imageNum , cols , rows )
223217 if inTmux () {
224218 placementSeq = wrapTmuxPassthrough (placementSeq )
225219 }
226220 output .WriteString (placementSeq )
227221
228- // Step 3: Generate placeholder characters
222+ // Step 3: Generate placeholder characters.
229223 placeholders := r .generateUnicodePlaceholders (imageNum , cols , rows )
230224 output .WriteString (placeholders )
231225
@@ -486,15 +480,9 @@ func (r *KittyRenderer) PlaceImageWithSize(imageID string, xCells, yCells, zInde
486480
487481 var output strings.Builder
488482
489- // Move to the starting position (absolute)
490- // Terminal coordinates are 1-based
491- output .WriteString (fmt .Sprintf ("\x1b [%d;%dH" , yCells + 1 , xCells + 1 ))
492-
493- // Generate the placeholder area string
494- // RenderPlaceholderAreaWithImageID handles color encoding and does NOT save/restore cursor
495- area := CreatePlaceholderArea (imgID , uint16 (heightCells ), uint16 (widthCells ))
496- placeholders := RenderPlaceholderAreaWithImageID (area , imgID )
497- output .WriteString (placeholders )
483+ // Render each row at an explicit absolute cursor position to avoid terminal-
484+ // specific newline behavior from shifting rows back to column 1.
485+ output .WriteString (renderAnchoredPlaceholderArea (imgID , xCells , yCells , widthCells , heightCells ))
498486
499487 if inTmux () {
500488 result := wrapTmuxPassthrough (output .String ())
@@ -505,6 +493,33 @@ func (r *KittyRenderer) PlaceImageWithSize(imageID string, xCells, yCells, zInde
505493 return err
506494}
507495
496+ // renderAnchoredPlaceholderArea emits each placeholder row with an absolute
497+ // cursor address instead of newlines so row alignment is stable across
498+ // terminals with different LF/CR handling.
499+ func renderAnchoredPlaceholderArea (imageID uint32 , xCells , yCells , widthCells , heightCells int ) string {
500+ if widthCells <= 0 || heightCells <= 0 {
501+ return ""
502+ }
503+
504+ area := CreatePlaceholderArea (imageID , uint16 (heightCells ), uint16 (widthCells ))
505+
506+ r := (imageID >> 16 ) & 0xFF
507+ g := (imageID >> 8 ) & 0xFF
508+ b := imageID & 0xFF
509+ colorStart := fmt .Sprintf ("\x1b [38;2;%d;%d;%dm" , r , g , b )
510+
511+ var builder strings.Builder
512+ for rowIdx , row := range area {
513+ builder .WriteString (fmt .Sprintf ("\x1b [%d;%dH" , yCells + rowIdx + 1 , xCells + 1 ))
514+ builder .WriteString (colorStart )
515+ for _ , placeholder := range row {
516+ builder .WriteString (placeholder )
517+ }
518+ builder .WriteString ("\x1b [39m" )
519+ }
520+ return builder .String ()
521+ }
522+
508523// SendFile optimizes transfer by sending file path instead of data when possible
509524func (r * KittyRenderer ) SendFile (filePath string , opts RenderOptions ) error {
510525 if filePath == "" {
@@ -686,19 +701,12 @@ func CreatePlaceholderArea(imageID uint32, rows, columns uint16) [][]string {
686701func RenderPlaceholderAreaWithImageID (area [][]string , imageID uint32 ) string {
687702 var builder strings.Builder
688703
689- // Build color encoding for the image ID
690- // For IDs <= 255, use 256-color mode for better compatibility
691- // For larger IDs, use 24-bit RGB encoding
692- var colorStart string
693- if imageID <= 255 {
694- colorStart = fmt .Sprintf ("\x1b [38;5;%dm" , imageID )
695- } else {
696- // Encode ID in RGB: R = id & 0xFF, G = (id >> 8) & 0xFF, B = (id >> 16) & 0xFF
697- r := imageID & 0xFF
698- g := (imageID >> 8 ) & 0xFF
699- b := (imageID >> 16 ) & 0xFF
700- colorStart = fmt .Sprintf ("\x1b [38;2;%d;%d;%dm" , r , g , b )
701- }
704+ // Encode ID in truecolor RGB: (R<<16 | G<<8 | B) == imageID&0xFFFFFF.
705+ // Using 38;2 avoids palette-index ambiguity from 38;5 in terminals.
706+ r := (imageID >> 16 ) & 0xFF
707+ g := (imageID >> 8 ) & 0xFF
708+ b := imageID & 0xFF
709+ colorStart := fmt .Sprintf ("\x1b [38;2;%d;%d;%dm" , r , g , b )
702710
703711 // Set foreground color to encode image ID
704712 builder .WriteString (colorStart )
0 commit comments