1 #include "internal.h"
2 #include "base64.h"
3 #ifdef USE_DEFLATE
4 #include <libdeflate.h>
5 #endif
6 
7 // Kitty has its own bitmap graphics protocol, rather superior to DEC Sixel.
8 // A header is written with various directives, followed by a number of
9 // chunks. Each chunk carries up to 4096B of base64-encoded pixels. Bitmaps
10 // can be ordered on a z-axis, with text at a logical z=0. A bitmap at a
11 // positive coordinate will be drawn above text; a negative coordinate will
12 // be drawn below text. It is not possible for a single bitmap to be under
13 // some text and above other text; since we need both, we draw at a positive
14 // coordinate (above all text), and cut out sections by setting their alpha
15 // values to 0. We thus require RGBA, meaning 768 pixels per 4096B chunk
16 // (768pix * 4Bpp * 4/3 base64 overhead == 4096B).
17 //
18 // 0.20.0 introduced an animation protocol which drastically reduces the
19 // bandwidth necessary for wipe-and-rebuild. 0.21.1 improved it further.
20 // we thus have three strategies:
21 //
22 // pre-0.20.0: keep an auxvec for each wiped cell, with a byte per pixel.
23 //  on wipe, copy the alphas into the auxvec, and set them to 0 in the
24 //  encoded graphic. on rebuild, rewrite the alphas from the auxvec. both
25 //  operations require delicate edits directly to the encoded form. the
26 //  graphic is updated by completely retransmitting it.
27 //
28 // 0.20.0: we make a copy of the RGBA data, populating all auxvecs upon
29 //  blit. to wipe, we generate a cell's woth of 0s, and merge them into
30 //  the existing image. to rebuild, we merge the original data into the
31 //  existing image. this cuts down on bandwidth--unchanged cells are not
32 //  retransmitted. it does require a fairly expensive copy of the source,
33 //  even though we might never use it.
34 //
35 // 0.21.1+: our auxvecs are now a single word -- the sprixcell state prior
36 //  to annihilation. we never need retransmit the original RGBA on
37 //  restore, as we can instead use composition with reflection.
38 //
39 // if a graphic needs be moved, we can move it with a control operation,
40 // rather than erasing it and redrawing it manually.
41 //
42 // It has some interesting features of which we do not yet take advantage:
43 //  * in-terminal scaling of image data (we prescale)
44 //  * subregion display of a transmitted bitmap
45 //
46 // https://sw.kovidgoyal.net/kitty/graphics-protocol.html
47 
48 // convert a base64 character into its equivalent integer 0..63
49 static inline int
b64idx(char b64)50 b64idx(char b64){
51   if(b64 >= 'A' && b64 <= 'Z'){
52     return b64 - 'A';
53   }else if(b64 >= 'a' && b64 <= 'z'){
54     return b64 - 'a' + 26;
55   }else if(b64 >= '0' && b64 <= '9'){
56     return b64 - '0' + 52;
57   }else if(b64 == '+'){
58     return 62;
59   }else{
60     return 63;
61   }
62 }
63 
64 // null out part of a triplet (a triplet is 3 pixels, which map to 12 bytes, which map to
65 // 16 bytes when base64 encoded). skip the initial |skip| pixels, and null out a maximum
66 // of |max| pixels after that. returns the number of pixels nulled out. |max| must be
67 // positive. |skip| must be non-negative, and less than 3. |pleft| is the number of pixels
68 // available in the chunk. the RGB is 24 bits, and thus 4 base64 bytes, but
69 // unfortunately don't always start on a byte boundary =[.
70 // 0: R1(0..5)
71 // 1: R1(6..7), G1(0..3)
72 // 2: G1(4..7), B1(0..1)
73 // 3: B1(2..7)
74 // 4: A1(0..5)
75 // 5: A1(6..7), R2(0..3)
76 // 6: R2(4..7), G2(0..1)
77 // 7: G2(2..7)
78 // 8: B2(0..5)
79 // 9: B2(6..7), A2(0..3)
80 // A: A2(4..7), R3(0..1)
81 // B: R3(2..7)
82 // C: G3(0..5)
83 // D: G3(6..7), B3(0..3)
84 // E: B3(4..7), A3(0..1)
85 // F: A3(2..7)
86 // so we will only ever zero out bytes 4, 5, 9, A, E, and F
87 
88 // get the first alpha from the triplet
89 static inline uint8_t
triplet_alpha1(const char * triplet)90 triplet_alpha1(const char* triplet){
91   uint8_t c1 = b64idx(triplet[0x4]);
92   uint8_t c2 = b64idx(triplet[0x5]);
93   return (c1 << 2u) | ((c2 & 0x30) >> 4);
94 }
95 
96 static inline uint8_t
triplet_alpha2(const char * triplet)97 triplet_alpha2(const char* triplet){
98   uint8_t c1 = b64idx(triplet[0x9]);
99   uint8_t c2 = b64idx(triplet[0xA]);
100   return ((c1 & 0xf) << 4u) | ((c2 & 0x3c) >> 2);
101 }
102 
103 static inline uint8_t
triplet_alpha3(const char * triplet)104 triplet_alpha3(const char* triplet){
105   uint8_t c1 = b64idx(triplet[0xE]);
106   uint8_t c2 = b64idx(triplet[0xF]);
107   return ((c1 & 0x3) << 6u) | c2;
108 }
109 
110 static inline int
kitty_null(char * triplet,int skip,int max,int pleft,uint8_t * auxvec)111 kitty_null(char* triplet, int skip, int max, int pleft, uint8_t* auxvec){
112 //fprintf(stderr, "SKIP/MAX/PLEFT %d/%d/%d\n", skip, max, pleft);
113   if(pleft > 3){
114     pleft = 3;
115   }
116   if(max + skip > pleft){
117     max = pleft - skip;
118   }
119 //fprintf(stderr, "alpha-nulling %d after %d\n", max, skip);
120   if(skip == 0){
121     auxvec[0] = triplet_alpha1(triplet);
122     triplet[0x4] = b64subs[0];
123     triplet[0x5] = b64subs[b64idx(triplet[0x5]) & 0xf];
124     if(max > 1){
125       auxvec[1] = triplet_alpha2(triplet);
126       triplet[0x9] = b64subs[b64idx(triplet[0x9]) & 0x30];
127       triplet[0xA] = b64subs[b64idx(triplet[0xA]) & 0x3];
128     }
129     if(max == 3){
130       auxvec[2] = triplet_alpha3(triplet);
131       triplet[0xE] = b64subs[b64idx(triplet[0xE]) & 0x3c];
132       triplet[0xF] = b64subs[0];
133     }
134   }else if(skip == 1){
135     auxvec[0] = triplet_alpha2(triplet);
136     triplet[0x9] = b64subs[b64idx(triplet[0x9]) & 0x30];
137     triplet[0xA] = b64subs[b64idx(triplet[0xA]) & 0x3];
138     if(max == 2){
139       auxvec[1] = triplet_alpha3(triplet);
140       triplet[0xE] = b64subs[b64idx(triplet[0xE]) & 0x3c];
141       triplet[0xF] = b64subs[0];
142     }
143   }else{ // skip == 2
144     auxvec[0] = triplet_alpha3(triplet);
145     triplet[0xE] = b64subs[b64idx(triplet[0xE]) & 0x3c];
146     triplet[0xF] = b64subs[0];
147   }
148   return max;
149 }
150 
151 // restore part of a triplet (a triplet is 3 pixels, which map to 12 bytes,
152 // which map to 16 bytes when base64 encoded). skip the initial |skip| pixels,
153 // and restore a maximum of |max| pixels after that. returns the number of
154 // pixels restored. |max| must be positive. |skip| must be non-negative, and
155 // less than 3. |pleft| is the number of pixels available in the chunk.
156 // |state| is set to MIXED if we find transparent pixels.
157 static inline int
kitty_restore(char * triplet,int skip,int max,int pleft,const uint8_t * auxvec,sprixcell_e * state)158 kitty_restore(char* triplet, int skip, int max, int pleft,
159               const uint8_t* auxvec, sprixcell_e* state){
160 //fprintf(stderr, "SKIP/MAX/PLEFT %d/%d/%d auxvec %p\n", skip, max, pleft, auxvec);
161   if(pleft > 3){
162     pleft = 3;
163   }
164   if(max + skip > pleft){
165     max = pleft - skip;
166   }
167   if(skip == 0){
168     int a = auxvec[0];
169     if(a == 0){
170       *state = SPRIXCELL_MIXED_KITTY;
171     }
172     triplet[0x4] = b64subs[(a & 0xfc) >> 2];
173     triplet[0x5] = b64subs[((a & 0x3) << 4) | (b64idx(triplet[0x5]) & 0xf)];
174     if(max > 1){
175       a = auxvec[1];
176       if(a == 0){
177         *state = SPRIXCELL_MIXED_KITTY;
178       }
179       triplet[0x9] = b64subs[(b64idx(triplet[0x9]) & 0x30) | ((a & 0xf0) >> 4)];
180       triplet[0xA] = b64subs[((a & 0xf) << 2) | (b64idx(triplet[0xA]) & 0x3)];
181     }
182     if(max == 3){
183       a = auxvec[2];
184       if(a == 0){
185         *state = SPRIXCELL_MIXED_KITTY;
186       }
187       triplet[0xE] = b64subs[((a & 0xc0) >> 6) | (b64idx(triplet[0xE]) & 0x3c)];
188       triplet[0xF] = b64subs[(a & 0x3f)];
189     }
190   }else if(skip == 1){
191     int a = auxvec[0];
192     if(a == 0){
193       *state = SPRIXCELL_MIXED_KITTY;
194     }
195     triplet[0x9] = b64subs[(b64idx(triplet[0x9]) & 0x30) | ((a & 0xf0) >> 4)];
196     triplet[0xA] = b64subs[((a & 0xf) << 2) | (b64idx(triplet[0xA]) & 0x3)];
197     if(max == 2){
198       a = auxvec[1];
199       if(a == 0){
200         *state = SPRIXCELL_MIXED_KITTY;
201       }
202       triplet[0xE] = b64subs[((a & 0xc0) >> 6) | (b64idx(triplet[0xE]) & 0x3c)];
203       triplet[0xF] = b64subs[(a & 0x3f)];
204     }
205   }else{ // skip == 2
206     int a = auxvec[0];
207     if(a == 0){
208       *state = SPRIXCELL_MIXED_KITTY;
209     }
210     triplet[0xE] = b64subs[((a & 0xc0) >> 6) | (b64idx(triplet[0xE]) & 0x3c)];
211     triplet[0xF] = b64subs[(a & 0x3f)];
212   }
213   return max;
214 }
215 
216 // if there is no mstreamfp open, create one, using glyph and glyphlen as the
217 // base. we're blowing away the glyph.
218 static int
init_sprixel_animation(sprixel * s)219 init_sprixel_animation(sprixel* s){
220   if(s->animating){
221     return 0;
222   }
223   fbuf_free(&s->glyph);
224   if(fbuf_init(&s->glyph)){
225     return -1;
226   }
227   s->animating = true;
228   return 0;
229 }
230 
231 #define RGBA_MAXLEN 768 // 768 base64-encoded pixels in 4096 bytes
232 // restore an annihilated sprixcell by copying the alpha values from the
233 // auxiliary vector back into the actual data. we then free the auxvector.
kitty_rebuild(sprixel * s,int ycell,int xcell,uint8_t * auxvec)234 int kitty_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
235   const int totalpixels = s->pixy * s->pixx;
236   const int xpixels = ncplane_pile(s->n)->cellpxx;
237   const int ypixels = ncplane_pile(s->n)->cellpxy;
238   int targx = xpixels;
239   if((xcell + 1) * xpixels > s->pixx){
240     targx = s->pixx - xcell * xpixels;
241   }
242   int targy = ypixels;
243   if((ycell + 1) * ypixels > s->pixy){
244     targy = s->pixy - ycell * ypixels;
245   }
246   char* c = (char*)s->glyph.buf + s->parse_start;
247   int nextpixel = (s->pixx * ycell * ypixels) + (xpixels * xcell);
248   int thisrow = targx;
249   int chunkedhandled = 0;
250   sprixcell_e state = SPRIXCELL_OPAQUE_KITTY;
251   const int chunks = totalpixels / RGBA_MAXLEN + !!(totalpixels % RGBA_MAXLEN);
252   int auxvecidx = 0;
253   while(targy && chunkedhandled < chunks){ // need to null out |targy| rows of |targx| pixels, track with |thisrow|
254     int inchunk = totalpixels - chunkedhandled * RGBA_MAXLEN;
255     if(inchunk > RGBA_MAXLEN){
256       inchunk = RGBA_MAXLEN;
257     }
258     const int curpixel = chunkedhandled * RGBA_MAXLEN;
259     // a full chunk is 4096 + 2 + 7 (5005)
260     while(nextpixel - curpixel < RGBA_MAXLEN && thisrow){
261       // our next pixel is within this chunk. find the pixel offset of the
262       // first pixel (within the chunk).
263       int pixoffset = nextpixel - curpixel;
264       int triples = pixoffset / 3;
265       int tripbytes = triples * 16;
266       int tripskip = pixoffset - triples * 3;
267       // we start within a 16-byte chunk |tripbytes| into the chunk. determine
268       // the number of bits.
269 //fprintf(stderr, "pixoffset: %d next: %d tripbytes: %d tripskip: %d thisrow: %d\n", pixoffset, nextpixel, tripbytes, tripskip, thisrow);
270       // the maximum number of pixels we can convert is the minimum of the
271       // pixels remaining in the target row, and the pixels left in the chunk.
272 //fprintf(stderr, "inchunk: %d total: %d triples: %d\n", inchunk, totalpixels, triples);
273       int chomped = kitty_restore(c + tripbytes, tripskip, thisrow,
274                                   inchunk - triples * 3, auxvec + auxvecidx,
275                                   &state);
276       assert(chomped >= 0);
277       auxvecidx += chomped;
278       thisrow -= chomped;
279 //fprintf(stderr, "POSTCHIMP CHOMP: %d pixoffset: %d next: %d tripbytes: %d tripskip: %d thisrow: %d\n", chomped, pixoffset, nextpixel, tripbytes, tripskip, thisrow);
280       if(thisrow == 0){
281 //fprintf(stderr, "CLEARED ROW, TARGY: %d\n", targy - 1);
282         if(--targy == 0){
283           s->n->tam[s->dimx * ycell + xcell].state = state;
284           s->invalidated = SPRIXEL_INVALIDATED;
285           return 1;
286         }
287         thisrow = targx;
288 //fprintf(stderr, "BUMP IT: %d %d %d %d\n", nextpixel, s->pixx, targx, chomped);
289         nextpixel += s->pixx - targx + chomped;
290       }else{
291         nextpixel += chomped;
292       }
293     }
294     c += RGBA_MAXLEN * 4 * 4 / 3; // 4bpp * 4/3 for base64, 4096b per chunk
295     c += 8; // new chunk header
296     ++chunkedhandled;
297 //fprintf(stderr, "LOOKING NOW AT %u [%s]\n", c - s->glyph, c);
298     while(*c != ';'){
299       ++c;
300     }
301     ++c;
302   }
303   return -1;
304 }
305 
306 // does this auxvec correspond to a sprixcell which was nulled out during the
307 // blitting of the frame (can only happen with a multiframe that's seen some
308 // wiping)?
309 static inline unsigned
kitty_anim_auxvec_blitsource_p(const sprixel * s,const uint8_t * auxvec)310 kitty_anim_auxvec_blitsource_p(const sprixel* s, const uint8_t* auxvec){
311   size_t off = ncplane_pile(s->n)->cellpxy * ncplane_pile(s->n)->cellpxx * 4;
312   if(auxvec[off]){
313     return 1;
314   }
315   return 0;
316 }
317 
318 // an animation auxvec requires storing all the pixel data for the cell,
319 // instead of just the alpha channel. pass the start of the RGBA to be
320 // copied, and the rowstride. dimy and dimx are the source image's total
321 // size in pixels. posy and posx are the origin of the cell to be copied,
322 // again in pixels. data is the image source. around the edges, we might
323 // get truncated regions. we also need to store a final byte indicating
324 // whether the null write originated in blitting or wiping, as that affects
325 // our rebuild animation.
326 static inline void*
kitty_anim_auxvec(int dimy,int dimx,int posy,int posx,int cellpxy,int cellpxx,const uint32_t * data,int rowstride,uint8_t * existing,uint32_t transcolor)327 kitty_anim_auxvec(int dimy, int dimx, int posy, int posx,
328                   int cellpxy, int cellpxx, const uint32_t* data,
329                   int rowstride, uint8_t* existing, uint32_t transcolor){
330   const size_t slen = 4 * cellpxy * cellpxx + 1;
331   uint32_t* a = existing ? existing : malloc(slen);
332   if(a){
333     for(int y = posy ; y < posy + cellpxy && y < dimy ; ++y){
334       int pixels = cellpxx;
335       if(pixels + posx > dimx){
336         pixels = dimx - posx;
337       }
338       /*logtrace("Copying %d (%d) from %p to %p %d/%d\n",
339                pixels * 4, y,
340                data + y * (rowstride / 4) + posx,
341                a + (y - posy) * (pixels * 4),
342                posy / cellpxy, posx / cellpxx);*/
343       memcpy(a + (y - posy) * pixels, data + y * (rowstride / 4) + posx, pixels * 4);
344       for(int x = posx ; x < posx + cellpxx && x < dimx ; ++x){
345         uint32_t pixel = data[y * (rowstride / 4) + x];
346         if(rgba_trans_p(pixel, transcolor)){
347           uint32_t* ap = a + (y - posy) * pixels + (x - posx);
348           ncpixel_set_a(ap, 0);
349         }
350       }
351     }
352     ((uint8_t*)a)[slen - 1] = 0; // reset blitsource ownership
353   }
354   return a;
355 }
356 
kitty_trans_auxvec(const ncpile * p)357 uint8_t* kitty_trans_auxvec(const ncpile* p){
358   const size_t slen = p->cellpxy * p->cellpxx;
359   uint8_t* a = malloc(slen);
360   if(a){
361     memset(a, 0, slen);
362   }
363   return a;
364 }
365 
366 // just dump the wipe into the fbuf -- don't manipulate any state. used both
367 // by the wipe proper, and when blitting a new frame with annihilations.
368 static int
kitty_blit_wipe_selfref(sprixel * s,fbuf * f,int ycell,int xcell)369 kitty_blit_wipe_selfref(sprixel* s, fbuf* f, int ycell, int xcell){
370   const int cellpxx = ncplane_pile(s->n)->cellpxx;
371   const int cellpxy = ncplane_pile(s->n)->cellpxy;
372   if(fbuf_printf(f, "\x1b_Ga=f,x=%d,y=%d,s=%d,v=%d,i=%d,X=1,r=2,c=1,q=2;",
373                  xcell * cellpxx, ycell * cellpxy, cellpxx, cellpxy, s->id) < 0){
374     return -1;
375   }
376   // FIXME ought be smaller around the fringes!
377   int totalp = cellpxy * cellpxx;
378   // FIXME preserve so long as cellpixel geom stays constant?
379   #define TRINULLALPHA "AAAAAAAAAAAAAAAA"
380   for(int p = 0 ; p + 3 <= totalp ; p += 3){
381     if(fbuf_putn(f, TRINULLALPHA, strlen(TRINULLALPHA)) < 0){
382       return -1;
383     }
384   }
385   #undef TRINULLALPHA
386   if(totalp % 3 == 1){
387   #define UNUMNULLALPHA "AAAAAA=="
388     if(fbuf_putn(f, UNUMNULLALPHA, strlen(UNUMNULLALPHA)) < 0){
389       return -1;
390     }
391   #undef UNUMNULLALPHA
392   }else if(totalp % 3 == 2){
393   #define DUONULLALPHA "AAAAAAAAAAA="
394     if(fbuf_putn(f, DUONULLALPHA, strlen(DUONULLALPHA)) < 0){
395       return -1;
396     }
397   #undef DUONULLALPHA
398   }
399   // FIXME need chunking for cells of 768+ pixels
400   if(fbuf_printf(f, "\x1b\\\x1b_Ga=a,i=%d,c=2,q=2\x1b\\", s->id) < 0){
401     return -1;
402   }
403   return 0;
404 }
405 
406 // we lay a cell-sixed animation block atop the graphic, giving it a
407 // cell id with which we can delete it in O(1) for a rebuild. this
408 // way, we needn't delete and redraw the entire sprixel.
kitty_wipe_animation(sprixel * s,int ycell,int xcell)409 int kitty_wipe_animation(sprixel* s, int ycell, int xcell){
410   logdebug("wiping sprixel %u at %d/%d\n", s->id, ycell, xcell);
411   if(init_sprixel_animation(s)){
412     return -1;
413   }
414   fbuf* f = &s->glyph;
415   if(kitty_blit_wipe_selfref(s, f, ycell, xcell) < 0){
416     return -1;
417   }
418   int tamidx = ycell * s->dimx + xcell;
419   uint8_t* auxvec = s->n->tam[tamidx].auxvector;
420   auxvec[ncplane_pile(s->n)->cellpxx * ncplane_pile(s->n)->cellpxy * 4] = 0;
421   s->invalidated = SPRIXEL_INVALIDATED;
422   return 1;
423 }
424 
kitty_wipe_selfref(sprixel * s,int ycell,int xcell)425 int kitty_wipe_selfref(sprixel* s, int ycell, int xcell){
426   if(init_sprixel_animation(s)){
427     return -1;
428   }
429   const int tyx = xcell + ycell * s->dimx;
430   int state = s->n->tam[tyx].state;
431   void* auxvec = s->n->tam[tyx].auxvector;
432   logdebug("Wiping sprixel %u at %d/%d auxvec: %p state: %d\n", s->id, ycell, xcell, auxvec, state);
433   fbuf* f = &s->glyph;
434   if(kitty_blit_wipe_selfref(s, f, ycell, xcell)){
435     return -1;
436   }
437   s->invalidated = SPRIXEL_INVALIDATED;
438   memcpy(auxvec, &state, sizeof(state));
439   return 1;
440 }
441 
kitty_recycle(ncplane * n)442 sprixel* kitty_recycle(ncplane* n){
443   assert(n->sprite);
444   sprixel* hides = n->sprite;
445   int dimy = hides->dimy;
446   int dimx = hides->dimx;
447   sprixel_hide(hides);
448   return sprixel_alloc(n, dimy, dimx);
449 }
450 
451 // for pre-animation kitty (NCPIXEL_KITTY_STATIC), we need a byte per pixel,
452 // in which we stash the alpha.
453 static inline uint8_t*
kitty_auxiliary_vector(const sprixel * s)454 kitty_auxiliary_vector(const sprixel* s){
455   int pixels = ncplane_pile(s->n)->cellpxy * ncplane_pile(s->n)->cellpxx;
456   uint8_t* ret = malloc(sizeof(*ret) * pixels);
457   if(ret){
458     memset(ret, 0, sizeof(*ret) * pixels);
459   }
460   return ret;
461 }
462 
kitty_wipe(sprixel * s,int ycell,int xcell)463 int kitty_wipe(sprixel* s, int ycell, int xcell){
464 //fprintf(stderr, "NEW WIPE %d %d/%d\n", s->id, ycell, xcell);
465   uint8_t* auxvec = kitty_auxiliary_vector(s);
466   if(auxvec == NULL){
467     return -1;
468   }
469   const int totalpixels = s->pixy * s->pixx;
470   const int xpixels = ncplane_pile(s->n)->cellpxx;
471   const int ypixels = ncplane_pile(s->n)->cellpxy;
472   // if the cell is on the right or bottom borders, it might only be partially
473   // filled by actual graphic data, and we need to cap our target area.
474   int targx = xpixels;
475   if((xcell + 1) * xpixels > s->pixx){
476     targx = s->pixx - xcell * xpixels;
477   }
478   int targy = ypixels;
479   if((ycell + 1) * ypixels > s->pixy){
480     targy = s->pixy - ycell * ypixels;
481   }
482   char* c = (char*)s->glyph.buf + s->parse_start;
483 //fprintf(stderr, "TARGET AREA: %d x %d @ %dx%d of %d/%d (%d/%d) len %zu\n", targy, targx, ycell, xcell, s->dimy, s->dimx, s->pixy, s->pixx, strlen(c));
484   // every pixel was 4 source bytes, 32 bits, 6.33 base64 bytes. every 3 input pixels is
485   // 12 bytes (96 bits), an even 16 base64 bytes. there is chunking to worry about. there
486   // are up to 768 pixels in a chunk.
487   int nextpixel = (s->pixx * ycell * ypixels) + (xpixels * xcell);
488   int thisrow = targx;
489   int chunkedhandled = 0;
490   const int chunks = totalpixels / RGBA_MAXLEN + !!(totalpixels % RGBA_MAXLEN);
491   int auxvecidx = 0;
492   while(targy && chunkedhandled < chunks){ // need to null out |targy| rows of |targx| pixels, track with |thisrow|
493 //fprintf(stderr, "PLUCKING FROM [%s]\n", c);
494     int inchunk = totalpixels - chunkedhandled * RGBA_MAXLEN;
495     if(inchunk > RGBA_MAXLEN){
496       inchunk = RGBA_MAXLEN;
497     }
498     const int curpixel = chunkedhandled * RGBA_MAXLEN;
499     // a full chunk is 4096 + 2 + 7 (5005)
500     while(nextpixel - curpixel < RGBA_MAXLEN && thisrow){
501       // our next pixel is within this chunk. find the pixel offset of the
502       // first pixel (within the chunk).
503       int pixoffset = nextpixel - curpixel;
504       int triples = pixoffset / 3;
505       int tripbytes = triples * 16;
506       // we start within a 16-byte chunk |tripbytes| into the chunk. determine
507       // the number of bits.
508       int tripskip = pixoffset - triples * 3;
509 //fprintf(stderr, "pixoffset: %d next: %d tripbytes: %d tripskip: %d thisrow: %d\n", pixoffset, nextpixel, tripbytes, tripskip, thisrow);
510       // the maximum number of pixels we can convert is the minimum of the
511       // pixels remaining in the target row, and the pixels left in the chunk.
512 //fprintf(stderr, "inchunk: %d total: %d triples: %d\n", inchunk, totalpixels, triples);
513 //fprintf(stderr, "PRECHOMP:  [%.16s]\n", c + tripbytes);
514       int chomped = kitty_null(c + tripbytes, tripskip, thisrow,
515                                inchunk - triples * 3, auxvec + auxvecidx);
516 //fprintf(stderr, "POSTCHOMP: [%.16s]\n", c + tripbytes);
517       assert(chomped >= 0);
518       auxvecidx += chomped;
519       assert(auxvecidx <= ypixels * xpixels);
520       thisrow -= chomped;
521 //fprintf(stderr, "POSTCHIMP CHOMP: %d pixoffset: %d next: %d tripbytes: %d tripskip: %d thisrow: %d\n", chomped, pixoffset, nextpixel, tripbytes, tripskip, thisrow);
522       if(thisrow == 0){
523 //fprintf(stderr, "CLEARED ROW, TARGY: %d\n", targy - 1);
524         if(--targy == 0){
525           s->n->tam[s->dimx * ycell + xcell].auxvector = auxvec;
526           s->invalidated = SPRIXEL_INVALIDATED;
527           return 1;
528         }
529         thisrow = targx;
530 //fprintf(stderr, "BUMP IT: %d %d %d %d\n", nextpixel, s->pixx, targx, chomped);
531         nextpixel += s->pixx - targx + chomped;
532       }else{
533         nextpixel += chomped;
534       }
535     }
536     c += RGBA_MAXLEN * 4 * 4 / 3; // 4bpp * 4/3 for base64, 4096b per chunk
537     c += 8; // new chunk header
538     ++chunkedhandled;
539 //fprintf(stderr, "LOOKING NOW AT %u [%s]\n", c - s->glyph, c);
540     while(*c != ';'){
541       ++c;
542     }
543     ++c;
544   }
545   free(auxvec);
546   return -1;
547 }
548 
kitty_commit(fbuf * f,sprixel * s,unsigned noscroll)549 int kitty_commit(fbuf* f, sprixel* s, unsigned noscroll){
550   loginfo("Committing Kitty graphic id %u\n", s->id);
551   int i;
552   if(s->pxoffx || s->pxoffy){
553     i = fbuf_printf(f, "\e_Ga=p,i=%u,p=1,X=%u,Y=%u%s,q=2\e\\", s->id,
554                     s->pxoffx, s->pxoffy, noscroll ? ",C=1" : "");
555   }else{
556     i = fbuf_printf(f, "\e_Ga=p,i=%u,p=1,q=2%s\e\\", s->id, noscroll ? ",C=1" : "");
557   }
558   if(i < 0){
559     return -1;
560   }
561   s->invalidated = SPRIXEL_QUIESCENT;
562   return 0;
563 }
564 
565 // chunkify and write the collected buffer in the animated case. this might
566 // or might not be compressed (depends on whether compression was useful).
567 static int
encode_and_chunkify(fbuf * f,const unsigned char * buf,size_t blen,unsigned compressed)568 encode_and_chunkify(fbuf* f, const unsigned char* buf, size_t blen, unsigned compressed){
569   // need to terminate the header, requiring semicolon
570   if(compressed){
571     if(fbuf_putn(f, ",o=z", 4) < 0){
572       return -1;
573     }
574   }
575   if(blen > 4096 * 3 / 4){
576     if(fbuf_putn(f, ",m=1", 4) < 0){
577       return -1;
578     }
579   }
580   if(fbuf_putc(f, ';') < 0){
581     return -1;
582   }
583   bool first = true;
584   unsigned long i = 0;
585   char b64d[4];
586   while(blen - i > 4096 * 3 / 4){
587     if(!first){
588       if(fbuf_putn(f, "\x1b_Gm=1;", 7) < 0){
589         return -1;
590       }
591     }
592     unsigned long max = i + 4096 * 3 / 4;
593     while(i < max){
594       base64x3(buf + i, b64d);
595       if(fbuf_putn(f, b64d, 4) < 0){
596         return -1;
597       }
598       i += 3;
599     }
600     first = false;
601     if(fbuf_putn(f, "\x1b\\", 2) < 0){
602       return -1;
603     }
604   }
605   if(!first){
606     if(fbuf_putn(f, "\x1b_Gm=0;", 7) < 0){
607       return -1;
608     }
609   }
610   while(i < blen){
611     if(blen - i < 3){
612       base64final(buf + i, b64d, blen - i);
613       if(fbuf_putn(f, b64d, 4) < 0){
614         return -1;
615       }
616       i += blen - i;
617     }else{
618       base64x3(buf + i, b64d);
619       if(fbuf_putn(f, b64d, 4) < 0){
620         return -1;
621       }
622       i += 3;
623     }
624   }
625   if(fbuf_putn(f, "\x1b\\", 2) < 0){
626     return -1;
627   }
628   return 0;
629 }
630 
631 static int
deflate_buf(void * buf,fbuf * f,int dimy,int dimx)632 deflate_buf(void* buf, fbuf* f, int dimy, int dimx){
633   const size_t blen = dimx * dimy * 4;
634   void* cbuf = NULL;
635   size_t clen = 0;
636 #ifdef USE_DEFLATE
637   // 2 has been shown to work pretty well for things that are actually going
638   // to compress; results per unit time fall off quickly after 2.
639   struct libdeflate_compressor* cmp = libdeflate_alloc_compressor(2);
640   if(cmp == NULL){
641     logerror("couldn't get libdeflate context\n");
642     return -1;
643   }
644   // if this allocation fails, just skip compression, no need to bail
645   cbuf = malloc(blen);
646   if(cbuf){
647     clen = libdeflate_zlib_compress(cmp, buf, blen, cbuf, blen);
648   }
649   libdeflate_free_compressor(cmp);
650 #endif
651   int ret;
652   if(0 == clen){ // wasn't enough room; compressed data is larger than original
653     loginfo("deflated in vain; using original %" PRIuPTR "B\n", blen);
654     ret = encode_and_chunkify(f, buf, blen, 0);
655   }else{
656     loginfo("deflated %" PRIuPTR "B to %" PRIuPTR "B\n", blen, clen);
657     ret = encode_and_chunkify(f, cbuf, clen, 1);
658   }
659   free(cbuf);
660   return ret;
661 }
662 
663 // copy |encodeable| ([1..3]) pixels from |src| to the buffer |dst|, setting
664 // alpha along the way according to |wipe|.
665 static inline int
add_to_buf(uint32_t * dst,const uint32_t * src,int encodeable,bool wipe[static3])666 add_to_buf(uint32_t *dst, const uint32_t* src, int encodeable, bool wipe[static 3]){
667   dst[0] = *src++;
668   if(wipe[0] || rgba_trans_p(dst[0], 0)){
669     ncpixel_set_a(&dst[0], 0);
670   }
671   if(encodeable > 1){
672     dst[1] = *src++;
673     if(wipe[1] || rgba_trans_p(dst[1], 0)){
674       ncpixel_set_a(&dst[1], 0);
675     }
676     if(encodeable > 2){
677       dst[2] = *src++;
678       if(wipe[2] || rgba_trans_p(dst[2], 0)){
679         ncpixel_set_a(&dst[2], 0);
680       }
681     }
682   }
683   return 0;
684 }
685 
686 // writes to |*animated| based on normalized |level|. if we're not animated,
687 // we won't be using compression.
688 static inline int
prep_animation(ncpixelimpl_e level,uint32_t ** buf,int leny,int lenx,unsigned * animated)689 prep_animation(ncpixelimpl_e level, uint32_t** buf, int leny, int lenx, unsigned* animated){
690   if(level < NCPIXEL_KITTY_ANIMATED){
691     *animated = false;
692     *buf = NULL;
693     return 0;
694   }
695   *animated = true;
696   if((*buf = malloc(lenx * leny * sizeof(uint32_t))) == NULL){
697     return -1;
698   }
699   return 0;
700 }
701 
702 // if we're NCPIXEL_KITTY_SELFREF, and we're blitting a secondary frame, we need
703 // carry through the TAM's annihilation entires...but we also need load the
704 // frame *without* annihilations, lest we be unable to build it. we thus go
705 // back through the TAM following a selfref blit, and any sprixcells which
706 // are annihilated will have their annhilation appended to the main blit.
707 // ought only be called for NCPIXEL_KITTY_SELFREF.
708 static int
finalize_multiframe_selfref(sprixel * s,fbuf * f)709 finalize_multiframe_selfref(sprixel* s, fbuf* f){
710   int prewiped = 0;
711   for(unsigned y = 0 ; y < s->dimy ; ++y){
712     for(unsigned x = 0 ; x < s->dimx ; ++x){
713       unsigned tyxidx = y * s->dimx + x;
714       unsigned state = s->n->tam[tyxidx].state;
715       if(state >= SPRIXCELL_ANNIHILATED){
716         if(kitty_blit_wipe_selfref(s, f, y, x)){
717           return -1;
718         }
719         ++prewiped;
720       }
721     }
722   }
723   loginfo("transitively wiped %d/%u\n", prewiped, s->dimy * s->dimx);
724   return 0;
725 }
726 
727 // we can only write 4KiB at a time. we're writing base64-encoded RGBA. each
728 // pixel is 4B raw (32 bits). each chunk of three pixels is then 12 bytes, or
729 // 16 base64-encoded bytes. 4096 / 16 == 256 3-pixel groups, or 768 pixels.
730 // closes |fp| on all paths.
731 static int
write_kitty_data(fbuf * f,int linesize,int leny,int lenx,int cols,const uint32_t * data,const blitterargs * bargs,tament * tam,int * parse_start,ncpixelimpl_e level)732 write_kitty_data(fbuf* f, int linesize, int leny, int lenx, int cols,
733                  const uint32_t* data, const blitterargs* bargs,
734                  tament* tam, int* parse_start, ncpixelimpl_e level){
735   if(linesize % sizeof(*data)){
736     logerror("stride (%d) badly aligned\n", linesize);
737     return -1;
738   }
739   unsigned animated;
740   uint32_t* buf;
741   // we'll be collecting the pixels, modified to reflect alpha nullification
742   // due to preexisting wipes, into a temporary buffer for compression (iff
743   // we're animated). pixels are 32 bits each.
744   if(prep_animation(level, &buf, leny, lenx, &animated)){
745     return -1;
746   }
747   unsigned bufidx = 0; // an index; the actual offset is bufidx * 4
748   bool translucent = bargs->flags & NCVISUAL_OPTION_BLEND;
749   sprixel* s = bargs->u.pixel.spx;
750   const int cdimy = bargs->u.pixel.cellpxy;
751   const int cdimx = bargs->u.pixel.cellpxx;
752   assert(0 != cdimy);
753   assert(0 != cdimx);
754   const uint32_t transcolor = bargs->transcolor;
755   int total = leny * lenx; // total number of pixels (4 * total == bytecount)
756   // number of 4KiB chunks we'll need
757   int chunks = (total + (RGBA_MAXLEN - 1)) / RGBA_MAXLEN;
758   int totalout = 0; // total pixels of payload out
759   int y = 0; // position within source image (pixels)
760   int x = 0;
761   int targetout = 0; // number of pixels expected out after this chunk
762 //fprintf(stderr, "total: %d chunks = %d, s=%d,v=%d\n", total, chunks, lenx, leny);
763   char out[17]; // three pixels base64 to no more than 17 bytes
764   // set high if we are (1) reloading a frame with (2) annihilated cells copied over
765   // from the TAM and (3) we are NCPIXEL_KITTY_SELFREF. calls finalize_multiframe_selfref().
766   bool selfref_annihilated = false;
767   while(chunks--){
768     // q=2 has been able to go on chunks other than the last chunk since
769     // 2021-03, but there's no harm in this small bit of backwards compat.
770     if(totalout == 0){
771       // older versions of kitty will delete uploaded images when scrolling,
772       // alas. see https://github.com/dankamongmen/notcurses/issues/1910 =[.
773       // parse_start isn't used in animation mode, so no worries about the
774       // fact that this doesn't complete the header in that case.
775       *parse_start = fbuf_printf(f, "\e_Gf=32,s=%d,v=%d,i=%d,p=1,a=t,%s",
776                                  lenx, leny, s->id,
777                                  animated ? "q=2" : chunks ? "m=1;" : "q=2;");
778       if(*parse_start < 0){
779         goto err;
780       }
781       // so if we're animated, we've printed q=2, but no semicolon to close
782       // the control block, since we're not yet sure what m= to write. we've
783       // otherwise written q=2; if we're the only chunk, and m=1; otherwise.
784       // if we're *not* animated, we'll get q=2,m=0; below. otherwise, it's
785       // handled following deflate.
786     }else{
787       if(!animated){
788         if(fbuf_printf(f, "\e_G%sm=%d;", chunks ? "" : "q=2,", chunks ? 1 : 0) < 0){
789           goto err;
790         }
791       }
792     }
793     if((targetout += RGBA_MAXLEN) > total){
794       targetout = total;
795     }
796     while(totalout < targetout){
797       int encodeable = targetout - totalout;
798       if(encodeable > 3){
799         encodeable = 3;
800       }
801       uint32_t source[3]; // we encode up to 3 pixels at a time
802       bool wipe[3];
803       for(int e = 0 ; e < encodeable ; ++e){
804         if(x == lenx){
805           x = 0;
806           ++y;
807         }
808         const uint32_t* line = data + (linesize / sizeof(*data)) * y;
809         source[e] = line[x];
810         if(translucent){
811           ncpixel_set_a(&source[e], ncpixel_a(source[e]) / 2);
812         }
813         int xcell = x / cdimx;
814         int ycell = y / cdimy;
815         int tyx = xcell + ycell * cols;
816 //fprintf(stderr, "Tyx: %d y: %d (%d) * %d x: %d (%d) state %d %p\n", tyx, y, y / cdimy, cols, x, x / cdimx, tam[tyx].state, tam[tyx].auxvector);
817         // old-style animated auxvecs carry the entirety of the replacement
818         // data in them. on the first pixel of the cell, ditch the previous
819         // auxvec in its entirety, and copy over the entire cell.
820         if(x % cdimx == 0 && y % cdimy == 0){
821           if(level == NCPIXEL_KITTY_ANIMATED){
822             uint8_t* tmp;
823             tmp = kitty_anim_auxvec(leny, lenx, y, x, cdimy, cdimx,
824                                     data, linesize, tam[tyx].auxvector,
825                                     transcolor);
826             if(tmp == NULL){
827               logerror("got a NULL auxvec at %d/%d\n", y, x);
828               goto err;
829             }
830             tam[tyx].auxvector = tmp;
831           }else if(level == NCPIXEL_KITTY_SELFREF){
832             if(tam[tyx].auxvector == NULL){
833               tam[tyx].auxvector = malloc(sizeof(tam[tyx].state));
834               if(tam[tyx].auxvector == NULL){
835                 logerror("got a NULL auxvec at %d\n", tyx);
836                 goto err;
837               }
838             }
839             memcpy(tam[tyx].auxvector, &tam[tyx].state, sizeof(tam[tyx].state));
840           }
841         }
842         if(tam[tyx].state >= SPRIXCELL_ANNIHILATED){
843           if(!animated){
844             // this pixel is part of a cell which is currently wiped (alpha-nulled
845             // out, to present a glyph "atop" it). we will continue to mark it
846             // transparent, but we need to update the auxiliary vector.
847             const int vyx = (y % cdimy) * cdimx + (x % cdimx);
848             ((uint8_t*)tam[tyx].auxvector)[vyx] =  ncpixel_a(source[e]);
849             wipe[e] = 1;
850           }else if(level == NCPIXEL_KITTY_SELFREF){
851             selfref_annihilated = true;
852           }else{
853             ((uint8_t*)tam[tyx].auxvector)[cdimx * cdimy * 4] = 1;
854             wipe[e] = 1;
855           }
856           if(rgba_trans_p(source[e], transcolor)){
857             ncpixel_set_a(&source[e], 0); // in case it was transcolor
858             if(x % cdimx == 0 && y % cdimy == 0){
859               tam[tyx].state = SPRIXCELL_ANNIHILATED_TRANS;
860               if(level == NCPIXEL_KITTY_SELFREF){
861                 *(sprixcell_e*)tam[tyx].auxvector = SPRIXCELL_TRANSPARENT;
862               }
863             }else if(level == NCPIXEL_KITTY_SELFREF && tam[tyx].state == SPRIXCELL_ANNIHILATED_TRANS){
864                 *(sprixcell_e*)tam[tyx].auxvector = SPRIXCELL_MIXED_KITTY;
865             }
866           }else{
867             if(x % cdimx == 0 && y % cdimy == 0 && level == NCPIXEL_KITTY_SELFREF){
868               *(sprixcell_e*)tam[tyx].auxvector = SPRIXCELL_OPAQUE_KITTY;
869             }else if(level == NCPIXEL_KITTY_SELFREF && *(sprixcell_e*)tam[tyx].auxvector == SPRIXCELL_TRANSPARENT){
870               *(sprixcell_e*)tam[tyx].auxvector = SPRIXCELL_MIXED_KITTY;
871             }
872             tam[tyx].state = SPRIXCELL_ANNIHILATED;
873           }
874         }else{
875           wipe[e] = 0;
876           if(rgba_trans_p(source[e], transcolor)){
877             ncpixel_set_a(&source[e], 0); // in case it was transcolor
878             if(x % cdimx == 0 && y % cdimy == 0){
879               tam[tyx].state = SPRIXCELL_TRANSPARENT;
880             }else if(tam[tyx].state == SPRIXCELL_OPAQUE_KITTY){
881               tam[tyx].state = SPRIXCELL_MIXED_KITTY;
882             }
883           }else{
884             if(x % cdimx == 0 && y % cdimy == 0){
885               tam[tyx].state = SPRIXCELL_OPAQUE_KITTY;
886             }else if(tam[tyx].state == SPRIXCELL_TRANSPARENT){
887               tam[tyx].state = SPRIXCELL_MIXED_KITTY;
888             }
889           }
890         }
891         ++x;
892       }
893       totalout += encodeable;
894       if(animated){
895         if(add_to_buf(buf + bufidx, source, encodeable, wipe)){
896           goto err;
897         }
898         bufidx += encodeable;
899       }else{
900         // we already took transcolor to alpha 0; there's no need to
901         // check it again, so pass 0.
902         base64_rgba3(source, encodeable, out, wipe, 0);
903         if(fbuf_puts(f, out) < 0){
904           goto err;
905         }
906       }
907     }
908     if(!animated){
909       if(fbuf_putn(f, "\x1b\\", 2) < 0){
910         goto err;
911       }
912     }
913   }
914   // we only deflate if we're using animation, since otherwise we need be able
915   // to edit the encoded bitmap in-place for wipes/restores.
916   if(animated){
917     if(deflate_buf(buf, f, leny, lenx)){
918       goto err;
919     }
920     if(selfref_annihilated){
921       if(finalize_multiframe_selfref(s, f)){
922         goto err;
923       }
924     }
925   }
926   scrub_tam_boundaries(tam, leny, lenx, cdimy, cdimx);
927   free(buf);
928   return 0;
929 
930 err:
931   logerror("failed blitting kitty graphics\n");
932   cleanup_tam(tam, (leny + cdimy - 1) / cdimy, (lenx + cdimx - 1) / cdimx);
933   free(buf);
934   return -1;
935 }
936 
937 // with t=z, we can reference the original frame, and say "redraw this region",
938 // thus avoiding the need to carry the original data around in our auxvecs.
kitty_rebuild_selfref(sprixel * s,int ycell,int xcell,uint8_t * auxvec)939 int kitty_rebuild_selfref(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
940   if(init_sprixel_animation(s)){
941     return -1;
942   }
943   fbuf* f = &s->glyph;
944   const int cellpxy = ncplane_pile(s->n)->cellpxy;
945   const int cellpxx = ncplane_pile(s->n)->cellpxx;
946   const int ystart = ycell * cellpxy;
947   const int xstart = xcell * cellpxx;
948   const int xlen = xstart + cellpxx > s->pixx ? s->pixx - xstart : cellpxx;
949   const int ylen = ystart + cellpxy > s->pixy ? s->pixy - ystart : cellpxy;
950   logdebug("rematerializing %u at %d/%d (%dx%d)\n", s->id, ycell, xcell, ylen, xlen);
951   fbuf_printf(f, "\e_Ga=c,x=%d,y=%d,X=%d,Y=%d,w=%d,h=%d,i=%d,r=1,c=2,q=2;\x1b\\",
952               xcell * cellpxx, ycell * cellpxy,
953               xcell * cellpxx, ycell * cellpxy,
954               xlen, ylen, s->id);
955   const int tyx = xcell + ycell * s->dimx;
956   memcpy(&s->n->tam[tyx].state, auxvec, sizeof(s->n->tam[tyx].state));
957   s->invalidated = SPRIXEL_INVALIDATED;
958   return 0;
959 }
960 
kitty_rebuild_animation(sprixel * s,int ycell,int xcell,uint8_t * auxvec)961 int kitty_rebuild_animation(sprixel* s, int ycell, int xcell, uint8_t* auxvec){
962   logdebug("rebuilding sprixel %u %d at %d/%d\n", s->id, s->invalidated, ycell, xcell);
963   if(init_sprixel_animation(s)){
964     return -1;
965   }
966   fbuf* f = &s->glyph;
967   const int cellpxy = ncplane_pile(s->n)->cellpxy;
968   const int cellpxx = ncplane_pile(s->n)->cellpxx;
969   const int ystart = ycell * cellpxy;
970   const int xstart = xcell * cellpxx;
971   const int xlen = xstart + cellpxx > s->pixx ? s->pixx - xstart : cellpxx;
972   const int ylen = ystart + cellpxy > s->pixy ? s->pixy - ystart : cellpxy;
973   const int linesize = xlen * 4;
974   const int total = xlen * ylen;
975   const int tyx = xcell + ycell * s->dimx;
976   int chunks = (total + (RGBA_MAXLEN - 1)) / RGBA_MAXLEN;
977   int totalout = 0; // total pixels of payload out
978   int y = 0; // position within source image (pixels)
979   int x = 0;
980   int targetout = 0; // number of pixels expected out after this chunk
981 //fprintf(stderr, "total: %d chunks = %d, s=%d,v=%d\n", total, chunks, lenx, leny);
982   // FIXME this ought be factored out and shared with write_kitty_data()
983   logdebug("placing %d/%d at %d/%d\n", ylen, xlen, ycell * cellpxy, xcell * cellpxx);
984   while(chunks--){
985     if(totalout == 0){
986       const int c = kitty_anim_auxvec_blitsource_p(s, auxvec) ? 2 : 1;
987       const int r = kitty_anim_auxvec_blitsource_p(s, auxvec) ? 1 : 2;
988       if(fbuf_printf(f, "\e_Ga=f,x=%d,y=%d,s=%d,v=%d,i=%d,X=1,c=%d,r=%d,%s;",
989                      xcell * cellpxx, ycell * cellpxy, xlen, ylen,
990                      s->id, c, r, chunks ? "m=1" : "q=2") < 0){
991         return -1;
992       }
993     }else{
994       if(fbuf_putn(f, "\x1b_G", 3) < 0){
995         return -1;
996       }
997       if(!chunks){
998         if(fbuf_putn(f, "q=2,", 4) < 0){
999           return -1;
1000         }
1001       }
1002       if(fbuf_putn(f, "m=", 2) < 0){
1003         return -1;
1004       }
1005       if(fbuf_putint(f, chunks ? 1 : 0) < 0){
1006         return -1;
1007       }
1008       if(fbuf_putc(f, ';') != 1){
1009         return -1;
1010       }
1011     }
1012     if((targetout += RGBA_MAXLEN) > total){
1013       targetout = total;
1014     }
1015     while(totalout < targetout){
1016       int encodeable = targetout - totalout;
1017       if(encodeable > 3){
1018         encodeable = 3;
1019       }
1020       uint32_t source[3]; // we encode up to 3 pixels at a time
1021       bool wipe[3];
1022       for(int e = 0 ; e < encodeable ; ++e){
1023         if(x == xlen){
1024           x = 0;
1025           ++y;
1026         }
1027         const uint32_t* line = (const uint32_t*)(auxvec + linesize * y);
1028         source[e] = line[x];
1029 //fprintf(stderr, "%u/%u/%u -> %c%c%c%c %u %u %u %u\n", r, g, b, b64[0], b64[1], b64[2], b64[3], b64[0], b64[1], b64[2], b64[3]);
1030 //fprintf(stderr, "Tyx: %d y: %d (%d) * %d x: %d (%d) state %d %p\n", tyx, y, y / cdimy, cols, x, x / cdimx, tam[tyx].state, tam[tyx].auxvector);
1031         wipe[e] = 0;
1032         if(rgba_trans_p(source[e], 0)){
1033           if(x % cellpxx == 0 && y % cellpxy == 0){
1034             s->n->tam[tyx].state = SPRIXCELL_TRANSPARENT;
1035           }else if(s->n->tam[tyx].state == SPRIXCELL_OPAQUE_KITTY){
1036             s->n->tam[tyx].state = SPRIXCELL_MIXED_KITTY;
1037           }
1038         }else{
1039           if(x % cellpxx == 0 && y % cellpxy == 0){
1040             s->n->tam[tyx].state = SPRIXCELL_OPAQUE_KITTY;
1041           }else if(s->n->tam[tyx].state == SPRIXCELL_TRANSPARENT){
1042             s->n->tam[tyx].state = SPRIXCELL_MIXED_KITTY;
1043           }
1044         }
1045         ++x;
1046       }
1047       totalout += encodeable;
1048       char out[17];
1049       base64_rgba3(source, encodeable, out, wipe, 0);
1050       if(fbuf_puts(f, out) < 0){
1051         return -1;
1052       }
1053     }
1054     if(fbuf_putn(f, "\x1b\\", 2) < 0){
1055       return -1;
1056     }
1057   }
1058 //fprintf(stderr, "EMERGED WITH TAM STATE %d\n", s->n->tam[tyx].state);
1059   s->invalidated = SPRIXEL_INVALIDATED;
1060   return 0;
1061 }
1062 #undef RGBA_MAXLEN
1063 
1064 // Kitty graphics blitter. Kitty can take in up to 4KiB at a time of (optionally
1065 // deflate-compressed) 24bit RGB. Returns -1 on error, 1 on success.
1066 static inline int
kitty_blit_core(ncplane * n,int linesize,const void * data,int leny,int lenx,const blitterargs * bargs,ncpixelimpl_e level)1067 kitty_blit_core(ncplane* n, int linesize, const void* data, int leny, int lenx,
1068                 const blitterargs* bargs, ncpixelimpl_e level){
1069   int cols = bargs->u.pixel.spx->dimx;
1070   sprixel* s = bargs->u.pixel.spx;
1071   if(init_sprixel_animation(s)){
1072     return -1;
1073   }
1074   int parse_start = 0;
1075   fbuf* f = &s->glyph;
1076   int pxoffx = bargs->u.pixel.pxoffx;
1077   int pxoffy = bargs->u.pixel.pxoffy;
1078   if(write_kitty_data(f, linesize, leny, lenx, cols, data,
1079                       bargs, n->tam, &parse_start, level)){
1080     goto error;
1081   }
1082   // FIXME need set pxoffx and pxoffy in sprixel
1083   if(level == NCPIXEL_KITTY_STATIC){
1084     s->animating = false;
1085   }
1086   // take ownership of |buf| and |tam| on success.
1087   if(plane_blit_sixel(s, &s->glyph, leny + pxoffy, lenx + pxoffx, parse_start,
1088                       n->tam, SPRIXEL_UNSEEN) < 0){
1089     goto error;
1090   }
1091   s->pxoffx = pxoffx;
1092   s->pxoffy = pxoffy;
1093   return 1;
1094 
1095 error:
1096   cleanup_tam(n->tam, bargs->u.pixel.spx->dimy, bargs->u.pixel.spx->dimx);
1097   fbuf_free(&s->glyph);
1098   return -1;
1099 }
1100 
kitty_blit(ncplane * n,int linesize,const void * data,int leny,int lenx,const blitterargs * bargs)1101 int kitty_blit(ncplane* n, int linesize, const void* data, int leny, int lenx,
1102                const blitterargs* bargs){
1103   return kitty_blit_core(n, linesize, data, leny, lenx, bargs,
1104                          NCPIXEL_KITTY_STATIC);
1105 }
1106 
kitty_blit_animated(ncplane * n,int linesize,const void * data,int leny,int lenx,const blitterargs * bargs)1107 int kitty_blit_animated(ncplane* n, int linesize, const void* data,
1108                         int leny, int lenx, const blitterargs* bargs){
1109   return kitty_blit_core(n, linesize, data, leny, lenx, bargs,
1110                          NCPIXEL_KITTY_ANIMATED);
1111 }
1112 
kitty_blit_selfref(ncplane * n,int linesize,const void * data,int leny,int lenx,const blitterargs * bargs)1113 int kitty_blit_selfref(ncplane* n, int linesize, const void* data,
1114                        int leny, int lenx, const blitterargs* bargs){
1115   return kitty_blit_core(n, linesize, data, leny, lenx, bargs,
1116                          NCPIXEL_KITTY_SELFREF);
1117 }
1118 
kitty_remove(int id,fbuf * f)1119 int kitty_remove(int id, fbuf* f){
1120   loginfo("Removing graphic %u\n", id);
1121   if(fbuf_printf(f, "\e_Ga=d,d=I,i=%d\e\\", id) < 0){
1122     return -1;
1123   }
1124   return 0;
1125 }
1126 
1127 // damages cells underneath the graphic which were OPAQUE
kitty_scrub(const ncpile * p,sprixel * s)1128 int kitty_scrub(const ncpile* p, sprixel* s){
1129 //fprintf(stderr, "FROM: %d/%d state: %d s->n: %p\n", s->movedfromy, s->movedfromx, s->invalidated, s->n);
1130   for(unsigned yy = s->movedfromy ; yy < s->movedfromy + s->dimy && yy < p->dimy ; ++yy){
1131     for(unsigned xx = s->movedfromx ; xx < s->movedfromx + s->dimx && xx < p->dimx ; ++xx){
1132       const int ridx = yy * p->dimx + xx;
1133       assert(0 <= ridx);
1134       struct crender *r = &p->crender[ridx];
1135       if(!r->sprixel){
1136         if(s->n){
1137 //fprintf(stderr, "CHECKING %d/%d\n", yy - s->movedfromy, xx - s->movedfromx);
1138           sprixcell_e state = sprixel_state(s, yy - s->movedfromy + s->n->absy,
1139                                               xx - s->movedfromx + s->n->absx);
1140           if(state == SPRIXCELL_OPAQUE_KITTY){
1141             r->s.damaged = 1;
1142           }else if(s->invalidated == SPRIXEL_MOVED){
1143             // ideally, we wouldn't damage our annihilated sprixcells, but if
1144             // we're being annihilated only during this cycle, we need to go
1145             // ahead and damage it.
1146             r->s.damaged = 1;
1147           }
1148         }else{
1149           // need this to damage cells underneath a sprixel we're removing
1150           r->s.damaged = 1;
1151         }
1152       }
1153     }
1154   }
1155   return 0;
1156 }
1157 
1158 // returns the number of bytes written
kitty_draw(const tinfo * ti,const ncpile * p,sprixel * s,fbuf * f,int yoff,int xoff)1159 int kitty_draw(const tinfo* ti, const ncpile* p, sprixel* s, fbuf* f,
1160                int yoff, int xoff){
1161   (void)ti;
1162   (void)p;
1163   bool animated = false;
1164   if(s->animating){ // active animation
1165     s->animating = false;
1166     animated = true;
1167   }
1168   int ret = s->glyph.used;
1169   logdebug("dumping %" PRIu64 "b for %u at %d %d\n", s->glyph.used, s->id, yoff, xoff);
1170   if(ret){
1171     if(fbuf_putn(f, s->glyph.buf, s->glyph.used) < 0){
1172       ret = -1;
1173     }
1174   }
1175   if(animated){
1176     fbuf_free(&s->glyph);
1177   }
1178   s->invalidated = SPRIXEL_LOADED;
1179   return ret;
1180 }
1181 
1182 // returns -1 on failure, 0 on success (move bytes do not count for sprixel stats)
kitty_move(sprixel * s,fbuf * f,unsigned noscroll,int yoff,int xoff)1183 int kitty_move(sprixel* s, fbuf* f, unsigned noscroll, int yoff, int xoff){
1184   const int targy = s->n->absy;
1185   const int targx = s->n->absx;
1186   logdebug("moving %u to %d %d\n", s->id, targy, targx);
1187   int ret = 0;
1188   if(goto_location(ncplane_notcurses(s->n), f, targy + yoff, targx + xoff, s->n)){
1189     ret = -1;
1190   }else if(fbuf_printf(f, "\e_Ga=p,i=%d,p=1,q=2%s\e\\", s->id,
1191                        noscroll ? ",C=1" : "") < 0){
1192     ret = -1;
1193   }
1194   s->invalidated = SPRIXEL_QUIESCENT;
1195   return ret;
1196 }
1197 
1198 // clears all kitty bitmaps
kitty_clear_all(fbuf * f)1199 int kitty_clear_all(fbuf* f){
1200 //fprintf(stderr, "KITTY UNIVERSAL ERASE\n");
1201   if(fbuf_putn(f, "\x1b_Ga=d,q=2\x1b\\", 12) < 0){
1202     return -1;
1203   }
1204   return 0;
1205 }
1206